From 6ef1afc28f7d1bab806938d05cf06fbcf5e9c2b6 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 20 Sep 2024 18:24:44 +0100 Subject: [PATCH 01/55] Concentrate DOM creation of renderer in the createCanvas method --- src/core/main.js | 7 +- src/core/p5.Renderer.js | 4 + src/core/p5.Renderer2D.js | 80 +++++++++++++----- src/core/rendering.js | 167 ++++++++++++++++++++----------------- src/webgl/p5.RendererGL.js | 6 ++ 5 files changed, 162 insertions(+), 102 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index a4dc7ce29f..f0e44c6098 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -90,8 +90,6 @@ class p5 { this._lcg_random_state = null; // NOTE: move to random.js this._gaussian_previous = false; // NOTE: move to random.js - this._loadingScreenId = 'p5_loading'; - if (window.DeviceOrientationEvent) { this._events.deviceorientation = null; } @@ -351,10 +349,7 @@ class p5 { if(this._startListener){ window.removeEventListener('load', this._startListener, false); } - const loadingScreen = document.getElementById(this._loadingScreenId); - if (loadingScreen) { - loadingScreen.parentNode.removeChild(loadingScreen); - } + if (this._curElement) { // stop draw this._loop = false; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index e1dea6e8cf..34172191d7 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -61,6 +61,10 @@ p5.Renderer = class Renderer { this._pInst.height = this.height; } + remove() { + + } + // Makes a shallow copy of the current states // and push it into the push pop stack push() { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 579cd70ebd..da2d7c15c2 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -15,22 +15,20 @@ const styleEmpty = 'rgba(0,0,0,0)'; class Renderer2D extends Renderer { constructor(elt, pInst, isMainCanvas) { super(elt, pInst, isMainCanvas); - this.elt = elt; - this.canvas = elt; - this.drawingContext = this.canvas.getContext('2d'); - this._pInst.drawingContext = this.drawingContext; - if (isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } + this._isMainCanvas = isMainCanvas; + + // if (isMainCanvas) { + // // for pixel method sharing with pimage + // this._pInst._curElement = this; + // this._pInst.canvas = this.canvas; + // } else { + // // hide if offscreen buffer by default + // this.canvas.style.display = 'none'; + // } // Extend renderer with methods of p5.Element with getters - this.wrappedElt = new p5.Element(elt, pInst); + // this.wrappedElt = new p5.Element(elt, pInst); for (const p of Object.getOwnPropertyNames(p5.Element.prototype)) { if (p !== 'constructor' && p[0] !== '_') { Object.defineProperty(this, p, { @@ -42,18 +40,58 @@ class Renderer2D extends Renderer { } } - // NOTE: renderer won't be created until instance createCanvas was called - // This createCanvas should handle the HTML stuff while other createCanvas - // be generic createCanvas(w, h, canvas) { super.createCanvas(w, h); - // this.canvas = this.elt = canvas || document.createElement('canvas'); - // this.drawingContext = this.canvas.getContext('2d'); - // this._pInst.drawingContext = this.drawingContext; + + // Create new canvas + this.canvas = this.elt = canvas || document.createElement('canvas'); + //////// + if (this._isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } + //////// + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); + + // Set canvas size + this.elt.width = w * this._pInst._pixelDensity; + this.elt.height = h * this._pInst._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; + + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName('main').length === 0) { + let m = document.createElement('main'); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName('main')[0].appendChild(this.elt); + } + + // Get and store drawing context + this.drawingContext = this.canvas.getContext('2d'); + this._pInst.drawingContext = this.drawingContext; + + // Set and return p5.Element + this.wrappedElt = new p5.Element(this.elt, this._pInst); return this.wrappedElt; } + remove(){ + this.wrappedElt.remove(); + } + getFilterGraphicsLayer() { // create hidden webgl renderer if it doesn't exist if (!this.filterGraphicsLayer) { @@ -142,6 +180,8 @@ class Renderer2D extends Renderer { this.blendMode(this._cachedBlendMode); } + console.log('background', this.drawingContext.fillStyle, this.drawingContext.canvas); + console.trace(); this.drawingContext.fillRect(0, 0, this.width, this.height); // reset fill this._setFill(curFill); @@ -1205,6 +1245,8 @@ class Renderer2D extends Renderer { _setFill(fillStyle) { if (fillStyle !== this._cachedFillStyle) { this.drawingContext.fillStyle = fillStyle; + // console.log('here', this.drawingContext.fillStyle); + // console.trace(); this._cachedFillStyle = fillStyle; } } diff --git a/src/core/rendering.js b/src/core/rendering.js index 2729843070..5cab3e33db 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -140,7 +140,7 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { } - + ///////////////////////////////// let r; if (arguments[2] instanceof HTMLCanvasElement) { renderer = constants.P2D; @@ -149,90 +149,103 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { r = renderer || constants.P2D; } - let c; + // let c; - if (canvas) { - // NOTE: this is to guard against multiple default canvas being created - c = document.getElementById(defaultId); - if (c) { - c.parentNode.removeChild(c); //replace the existing defaultCanvas - } - c = canvas; - this._defaultGraphicsCreated = false; - } else { - if (r === constants.WEBGL) { - c = document.getElementById(defaultId); - if (c) { - //if defaultCanvas already exists - c.parentNode.removeChild(c); //replace the existing defaultCanvas - const thisRenderer = this._renderer; - this._elements = this._elements.filter(e => e !== thisRenderer); - } - c = document.createElement('canvas'); - c.id = defaultId; - c.classList.add(defaultClass); - } else { - if (!this._defaultGraphicsCreated) { - if (canvas) { - c = canvas; - } else { - c = document.createElement('canvas'); - } - let i = 0; - while (document.getElementById(`defaultCanvas${i}`)) { - i++; - } - defaultId = `defaultCanvas${i}`; - c.id = defaultId; - c.classList.add(defaultClass); - } else if ( - this._renderer && - Object.getPrototypeOf(this._renderer) !== renderers[r].prototype - ) { - // Handle createCanvas() called with 2D mode after a 3D canvas is made - if (this.canvas.parentNode) { - this.canvas.parentNode.removeChild(this.canvas); //replace the existing defaultCanvas - } - const thisRenderer = this._renderer; - this._elements = this._elements.filter(e => e !== thisRenderer); - c = document.createElement('canvas'); - c.id = defaultId; - c.classList.add(defaultClass); - } else { - // resize the default canvas if new one is created - c = this.canvas; - } - } + // if (canvas) { + // // NOTE: this is to guard against multiple default canvas being created + // c = document.getElementById(defaultId); + // if (c) { + // c.parentNode.removeChild(c); //replace the existing defaultCanvas + // } + // c = canvas; + // this._defaultGraphicsCreated = false; + // } else { + // if (r === constants.WEBGL) { + // c = document.getElementById(defaultId); + // if (c) { + // //if defaultCanvas already exists + // c.parentNode.removeChild(c); //replace the existing defaultCanvas + // const thisRenderer = this._renderer; + // this._elements = this._elements.filter(e => e !== thisRenderer); + // } + // c = document.createElement('canvas'); + // c.id = defaultId; + // c.classList.add(defaultClass); + // } else { + // if (!this._defaultGraphicsCreated) { + // if (canvas) { + // c = canvas; + // } else { + // c = document.createElement('canvas'); + // } + // let i = 0; + // while (document.getElementById(`defaultCanvas${i}`)) { + // i++; + // } + // defaultId = `defaultCanvas${i}`; + // c.id = defaultId; + // c.classList.add(defaultClass); + // } else if ( + // this._renderer && + // Object.getPrototypeOf(this._renderer) !== renderers[r].prototype + // ) { + // // Handle createCanvas() called with 2D mode after a 3D canvas is made + // if (this.canvas.parentNode) { + // this.canvas.parentNode.removeChild(this.canvas); //replace the existing defaultCanvas + // } + // const thisRenderer = this._renderer; + // this._elements = this._elements.filter(e => e !== thisRenderer); + // c = document.createElement('canvas'); + // c.id = defaultId; + // c.classList.add(defaultClass); + // } else { + // // resize the default canvas if new one is created + // c = this.canvas; + // } + // } - // set to invisible if still in setup (to prevent flashing with manipulate) - if (!this._setupDone) { - c.dataset.hidden = true; // tag to show later - c.style.visibility = 'hidden'; - } + // // set to invisible if still in setup (to prevent flashing with manipulate) + // // if (!this._setupDone) { + // // c.dataset.hidden = true; // tag to show later + // // c.style.visibility = 'hidden'; + // // } - if (this._userNode) { - // user input node case - this._userNode.appendChild(c); - } else { - //create main element - if (document.getElementsByTagName('main').length === 0) { - let m = document.createElement('main'); - document.body.appendChild(m); - } - //append canvas to main - document.getElementsByTagName('main')[0].appendChild(c); - } - } + // if (this._userNode) { + // // user input node case + // this._userNode.appendChild(c); + // } else { + // //create main element + // if (document.getElementsByTagName('main').length === 0) { + // let m = document.createElement('main'); + // document.body.appendChild(m); + // } + // //append canvas to main + // document.getElementsByTagName('main')[0].appendChild(c); + // } + // } + + // if (this._userNode) { + // // user input node case + // this._userNode.appendChild(canvas); + // } else { + // //create main element + // if (document.getElementsByTagName('main').length === 0) { + // let m = document.createElement('main'); + // document.body.appendChild(m); + // } + // //append canvas to main + // document.getElementsByTagName('main')[0].appendChild(canvas); + // } // Init our graphics renderer - this._renderer = new renderers[r](c, this, true); + if(this._renderer) this._renderer.remove(); + this._renderer = new renderers[r](canvas, this, true); + const element = this._renderer.createCanvas(w, h, canvas); this._defaultGraphicsCreated = true; this._elements.push(this._renderer); - this._renderer.resize(w, h); this._renderer._applyDefaults(); - this._renderer.createCanvas(w, h, canvas); - // return this._renderer.createCanvas(w, h, canvas); - return this._renderer; + // return this._renderer; + return element; }; /** diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index a55bf65d0c..6a5f7900b5 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -437,6 +437,7 @@ p5.RendererGL = class RendererGL extends Renderer { super(elt, pInst, isMainCanvas); this.elt = elt; this.canvas = elt; + this.wrappedElt = new p5.Element(elt, pInst); this._setAttributeDefaults(pInst); this._initContext(); this.isP3D = true; //lets us know we're in 3d mode @@ -678,6 +679,11 @@ p5.RendererGL = class RendererGL extends Renderer { this._curShader = undefined; } + createCanvas(w, h, canvas){ + super.createCanvas(w, h); + return this.wrappedElt; + } + /** * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added * to the geometry and then returned when From 2f7f82483c95438c94d8b8253f56f91abea50971 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 20 Sep 2024 18:25:53 +0100 Subject: [PATCH 02/55] Indentation --- src/core/p5.Graphics.js | 434 ++++++++++++++++++++-------------------- 1 file changed, 217 insertions(+), 217 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index ffce6db35c..5d8e7c45ce 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -152,164 +152,164 @@ p5.Graphics = class Graphics extends p5.Element { } /** - * Resets the graphics buffer's transformations and lighting. - * - * By default, the main canvas resets certain transformation and lighting - * values each time draw() executes. `p5.Graphics` - * objects must reset these values manually by calling `myGraphics.reset()`. - * - * - * @example - *
- * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Graphics object. - * pg = createGraphics(60, 60); - * - * describe('A white circle moves downward slowly within a dark square. The circle resets at the top of the dark square when the user presses the mouse.'); - * } - * - * function draw() { - * background(200); - * - * // Translate the p5.Graphics object's coordinate system. - * // The translation accumulates; the white circle moves. - * pg.translate(0, 0.1); - * - * // Draw to the p5.Graphics object. - * pg.background(100); - * pg.circle(30, 0, 10); - * - * // Display the p5.Graphics object. - * image(pg, 20, 20); - * - * // Translate the main canvas' coordinate system. - * // The translation doesn't accumulate; the dark - * // square is always in the same place. - * translate(0, 0.1); - * - * // Reset the p5.Graphics object when the - * // user presses the mouse. - * if (mouseIsPressed === true) { - * pg.reset(); - * } - * } - * - *
- * - *
- * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Graphics object. - * pg = createGraphics(60, 60); - * - * describe('A white circle at the center of a dark gray square. The image is drawn on a light gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Translate the p5.Graphics object's coordinate system. - * pg.translate(30, 30); - * - * // Draw to the p5.Graphics object. - * pg.background(100); - * pg.circle(0, 0, 10); - * - * // Display the p5.Graphics object. - * image(pg, 20, 20); - * - * // Reset the p5.Graphics object automatically. - * pg.reset(); - * } - * - *
- * - *
- * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Graphics object using WebGL mode. - * pg = createGraphics(100, 100, WEBGL); - * - * describe("A sphere lit from above with a red light. The sphere's surface becomes glossy while the user clicks and holds the mouse."); - * } - * - * function draw() { - * background(200); - * - * // Add a red point light from the top-right. - * pg.pointLight(255, 0, 0, 50, -100, 50); - * - * // Style the sphere. - * // It should appear glossy when the - * // lighting values are reset. - * pg.noStroke(); - * pg.specularMaterial(255); - * pg.shininess(100); - * - * // Draw the sphere. - * pg.sphere(30); - * - * // Display the p5.Graphics object. - * image(pg, -50, -50); - * - * // Reset the p5.Graphics object when - * // the user presses the mouse. - * if (mouseIsPressed === true) { - * pg.reset(); - * } - * } - * - *
- * - *
- * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Graphics object using WebGL mode. - * pg = createGraphics(100, 100, WEBGL); - * - * describe('A sphere with a glossy surface is lit from the top-right by a red light.'); - * } - * - * function draw() { - * background(200); - * - * // Add a red point light from the top-right. - * pg.pointLight(255, 0, 0, 50, -100, 50); - * - * // Style the sphere. - * pg.noStroke(); - * pg.specularMaterial(255); - * pg.shininess(100); - * - * // Draw the sphere. - * pg.sphere(30); - * - * // Display the p5.Graphics object. - * image(pg, 0, 0); - * - * // Reset the p5.Graphics object automatically. - * pg.reset(); - * } - * - *
- */ + * Resets the graphics buffer's transformations and lighting. + * + * By default, the main canvas resets certain transformation and lighting + * values each time draw() executes. `p5.Graphics` + * objects must reset these values manually by calling `myGraphics.reset()`. + * + * + * @example + *
+ * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Graphics object. + * pg = createGraphics(60, 60); + * + * describe('A white circle moves downward slowly within a dark square. The circle resets at the top of the dark square when the user presses the mouse.'); + * } + * + * function draw() { + * background(200); + * + * // Translate the p5.Graphics object's coordinate system. + * // The translation accumulates; the white circle moves. + * pg.translate(0, 0.1); + * + * // Draw to the p5.Graphics object. + * pg.background(100); + * pg.circle(30, 0, 10); + * + * // Display the p5.Graphics object. + * image(pg, 20, 20); + * + * // Translate the main canvas' coordinate system. + * // The translation doesn't accumulate; the dark + * // square is always in the same place. + * translate(0, 0.1); + * + * // Reset the p5.Graphics object when the + * // user presses the mouse. + * if (mouseIsPressed === true) { + * pg.reset(); + * } + * } + * + *
+ * + *
+ * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Graphics object. + * pg = createGraphics(60, 60); + * + * describe('A white circle at the center of a dark gray square. The image is drawn on a light gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Translate the p5.Graphics object's coordinate system. + * pg.translate(30, 30); + * + * // Draw to the p5.Graphics object. + * pg.background(100); + * pg.circle(0, 0, 10); + * + * // Display the p5.Graphics object. + * image(pg, 20, 20); + * + * // Reset the p5.Graphics object automatically. + * pg.reset(); + * } + * + *
+ * + *
+ * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Graphics object using WebGL mode. + * pg = createGraphics(100, 100, WEBGL); + * + * describe("A sphere lit from above with a red light. The sphere's surface becomes glossy while the user clicks and holds the mouse."); + * } + * + * function draw() { + * background(200); + * + * // Add a red point light from the top-right. + * pg.pointLight(255, 0, 0, 50, -100, 50); + * + * // Style the sphere. + * // It should appear glossy when the + * // lighting values are reset. + * pg.noStroke(); + * pg.specularMaterial(255); + * pg.shininess(100); + * + * // Draw the sphere. + * pg.sphere(30); + * + * // Display the p5.Graphics object. + * image(pg, -50, -50); + * + * // Reset the p5.Graphics object when + * // the user presses the mouse. + * if (mouseIsPressed === true) { + * pg.reset(); + * } + * } + * + *
+ * + *
+ * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Graphics object using WebGL mode. + * pg = createGraphics(100, 100, WEBGL); + * + * describe('A sphere with a glossy surface is lit from the top-right by a red light.'); + * } + * + * function draw() { + * background(200); + * + * // Add a red point light from the top-right. + * pg.pointLight(255, 0, 0, 50, -100, 50); + * + * // Style the sphere. + * pg.noStroke(); + * pg.specularMaterial(255); + * pg.shininess(100); + * + * // Draw the sphere. + * pg.sphere(30); + * + * // Display the p5.Graphics object. + * image(pg, 0, 0); + * + * // Reset the p5.Graphics object automatically. + * pg.reset(); + * } + * + *
+ */ reset() { this._renderer.resetMatrix(); if (this._renderer.isP3D) { @@ -318,65 +318,65 @@ p5.Graphics = class Graphics extends p5.Element { } /** - * Removes the graphics buffer from the web page. - * - * Calling `myGraphics.remove()` removes the graphics buffer's - * `<canvas>` element from the web page. The graphics buffer also uses - * a bit of memory on the CPU that can be freed like so: - * - * ```js - * // Remove the graphics buffer from the web page. - * myGraphics.remove(); - * - * // Delete the graphics buffer from CPU memory. - * myGraphics = undefined; - * ``` - * - * Note: All variables that reference the graphics buffer must be assigned - * the value `undefined` to delete the graphics buffer from CPU memory. If any - * variable still refers to the graphics buffer, then it won't be garbage - * collected. - * - * @example - *
- * - * // Double-click to remove the p5.Graphics object. - * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Graphics object. - * pg = createGraphics(60, 60); - * - * // Draw to the p5.Graphics object. - * pg.background(100); - * pg.circle(30, 30, 20); - * - * describe('A white circle at the center of a dark gray square disappears when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Display the p5.Graphics object if - * // it's available. - * if (pg) { - * image(pg, 20, 20); - * } - * } - * - * // Remove the p5.Graphics object when the - * // the user double-clicks. - * function doubleClicked() { - * // Remove the p5.Graphics object from the web page. - * pg.remove(); - * pg = undefined; - * } - * - *
- */ + * Removes the graphics buffer from the web page. + * + * Calling `myGraphics.remove()` removes the graphics buffer's + * `<canvas>` element from the web page. The graphics buffer also uses + * a bit of memory on the CPU that can be freed like so: + * + * ```js + * // Remove the graphics buffer from the web page. + * myGraphics.remove(); + * + * // Delete the graphics buffer from CPU memory. + * myGraphics = undefined; + * ``` + * + * Note: All variables that reference the graphics buffer must be assigned + * the value `undefined` to delete the graphics buffer from CPU memory. If any + * variable still refers to the graphics buffer, then it won't be garbage + * collected. + * + * @example + *
+ * + * // Double-click to remove the p5.Graphics object. + * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Graphics object. + * pg = createGraphics(60, 60); + * + * // Draw to the p5.Graphics object. + * pg.background(100); + * pg.circle(30, 30, 20); + * + * describe('A white circle at the center of a dark gray square disappears when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Display the p5.Graphics object if + * // it's available. + * if (pg) { + * image(pg, 20, 20); + * } + * } + * + * // Remove the p5.Graphics object when the + * // the user double-clicks. + * function doubleClicked() { + * // Remove the p5.Graphics object from the web page. + * pg.remove(); + * pg = undefined; + * } + * + *
+ */ remove() { if (this.elt.parentNode) { this.elt.parentNode.removeChild(this.elt); From 9c47a4cd64a6b69d3816f7bf4c8e9b4a407ef06f Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 21 Sep 2024 14:20:24 +0100 Subject: [PATCH 03/55] Fix p5.Graphics creation --- src/core/p5.Graphics.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 5d8e7c45ce..2b2f0b0a71 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -92,7 +92,7 @@ import * as constants from './constants'; * * */ -p5.Graphics = class Graphics extends p5.Element { +p5.Graphics = class Graphics { constructor(w, h, renderer, pInst, canvas) { let canvasTemp; if (canvas) { @@ -101,7 +101,6 @@ p5.Graphics = class Graphics extends p5.Element { canvasTemp = document.createElement('canvas'); } - super(canvasTemp, pInst); this.canvas = canvasTemp; const r = renderer || constants.P2D; @@ -127,16 +126,7 @@ p5.Graphics = class Graphics extends p5.Element { this.height = h; this._pixelDensity = pInst._pixelDensity; this._renderer = new p5.renderers[r](this.canvas, this, false); - - // if (r === constants.WEBGL) { - // this._renderer = new p5.RendererGL(this.canvas, this, false); - // const { adjustedWidth, adjustedHeight } = - // this._renderer._adjustDimensions(w, h); - // w = adjustedWidth; - // h = adjustedHeight; - // } else { - // this._renderer = new p5.Renderer2D(this.canvas, this, false); - // } + this._renderer.createCanvas(w, h, this.canvas); pInst._elements.push(this); @@ -146,7 +136,6 @@ p5.Graphics = class Graphics extends p5.Element { } }); - this._renderer.resize(w, h); this._renderer._applyDefaults(); return this; } From c39cfa62e007d1f966146a5fe7f8f5ae2b3cc1d6 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 21 Sep 2024 17:04:26 +0100 Subject: [PATCH 04/55] p5.Graphics acts as wrapper of p5.Renderer Some properties and methods of p5 instance still need to be taken cared of. Remove references to DOM in constructor. --- src/core/p5.Graphics.js | 66 +++++++++++++++++++-------------------- src/core/p5.Renderer2D.js | 52 +++++++++++++++--------------- 2 files changed, 57 insertions(+), 61 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 2b2f0b0a71..441f9c0921 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -94,52 +94,50 @@ import * as constants from './constants'; */ p5.Graphics = class Graphics { constructor(w, h, renderer, pInst, canvas) { - let canvasTemp; - if (canvas) { - canvasTemp = canvas; - } else { - canvasTemp = document.createElement('canvas'); - } + const r = renderer || constants.P2D; - this.canvas = canvasTemp; + // bind methods and props of p5 to the new object + // for (const p in p5.prototype) { + // if (!this[p]) { + // if (typeof p5.prototype[p] === 'function') { + // this[p] = p5.prototype[p].bind(this); + // } else if(p !== 'deltaTime') { + // this[p] = p5.prototype[p]; + // } + // } + // } + // p5.prototype._initializeInstanceVariables.apply(this); - const r = renderer || constants.P2D; + this._pInst = pInst; + this._pixelDensity = this._pInst._pixelDensity; + this._renderer = new p5.renderers[r](canvas, this._pInst, false); + this._renderer.createCanvas(w, h, canvas); - const node = pInst._userNode || document.body; - if (!canvas) { - node.appendChild(this.canvas); + // Attach renderer methods + for(const p of Object.getOwnPropertyNames(p5.renderers[r].prototype)) { + if(p !== 'constructor' && p[0] !== '_'){ + this[p] = this._renderer[p].bind(this._renderer); + } } - // bind methods and props of p5 to the new object - for (const p in p5.prototype) { - if (!this[p]) { - if (typeof p5.prototype[p] === 'function') { - this[p] = p5.prototype[p].bind(this); - } else { - this[p] = p5.prototype[p]; + // Attach renderer properties + for (const p in this._renderer) { + if(p[0] === '_') continue; + Object.defineProperty(this, p, { + get(){ + return this._renderer[p]; } - } + }) } - p5.prototype._initializeInstanceVariables.apply(this); - this.width = w; - this.height = h; - this._pixelDensity = pInst._pixelDensity; - this._renderer = new p5.renderers[r](this.canvas, this, false); - this._renderer.createCanvas(w, h, this.canvas); - - pInst._elements.push(this); - - Object.defineProperty(this, 'deltaTime', { - get() { - return this._pInst.deltaTime; - } - }); - this._renderer._applyDefaults(); return this; } + get deltaTime(){ + return this._pInst.deltaTime; + } + /** * Resets the graphics buffer's transformations and lighting. * diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index da2d7c15c2..8220157af5 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -17,6 +17,7 @@ class Renderer2D extends Renderer { super(elt, pInst, isMainCanvas); this._isMainCanvas = isMainCanvas; + this.pixels = []; // if (isMainCanvas) { // // for pixel method sharing with pimage @@ -180,8 +181,6 @@ class Renderer2D extends Renderer { this.blendMode(this._cachedBlendMode); } - console.log('background', this.drawingContext.fillStyle, this.drawingContext.canvas); - console.trace(); this.drawingContext.fillRect(0, 0, this.width, this.height); // reset fill this._setFill(curFill); @@ -502,7 +501,7 @@ class Renderer2D extends Renderer { // @todo this should actually set pixels per object, so diff buffers can // have diff pixel arrays. pixelsState.imageData = imageData; - pixelsState.pixels = imageData.data; + this.pixels = pixelsState.pixels = imageData.data; } set(x, y, imgOrCol) { @@ -1386,6 +1385,29 @@ class Renderer2D extends Renderer { return this.drawingContext.measureText(s).width; } + text(str, x, y, maxWidth, maxHeight) { + let baselineHacked; + + // baselineHacked: (HACK) + // A temporary fix to conform to Processing's implementation + // of BASELINE vertical alignment in a bounding box + + if (typeof maxWidth !== 'undefined') { + if (this.drawingContext.textBaseline === constants.BASELINE) { + baselineHacked = true; + this.drawingContext.textBaseline = constants.TOP; + } + } + + const p = Renderer.prototype.text.apply(this, arguments); + + if (baselineHacked) { + this.drawingContext.textBaseline = constants.BASELINE; + } + + return p; + } + _applyTextProperties() { let font; const p = this._pInst; @@ -1449,30 +1471,6 @@ class Renderer2D extends Renderer { } } -// Fix test -Renderer2D.prototype.text = function (str, x, y, maxWidth, maxHeight) { - let baselineHacked; - - // baselineHacked: (HACK) - // A temporary fix to conform to Processing's implementation - // of BASELINE vertical alignment in a bounding box - - if (typeof maxWidth !== 'undefined') { - if (this.drawingContext.textBaseline === constants.BASELINE) { - baselineHacked = true; - this.drawingContext.textBaseline = constants.TOP; - } - } - - const p = Renderer.prototype.text.apply(this, arguments); - - if (baselineHacked) { - this.drawingContext.textBaseline = constants.BASELINE; - } - - return p; -}; - p5.Renderer2D = Renderer2D; export default p5.Renderer2D; From 263ae577db9c945d4da2b1db655f8853b7a5d8eb Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 21 Sep 2024 17:24:35 +0100 Subject: [PATCH 05/55] Fix p5.Graphics.remove and simplify it --- src/core/p5.Graphics.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 441f9c0921..121ba1b7e4 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -365,20 +365,8 @@ p5.Graphics = class Graphics { * */ remove() { - if (this.elt.parentNode) { - this.elt.parentNode.removeChild(this.elt); - } - const idx = this._pInst._elements.indexOf(this); - if (idx !== -1) { - this._pInst._elements.splice(idx, 1); - } - for (const elt_ev in this._events) { - this.elt.removeEventListener(elt_ev, this._events[elt_ev]); - } - - this._renderer = undefined; - this.canvas = undefined; - this.elt = undefined; + this._renderer.remove(); + this._renderer = null; } From 84279785fa8e651733a4ac382a276b8f5da83009 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 21 Sep 2024 18:46:58 +0100 Subject: [PATCH 06/55] Fix webgl canvas creation --- src/webgl/p5.RendererGL.js | 167 +++++++++++++++++++++++++++---------- 1 file changed, 121 insertions(+), 46 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 6a5f7900b5..3e9ce9062b 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -435,11 +435,10 @@ export function readPixelWebGL( p5.RendererGL = class RendererGL extends Renderer { constructor(elt, pInst, isMainCanvas, attr) { super(elt, pInst, isMainCanvas); - this.elt = elt; - this.canvas = elt; - this.wrappedElt = new p5.Element(elt, pInst); + // this.elt = elt; + // this.canvas = elt; this._setAttributeDefaults(pInst); - this._initContext(); + // this._initContext(); this.isP3D = true; //lets us know we're in 3d mode // When constructing a new p5.Geometry, this will represent the builder @@ -447,17 +446,17 @@ p5.RendererGL = class RendererGL extends Renderer { // This redundant property is useful in reminding you that you are // interacting with WebGLRenderingContext, still worth considering future removal - this.GL = this.drawingContext; - this._pInst.drawingContext = this.drawingContext; - - if (isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } + // this.GL = this.drawingContext; + // this._pInst.drawingContext = this.drawingContext; + + // if (isMainCanvas) { + // // for pixel method sharing with pimage + // this._pInst._curElement = this; + // this._pInst.canvas = this.canvas; + // } else { + // // hide if offscreen buffer by default + // this.canvas.style.display = 'none'; + // } // Push/pop state this.states.uModelMatrix = new p5.Matrix(); @@ -535,11 +534,11 @@ p5.RendererGL = class RendererGL extends Renderer { this._cachedBlendMode = undefined; this._cachedFillStyle = [1, 1, 1, 1]; this._cachedStrokeStyle = [0, 0, 0, 1]; - if (this.webglVersion === constants.WEBGL2) { - this.blendExt = this.GL; - } else { - this.blendExt = this.GL.getExtension('EXT_blend_minmax'); - } + // if (this.webglVersion === constants.WEBGL2) { + // this.blendExt = this.GL; + // } else { + // this.blendExt = this.GL.getExtension('EXT_blend_minmax'); + // } this._isBlending = false; this._useLineColor = false; @@ -608,31 +607,31 @@ p5.RendererGL = class RendererGL extends Renderer { // Immediate Mode // Geometry and Material hashes stored here - this.immediateMode = { - geometry: new p5.Geometry(), - shapeMode: constants.TRIANGLE_FAN, - contourIndices: [], - _bezierVertex: [], - _quadraticVertex: [], - _curveVertex: [], - buffers: { - fill: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - point: this.GL.createBuffer() - } - }; + // this.immediateMode = { + // geometry: new p5.Geometry(), + // shapeMode: constants.TRIANGLE_FAN, + // contourIndices: [], + // _bezierVertex: [], + // _quadraticVertex: [], + // _curveVertex: [], + // buffers: { + // fill: [ + // new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + // new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + // new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + // new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + // new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + // ], + // stroke: [ + // new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + // new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + // new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + // new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + // new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + // ], + // point: this.GL.createBuffer() + // } + // }; this.pointSize = 5.0; //default point size this.curStrokeWeight = 1; @@ -681,6 +680,82 @@ p5.RendererGL = class RendererGL extends Renderer { createCanvas(w, h, canvas){ super.createCanvas(w, h); + + // Create new canvas + this.canvas = this.elt = canvas || document.createElement('canvas'); + + if (this._isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); + + // Set canvas size + this.elt.width = w * this._pInst._pixelDensity; + this.elt.height = h * this._pInst._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; + + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName('main').length === 0) { + let m = document.createElement('main'); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName('main')[0].appendChild(this.elt); + } + + // Get and store drawing context + this._initContext(); + this.GL = this.drawingContext; + this._pInst.drawingContext = this.drawingContext; + + if (this.webglVersion === constants.WEBGL2) { + this.blendExt = this.GL; + } else { + this.blendExt = this.GL.getExtension('EXT_blend_minmax'); + } + + this.immediateMode = { + geometry: new p5.Geometry(), + shapeMode: constants.TRIANGLE_FAN, + contourIndices: [], + _bezierVertex: [], + _quadraticVertex: [], + _curveVertex: [], + buffers: { + fill: [ + new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + stroke: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + ], + point: this.GL.createBuffer() + } + }; + + this.wrappedElt = new p5.Element(this.canvas, this._pInst); + + + return this.wrappedElt; } @@ -1448,7 +1523,7 @@ p5.RendererGL = class RendererGL extends Renderer { * @param {Number} h [description] */ resize(w, h) { - Renderer.prototype.resize.call(this, w, h); + super.resize(w, h); this.canvas.width = w * this._pInst._pixelDensity; this.canvas.height = h * this._pInst._pixelDensity; this.canvas.style.width = `${w}px`; From 5c68e24d3d9764dcec014cf78e49d46717304244 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 21 Sep 2024 19:01:50 +0100 Subject: [PATCH 07/55] Minor adjustment to p5.Renderer and p5.Graphics remove --- src/core/p5.Graphics.js | 4 ++-- src/core/p5.Renderer2D.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 121ba1b7e4..9e50cf0c3a 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -115,7 +115,7 @@ p5.Graphics = class Graphics { // Attach renderer methods for(const p of Object.getOwnPropertyNames(p5.renderers[r].prototype)) { - if(p !== 'constructor' && p[0] !== '_'){ + if(p !== 'constructor' && p[0] !== '_' && !(p in this)){ this[p] = this._renderer[p].bind(this._renderer); } } @@ -125,7 +125,7 @@ p5.Graphics = class Graphics { if(p[0] === '_') continue; Object.defineProperty(this, p, { get(){ - return this._renderer[p]; + return this._renderer?.[p]; } }) } diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 8220157af5..9a58fcef59 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -91,6 +91,9 @@ class Renderer2D extends Renderer { remove(){ this.wrappedElt.remove(); + this.wrappedElt = null; + this.canvas = null; + this.elt = null; } getFilterGraphicsLayer() { From e3c3683e23fd3645428b1dce69a26e2d54cfd809 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 22 Sep 2024 13:07:52 +0100 Subject: [PATCH 08/55] Make resizeCanvas() independent of DOM --- src/core/environment.js | 2 +- src/core/main.js | 1 - src/core/p5.Graphics.js | 14 ++++++++++++++ src/core/p5.Renderer2D.js | 19 +++++++++++++++++++ src/core/rendering.js | 23 +---------------------- src/webgl/p5.RendererGL.js | 28 ++++++++++++++++++++++++++-- 6 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/core/environment.js b/src/core/environment.js index 2e4b711632..7f5dcaee24 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -1065,7 +1065,7 @@ p5.prototype.pixelDensity = function(val) { let returnValue; if (typeof val === 'number') { if (val !== this._pixelDensity) { - this._pixelDensity = this._maxAllowedPixelDimensions = val; + this._pixelDensity = val; } returnValue = this; this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas diff --git a/src/core/main.js b/src/core/main.js index f0e44c6098..b448fe371e 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -50,7 +50,6 @@ class p5 { this._setupDone = false; // for handling hidpi this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; - this._maxAllowedPixelDimensions = 0; this._userNode = node; this._curElement = null; this._elements = []; diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 9e50cf0c3a..61c2ca76ea 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -138,6 +138,20 @@ p5.Graphics = class Graphics { return this._pInst.deltaTime; } + pixelDensity(val){ + let returnValue; + if (typeof val === 'number') { + if (val !== this._pixelDensity) { + this._pixelDensity = val; + } + returnValue = this; + this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas + } else { + returnValue = this._pixelDensity; + } + return returnValue; + } + /** * Resets the graphics buffer's transformations and lighting. * diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 9a58fcef59..1e83528d1e 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -140,6 +140,16 @@ class Renderer2D extends Renderer { resize(w, h) { super.resize(w, h); + + // save canvas properties + const props = {}; + for (const key in this.drawingContext) { + const val = this.drawingContext[key]; + if (typeof val !== 'object' && typeof val !== 'function') { + props[key] = val; + } + } + this.canvas.width = w * this._pInst._pixelDensity; this.canvas.height = h * this._pInst._pixelDensity; this.canvas.style.width = `${w}px`; @@ -148,6 +158,15 @@ class Renderer2D extends Renderer { this._pInst._pixelDensity, this._pInst._pixelDensity ); + + // reset canvas properties + for (const savedKey in props) { + try { + this.drawingContext[savedKey] = props[savedKey]; + } catch (err) { + // ignore read-only property errors + } + } } ////////////////////////////////////////////// diff --git a/src/core/rendering.js b/src/core/rendering.js index 5cab3e33db..44ff844b8b 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -336,33 +336,12 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { p5.prototype.resizeCanvas = function (w, h, noRedraw) { p5._validateParameters('resizeCanvas', arguments); if (this._renderer) { - // save canvas properties - const props = {}; - for (const key in this.drawingContext) { - const val = this.drawingContext[key]; - if (typeof val !== 'object' && typeof val !== 'function') { - props[key] = val; - } - } - if (this._renderer instanceof p5.RendererGL) { - const dimensions = - this._renderer._adjustDimensions(w, h); - w = dimensions.adjustedWidth; - h = dimensions.adjustedHeight; - } this.width = w; this.height = h; // Make sure width and height are updated before the renderer resizes so // that framebuffers updated from the resize read the correct size this._renderer.resize(w, h); - // reset canvas properties - for (const savedKey in props) { - try { - this.drawingContext[savedKey] = props[savedKey]; - } catch (err) { - // ignore read-only property errors - } - } + if (!noRedraw) { this.redraw(); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 3e9ce9062b..b4bb916d3f 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -889,9 +889,8 @@ p5.RendererGL = class RendererGL extends Renderer { this._maxTextureSize = this._getMaxTextureSize(); } let maxTextureSize = this._maxTextureSize; - let maxAllowedPixelDimensions = p5.prototype._maxAllowedPixelDimensions; - maxAllowedPixelDimensions = Math.floor( + let maxAllowedPixelDimensions = Math.floor( maxTextureSize / this.pixelDensity() ); let adjustedWidth = Math.min( @@ -1524,6 +1523,22 @@ p5.RendererGL = class RendererGL extends Renderer { */ resize(w, h) { super.resize(w, h); + + // save canvas properties + const props = {}; + for (const key in this.drawingContext) { + const val = this.drawingContext[key]; + if (typeof val !== 'object' && typeof val !== 'function') { + props[key] = val; + } + } + + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; + this._pInst.width = w; + this._pInst.height = h; + this.canvas.width = w * this._pInst._pixelDensity; this.canvas.height = h * this._pInst._pixelDensity; this.canvas.style.width = `${w}px`; @@ -1553,6 +1568,15 @@ p5.RendererGL = class RendererGL extends Renderer { // can also update their size framebuffer._canvasSizeChanged(); } + + // reset canvas properties + for (const savedKey in props) { + try { + this.drawingContext[savedKey] = props[savedKey]; + } catch (err) { + // ignore read-only property errors + } + } } /** From f1d87355ed4b8aca6403464cbeca7d1f751d9195 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 22 Sep 2024 16:38:54 +0100 Subject: [PATCH 09/55] Remove renderer createCanvas() method as it is redundant with constructor --- src/core/p5.Graphics.js | 7 +- src/core/p5.Renderer.js | 17 +-- src/core/p5.Renderer2D.js | 68 ++++++------ src/core/rendering.js | 24 ++--- src/webgl/p5.RendererGL.js | 206 ++++++++++++++----------------------- 5 files changed, 131 insertions(+), 191 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 61c2ca76ea..aa6f04482c 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -110,8 +110,7 @@ p5.Graphics = class Graphics { this._pInst = pInst; this._pixelDensity = this._pInst._pixelDensity; - this._renderer = new p5.renderers[r](canvas, this._pInst, false); - this._renderer.createCanvas(w, h, canvas); + this._renderer = new p5.renderers[r](this._pInst, w, h, false, canvas); // Attach renderer methods for(const p of Object.getOwnPropertyNames(p5.renderers[r].prototype)) { @@ -152,6 +151,10 @@ p5.Graphics = class Graphics { return returnValue; } + resizeCanvas(w, h){ + this._renderer.resize(w, h, this._pixelDensity); + } + /** * Resets the graphics buffer's transformations and lighting. * diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 34172191d7..cea5aed086 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -18,8 +18,16 @@ import * as constants from '../core/constants'; * @param {Boolean} [isMainCanvas] whether we're using it as main canvas */ p5.Renderer = class Renderer { - constructor(elt, pInst, isMainCanvas) { + constructor(pInst, w, h, isMainCanvas) { this._pInst = this._pixelsState = pInst; + this._isMainCanvas = isMainCanvas; + this.pixels = []; + + this.width = w; + this.height = h; + this._pInst.width = this.width; + this._pInst.height = this.height; + this._events = {}; if (isMainCanvas) { @@ -54,13 +62,6 @@ p5.Renderer = class Renderer { this._curveTightness = 0; } - createCanvas(w, h) { - this.width = w; - this.height = h; - this._pInst.width = this.width; - this._pInst.height = this.height; - } - remove() { } diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 1e83528d1e..0c35f9c040 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -13,20 +13,22 @@ const styleEmpty = 'rgba(0,0,0,0)'; * @private */ class Renderer2D extends Renderer { - constructor(elt, pInst, isMainCanvas) { - super(elt, pInst, isMainCanvas); + constructor(pInst, w, h, isMainCanvas, elt) { + super(pInst, w, h, isMainCanvas); - this._isMainCanvas = isMainCanvas; - this.pixels = []; + this.canvas = this.elt = elt || document.createElement('canvas'); - // if (isMainCanvas) { - // // for pixel method sharing with pimage - // this._pInst._curElement = this; - // this._pInst.canvas = this.canvas; - // } else { - // // hide if offscreen buffer by default - // this.canvas.style.display = 'none'; - // } + if (isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } + + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); // Extend renderer with methods of p5.Element with getters // this.wrappedElt = new p5.Element(elt, pInst); @@ -39,25 +41,6 @@ class Renderer2D extends Renderer { }) } } - } - - createCanvas(w, h, canvas) { - super.createCanvas(w, h); - - // Create new canvas - this.canvas = this.elt = canvas || document.createElement('canvas'); - //////// - if (this._isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } - //////// - this.elt.id = 'defaultCanvas0'; - this.elt.classList.add('p5Canvas'); // Set canvas size this.elt.width = w * this._pInst._pixelDensity; @@ -85,8 +68,6 @@ class Renderer2D extends Renderer { // Set and return p5.Element this.wrappedElt = new p5.Element(this.elt, this._pInst); - - return this.wrappedElt; } remove(){ @@ -138,7 +119,7 @@ class Renderer2D extends Renderer { this.drawingContext.font = 'normal 12px sans-serif'; } - resize(w, h) { + resize(w, h, pixelDensity = this._pInst._pixelDensity) { super.resize(w, h); // save canvas properties @@ -150,13 +131,13 @@ class Renderer2D extends Renderer { } } - this.canvas.width = w * this._pInst._pixelDensity; - this.canvas.height = h * this._pInst._pixelDensity; + this.canvas.width = w * pixelDensity; + this.canvas.height = h * pixelDensity; this.canvas.style.width = `${w}px`; this.canvas.style.height = `${h}px`; this.drawingContext.scale( - this._pInst._pixelDensity, - this._pInst._pixelDensity + pixelDensity, + pixelDensity ); // reset canvas properties @@ -167,6 +148,8 @@ class Renderer2D extends Renderer { // ignore read-only property errors } } + + console.log(this.elt.style.width); } ////////////////////////////////////////////// @@ -366,6 +349,15 @@ class Renderer2D extends Renderer { if (this._isErasing) { this.blendMode(this._cachedBlendMode); } + // console.log(this.elt, cnv, + // s * sx, + // s * sy, + // s * sWidth, + // s * sHeight, + // dx, + // dy, + // dWidth, + // dHeight); this.drawingContext.drawImage( cnv, s * sx, diff --git a/src/core/rendering.js b/src/core/rendering.js index 44ff844b8b..b51582699e 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -129,7 +129,7 @@ const renderers = p5.renderers = { * @param {HTMLCanvasElement} [canvas] * @return {p5.Renderer} */ -p5.prototype.createCanvas = function (w, h, renderer, canvas) { +p5.prototype.createCanvas = function (w, h, renderer, ...args) { p5._validateParameters('createCanvas', arguments); //optional: renderer, otherwise defaults to p2d @@ -137,17 +137,19 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { // Check third argument whether it is renderer constants if(Reflect.ownKeys(renderers).includes(renderer)){ selectedRenderer = renderer; + }else{ + args.unshift(renderer); } ///////////////////////////////// - let r; - if (arguments[2] instanceof HTMLCanvasElement) { - renderer = constants.P2D; - canvas = arguments[2]; - } else { - r = renderer || constants.P2D; - } + // let r; + // if (arguments[2] instanceof HTMLCanvasElement) { + // renderer = constants.P2D; + // canvas = arguments[2]; + // } else { + // r = renderer || constants.P2D; + // } // let c; @@ -239,13 +241,11 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { // Init our graphics renderer if(this._renderer) this._renderer.remove(); - this._renderer = new renderers[r](canvas, this, true); - const element = this._renderer.createCanvas(w, h, canvas); + this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args); this._defaultGraphicsCreated = true; this._elements.push(this._renderer); this._renderer._applyDefaults(); - // return this._renderer; - return element; + return this._renderer; }; /** diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b4bb916d3f..d6ec479081 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -433,12 +433,45 @@ export function readPixelWebGL( * rendering (FBO). */ p5.RendererGL = class RendererGL extends Renderer { - constructor(elt, pInst, isMainCanvas, attr) { - super(elt, pInst, isMainCanvas); - // this.elt = elt; - // this.canvas = elt; + constructor(pInst, w, h, isMainCanvas, elt, attr) { + super(pInst, w, h, isMainCanvas); + + // Create new canvas + this.canvas = this.elt = elt || document.createElement('canvas'); + + if (this._isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); + + // Set canvas size + this.elt.width = w * this._pInst._pixelDensity; + this.elt.height = h * this._pInst._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; + + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName('main').length === 0) { + let m = document.createElement('main'); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName('main')[0].appendChild(this.elt); + } + this._setAttributeDefaults(pInst); - // this._initContext(); + this._initContext(); this.isP3D = true; //lets us know we're in 3d mode // When constructing a new p5.Geometry, this will represent the builder @@ -446,17 +479,8 @@ p5.RendererGL = class RendererGL extends Renderer { // This redundant property is useful in reminding you that you are // interacting with WebGLRenderingContext, still worth considering future removal - // this.GL = this.drawingContext; - // this._pInst.drawingContext = this.drawingContext; - - // if (isMainCanvas) { - // // for pixel method sharing with pimage - // this._pInst._curElement = this; - // this._pInst.canvas = this.canvas; - // } else { - // // hide if offscreen buffer by default - // this.canvas.style.display = 'none'; - // } + this.GL = this.drawingContext; + this._pInst.drawingContext = this.drawingContext; // Push/pop state this.states.uModelMatrix = new p5.Matrix(); @@ -528,17 +552,15 @@ p5.RendererGL = class RendererGL extends Renderer { // p5.framebuffer for this are calculated in getSpecularTexture function this.specularTextures = new Map(); - - this.preEraseBlend = undefined; this._cachedBlendMode = undefined; this._cachedFillStyle = [1, 1, 1, 1]; this._cachedStrokeStyle = [0, 0, 0, 1]; - // if (this.webglVersion === constants.WEBGL2) { - // this.blendExt = this.GL; - // } else { - // this.blendExt = this.GL.getExtension('EXT_blend_minmax'); - // } + if (this.webglVersion === constants.WEBGL2) { + this.blendExt = this.GL; + } else { + this.blendExt = this.GL.getExtension('EXT_blend_minmax'); + } this._isBlending = false; this._useLineColor = false; @@ -607,31 +629,31 @@ p5.RendererGL = class RendererGL extends Renderer { // Immediate Mode // Geometry and Material hashes stored here - // this.immediateMode = { - // geometry: new p5.Geometry(), - // shapeMode: constants.TRIANGLE_FAN, - // contourIndices: [], - // _bezierVertex: [], - // _quadraticVertex: [], - // _curveVertex: [], - // buffers: { - // fill: [ - // new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - // new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - // new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - // new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - // new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - // ], - // stroke: [ - // new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - // new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - // new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - // new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - // new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - // ], - // point: this.GL.createBuffer() - // } - // }; + this.immediateMode = { + geometry: new p5.Geometry(), + shapeMode: constants.TRIANGLE_FAN, + contourIndices: [], + _bezierVertex: [], + _quadraticVertex: [], + _curveVertex: [], + buffers: { + fill: [ + new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + stroke: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + ], + point: this.GL.createBuffer() + } + }; this.pointSize = 5.0; //default point size this.curStrokeWeight = 1; @@ -678,87 +700,6 @@ p5.RendererGL = class RendererGL extends Renderer { this._curShader = undefined; } - createCanvas(w, h, canvas){ - super.createCanvas(w, h); - - // Create new canvas - this.canvas = this.elt = canvas || document.createElement('canvas'); - - if (this._isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } - this.elt.id = 'defaultCanvas0'; - this.elt.classList.add('p5Canvas'); - - // Set canvas size - this.elt.width = w * this._pInst._pixelDensity; - this.elt.height = h * this._pInst._pixelDensity; - this.elt.style.width = `${w}px`; - this.elt.style.height = `${h}px`; - - // Attach canvas element to DOM - if (this._pInst._userNode) { - // user input node case - this._pInst._userNode.appendChild(this.elt); - } else { - //create main element - if (document.getElementsByTagName('main').length === 0) { - let m = document.createElement('main'); - document.body.appendChild(m); - } - //append canvas to main - document.getElementsByTagName('main')[0].appendChild(this.elt); - } - - // Get and store drawing context - this._initContext(); - this.GL = this.drawingContext; - this._pInst.drawingContext = this.drawingContext; - - if (this.webglVersion === constants.WEBGL2) { - this.blendExt = this.GL; - } else { - this.blendExt = this.GL.getExtension('EXT_blend_minmax'); - } - - this.immediateMode = { - geometry: new p5.Geometry(), - shapeMode: constants.TRIANGLE_FAN, - contourIndices: [], - _bezierVertex: [], - _quadraticVertex: [], - _curveVertex: [], - buffers: { - fill: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - point: this.GL.createBuffer() - } - }; - - this.wrappedElt = new p5.Element(this.canvas, this._pInst); - - - - return this.wrappedElt; - } - /** * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added * to the geometry and then returned when @@ -1536,8 +1477,11 @@ p5.RendererGL = class RendererGL extends Renderer { const dimensions = this._adjustDimensions(w, h); w = dimensions.adjustedWidth; h = dimensions.adjustedHeight; - this._pInst.width = w; - this._pInst.height = h; + + if (this._isMainCanvas) { + this._pInst.width = w; + this._pInst.height = h; + } this.canvas.width = w * this._pInst._pixelDensity; this.canvas.height = h * this._pInst._pixelDensity; From 7a91e538439af6bf3a2fcf3f931b485bb16720c8 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 22 Sep 2024 17:26:37 +0100 Subject: [PATCH 10/55] Global width/height read directly from renderer --- src/core/environment.js | 12 ++++- src/core/p5.Renderer.js | 10 +--- src/core/rendering.js | 100 ------------------------------------- src/dom/dom.js | 4 +- src/webgl/p5.RendererGL.js | 5 -- 5 files changed, 14 insertions(+), 117 deletions(-) diff --git a/src/core/environment.js b/src/core/environment.js index 7f5dcaee24..5b6f06baec 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -876,7 +876,11 @@ p5.prototype._updateWindowSize = function() { * @property {Number} width * @readOnly */ -p5.prototype.width = 0; +Object.defineProperty(p5.prototype, 'width', { + get(){ + return this._renderer.width; + } +}); /** * A `Number` variable that stores the height of the canvas in pixels. @@ -945,7 +949,11 @@ p5.prototype.width = 0; * @property {Number} height * @readOnly */ -p5.prototype.height = 0; +Object.defineProperty(p5.prototype, 'height', { + get(){ + return this._renderer.height; + } +}); /** * Toggles full-screen mode or returns the current mode. diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index cea5aed086..91dbff68cd 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -25,8 +25,6 @@ p5.Renderer = class Renderer { this.width = w; this.height = h; - this._pInst.width = this.width; - this._pInst.height = this.height; this._events = {}; @@ -106,15 +104,11 @@ p5.Renderer = class Renderer { } /** - * Resize our canvas element. - */ + * Resize our canvas element. + */ resize(w, h) { this.width = w; this.height = h; - if (this._isMainCanvas) { - this._pInst.width = this.width; - this._pInst.height = this.height; - } } get(x, y, w, h) { diff --git a/src/core/rendering.js b/src/core/rendering.js index b51582699e..291eab3db6 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -141,104 +141,6 @@ p5.prototype.createCanvas = function (w, h, renderer, ...args) { args.unshift(renderer); } - - ///////////////////////////////// - // let r; - // if (arguments[2] instanceof HTMLCanvasElement) { - // renderer = constants.P2D; - // canvas = arguments[2]; - // } else { - // r = renderer || constants.P2D; - // } - - // let c; - - // if (canvas) { - // // NOTE: this is to guard against multiple default canvas being created - // c = document.getElementById(defaultId); - // if (c) { - // c.parentNode.removeChild(c); //replace the existing defaultCanvas - // } - // c = canvas; - // this._defaultGraphicsCreated = false; - // } else { - // if (r === constants.WEBGL) { - // c = document.getElementById(defaultId); - // if (c) { - // //if defaultCanvas already exists - // c.parentNode.removeChild(c); //replace the existing defaultCanvas - // const thisRenderer = this._renderer; - // this._elements = this._elements.filter(e => e !== thisRenderer); - // } - // c = document.createElement('canvas'); - // c.id = defaultId; - // c.classList.add(defaultClass); - // } else { - // if (!this._defaultGraphicsCreated) { - // if (canvas) { - // c = canvas; - // } else { - // c = document.createElement('canvas'); - // } - // let i = 0; - // while (document.getElementById(`defaultCanvas${i}`)) { - // i++; - // } - // defaultId = `defaultCanvas${i}`; - // c.id = defaultId; - // c.classList.add(defaultClass); - // } else if ( - // this._renderer && - // Object.getPrototypeOf(this._renderer) !== renderers[r].prototype - // ) { - // // Handle createCanvas() called with 2D mode after a 3D canvas is made - // if (this.canvas.parentNode) { - // this.canvas.parentNode.removeChild(this.canvas); //replace the existing defaultCanvas - // } - // const thisRenderer = this._renderer; - // this._elements = this._elements.filter(e => e !== thisRenderer); - // c = document.createElement('canvas'); - // c.id = defaultId; - // c.classList.add(defaultClass); - // } else { - // // resize the default canvas if new one is created - // c = this.canvas; - // } - // } - - // // set to invisible if still in setup (to prevent flashing with manipulate) - // // if (!this._setupDone) { - // // c.dataset.hidden = true; // tag to show later - // // c.style.visibility = 'hidden'; - // // } - - // if (this._userNode) { - // // user input node case - // this._userNode.appendChild(c); - // } else { - // //create main element - // if (document.getElementsByTagName('main').length === 0) { - // let m = document.createElement('main'); - // document.body.appendChild(m); - // } - // //append canvas to main - // document.getElementsByTagName('main')[0].appendChild(c); - // } - // } - - // if (this._userNode) { - // // user input node case - // this._userNode.appendChild(canvas); - // } else { - // //create main element - // if (document.getElementsByTagName('main').length === 0) { - // let m = document.createElement('main'); - // document.body.appendChild(m); - // } - // //append canvas to main - // document.getElementsByTagName('main')[0].appendChild(canvas); - // } - // Init our graphics renderer if(this._renderer) this._renderer.remove(); this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args); @@ -336,8 +238,6 @@ p5.prototype.createCanvas = function (w, h, renderer, ...args) { p5.prototype.resizeCanvas = function (w, h, noRedraw) { p5._validateParameters('resizeCanvas', arguments); if (this._renderer) { - this.width = w; - this.height = h; // Make sure width and height are updated before the renderer resizes so // that framebuffers updated from the resize read the correct size this._renderer.resize(w, h); diff --git a/src/dom/dom.js b/src/dom/dom.js index 75a0726f30..a2d16f125a 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -3701,8 +3701,8 @@ p5.Element.prototype.size = function (w, h) { if (this._pInst && this._pInst._curElement) { // main canvas associated with p5 instance if (this._pInst._curElement.elt === this.elt) { - this._pInst.width = aW; - this._pInst.height = aH; + this._pInst._renderer.width = aW; + this._pInst._renderer.height = aH; } } } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index d6ec479081..019d3233c3 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1478,11 +1478,6 @@ p5.RendererGL = class RendererGL extends Renderer { w = dimensions.adjustedWidth; h = dimensions.adjustedHeight; - if (this._isMainCanvas) { - this._pInst.width = w; - this._pInst.height = h; - } - this.canvas.width = w * this._pInst._pixelDensity; this.canvas.height = h * this._pInst._pixelDensity; this.canvas.style.width = `${w}px`; From b07b43866a21b356b7fd75d004d176d90d0fcfcb Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 22 Sep 2024 19:41:38 +0100 Subject: [PATCH 11/55] Move ownership of pixel density to renderer --- src/accessibility/outputs.js | 8 ++++---- src/core/environment.js | 6 +++--- src/core/main.js | 2 -- src/core/p5.Graphics.js | 9 ++++----- src/core/p5.Renderer.js | 15 +++++++++++++++ src/core/p5.Renderer2D.js | 20 +++++++++----------- src/dom/dom.js | 6 +++--- src/image/loading_displaying.js | 2 +- src/image/p5.Image.js | 2 +- src/webgl/p5.RendererGL.js | 14 +++++++------- 10 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/accessibility/outputs.js b/src/accessibility/outputs.js index eb785b7458..8d1f399969 100644 --- a/src/accessibility/outputs.js +++ b/src/accessibility/outputs.js @@ -547,8 +547,8 @@ function outputs(p5, fn){ this.drawingContext.getTransform(); const { x: transformedX, y: transformedY } = untransformedPosition .matrixTransform(currentTransform); - const canvasWidth = this.width * this._pixelDensity; - const canvasHeight = this.height * this._pixelDensity; + const canvasWidth = this.width * this._renderer._pixelDensity; + const canvasHeight = this.height * this._renderer._pixelDensity; if (transformedX < 0.4 * canvasWidth) { if (transformedY < 0.4 * canvasHeight) { return 'top left'; @@ -653,8 +653,8 @@ function outputs(p5, fn){ // (Ax( By − Cy) + Bx(Cy − Ay) + Cx(Ay − By ))/2 } // Store the positions of the canvas corners - const canvasWidth = this.width * this._pixelDensity; - const canvasHeight = this.height * this._pixelDensity; + const canvasWidth = this.width * this._renderer._pixelDensity; + const canvasHeight = this.height * this._renderer._pixelDensity; const canvasCorners = [ new DOMPoint(0, 0), new DOMPoint(canvasWidth, 0), diff --git a/src/core/environment.js b/src/core/environment.js index 5b6f06baec..ab7f6d46d7 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -1072,13 +1072,13 @@ p5.prototype.pixelDensity = function(val) { p5._validateParameters('pixelDensity', arguments); let returnValue; if (typeof val === 'number') { - if (val !== this._pixelDensity) { - this._pixelDensity = val; + if (val !== this._renderer._pixelDensity) { + this._renderer._pixelDensity = val; } returnValue = this; this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas } else { - returnValue = this._pixelDensity; + returnValue = this._renderer._pixelDensity; } return returnValue; }; diff --git a/src/core/main.js b/src/core/main.js index b448fe371e..9c8f2875ee 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -48,8 +48,6 @@ class p5 { ////////////////////////////////////////////// this._setupDone = false; - // for handling hidpi - this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; this._userNode = node; this._curElement = null; this._elements = []; diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index aa6f04482c..8560f2c6b4 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -109,7 +109,6 @@ p5.Graphics = class Graphics { // p5.prototype._initializeInstanceVariables.apply(this); this._pInst = pInst; - this._pixelDensity = this._pInst._pixelDensity; this._renderer = new p5.renderers[r](this._pInst, w, h, false, canvas); // Attach renderer methods @@ -140,19 +139,19 @@ p5.Graphics = class Graphics { pixelDensity(val){ let returnValue; if (typeof val === 'number') { - if (val !== this._pixelDensity) { - this._pixelDensity = val; + if (val !== this._renderer._pixelDensity) { + this._renderer._pixelDensity = val; } returnValue = this; this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas } else { - returnValue = this._pixelDensity; + returnValue = this._renderer._pixelDensity; } return returnValue; } resizeCanvas(w, h){ - this._renderer.resize(w, h, this._pixelDensity); + this._renderer.resize(w, h); } /** diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 91dbff68cd..fbb081a57e 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -22,6 +22,7 @@ p5.Renderer = class Renderer { this._pInst = this._pixelsState = pInst; this._isMainCanvas = isMainCanvas; this.pixels = []; + this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; this.width = w; this.height = h; @@ -64,6 +65,20 @@ p5.Renderer = class Renderer { } + 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() { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 0c35f9c040..430393e4d6 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -43,8 +43,8 @@ class Renderer2D extends Renderer { } // Set canvas size - this.elt.width = w * this._pInst._pixelDensity; - this.elt.height = h * this._pInst._pixelDensity; + this.elt.width = w * this._pixelDensity; + this.elt.height = h * this._pixelDensity; this.elt.style.width = `${w}px`; this.elt.style.height = `${h}px`; @@ -119,7 +119,7 @@ class Renderer2D extends Renderer { this.drawingContext.font = 'normal 12px sans-serif'; } - resize(w, h, pixelDensity = this._pInst._pixelDensity) { + resize(w, h) { super.resize(w, h); // save canvas properties @@ -131,13 +131,13 @@ class Renderer2D extends Renderer { } } - this.canvas.width = w * pixelDensity; - this.canvas.height = h * pixelDensity; + this.canvas.width = w * this._pixelDensity; + this.canvas.height = h * this._pixelDensity; this.canvas.style.width = `${w}px`; this.canvas.style.height = `${h}px`; this.drawingContext.scale( - pixelDensity, - pixelDensity + this._pixelDensity, + this._pixelDensity ); // reset canvas properties @@ -148,8 +148,6 @@ class Renderer2D extends Renderer { // ignore read-only property errors } } - - console.log(this.elt.style.width); } ////////////////////////////////////////////// @@ -1326,8 +1324,8 @@ class Renderer2D extends Renderer { resetMatrix() { this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); this.drawingContext.scale( - this._pInst._pixelDensity, - this._pInst._pixelDensity + this._pixelDensity, + this._pixelDensity ); return this; } diff --git a/src/dom/dom.js b/src/dom/dom.js index a2d16f125a..80cc552b9a 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -3682,11 +3682,11 @@ p5.Element.prototype.size = function (w, h) { for (prop in k) { j[prop] = k[prop]; } - this.elt.setAttribute('width', aW * this._pInst._pixelDensity); - this.elt.setAttribute('height', aH * this._pInst._pixelDensity); + this.elt.setAttribute('width', aW * this._pInst._renderer._pixelDensity); + this.elt.setAttribute('height', aH * this._pInst._renderer._pixelDensity); this.elt.style.width = aW + 'px'; this.elt.style.height = aH + 'px'; - this._pInst.scale(this._pInst._pixelDensity, this._pInst._pixelDensity); + this._pInst.scale(this._pInst._renderer._pixelDensity, this._pInst._renderer._pixelDensity); for (prop in j) { this.elt.getContext('2d')[prop] = j[prop]; } diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 87a879d3aa..2044478a8e 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -375,7 +375,7 @@ function loadingDisplaying(p5, fn){ let frameIterator = nFramesDelay; this.frameCount = frameIterator; - const lastPixelDensity = this._pixelDensity; + const lastPixelDensity = this._renderer._pixelDensity; this.pixelDensity(1); // We first take every frame that we are going to use for the animation diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 54781e20c9..ec110e699e 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -956,7 +956,7 @@ function image(p5, fn){ let imgScaleFactor = this._pixelDensity; let maskScaleFactor = 1; if (p5Image instanceof p5.Renderer) { - maskScaleFactor = p5Image._pInst._pixelDensity; + maskScaleFactor = p5Image._pInst._renderer._pixelDensity; } const copyArgs = [ diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 019d3233c3..b8a005f3a0 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -451,8 +451,8 @@ p5.RendererGL = class RendererGL extends Renderer { this.elt.classList.add('p5Canvas'); // Set canvas size - this.elt.width = w * this._pInst._pixelDensity; - this.elt.height = h * this._pInst._pixelDensity; + this.elt.width = w * this._pixelDensity; + this.elt.height = h * this._pixelDensity; this.elt.style.width = `${w}px`; this.elt.style.height = `${h}px`; @@ -832,7 +832,7 @@ p5.RendererGL = class RendererGL extends Renderer { let maxTextureSize = this._maxTextureSize; let maxAllowedPixelDimensions = Math.floor( - maxTextureSize / this.pixelDensity() + maxTextureSize / this._pixelDensity ); let adjustedWidth = Math.min( width, maxAllowedPixelDimensions @@ -1391,7 +1391,7 @@ p5.RendererGL = class RendererGL extends Renderer { return; } - const pd = this._pInst._pixelDensity; + const pd = this._pixelDensity; const gl = this.GL; pixelsState.pixels = @@ -1478,8 +1478,8 @@ p5.RendererGL = class RendererGL extends Renderer { w = dimensions.adjustedWidth; h = dimensions.adjustedHeight; - this.canvas.width = w * this._pInst._pixelDensity; - this.canvas.height = h * this._pInst._pixelDensity; + this.canvas.width = w * this._pixelDensity; + this.canvas.height = h * this._pixelDensity; this.canvas.style.width = `${w}px`; this.canvas.style.height = `${h}px`; this._origViewport = { @@ -2293,7 +2293,7 @@ p5.RendererGL = class RendererGL extends Renderer { // should be they be same var? pointShader.setUniform( 'uPointSize', - this.pointSize * this._pInst._pixelDensity + this.pointSize * this._pixelDensity ); } From 55c45eda97da27fe15fbd66376508dcd40982263 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 22 Sep 2024 19:49:59 +0100 Subject: [PATCH 12/55] Fix a few tests --- src/core/p5.Graphics.js | 2 +- src/core/p5.Renderer.js | 2 +- src/core/p5.Renderer2D.js | 24 ++++++++++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 8560f2c6b4..28e0afdc79 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -382,7 +382,7 @@ p5.Graphics = class Graphics { */ remove() { this._renderer.remove(); - this._renderer = null; + this._renderer = undefined; } diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index fbb081a57e..29abe11a8f 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -128,7 +128,7 @@ p5.Renderer = class Renderer { get(x, y, w, h) { const pixelsState = this._pixelsState; - const pd = pixelsState._pixelDensity; + const pd = this._pixelDensity; const canvas = this.canvas; if (typeof x === 'undefined' && typeof y === 'undefined') { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 430393e4d6..224e4e914d 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -506,7 +506,7 @@ class Renderer2D extends Renderer { loadPixels() { const pixelsState = this._pixelsState; // if called by p5.Image - const pd = pixelsState._pixelDensity; + const pd = this._pixelDensity; const w = this.width * pd; const h = this.height * pd; const imageData = this.drawingContext.getImageData(0, 0, w, h); @@ -525,8 +525,8 @@ class Renderer2D extends Renderer { this.drawingContext.save(); this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); this.drawingContext.scale( - pixelsState._pixelDensity, - pixelsState._pixelDensity + this._pixelDensity, + this._pixelDensity ); this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); this.drawingContext.drawImage(imgOrCol.canvas, x, y); @@ -539,9 +539,9 @@ class Renderer2D extends Renderer { let idx = 4 * (y * - pixelsState._pixelDensity * - (this.width * pixelsState._pixelDensity) + - x * pixelsState._pixelDensity); + this._pixelDensity * + (this.width * this._pixelDensity) + + x * this._pixelDensity); if (!pixelsState.imageData) { pixelsState.loadPixels(); } @@ -574,15 +574,15 @@ class Renderer2D extends Renderer { } } // loop over pixelDensity * pixelDensity - for (let i = 0; i < pixelsState._pixelDensity; i++) { - for (let j = 0; j < pixelsState._pixelDensity; j++) { + for (let i = 0; i < this._pixelDensity; i++) { + for (let j = 0; j < this._pixelDensity; j++) { // loop over idx = 4 * - ((y * pixelsState._pixelDensity + j) * + ((y * this._pixelDensity + j) * this.width * - pixelsState._pixelDensity + - (x * pixelsState._pixelDensity + i)); + this._pixelDensity + + (x * this._pixelDensity + i)); pixelsState.pixels[idx] = r; pixelsState.pixels[idx + 1] = g; pixelsState.pixels[idx + 2] = b; @@ -594,7 +594,7 @@ class Renderer2D extends Renderer { updatePixels(x, y, w, h) { const pixelsState = this._pixelsState; - const pd = pixelsState._pixelDensity; + const pd = this._pixelDensity; if ( x === undefined && y === undefined && From beb432fe92017512450db6725d3c01ae54a7c8fc Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 22 Sep 2024 20:42:30 +0100 Subject: [PATCH 13/55] Fix a few more tests --- src/core/p5.Graphics.js | 11 ++++++++++- src/webgl/p5.RendererGL.js | 9 ++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 28e0afdc79..9b7f81293e 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -113,7 +113,12 @@ p5.Graphics = class Graphics { // Attach renderer methods for(const p of Object.getOwnPropertyNames(p5.renderers[r].prototype)) { - if(p !== 'constructor' && p[0] !== '_' && !(p in this)){ + if( + p !== 'constructor' && + p[0] !== '_' && + !(p in this) && + typeof this._renderer[p] === 'function' + ){ this[p] = this._renderer[p].bind(this._renderer); } } @@ -154,6 +159,10 @@ p5.Graphics = class Graphics { this._renderer.resize(w, h); } + get(...args){ + return this._pInst.get.apply(this, args); + } + /** * Resets the graphics buffer's transformations and lighting. * diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b8a005f3a0..acf1b1413c 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -894,9 +894,9 @@ p5.RendererGL = class RendererGL extends Renderer { renderer.resize(w, h); renderer._applyDefaults(); - if (!isPGraphics) { - this._pInst._elements.push(renderer); - } + // if (!isPGraphics) { + // this._pInst._elements.push(renderer); + // } if (typeof callback === 'function') { //setTimeout with 0 forces the task to the back of the queue, this ensures that @@ -1478,6 +1478,9 @@ p5.RendererGL = class RendererGL extends Renderer { w = dimensions.adjustedWidth; h = dimensions.adjustedHeight; + this.width = w; + this.height = h; + this.canvas.width = w * this._pixelDensity; this.canvas.height = h * this._pixelDensity; this.canvas.style.width = `${w}px`; From 8a60cf355cdfd418536750af8458d0b471e0394d Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Mon, 23 Sep 2024 14:47:30 +0100 Subject: [PATCH 14/55] Include p5 instance methods on p5.Graphics This need to be refactored out eventually as many don't make sense being on p5.Graphics --- src/app.js | 2 +- src/core/p5.Graphics.js | 29 +++++++++++++---------------- src/webgl/p5.RendererGL.js | 12 +++++------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/app.js b/src/app.js index ca17271be7..7451402b6a 100644 --- a/src/app.js +++ b/src/app.js @@ -8,7 +8,7 @@ import './core/friendly_errors/file_errors'; import './core/friendly_errors/fes_core'; import './core/friendly_errors/sketch_reader'; import './core/helpers'; -import './core/legacy'; +// import './core/legacy'; // import './core/preload'; import './core/p5.Element'; import './core/p5.Graphics'; diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 9b7f81293e..fa22c8e2d9 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -96,18 +96,6 @@ p5.Graphics = class Graphics { constructor(w, h, renderer, pInst, canvas) { const r = renderer || constants.P2D; - // bind methods and props of p5 to the new object - // for (const p in p5.prototype) { - // if (!this[p]) { - // if (typeof p5.prototype[p] === 'function') { - // this[p] = p5.prototype[p].bind(this); - // } else if(p !== 'deltaTime') { - // this[p] = p5.prototype[p]; - // } - // } - // } - // p5.prototype._initializeInstanceVariables.apply(this); - this._pInst = pInst; this._renderer = new p5.renderers[r](this._pInst, w, h, false, canvas); @@ -133,6 +121,19 @@ p5.Graphics = class Graphics { }) } + // bind methods and props of p5 to the new object + for (const p in p5.prototype) { + if (!this[p]) { + // console.log(p); + if (typeof p5.prototype[p] === 'function') { + this[p] = p5.prototype[p].bind(this); + } else if(p !== 'deltaTime') { + this[p] = p5.prototype[p]; + } + } + } + p5.prototype._initializeInstanceVariables.apply(this); + this._renderer._applyDefaults(); return this; } @@ -159,10 +160,6 @@ p5.Graphics = class Graphics { this._renderer.resize(w, h); } - get(...args){ - return this._pInst.get.apply(this, args); - } - /** * Resets the graphics buffer's transformations and lighting. * diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index acf1b1413c..fcc653a3bb 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -886,17 +886,15 @@ p5.RendererGL = class RendererGL extends Renderer { } const renderer = new p5.RendererGL( - this._pInst.canvas, this._pInst, - !isPGraphics + w, + h, + !isPGraphics, + this._pInst.canvas, ); this._pInst._renderer = renderer; - renderer.resize(w, h); - renderer._applyDefaults(); - // if (!isPGraphics) { - // this._pInst._elements.push(renderer); - // } + renderer._applyDefaults(); if (typeof callback === 'function') { //setTimeout with 0 forces the task to the back of the queue, this ensures that From 40b621d6e670e1267415091874caa0309452b4c2 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Mon, 23 Sep 2024 16:38:43 +0100 Subject: [PATCH 15/55] Refactor some WebGL sections to try to fix tests --- src/core/p5.Graphics.js | 2 +- src/core/p5.Renderer2D.js | 6 +--- src/webgl/3d_primitives.js | 4 +-- src/webgl/light.js | 56 ++++++++++++++++++++------------------ src/webgl/material.js | 12 +++++--- src/webgl/p5.RendererGL.js | 28 ++++++++++++++----- 6 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index fa22c8e2d9..a59ab7ff7d 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -634,7 +634,7 @@ p5.Graphics = class Graphics { * */ createFramebuffer(options) { - return new p5.Framebuffer(this, options); + return new p5.Framebuffer(this._pInst, options); } }; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 224e4e914d..846f439ce8 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -80,11 +80,7 @@ class Renderer2D extends Renderer { getFilterGraphicsLayer() { // create hidden webgl renderer if it doesn't exist if (!this.filterGraphicsLayer) { - // the real _pInst is buried when this is a secondary p5.Graphics - const pInst = - this._pInst instanceof p5.Graphics ? - this._pInst._pInst : - this._pInst; + const pInst = this._pInst; // create secondary layer this.filterGraphicsLayer = diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 9524b6b3aa..856cf2b826 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3408,10 +3408,10 @@ p5.RendererGL.prototype.image = function( this._pInst.push(); - this._pInst.noLights(); + this.noLights(); this._pInst.noStroke(); - this._pInst.texture(img); + this.texture(img); this._pInst.textureMode(constants.NORMAL); let u0 = 0; diff --git a/src/webgl/light.js b/src/webgl/light.js index 1d17ebdc0c..06c768eada 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -1744,34 +1744,38 @@ p5.prototype.noLights = function (...args) { this._assert3d('noLights'); p5._validateParameters('noLights', args); - this._renderer.states.activeImageLight = null; - this._renderer.states._enableLighting = false; - - this._renderer.states.ambientLightColors.length = 0; - this._renderer.states.specularColors = [1, 1, 1]; - - this._renderer.states.directionalLightDirections.length = 0; - this._renderer.states.directionalLightDiffuseColors.length = 0; - this._renderer.states.directionalLightSpecularColors.length = 0; - - this._renderer.states.pointLightPositions.length = 0; - this._renderer.states.pointLightDiffuseColors.length = 0; - this._renderer.states.pointLightSpecularColors.length = 0; - - this._renderer.states.spotLightPositions.length = 0; - this._renderer.states.spotLightDirections.length = 0; - this._renderer.states.spotLightDiffuseColors.length = 0; - this._renderer.states.spotLightSpecularColors.length = 0; - this._renderer.states.spotLightAngle.length = 0; - this._renderer.states.spotLightConc.length = 0; - - this._renderer.states.constantAttenuation = 1; - this._renderer.states.linearAttenuation = 0; - this._renderer.states.quadraticAttenuation = 0; - this._renderer.states._useShininess = 1; - this._renderer.states._useMetalness = 0; + this._renderer.noLights(); return this; }; +p5.RendererGL.prototype.noLights = function() { + this.states.activeImageLight = null; + this.states._enableLighting = false; + + this.states.ambientLightColors.length = 0; + this.states.specularColors = [1, 1, 1]; + + this.states.directionalLightDirections.length = 0; + this.states.directionalLightDiffuseColors.length = 0; + this.states.directionalLightSpecularColors.length = 0; + + this.states.pointLightPositions.length = 0; + this.states.pointLightDiffuseColors.length = 0; + this.states.pointLightSpecularColors.length = 0; + + this.states.spotLightPositions.length = 0; + this.states.spotLightDirections.length = 0; + this.states.spotLightDiffuseColors.length = 0; + this.states.spotLightSpecularColors.length = 0; + this.states.spotLightAngle.length = 0; + this.states.spotLightConc.length = 0; + + this.states.constantAttenuation = 1; + this.states.linearAttenuation = 0; + this.states.quadraticAttenuation = 0; + this.states._useShininess = 1; + this.states._useMetalness = 0; +} + export default p5; diff --git a/src/webgl/material.js b/src/webgl/material.js index 3ec7460a03..b34eba9dd5 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1892,14 +1892,18 @@ p5.prototype.texture = function (tex) { tex._animateGif(this); } - this._renderer.states.drawMode = constants.TEXTURE; - this._renderer.states._useNormalMaterial = false; - this._renderer.states._tex = tex; - this._renderer.states.doFill = true; + this._renderer.texture(tex); return this; }; +p5.RendererGL.prototype.texture = function(tex) { + this.states.drawMode = constants.TEXTURE; + this.states._useNormalMaterial = false; + this.states._tex = tex; + this.states.doFill = true; +} + /** * Changes the coordinate system used for textures when they’re applied to * custom shapes. diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index fcc653a3bb..ab958f0a46 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -438,6 +438,11 @@ p5.RendererGL = class RendererGL extends Renderer { // Create new canvas this.canvas = this.elt = elt || document.createElement('canvas'); + this._initContext(); + // This redundant property is useful in reminding you that you are + // interacting with WebGLRenderingContext, still worth considering future removal + this.GL = this.drawingContext; + this._pInst.drawingContext = this.drawingContext; if (this._isMainCanvas) { // for pixel method sharing with pimage @@ -450,11 +455,26 @@ p5.RendererGL = class RendererGL extends Renderer { this.elt.id = 'defaultCanvas0'; this.elt.classList.add('p5Canvas'); + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; + + this.width = w; + this.height = h; + // Set canvas size this.elt.width = w * this._pixelDensity; this.elt.height = h * this._pixelDensity; this.elt.style.width = `${w}px`; this.elt.style.height = `${h}px`; + this._origViewport = { + width: this.GL.drawingBufferWidth, + height: this.GL.drawingBufferHeight + }; + this.viewport( + this._origViewport.width, + this._origViewport.height + ); // Attach canvas element to DOM if (this._pInst._userNode) { @@ -471,17 +491,11 @@ p5.RendererGL = class RendererGL extends Renderer { } this._setAttributeDefaults(pInst); - this._initContext(); this.isP3D = true; //lets us know we're in 3d mode // When constructing a new p5.Geometry, this will represent the builder this.geometryBuilder = undefined; - // This redundant property is useful in reminding you that you are - // interacting with WebGLRenderingContext, still worth considering future removal - this.GL = this.drawingContext; - this._pInst.drawingContext = this.drawingContext; - // Push/pop state this.states.uModelMatrix = new p5.Matrix(); this.states.uViewMatrix = new p5.Matrix(); @@ -786,7 +800,7 @@ p5.RendererGL = class RendererGL extends Renderer { } _initContext() { - if (this._pInst._glAttributes.version !== 1) { + if (this._pInst._glAttributes?.version !== 1) { // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context this.drawingContext = this.canvas.getContext('webgl2', this._pInst._glAttributes); From 418251bc5e2b4fd870f89b0c5f5f2875dc5bbc4d Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Mon, 23 Sep 2024 16:40:33 +0100 Subject: [PATCH 16/55] Visual test use constants from direct import --- test/unit/visual/visualTest.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js index b7a831e81b..425a955f11 100644 --- a/test/unit/visual/visualTest.js +++ b/test/unit/visual/visualTest.js @@ -1,6 +1,6 @@ import p5 from '../../../src/app.js'; import { server } from '@vitest/browser/context' -import { THRESHOLD } from '../../../src/core/constants.js'; +import { THRESHOLD, DIFFERENCE, ERODE } from '../../../src/core/constants.js'; const { readFile, writeFile } = server.commands // By how much can each color channel value (0-255) differ before @@ -94,11 +94,11 @@ export async function checkMatch(actual, expected, p5) { cnv.pixelDensity(1); cnv.background(BG); cnv.image(actual, 0, 0); - cnv.blendMode(cnv.DIFFERENCE); + cnv.blendMode(DIFFERENCE); cnv.image(expectedWithBg, 0, 0); for (let i = 0; i < SHIFT_THRESHOLD; i++) { - cnv.filter(cnv.ERODE, false); - cnv.filter(cnv.ERODE, false); + cnv.filter(ERODE, false); + cnv.filter(ERODE, false); } const diff = cnv.get(); cnv.remove(); From eff9ac477dd556dfbb9c04bd0a2c1897d358476e Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 25 Sep 2024 21:45:52 +0100 Subject: [PATCH 17/55] Refactor shape out into its own module --- src/app.js | 6 +- src/core/shape/2d_primitives.js | 1451 -------------------- src/core/shape/attributes.js | 601 -------- src/core/shape/curves.js | 1171 ---------------- src/core/shape/vertex.js | 2256 ------------------------------ src/shape/2d_primitives.js | 1456 ++++++++++++++++++++ src/shape/attributes.js | 606 +++++++++ src/shape/curves.js | 1176 ++++++++++++++++ src/shape/index.js | 11 + src/shape/vertex.js | 2262 +++++++++++++++++++++++++++++++ 10 files changed, 5513 insertions(+), 5483 deletions(-) delete mode 100644 src/core/shape/2d_primitives.js delete mode 100644 src/core/shape/attributes.js delete mode 100644 src/core/shape/curves.js delete mode 100644 src/core/shape/vertex.js create mode 100644 src/shape/2d_primitives.js create mode 100644 src/shape/attributes.js create mode 100644 src/shape/curves.js create mode 100644 src/shape/index.js create mode 100644 src/shape/vertex.js diff --git a/src/app.js b/src/app.js index 7451402b6a..8ee08375b5 100644 --- a/src/app.js +++ b/src/app.js @@ -17,10 +17,8 @@ import './core/p5.Renderer2D'; import './core/rendering'; import './core/structure'; import './core/transform'; -import './core/shape/2d_primitives'; -import './core/shape/attributes'; -import './core/shape/curves'; -import './core/shape/vertex'; +import shape from './shape'; +shape(p5); //accessibility import accessibility from './accessibility'; diff --git a/src/core/shape/2d_primitives.js b/src/core/shape/2d_primitives.js deleted file mode 100644 index bc18e7647e..0000000000 --- a/src/core/shape/2d_primitives.js +++ /dev/null @@ -1,1451 +0,0 @@ -/** - * @module Shape - * @submodule 2D Primitives - * @for p5 - * @requires core - * @requires constants - */ - -import p5 from '../main'; -import * as constants from '../constants'; -import canvas from '../helpers'; -import '../friendly_errors/fes_core'; -import '../friendly_errors/file_errors'; -import '../friendly_errors/validate_params'; - -/** - * 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). - */ -p5.prototype._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 - constants.TWO_PI * Math.floor(start / constants.TWO_PI); - stop = stop - constants.TWO_PI * Math.floor(stop / constants.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), - constants.TWO_PI - Math.abs(start - stop) - ); - - // Optionally adjust the angles to counter linear scaling. - if (correctForScaling) { - if (start <= constants.HALF_PI) { - start = Math.atan(width / height * Math.tan(start)); - } else if (start > constants.HALF_PI && start <= 3 * constants.HALF_PI) { - start = Math.atan(width / height * Math.tan(start)) + constants.PI; - } else { - start = Math.atan(width / height * Math.tan(start)) + constants.TWO_PI; - } - if (stop <= constants.HALF_PI) { - stop = Math.atan(width / height * Math.tan(stop)); - } else if (stop > constants.HALF_PI && stop <= 3 * constants.HALF_PI) { - stop = Math.atan(width / height * Math.tan(stop)) + constants.PI; - } else { - stop = Math.atan(width / height * Math.tan(stop)) + constants.TWO_PI; - } - } - - // Ensure that start <= stop < start + TWO_PI. - if (start > stop) { - stop += constants.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.' - * ); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.' - * ); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- */ -p5.prototype.arc = function(x, y, w, h, start, stop, mode, detail) { - p5._validateParameters('arc', arguments); - - // if the current stroke and fill settings wouldn't result in something - // visible, exit immediately - if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { - return this; - } - - if (start === stop) { - return this; - } - - start = this._toRadians(start); - stop = this._toRadians(stop); - - // p5 supports negative width and heights for ellipses - w = Math.abs(w); - h = Math.abs(h); - - 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. - */ -p5.prototype.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.'); - * } - * - *
- */ -p5.prototype.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) -p5.prototype._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.doStroke && !this._renderer.states.doFill) { - return this; - } - - // p5 supports negative width and heights for rects - if (w < 0) { - w = Math.abs(w); - } - - if (typeof h === 'undefined') { - // Duplicate 3rd argument if only 3 given. - h = w; - } else if (h < 0) { - h = Math.abs(h); - } - - 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.' - * ); - * } - * - *
- * - *
- * - * 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.' - * ); - * } - * - *
- * - *
- * - * 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 - */ -p5.prototype.line = function(...args) { - p5._validateParameters('line', args); - - if (this._renderer.states.doStroke) { - 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); - * - * // 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.' - * ); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // 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.' - * ); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // 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.' - * ); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- * - *
- * - * 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 - */ -p5.prototype.point = function(...args) { - p5._validateParameters('point', args); - - if (this._renderer.states.doStroke) { - 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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 - */ -p5.prototype.quad = function(...args) { - p5._validateParameters('quad', args); - - if (this._renderer.states.doStroke || this._renderer.states.doFill) { - 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.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * rect(30, 20, 55, 40); - * - * describe('A white rectangle with a black outline on a gray canvas.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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 - */ -p5.prototype.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.'); - * } - * - *
- * - *
- * - * 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.' - * ); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- */ -p5.prototype.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 -p5.prototype._renderRect = function() { - if (this._renderer.states.doStroke || this._renderer.states.doFill) { - // 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 - ); - - 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- */ -p5.prototype.triangle = function(...args) { - p5._validateParameters('triangle', args); - - if (this._renderer.states.doStroke || this._renderer.states.doFill) { - this._renderer.triangle(args); - } - - //accessible outputs - if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { - this._accsOutput('triangle', args); - } - - return this; -}; - -export default p5; diff --git a/src/core/shape/attributes.js b/src/core/shape/attributes.js deleted file mode 100644 index 08f942bd0e..0000000000 --- a/src/core/shape/attributes.js +++ /dev/null @@ -1,601 +0,0 @@ -/** - * @module Shape - * @submodule Attributes - * @for p5 - * @requires core - * @requires constants - */ - -import p5 from '../main'; -import * as constants from '../constants'; - -/** - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.ellipseMode = function(m) { - p5._validateParameters('ellipseMode', arguments); - if ( - m === constants.CORNER || - m === constants.CORNERS || - m === constants.RADIUS || - m === constants.CENTER - ) { - this._renderer.states.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; - * - * // Load a pixelated heart image from an image data string. - * function preload() { - * heart = loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII='); - * } - * - * function setup() { - * 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); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * circle(0, 0, 80); - * - * describe('A white circle on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Disable smoothing. - * noSmooth(); - * - * background(200); - * - * circle(0, 0, 80); - * - * describe('A pixelated white circle on a gray background.'); - * } - * - *
- */ -p5.prototype.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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * rectMode(CORNER); - * fill(255); - * square(25, 25, 50); - * - * describe('A white square.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.rectMode = function(m) { - p5._validateParameters('rectMode', arguments); - if ( - m === constants.CORNER || - m === constants.CORNERS || - m === constants.RADIUS || - m === constants.CENTER - ) { - this._renderer.states.rectMode = m; - } - return this; -}; - -/** - * 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; - * - * // Load a pixelated heart image from an image data string. - * function preload() { - * heart = loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII='); - * } - * - * function setup() { - * 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); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * circle(0, 0, 80); - * - * describe('A white circle on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Disable smoothing. - * noSmooth(); - * - * background(200); - * - * circle(0, 0, 80); - * - * describe('A pixelated white circle on a gray background.'); - * } - * - *
- */ -p5.prototype.smooth = function() { - this.setAttributes('antialias', true); - if (!this._renderer.isP3D) { - if ('imageSmoothingEnabled' in this.drawingContext) { - this.drawingContext.imageSmoothingEnabled = 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.' - * ); - * } - * - *
- */ -p5.prototype.strokeCap = function(cap) { - p5._validateParameters('strokeCap', arguments); - if ( - cap === constants.ROUND || - cap === constants.SQUARE || - cap === constants.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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.strokeJoin = function(join) { - p5._validateParameters('strokeJoin', arguments); - if ( - join === constants.ROUND || - join === constants.BEVEL || - join === constants.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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.strokeWeight = function(w) { - p5._validateParameters('strokeWeight', arguments); - this._renderer.strokeWeight(w); - return this; -}; - -export default p5; diff --git a/src/core/shape/curves.js b/src/core/shape/curves.js deleted file mode 100644 index 47077b86a5..0000000000 --- a/src/core/shape/curves.js +++ /dev/null @@ -1,1171 +0,0 @@ -/** - * @module Shape - * @submodule Curves - * @for p5 - * @requires core - */ - -import p5 from '../main'; -import '../friendly_errors/fes_core'; -import '../friendly_errors/file_errors'; -import '../friendly_errors/validate_params'; - -/** - * 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 - * curve() 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.' - * ); - * } - * - *
- * - *
- * - * // 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; - * } - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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 - */ -p5.prototype.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.doStroke && !this._renderer.states.doFill) { - return this; - } - - this._renderer.bezier(...args); - - return this; -}; - -/** - * Sets the number of segments used to draw Bézier curves in WebGL mode. - * - * In WebGL mode, smooth shapes are drawn using many flat segments. Adding - * more flat segments makes shapes appear smoother. - * - * The parameter, `detail`, is the number of segments to use while drawing a - * Bézier curve. For example, calling `bezierDetail(5)` will use 5 segments to - * draw curves with the bezier() function. By - * default,`detail` is 20. - * - * Note: `bezierDetail()` has no effect in 2D mode. - * - * @method bezierDetail - * @param {Number} detail number of segments to use. Defaults to 20. - * @chainable - * - * @example - *
- * - * // Draw the original curve. - * - * 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.' - * ); - * } - * - *
- * - *
- * - * // Draw the curve with less detail. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Set the curveDetail() to 5. - * bezierDetail(5); - * - * // Draw the anchor points in black. - * stroke(0); - * strokeWeight(5); - * point(35, -30, 0); - * point(-35, 30, 0); - * - * // Draw the control points in red. - * stroke(255, 0, 0); - * point(-40, -40, 0); - * point(40, 40, 0); - * - * // Draw a black bezier curve. - * noFill(); - * stroke(0); - * strokeWeight(1); - * bezier(35, -30, 0, -40, -40, 0, 40, 40, 0, -35, 30, 0); - * - * // Draw red lines from the anchor points to the control points. - * stroke(255, 0, 0); - * line(35, -30, -40, -40); - * line(-35, 30, 40, 40); - * - * describe( - * 'A gray square with three curves. A black s-curve is drawn with jagged segments. Two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.' - * ); - * } - * - *
- */ -p5.prototype.bezierDetail = function(d) { - p5._validateParameters('bezierDetail', arguments); - this._bezierDetail = d; - 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 control point. - * @param {Number} b coordinate of first anchor point. - * @param {Number} c coordinate of second anchor point. - * @param {Number} d coordinate of second control 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.'); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- */ -p5.prototype.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.' - * ); - * } - * - *
- */ -p5.prototype.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. Splines are defined by two - * anchor points and two control points. - * - * The first two parameters, `x1` and `y1`, set the first control point. This - * point isn’t drawn and can be thought of as the curve’s starting point. - * - * The next four parameters, `x2`, `y2`, `x3`, and `y3`, set the two anchor - * points. The anchor points are the start and end points of the curve’s - * visible segment. - * - * The seventh and eighth parameters, `x4` and `y4`, set the last control - * point. This point isn’t drawn and can be thought of as the curve’s ending - * point. - * - * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of - * `curve()` has twelve arguments because each point has x-, y-, and - * z-coordinates. - * - * @method curve - * @param {Number} x1 x-coordinate of the first control point. - * @param {Number} y1 y-coordinate of the first control point. - * @param {Number} x2 x-coordinate of the first anchor point. - * @param {Number} y2 y-coordinate of the first anchor point. - * @param {Number} x3 x-coordinate of the second anchor point. - * @param {Number} y3 y-coordinate of the second anchor point. - * @param {Number} x4 x-coordinate of the second control point. - * @param {Number} y4 y-coordinate of the second control point. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Draw a black spline curve. - * noFill(); - * strokeWeight(1); - * stroke(0); - * curve(5, 26, 73, 24, 73, 61, 15, 65); - * - * // Draw red spline curves from the anchor points to the control points. - * stroke(255, 0, 0); - * curve(5, 26, 5, 26, 73, 24, 73, 61); - * curve(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 control 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.' - * ); - * } - * - *
- * - *
- * - * 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); - * - * // Draw a black spline curve. - * noFill(); - * strokeWeight(1); - * stroke(0); - * curve(x1, y1, 73, 24, 73, 61, 15, 65); - * - * // Draw red spline curves from the anchor points to the control points. - * stroke(255, 0, 0); - * curve(x1, y1, x1, y1, 73, 24, 73, 61); - * curve(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 control points in red. - * stroke(255, 0, 0); - * point(x1, y1); - * point(15, 65); - * } - * - * // Start changing the first control point if the user clicks near it. - * function mousePressed() { - * if (dist(mouseX, mouseY, x1, y1) < 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) { - * x1 = mouseX; - * y1 = mouseY; - * } - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background('skyblue'); - * - * // Draw the red balloon. - * fill('red'); - * curve(-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.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A red balloon in a blue sky.'); - * } - * - * function draw() { - * background('skyblue'); - * - * // Rotate around the y-axis. - * rotateY(frameCount * 0.01); - * - * // Draw the red balloon. - * fill('red'); - * curve(-200, 225, 0, 0, 10, 0, 0, 10, 0, 200, 225, 0); - * - * // Draw the balloon string. - * line(0, 10, 0, 0, 30, 0); - * } - * - *
- */ - -/** - * @method curve - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} z1 z-coordinate of the first control point. - * @param {Number} x2 - * @param {Number} y2 - * @param {Number} z2 z-coordinate of the first anchor point. - * @param {Number} x3 - * @param {Number} y3 - * @param {Number} z3 z-coordinate of the second anchor point. - * @param {Number} x4 - * @param {Number} y4 - * @param {Number} z4 z-coordinate of the second control point. - * @chainable - */ -p5.prototype.curve = function(...args) { - p5._validateParameters('curve', args); - - if (this._renderer.states.doStroke) { - this._renderer.curve(...args); - } - - return this; -}; - -/** - * Sets the number of segments used to draw spline curves in WebGL mode. - * - * In WebGL mode, smooth shapes are drawn using many flat segments. Adding - * more flat segments makes shapes appear smoother. - * - * The parameter, `detail`, is the number of segments to use while drawing a - * spline curve. For example, calling `curveDetail(5)` will use 5 segments to - * draw curves with the curve() function. By - * default,`detail` is 20. - * - * Note: `curveDetail()` has no effect in 2D mode. - * - * @method curveDetail - * @param {Number} resolution number of segments to use. Defaults to 20. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Draw a black spline curve. - * noFill(); - * strokeWeight(1); - * stroke(0); - * curve(5, 26, 73, 24, 73, 61, 15, 65); - * - * // Draw red spline curves from the anchor points to the control points. - * stroke(255, 0, 0); - * curve(5, 26, 5, 26, 73, 24, 73, 61); - * curve(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 control 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.' - * ); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Set the curveDetail() to 3. - * curveDetail(3); - * - * // Draw a black spline curve. - * noFill(); - * strokeWeight(1); - * stroke(0); - * curve(-45, -24, 0, 23, -26, 0, 23, 11, 0, -35, 15, 0); - * - * // Draw red spline curves from the anchor points to the control points. - * stroke(255, 0, 0); - * curve(-45, -24, 0, -45, -24, 0, 23, -26, 0, 23, 11, 0); - * curve(23, -26, 0, 23, 11, 0, -35, 15, 0, -35, 15, 0); - * - * // Draw the anchor points in black. - * strokeWeight(5); - * stroke(0); - * point(23, -26); - * point(23, 11); - * - * // Draw the control points in red. - * stroke(255, 0, 0); - * point(-45, -24); - * point(-35, 15); - * - * describe( - * 'A gray square with a jagged 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.' - * ); - * } - * - *
- */ -p5.prototype.curveDetail = function(d) { - p5._validateParameters('curveDetail', arguments); - if (d < 3) { - this._curveDetail = 3; - } else { - this._curveDetail = d; - } - return this; -}; - -/** - * Adjusts the way curve() and - * curveVertex() draw. - * - * Spline curves are like cables that are attached to a set of points. - * `curveTightness()` adjusts how tightly the cable is attached to the points. - * - * The parameter, `tightness`, determines how the curve fits to the vertex - * points. By default, `tightness` is set to 0. Setting tightness to 1, - * as in `curveTightness(1)`, connects the curve's points using straight - * lines. Values in the range from –5 to 5 deform curves while leaving them - * recognizable. - * - * @method curveTightness - * @param {Number} amount amount of tightness. - * @chainable - * - * @example - *
- * - * // Move the mouse left and right to see the curve change. - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A black curve forms a sideways U shape. The curve deforms as the user moves the mouse from left to right'); - * } - * - * function draw() { - * background(200); - * - * // Set the curve's tightness using the mouse. - * let t = map(mouseX, 0, 100, -5, 5, true); - * curveTightness(t); - * - * // Draw the curve. - * noFill(); - * beginShape(); - * curveVertex(10, 26); - * curveVertex(10, 26); - * curveVertex(83, 24); - * curveVertex(83, 61); - * curveVertex(25, 65); - * curveVertex(25, 65); - * endShape(); - * } - * - *
- */ -p5.prototype.curveTightness = function(t) { - p5._validateParameters('curveTightness', arguments); - this._renderer._curveTightness = t; - return this; -}; - -/** - * Calculates coordinates along a spline curve using interpolation. - * - * `curvePoint()` calculates coordinates along a spline curve using the - * anchor and control points. It expects points in the same order as the - * curve() function. `curvePoint()` 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 control point. - * - * The second and third parameters, `b` and `c`, are the coordinates of the - * anchor points. - * - * The fourth parameter, `d`, is the coordinate of the last control 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 curvePoint - * @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 curve. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Set the coordinates for the curve's anchor and control points. - * 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(); - * curve(x1, y1, x2, y2, x3, y3, x4, y4); - * - * // Draw circles along the curve's path. - * fill(255); - * - * // Top. - * let x = curvePoint(x1, x2, x3, x4, 0); - * let y = curvePoint(y1, y2, y3, y4, 0); - * circle(x, y, 5); - * - * // Center. - * x = curvePoint(x1, x2, x3, x4, 0.5); - * y = curvePoint(y1, y2, y3, y4, 0.5); - * circle(x, y, 5); - * - * // Bottom. - * x = curvePoint(x1, x2, x3, x4, 1); - * y = curvePoint(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.'); - * } - * - *
- * - *
- * - * 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 anchor and control points. - * 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(); - * curve(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 = curvePoint(x1, x2, x3, x4, t); - * let y = curvePoint(y1, y2, y3, y4, t); - * - * // Draw the circle. - * fill(255); - * circle(x, y, 5); - * } - * - *
- */ -p5.prototype.curvePoint = function(a, b, c, d, t) { - p5._validateParameters('curvePoint', arguments); - const s = this._renderer._curveTightness, - 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. - * - * `curveTangent()` calculates coordinates along a tangent line using the - * spline curve's anchor and control points. It expects points in the same - * order as the curve() function. `curveTangent()` - * 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 control point. - * - * The second and third parameters, `b` and `c`, are the coordinates of the - * anchor points. - * - * The fourth parameter, `d`, is the coordinate of the last control 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 curveTangent - * @param {Number} a coordinate of first control point. - * @param {Number} b coordinate of first anchor point. - * @param {Number} c coordinate of second anchor point. - * @param {Number} d coordinate of second control 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 = 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(); - * curve(x1, y1, x2, y2, x3, y3, x4, y4); - * - * // Draw tangents along the curve's path. - * fill(255); - * - * // Top circle. - * stroke(0); - * let x = curvePoint(x1, x2, x3, x4, 0); - * let y = curvePoint(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 * curveTangent(x1, x2, x3, x4, 0); - * let ty = 0.2 * curveTangent(y1, y2, y3, y4, 0); - * line(x + tx, y + ty, x - tx, y - ty); - * - * // Center circle. - * stroke(0); - * x = curvePoint(x1, x2, x3, x4, 0.5); - * y = curvePoint(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 * curveTangent(x1, x2, x3, x4, 0.5); - * ty = 0.2 * curveTangent(y1, y2, y3, y4, 0.5); - * line(x + tx, y + ty, x - tx, y - ty); - * - * // Bottom circle. - * stroke(0); - * x = curvePoint(x1, x2, x3, x4, 1); - * y = curvePoint(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 * curveTangent(x1, x2, x3, x4, 1); - * ty = 0.2 * curveTangent(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.' - * ); - * } - * - *
- */ -p5.prototype.curveTangent = function(a, b, c, d, t) { - p5._validateParameters('curveTangent', arguments); - - const s = this._renderer._curveTightness, - 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; -}; - -export default p5; diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js deleted file mode 100644 index 92459fe083..0000000000 --- a/src/core/shape/vertex.js +++ /dev/null @@ -1,2256 +0,0 @@ -/** - * @module Shape - * @submodule Vertex - * @for p5 - * @requires core - * @requires constants - */ - -import p5 from '../main'; -import * as constants from '../constants'; -let shapeKind = null; -let vertices = []; -let contourVertices = []; -let isBezier = false; -let isCurve = false; -let isQuadratic = false; -let isContour = false; -let isFirstContour = true; - -/** - * 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 - * @chainable - * - * @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(); - * - * // Stop drawing the shape. - * endShape(CLOSE); - * - * describe('A white square with a square hole in its center drawn on a gray background.'); - * } - * - *
- * - *
- * - * // 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(); - * - * // Stop drawing the shape. - * endShape(CLOSE); - * } - * - *
- */ -p5.prototype.beginContour = function() { - if (this._renderer.isP3D) { - this._renderer.beginContour(); - } else { - contourVertices = []; - isContour = true; - } - return this; -}; - -/** - * 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. By default, any - * irregular polygon can be drawn. The available modes for kind are: - * - * - `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. - * - `TESS` to create a filling curve by explicit tessellation (WebGL only). - * - * After calling `beginShape()`, shapes can be built by calling - * vertex(), - * bezierVertex(), - * quadraticVertex(), and/or - * curveVertex(). 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|TESS)} [kind] either POINTS, LINES, TRIANGLES, TRIANGLE_FAN - * TRIANGLE_STRIP, QUADS, QUAD_STRIP or TESS. - * @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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Start drawing the shape. - * // Draw a series of quadrilaterals. - * beginShape(TESS); - * - * // 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.'); - * } - * - *
- * - *
- * - * // 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(TESS); - * - * // 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); - * } - * - *
- */ -p5.prototype.beginShape = function(kind) { - p5._validateParameters('beginShape', arguments); - if (this._renderer.isP3D) { - this._renderer.beginShape(...arguments); - } else { - if ( - kind === constants.POINTS || - kind === constants.LINES || - kind === constants.TRIANGLES || - kind === constants.TRIANGLE_FAN || - kind === constants.TRIANGLE_STRIP || - kind === constants.QUADS || - kind === constants.QUAD_STRIP - ) { - shapeKind = kind; - } else { - shapeKind = null; - } - - vertices = []; - contourVertices = []; - } - return this; -}; - -/** - * 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. The curved segment uses - * the previous vertex as the first anchor point, so there must be at least - * one call to vertex() before `bezierVertex()` can - * be used. - * - * The first four parameters, `x2`, `y2`, `x3`, and `y3`, set the curve’s two - * control points. The control points "pull" the curve towards them. - * - * The fifth and sixth 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 - * `bezierVertex()` has eight arguments because each point has x-, y-, and - * z-coordinates. - * - * Note: `bezierVertex()` won’t work when an argument is passed to - * beginShape(). - * - * @method bezierVertex - * @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 anchor point. - * @param {Number} y4 y-coordinate of the anchor point. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the shape. - * noFill(); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the first anchor point. - * vertex(30, 20); - * - * // Add the Bézier vertex. - * bezierVertex(80, 0, 80, 75, 30, 75); - * - * // Stop drawing the shape. - * endShape(); - * - * describe('A black C curve on a gray background.'); - * } - * - *
- * - *
- * - * 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. - * vertex(30, 20); - * - * // Add the Bézier vertex. - * bezierVertex(80, 0, 80, 75, 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.' - * ); - * } - * - *
- * - *
- * - * // 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. - * vertex(30, 20); - * - * // Add the Bézier vertex. - * bezierVertex(x2, y2, 80, 75, 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; - * } - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the first anchor point. - * vertex(30, 20); - * - * // Add the Bézier vertices. - * bezierVertex(80, 0, 80, 75, 30, 75); - * bezierVertex(50, 80, 60, 25, 30, 20); - * - * // Stop drawing the shape. - * endShape(); - * - * describe('A crescent moon shape drawn in white on a gray background.'); - * } - * - *
- * - *
- * - * // 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(); - * vertex(-20, -30, 0); - * bezierVertex(30, -50, 0, 30, 25, 0, -20, 25, 0); - * bezierVertex(0, 30, 0, 10, -25, 0, -20, -30, 0); - * endShape(); - * - * // Draw the second moon. - * beginShape(); - * vertex(-20, -30, -20); - * bezierVertex(30, -50, -20, 30, 25, -20, -20, 25, -20); - * bezierVertex(0, 30, -20, 10, -25, -20, -20, -30, -20); - * endShape(); - * } - * - *
- */ - -/** - * @method bezierVertex - * @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 anchor point. - * @chainable - */ -p5.prototype.bezierVertex = function(...args) { - p5._validateParameters('bezierVertex', args); - if (this._renderer.isP3D) { - this._renderer.bezierVertex(...args); - } else { - if (vertices.length === 0) { - p5._friendlyError( - 'vertex() must be used once before calling bezierVertex()', - 'bezierVertex' - ); - } else { - isBezier = true; - const vert = []; - for (let i = 0; i < args.length; i++) { - vert[i] = args[i]; - } - vert.isVert = false; - if (isContour) { - contourVertices.push(vert); - } else { - vertices.push(vert); - } - } - } - return this; -}; - -/** - * Adds a spline curve segment to a custom shape. - * - * `curveVertex()` adds a curved segment to custom shapes. The spline curves - * it creates are defined like those made by the - * curve() function. `curveVertex()` must be called - * between the beginShape() and - * endShape() functions. - * - * Spline curves can form shapes and curves that slope gently. They’re like - * cables that are attached to a set of points. Splines are defined by two - * anchor points and two control points. `curveVertex()` must be called at - * least four times between - * beginShape() and - * endShape() in order to draw a curve: - * - * - * beginShape(); - * - * // Add the first control point. - * curveVertex(84, 91); - * - * // Add the anchor points to draw between. - * curveVertex(68, 19); - * curveVertex(21, 17); - * - * // Add the second control point. - * curveVertex(32, 91); - * - * endShape(); - * - * - * The code snippet above would only draw the curve between the anchor points, - * similar to the curve() function. The segments - * between the control and anchor points can be drawn by calling - * `curveVertex()` with the coordinates of the control points: - * - * - * beginShape(); - * - * // Add the first control point and draw a segment to it. - * curveVertex(84, 91); - * curveVertex(84, 91); - * - * // Add the anchor points to draw between. - * curveVertex(68, 19); - * curveVertex(21, 17); - * - * // Add the second control point. - * curveVertex(32, 91); - * - * // Uncomment the next line to draw the segment to the second control point. - * // curveVertex(32, 91); - * - * endShape(); - * - * - * The first two parameters, `x` and `y`, set the vertex’s location. For - * example, calling `curveVertex(10, 10)` adds a point to the curve at - * `(10, 10)`. - * - * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of - * `curveVertex()` has three arguments because each point has x-, y-, and - * z-coordinates. By default, the vertex’s z-coordinate is set to 0. - * - * Note: `curveVertex()` won’t work when an argument is passed to - * beginShape(). - * - * @method curveVertex - * @param {Number} x x-coordinate of the vertex - * @param {Number} y y-coordinate of the vertex - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the shape. - * noFill(); - * strokeWeight(1); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the first control point. - * curveVertex(32, 91); - * - * // Add the anchor points. - * curveVertex(21, 17); - * curveVertex(68, 19); - * - * // Add the second control point. - * curveVertex(84, 91); - * - * // Stop drawing the shape. - * endShape(); - * - * // Style the anchor and control points. - * strokeWeight(5); - * - * // Draw the anchor points in black. - * stroke(0); - * point(21, 17); - * point(68, 19); - * - * // Draw the control points in red. - * stroke(255, 0, 0); - * point(32, 91); - * point(84, 91); - * - * describe( - * 'A black curve drawn on a gray background. The curve has black dots at its ends. Two red dots appear near the bottom of the canvas.' - * ); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the shape. - * noFill(); - * strokeWeight(1); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the first control point and draw a segment to it. - * curveVertex(32, 91); - * curveVertex(32, 91); - * - * // Add the anchor points. - * curveVertex(21, 17); - * curveVertex(68, 19); - * - * // Add the second control point. - * curveVertex(84, 91); - * - * // Stop drawing the shape. - * endShape(); - * - * // Style the anchor and control points. - * strokeWeight(5); - * - * // Draw the anchor points in black. - * stroke(0); - * point(21, 17); - * point(68, 19); - * - * // Draw the control points in red. - * stroke(255, 0, 0); - * point(32, 91); - * point(84, 91); - * - * describe( - * 'A black curve drawn on a gray background. The curve passes through one red dot and two black dots. Another red dot appears near the bottom of the canvas.' - * ); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the shape. - * noFill(); - * strokeWeight(1); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the first control point and draw a segment to it. - * curveVertex(32, 91); - * curveVertex(32, 91); - * - * // Add the anchor points. - * curveVertex(21, 17); - * curveVertex(68, 19); - * - * // Add the second control point and draw a segment to it. - * curveVertex(84, 91); - * curveVertex(84, 91); - * - * // Stop drawing the shape. - * endShape(); - * - * // Style the anchor and control points. - * strokeWeight(5); - * - * // Draw the anchor points in black. - * stroke(0); - * point(21, 17); - * point(68, 19); - * - * // Draw the control points in red. - * stroke(255, 0, 0); - * point(32, 91); - * point(84, 91); - * - * describe( - * 'A black U curve drawn upside down on a gray background. The curve passes from one red dot through two black dots and ends at another red dot.' - * ); - * } - * - *
- * - *
- * - * // Click the mouse near the red dot in the bottom-left corner - * // and drag to change the curve's shape. - * - * let x1 = 32; - * let y1 = 91; - * let isChanging = false; - * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * 'A black U curve drawn upside down on a gray background. The curve passes from one red dot through two black dots and ends at another red dot.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Style the shape. - * noFill(); - * stroke(0); - * strokeWeight(1); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the first control point and draw a segment to it. - * curveVertex(x1, y1); - * curveVertex(x1, y1); - * - * // Add the anchor points. - * curveVertex(21, 17); - * curveVertex(68, 19); - * - * // Add the second control point and draw a segment to it. - * curveVertex(84, 91); - * curveVertex(84, 91); - * - * // Stop drawing the shape. - * endShape(); - * - * // Style the anchor and control points. - * strokeWeight(5); - * - * // Draw the anchor points in black. - * stroke(0); - * point(21, 17); - * point(68, 19); - * - * // Draw the control points in red. - * stroke(255, 0, 0); - * point(x1, y1); - * point(84, 91); - * } - * - * // Start changing the first control point if the user clicks near it. - * function mousePressed() { - * if (dist(mouseX, mouseY, x1, y1) < 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) { - * x1 = mouseX; - * y1 = mouseY; - * } - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the first control point and draw a segment to it. - * curveVertex(32, 91); - * curveVertex(32, 91); - * - * // Add the anchor points. - * curveVertex(21, 17); - * curveVertex(68, 19); - * - * // Add the second control point. - * curveVertex(84, 91); - * curveVertex(84, 91); - * - * // Stop drawing the shape. - * endShape(); - * - * describe('A ghost shape drawn in white on a gray background.'); - * } - * - *
- */ - -/** - * @method curveVertex - * @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(); - * curveVertex(-28, 41, 0); - * curveVertex(-28, 41, 0); - * curveVertex(-29, -33, 0); - * curveVertex(18, -31, 0); - * curveVertex(34, 41, 0); - * curveVertex(34, 41, 0); - * endShape(); - * - * // Draw the second ghost. - * noFill(); - * stroke('ghostwhite'); - * - * beginShape(); - * curveVertex(-28, 41, -20); - * curveVertex(-28, 41, -20); - * curveVertex(-29, -33, -20); - * curveVertex(18, -31, -20); - * curveVertex(34, 41, -20); - * curveVertex(34, 41, -20); - * endShape(); - * } - * - *
- */ -p5.prototype.curveVertex = function(...args) { - p5._validateParameters('curveVertex', args); - if (this._renderer.isP3D) { - this._renderer.curveVertex(...args); - } else { - isCurve = true; - this.vertex(args[0], args[1]); - } - return this; -}; - -/** - * 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(). - * - * 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 - * @chainable - * - * @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(); - * - * // Stop drawing the shape. - * endShape(CLOSE); - * - * describe('A white square with a square hole in its center drawn on a gray background.'); - * } - * - *
- * - *
- * - * // 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(); - * - * // Stop drawing the shape. - * endShape(CLOSE); - * } - * - *
- */ -p5.prototype.endContour = function() { - if (this._renderer.isP3D) { - return this; - } - - const vert = contourVertices[0].slice(); // copy all data - vert.isVert = contourVertices[0].isVert; - vert.moveTo = false; - contourVertices.push(vert); - - // prevent stray lines with multiple contours - if (isFirstContour) { - vertices.push(vertices[0]); - isFirstContour = false; - } - - for (let i = 0; i < contourVertices.length; i++) { - vertices.push(contourVertices[i]); - } - return this; -}; - -/** - * 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 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. - * - * 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(), - * quadraticVertex(), and/or - * curveVertex(). 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.' - * ); - * } - * - *
- * - *
- * - * // 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'); - * } - * - *
- */ -p5.prototype.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; - } - - if (this._renderer.isP3D) { - this._renderer.endShape( - mode, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind, - count - ); - } else { - if (count !== 1) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); - } - if (vertices.length === 0) { - return this; - } - if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { - return this; - } - - const closeShape = mode === constants.CLOSE; - - // if the shape is closed, the first element is also the last element - if (closeShape && !isContour) { - vertices.push(vertices[0]); - } - - this._renderer.endShape( - mode, - vertices, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind - ); - - // Reset some settings - isCurve = false; - isBezier = false; - isQuadratic = false; - isContour = false; - isFirstContour = true; - - // If the shape is closed, the first element was added as last element. - // We must remove it again to prevent the list of vertices from growing - // over successive calls to endShape(CLOSE) - if (closeShape) { - vertices.pop(); - } - } - return this; -}; - -/** - * Adds a quadratic Bézier curve segment to a custom shape. - * - * `quadraticVertex()` adds a curved segment to custom shapes. The Bézier - * curve segments it creates are similar to those made by the - * bezierVertex() function. - * `quadraticVertex()` must be called between the - * beginShape() and - * endShape() functions. The curved segment uses - * the previous vertex as the first anchor point, so there must be at least - * one call to vertex() before `quadraticVertex()` can - * be used. - * - * The first two parameters, `cx` and `cy`, set the curve’s control point. - * The control point "pulls" the curve towards its. - * - * The last two parameters, `x3`, and `y3`, 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 - * `bezierVertex()` has eight arguments because each point has x-, y-, and - * z-coordinates. - * - * Note: `quadraticVertex()` won’t work when an argument is passed to - * beginShape(). - * - * @method quadraticVertex - * @param {Number} cx x-coordinate of the control point. - * @param {Number} cy y-coordinate of the control point. - * @param {Number} x3 x-coordinate of the anchor point. - * @param {Number} y3 y-coordinate of the anchor point. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the curve. - * noFill(); - * - * // Draw the curve. - * beginShape(); - * vertex(20, 20); - * quadraticVertex(80, 20, 50, 50); - * endShape(); - * - * describe('A black curve drawn on a gray square. The curve starts at the top-left corner and ends at the center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Draw the curve. - * noFill(); - * beginShape(); - * vertex(20, 20); - * quadraticVertex(80, 20, 50, 50); - * endShape(); - * - * // Draw red lines from the anchor points to the control point. - * stroke(255, 0, 0); - * line(20, 20, 80, 20); - * line(50, 50, 80, 20); - * - * // Draw the anchor points in black. - * strokeWeight(5); - * stroke(0); - * point(20, 20); - * point(50, 50); - * - * // Draw the control point in red. - * stroke(255, 0, 0); - * point(80, 20); - * - * describe('A black curve that starts at the top-left corner and ends at the center. Its anchor and control points are marked with dots. Red lines connect both anchor points to the control point.'); - * } - * - *
- * - *
- * - * // 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 = 20; - * let isChanging = false; - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A black curve that starts at the top-left corner and ends at the center. Its anchor and control points are marked with dots. Red lines connect both anchor points to the control point.'); - * } - * - * function draw() { - * background(200); - * - * // Style the curve. - * noFill(); - * strokeWeight(1); - * stroke(0); - * - * // Draw the curve. - * beginShape(); - * vertex(20, 20); - * quadraticVertex(x2, y2, 50, 50); - * endShape(); - * - * // Draw red lines from the anchor points to the control point. - * stroke(255, 0, 0); - * line(20, 20, x2, y2); - * line(50, 50, x2, y2); - * - * // Draw the anchor points in black. - * strokeWeight(5); - * stroke(0); - * point(20, 20); - * point(50, 50); - * - * // Draw the control point in red. - * stroke(255, 0, 0); - * point(x2, y2); - * } - * - * // 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; - * } - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Start drawing the shape. - * beginShape(); - * - * // Add the curved segments. - * vertex(20, 20); - * quadraticVertex(80, 20, 50, 50); - * quadraticVertex(20, 80, 80, 80); - * - * // Add the straight segments. - * vertex(80, 10); - * vertex(20, 10); - * vertex(20, 20); - * - * // Stop drawing the shape. - * endShape(); - * - * describe('A white puzzle piece drawn on a gray background.'); - * } - * - *
- * - *
- * - * // Click the and drag the mouse to view the scene from a different angle. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white puzzle piece on a dark gray background. When the user clicks and drags the scene, the outline of a second puzzle piece is revealed.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the first puzzle piece. - * noStroke(); - * fill(255); - * - * // Draw the first puzzle piece. - * beginShape(); - * vertex(-30, -30, 0); - * quadraticVertex(30, -30, 0, 0, 0, 0); - * quadraticVertex(-30, 30, 0, 30, 30, 0); - * vertex(30, -40, 0); - * vertex(-30, -40, 0); - * vertex(-30, -30, 0); - * endShape(); - * - * // Style the second puzzle piece. - * stroke(255); - * noFill(); - * - * // Draw the second puzzle piece. - * beginShape(); - * vertex(-30, -30, -20); - * quadraticVertex(30, -30, -20, 0, 0, -20); - * quadraticVertex(-30, 30, -20, 30, 30, -20); - * vertex(30, -40, -20); - * vertex(-30, -40, -20); - * vertex(-30, -30, -20); - * endShape(); - * } - * - *
- */ - -/** - * @method quadraticVertex - * @param {Number} cx - * @param {Number} cy - * @param {Number} cz z-coordinate of the control point. - * @param {Number} x3 - * @param {Number} y3 - * @param {Number} z3 z-coordinate of the anchor point. - */ -p5.prototype.quadraticVertex = function(...args) { - p5._validateParameters('quadraticVertex', args); - if (this._renderer.isP3D) { - this._renderer.quadraticVertex(...args); - } else { - //if we're drawing a contour, put the points into an - // array for inside drawing - if (this._contourInited) { - const pt = {}; - pt.x = args[0]; - pt.y = args[1]; - pt.x3 = args[2]; - pt.y3 = args[3]; - pt.type = constants.QUADRATIC; - this._contourVertices.push(pt); - - return this; - } - if (vertices.length > 0) { - isQuadratic = true; - const vert = []; - for (let i = 0; i < args.length; i++) { - vert[i] = args[i]; - } - vert.isVert = false; - if (isContour) { - contourVertices.push(vert); - } else { - vertices.push(vert); - } - } else { - p5._friendlyError( - 'vertex() must be used once before calling quadraticVertex()', - 'quadraticVertex' - ); - } - } - return this; -}; - -/** - * 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. - * @chainable - * - * @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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- * - *
- * - * let img; - * - * // Load an image to apply as a texture. - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } - * - * function setup() { - * 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(); - * } - * - *
- */ -/** - * @method vertex - * @param {Number} x - * @param {Number} y - * @param {Number} [z] z-coordinate of the vertex. Defaults to 0. - * @chainable - */ -/** - * @method vertex - * @param {Number} x - * @param {Number} y - * @param {Number} [z] - * @param {Number} [u] u-coordinate of the vertex's texture. Defaults to 0. - * @param {Number} [v] v-coordinate of the vertex's texture. Defaults to 0. - * @chainable - */ -p5.prototype.vertex = function(x, y, moveTo, u, v) { - if (this._renderer.isP3D) { - this._renderer.vertex(...arguments); - } else { - const vert = []; - vert.isVert = true; - vert[0] = x; - vert[1] = y; - vert[2] = 0; - vert[3] = 0; - vert[4] = 0; - vert[5] = this._renderer._getFill(); - vert[6] = this._renderer._getStroke(); - - if (moveTo) { - vert.moveTo = moveTo; - } - if (isContour) { - if (contourVertices.length === 0) { - vert.moveTo = true; - } - contourVertices.push(vert); - } else { - vertices.push(vert); - } - } - return this; -}; - -/** - * 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: - * - * - * 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(); - * } - * - *
- * - *
- * - * // 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(); - * } - * - *
- * - *
- * - * // 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 - */ -p5.prototype.normal = function(x, y, z) { - this._assert3d('normal'); - p5._validateParameters('normal', arguments); - this._renderer.normal(...arguments); - - return this; -}; - -export default p5; diff --git a/src/shape/2d_primitives.js b/src/shape/2d_primitives.js new file mode 100644 index 0000000000..0fe9f6bb60 --- /dev/null +++ b/src/shape/2d_primitives.js @@ -0,0 +1,1456 @@ +/** + * @module Shape + * @submodule 2D Primitives + * @for p5 + * @requires core + * @requires constants + */ + +import * as constants from '../core/constants'; +import canvas from '../core/helpers'; +import '../core/friendly_errors/fes_core'; +import '../core/friendly_errors/file_errors'; +import '../core/friendly_errors/validate_params'; + +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 - constants.TWO_PI * Math.floor(start / constants.TWO_PI); + stop = stop - constants.TWO_PI * Math.floor(stop / constants.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), + constants.TWO_PI - Math.abs(start - stop) + ); + + // Optionally adjust the angles to counter linear scaling. + if (correctForScaling) { + if (start <= constants.HALF_PI) { + start = Math.atan(width / height * Math.tan(start)); + } else if (start > constants.HALF_PI && start <= 3 * constants.HALF_PI) { + start = Math.atan(width / height * Math.tan(start)) + constants.PI; + } else { + start = Math.atan(width / height * Math.tan(start)) + constants.TWO_PI; + } + if (stop <= constants.HALF_PI) { + stop = Math.atan(width / height * Math.tan(stop)); + } else if (stop > constants.HALF_PI && stop <= 3 * constants.HALF_PI) { + stop = Math.atan(width / height * Math.tan(stop)) + constants.PI; + } else { + stop = Math.atan(width / height * Math.tan(stop)) + constants.TWO_PI; + } + } + + // Ensure that start <= stop < start + TWO_PI. + if (start > stop) { + stop += constants.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.' + * ); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.' + * ); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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) { + p5._validateParameters('arc', arguments); + + // if the current stroke and fill settings wouldn't result in something + // visible, exit immediately + if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { + return this; + } + + if (start === stop) { + return this; + } + + start = this._toRadians(start); + stop = this._toRadians(stop); + + // p5 supports negative width and heights for ellipses + w = Math.abs(w); + h = Math.abs(h); + + 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.doStroke && !this._renderer.states.doFill) { + return this; + } + + // p5 supports negative width and heights for rects + if (w < 0) { + w = Math.abs(w); + } + + if (typeof h === 'undefined') { + // Duplicate 3rd argument if only 3 given. + h = w; + } else if (h < 0) { + h = Math.abs(h); + } + + 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.' + * ); + * } + * + *
+ * + *
+ * + * 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.' + * ); + * } + * + *
+ * + *
+ * + * 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.doStroke) { + 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); + * + * // 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.' + * ); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // 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.' + * ); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // 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.' + * ); + * } + * + *
+ * + *
+ * + * 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); + * } + * + *
+ * + *
+ * + * 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.doStroke) { + 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.doStroke || this._renderer.states.doFill) { + 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.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * rect(30, 20, 55, 40); + * + * describe('A white rectangle with a black outline on a gray canvas.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.' + * ); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.doStroke || this._renderer.states.doFill) { + // 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 + ); + + 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.doStroke || this._renderer.states.doFill) { + this._renderer.triangle(args); + } + + //accessible outputs + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._accsOutput('triangle', args); + } + + return this; + }; +} + +export default primitives; + +if(typeof p5 !== 'undefined'){ + primitives(p5, p5.prototype); +} diff --git a/src/shape/attributes.js b/src/shape/attributes.js new file mode 100644 index 0000000000..9160e009f3 --- /dev/null +++ b/src/shape/attributes.js @@ -0,0 +1,606 @@ +/** + * @module Shape + * @submodule Attributes + * @for p5 + * @requires core + * @requires constants + */ + +import * as constants from '../core/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.'); + * } + * + *
+ * + *
+ * + * 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 === constants.CORNER || + m === constants.CORNERS || + m === constants.RADIUS || + m === constants.CENTER + ) { + this._renderer.states.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; + * + * // Load a pixelated heart image from an image data string. + * function preload() { + * heart = loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII='); + * } + * + * function setup() { + * 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); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * circle(0, 0, 80); + * + * describe('A white circle on a gray background.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * rectMode(CORNER); + * fill(255); + * square(25, 25, 50); + * + * describe('A white square.'); + * } + * + *
+ * + *
+ * + * 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 === constants.CORNER || + m === constants.CORNERS || + m === constants.RADIUS || + m === constants.CENTER + ) { + this._renderer.states.rectMode = m; + } + return this; + }; + + /** + * 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; + * + * // Load a pixelated heart image from an image data string. + * function preload() { + * heart = loadImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAEZJREFUGFd9jcsNACAIQ9tB2MeR3YdBMBBq8CIXPi2vBICIiOwkOedatllqWO6Y8yOWoyuNf1GZwgmf+RRG2YXr+xVFmA8HZ9Mx/KGPMtcAAAAASUVORK5CYII='); + * } + * + * function setup() { + * 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); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * circle(0, 0, 80); + * + * describe('A white circle on a gray background.'); + * } + * + *
+ * + *
+ * + * 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() { + this.setAttributes('antialias', true); + if (!this._renderer.isP3D) { + if ('imageSmoothingEnabled' in this.drawingContext) { + this.drawingContext.imageSmoothingEnabled = 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 === constants.ROUND || + cap === constants.SQUARE || + cap === constants.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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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 === constants.ROUND || + join === constants.BEVEL || + join === constants.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.'); + * } + * + *
+ * + *
+ * + * 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; + }; +} + +export default attributes; + +if(typeof p5 !== 'undefined'){ + attributes(p5, p5.prototype); +} diff --git a/src/shape/curves.js b/src/shape/curves.js new file mode 100644 index 0000000000..97ace85d22 --- /dev/null +++ b/src/shape/curves.js @@ -0,0 +1,1176 @@ +/** + * @module Shape + * @submodule Curves + * @for p5 + * @requires core + */ + +import '../core/friendly_errors/fes_core'; +import '../core/friendly_errors/file_errors'; +import '../core/friendly_errors/validate_params'; + +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 + * curve() 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.' + * ); + * } + * + *
+ * + *
+ * + * // 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; + * } + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.doStroke && !this._renderer.states.doFill) { + return this; + } + + this._renderer.bezier(...args); + + return this; + }; + + /** + * Sets the number of segments used to draw Bézier curves in WebGL mode. + * + * In WebGL mode, smooth shapes are drawn using many flat segments. Adding + * more flat segments makes shapes appear smoother. + * + * The parameter, `detail`, is the number of segments to use while drawing a + * Bézier curve. For example, calling `bezierDetail(5)` will use 5 segments to + * draw curves with the bezier() function. By + * default,`detail` is 20. + * + * Note: `bezierDetail()` has no effect in 2D mode. + * + * @method bezierDetail + * @param {Number} detail number of segments to use. Defaults to 20. + * @chainable + * + * @example + *
+ * + * // Draw the original curve. + * + * 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.' + * ); + * } + * + *
+ * + *
+ * + * // Draw the curve with less detail. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Set the curveDetail() to 5. + * bezierDetail(5); + * + * // Draw the anchor points in black. + * stroke(0); + * strokeWeight(5); + * point(35, -30, 0); + * point(-35, 30, 0); + * + * // Draw the control points in red. + * stroke(255, 0, 0); + * point(-40, -40, 0); + * point(40, 40, 0); + * + * // Draw a black bezier curve. + * noFill(); + * stroke(0); + * strokeWeight(1); + * bezier(35, -30, 0, -40, -40, 0, 40, 40, 0, -35, 30, 0); + * + * // Draw red lines from the anchor points to the control points. + * stroke(255, 0, 0); + * line(35, -30, -40, -40); + * line(-35, 30, 40, 40); + * + * describe( + * 'A gray square with three curves. A black s-curve is drawn with jagged segments. Two straight, red lines that extend from its ends. The endpoints of all the curves are marked with dots.' + * ); + * } + * + *
+ */ + fn.bezierDetail = function(d) { + p5._validateParameters('bezierDetail', arguments); + this._bezierDetail = d; + 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 control point. + * @param {Number} b coordinate of first anchor point. + * @param {Number} c coordinate of second anchor point. + * @param {Number} d coordinate of second control 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.'); + * } + * + *
+ * + *
+ * + * 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. Splines are defined by two + * anchor points and two control points. + * + * The first two parameters, `x1` and `y1`, set the first control point. This + * point isn’t drawn and can be thought of as the curve’s starting point. + * + * The next four parameters, `x2`, `y2`, `x3`, and `y3`, set the two anchor + * points. The anchor points are the start and end points of the curve’s + * visible segment. + * + * The seventh and eighth parameters, `x4` and `y4`, set the last control + * point. This point isn’t drawn and can be thought of as the curve’s ending + * point. + * + * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of + * `curve()` has twelve arguments because each point has x-, y-, and + * z-coordinates. + * + * @method curve + * @param {Number} x1 x-coordinate of the first control point. + * @param {Number} y1 y-coordinate of the first control point. + * @param {Number} x2 x-coordinate of the first anchor point. + * @param {Number} y2 y-coordinate of the first anchor point. + * @param {Number} x3 x-coordinate of the second anchor point. + * @param {Number} y3 y-coordinate of the second anchor point. + * @param {Number} x4 x-coordinate of the second control point. + * @param {Number} y4 y-coordinate of the second control point. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Draw a black spline curve. + * noFill(); + * strokeWeight(1); + * stroke(0); + * curve(5, 26, 73, 24, 73, 61, 15, 65); + * + * // Draw red spline curves from the anchor points to the control points. + * stroke(255, 0, 0); + * curve(5, 26, 5, 26, 73, 24, 73, 61); + * curve(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 control 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.' + * ); + * } + * + *
+ * + *
+ * + * 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); + * + * // Draw a black spline curve. + * noFill(); + * strokeWeight(1); + * stroke(0); + * curve(x1, y1, 73, 24, 73, 61, 15, 65); + * + * // Draw red spline curves from the anchor points to the control points. + * stroke(255, 0, 0); + * curve(x1, y1, x1, y1, 73, 24, 73, 61); + * curve(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 control points in red. + * stroke(255, 0, 0); + * point(x1, y1); + * point(15, 65); + * } + * + * // Start changing the first control point if the user clicks near it. + * function mousePressed() { + * if (dist(mouseX, mouseY, x1, y1) < 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) { + * x1 = mouseX; + * y1 = mouseY; + * } + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background('skyblue'); + * + * // Draw the red balloon. + * fill('red'); + * curve(-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.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A red balloon in a blue sky.'); + * } + * + * function draw() { + * background('skyblue'); + * + * // Rotate around the y-axis. + * rotateY(frameCount * 0.01); + * + * // Draw the red balloon. + * fill('red'); + * curve(-200, 225, 0, 0, 10, 0, 0, 10, 0, 200, 225, 0); + * + * // Draw the balloon string. + * line(0, 10, 0, 0, 30, 0); + * } + * + *
+ */ + + /** + * @method curve + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} z1 z-coordinate of the first control point. + * @param {Number} x2 + * @param {Number} y2 + * @param {Number} z2 z-coordinate of the first anchor point. + * @param {Number} x3 + * @param {Number} y3 + * @param {Number} z3 z-coordinate of the second anchor point. + * @param {Number} x4 + * @param {Number} y4 + * @param {Number} z4 z-coordinate of the second control point. + * @chainable + */ + fn.curve = function(...args) { + p5._validateParameters('curve', args); + + if (this._renderer.states.doStroke) { + this._renderer.curve(...args); + } + + return this; + }; + + /** + * Sets the number of segments used to draw spline curves in WebGL mode. + * + * In WebGL mode, smooth shapes are drawn using many flat segments. Adding + * more flat segments makes shapes appear smoother. + * + * The parameter, `detail`, is the number of segments to use while drawing a + * spline curve. For example, calling `curveDetail(5)` will use 5 segments to + * draw curves with the curve() function. By + * default,`detail` is 20. + * + * Note: `curveDetail()` has no effect in 2D mode. + * + * @method curveDetail + * @param {Number} resolution number of segments to use. Defaults to 20. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Draw a black spline curve. + * noFill(); + * strokeWeight(1); + * stroke(0); + * curve(5, 26, 73, 24, 73, 61, 15, 65); + * + * // Draw red spline curves from the anchor points to the control points. + * stroke(255, 0, 0); + * curve(5, 26, 5, 26, 73, 24, 73, 61); + * curve(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 control 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.' + * ); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Set the curveDetail() to 3. + * curveDetail(3); + * + * // Draw a black spline curve. + * noFill(); + * strokeWeight(1); + * stroke(0); + * curve(-45, -24, 0, 23, -26, 0, 23, 11, 0, -35, 15, 0); + * + * // Draw red spline curves from the anchor points to the control points. + * stroke(255, 0, 0); + * curve(-45, -24, 0, -45, -24, 0, 23, -26, 0, 23, 11, 0); + * curve(23, -26, 0, 23, 11, 0, -35, 15, 0, -35, 15, 0); + * + * // Draw the anchor points in black. + * strokeWeight(5); + * stroke(0); + * point(23, -26); + * point(23, 11); + * + * // Draw the control points in red. + * stroke(255, 0, 0); + * point(-45, -24); + * point(-35, 15); + * + * describe( + * 'A gray square with a jagged 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.' + * ); + * } + * + *
+ */ + fn.curveDetail = function(d) { + p5._validateParameters('curveDetail', arguments); + if (d < 3) { + this._curveDetail = 3; + } else { + this._curveDetail = d; + } + return this; + }; + + /** + * Adjusts the way curve() and + * curveVertex() draw. + * + * Spline curves are like cables that are attached to a set of points. + * `curveTightness()` adjusts how tightly the cable is attached to the points. + * + * The parameter, `tightness`, determines how the curve fits to the vertex + * points. By default, `tightness` is set to 0. Setting tightness to 1, + * as in `curveTightness(1)`, connects the curve's points using straight + * lines. Values in the range from –5 to 5 deform curves while leaving them + * recognizable. + * + * @method curveTightness + * @param {Number} amount amount of tightness. + * @chainable + * + * @example + *
+ * + * // Move the mouse left and right to see the curve change. + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A black curve forms a sideways U shape. The curve deforms as the user moves the mouse from left to right'); + * } + * + * function draw() { + * background(200); + * + * // Set the curve's tightness using the mouse. + * let t = map(mouseX, 0, 100, -5, 5, true); + * curveTightness(t); + * + * // Draw the curve. + * noFill(); + * beginShape(); + * curveVertex(10, 26); + * curveVertex(10, 26); + * curveVertex(83, 24); + * curveVertex(83, 61); + * curveVertex(25, 65); + * curveVertex(25, 65); + * endShape(); + * } + * + *
+ */ + fn.curveTightness = function(t) { + p5._validateParameters('curveTightness', arguments); + this._renderer._curveTightness = t; + return this; + }; + + /** + * Calculates coordinates along a spline curve using interpolation. + * + * `curvePoint()` calculates coordinates along a spline curve using the + * anchor and control points. It expects points in the same order as the + * curve() function. `curvePoint()` 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 control point. + * + * The second and third parameters, `b` and `c`, are the coordinates of the + * anchor points. + * + * The fourth parameter, `d`, is the coordinate of the last control 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 curvePoint + * @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 curve. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Set the coordinates for the curve's anchor and control points. + * 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(); + * curve(x1, y1, x2, y2, x3, y3, x4, y4); + * + * // Draw circles along the curve's path. + * fill(255); + * + * // Top. + * let x = curvePoint(x1, x2, x3, x4, 0); + * let y = curvePoint(y1, y2, y3, y4, 0); + * circle(x, y, 5); + * + * // Center. + * x = curvePoint(x1, x2, x3, x4, 0.5); + * y = curvePoint(y1, y2, y3, y4, 0.5); + * circle(x, y, 5); + * + * // Bottom. + * x = curvePoint(x1, x2, x3, x4, 1); + * y = curvePoint(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.'); + * } + * + *
+ * + *
+ * + * 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 anchor and control points. + * 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(); + * curve(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 = curvePoint(x1, x2, x3, x4, t); + * let y = curvePoint(y1, y2, y3, y4, t); + * + * // Draw the circle. + * fill(255); + * circle(x, y, 5); + * } + * + *
+ */ + fn.curvePoint = function(a, b, c, d, t) { + p5._validateParameters('curvePoint', arguments); + const s = this._renderer._curveTightness, + 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. + * + * `curveTangent()` calculates coordinates along a tangent line using the + * spline curve's anchor and control points. It expects points in the same + * order as the curve() function. `curveTangent()` + * 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 control point. + * + * The second and third parameters, `b` and `c`, are the coordinates of the + * anchor points. + * + * The fourth parameter, `d`, is the coordinate of the last control 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 curveTangent + * @param {Number} a coordinate of first control point. + * @param {Number} b coordinate of first anchor point. + * @param {Number} c coordinate of second anchor point. + * @param {Number} d coordinate of second control 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 = 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(); + * curve(x1, y1, x2, y2, x3, y3, x4, y4); + * + * // Draw tangents along the curve's path. + * fill(255); + * + * // Top circle. + * stroke(0); + * let x = curvePoint(x1, x2, x3, x4, 0); + * let y = curvePoint(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 * curveTangent(x1, x2, x3, x4, 0); + * let ty = 0.2 * curveTangent(y1, y2, y3, y4, 0); + * line(x + tx, y + ty, x - tx, y - ty); + * + * // Center circle. + * stroke(0); + * x = curvePoint(x1, x2, x3, x4, 0.5); + * y = curvePoint(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 * curveTangent(x1, x2, x3, x4, 0.5); + * ty = 0.2 * curveTangent(y1, y2, y3, y4, 0.5); + * line(x + tx, y + ty, x - tx, y - ty); + * + * // Bottom circle. + * stroke(0); + * x = curvePoint(x1, x2, x3, x4, 1); + * y = curvePoint(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 * curveTangent(x1, x2, x3, x4, 1); + * ty = 0.2 * curveTangent(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.curveTangent = function(a, b, c, d, t) { + p5._validateParameters('curveTangent', arguments); + + const s = this._renderer._curveTightness, + 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; + }; +} + +export default curves; + +if(typeof p5 !== 'undefined'){ + curves(p5, p5.prototype); +} diff --git a/src/shape/index.js b/src/shape/index.js new file mode 100644 index 0000000000..7df6b30698 --- /dev/null +++ b/src/shape/index.js @@ -0,0 +1,11 @@ +import primitives from './2d_primitives.js'; +import attributes from './attributes.js'; +import curves from './curves.js'; +import vertex from './vertex.js'; + +export default function(p5){ + p5.registerAddon(primitives); + p5.registerAddon(attributes); + p5.registerAddon(curves); + p5.registerAddon(vertex); +} diff --git a/src/shape/vertex.js b/src/shape/vertex.js new file mode 100644 index 0000000000..ab60e8fe16 --- /dev/null +++ b/src/shape/vertex.js @@ -0,0 +1,2262 @@ +/** + * @module Shape + * @submodule Vertex + * @for p5 + * @requires core + * @requires constants + */ + +import * as constants from '../core/constants'; + +function vertex(p5, fn){ + let shapeKind = null; + let vertices = []; + let contourVertices = []; + let isBezier = false; + let isCurve = false; + let isQuadratic = false; + let isContour = false; + let isFirstContour = true; + + /** + * 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 + * @chainable + * + * @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(); + * + * // Stop drawing the shape. + * endShape(CLOSE); + * + * describe('A white square with a square hole in its center drawn on a gray background.'); + * } + * + *
+ * + *
+ * + * // 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(); + * + * // Stop drawing the shape. + * endShape(CLOSE); + * } + * + *
+ */ + fn.beginContour = function() { + if (this._renderer.isP3D) { + this._renderer.beginContour(); + } else { + contourVertices = []; + isContour = true; + } + return this; + }; + + /** + * 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. By default, any + * irregular polygon can be drawn. The available modes for kind are: + * + * - `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. + * - `TESS` to create a filling curve by explicit tessellation (WebGL only). + * + * After calling `beginShape()`, shapes can be built by calling + * vertex(), + * bezierVertex(), + * quadraticVertex(), and/or + * curveVertex(). 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|TESS)} [kind] either POINTS, LINES, TRIANGLES, TRIANGLE_FAN + * TRIANGLE_STRIP, QUADS, QUAD_STRIP or TESS. + * @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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Start drawing the shape. + * // Draw a series of quadrilaterals. + * beginShape(TESS); + * + * // 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.'); + * } + * + *
+ * + *
+ * + * // 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(TESS); + * + * // 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); + if (this._renderer.isP3D) { + this._renderer.beginShape(...arguments); + } else { + if ( + kind === constants.POINTS || + kind === constants.LINES || + kind === constants.TRIANGLES || + kind === constants.TRIANGLE_FAN || + kind === constants.TRIANGLE_STRIP || + kind === constants.QUADS || + kind === constants.QUAD_STRIP + ) { + shapeKind = kind; + } else { + shapeKind = null; + } + + vertices = []; + contourVertices = []; + } + return this; + }; + + /** + * 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. The curved segment uses + * the previous vertex as the first anchor point, so there must be at least + * one call to vertex() before `bezierVertex()` can + * be used. + * + * The first four parameters, `x2`, `y2`, `x3`, and `y3`, set the curve’s two + * control points. The control points "pull" the curve towards them. + * + * The fifth and sixth 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 + * `bezierVertex()` has eight arguments because each point has x-, y-, and + * z-coordinates. + * + * Note: `bezierVertex()` won’t work when an argument is passed to + * beginShape(). + * + * @method bezierVertex + * @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 anchor point. + * @param {Number} y4 y-coordinate of the anchor point. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the shape. + * noFill(); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the first anchor point. + * vertex(30, 20); + * + * // Add the Bézier vertex. + * bezierVertex(80, 0, 80, 75, 30, 75); + * + * // Stop drawing the shape. + * endShape(); + * + * describe('A black C curve on a gray background.'); + * } + * + *
+ * + *
+ * + * 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. + * vertex(30, 20); + * + * // Add the Bézier vertex. + * bezierVertex(80, 0, 80, 75, 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.' + * ); + * } + * + *
+ * + *
+ * + * // 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. + * vertex(30, 20); + * + * // Add the Bézier vertex. + * bezierVertex(x2, y2, 80, 75, 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; + * } + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the first anchor point. + * vertex(30, 20); + * + * // Add the Bézier vertices. + * bezierVertex(80, 0, 80, 75, 30, 75); + * bezierVertex(50, 80, 60, 25, 30, 20); + * + * // Stop drawing the shape. + * endShape(); + * + * describe('A crescent moon shape drawn in white on a gray background.'); + * } + * + *
+ * + *
+ * + * // 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(); + * vertex(-20, -30, 0); + * bezierVertex(30, -50, 0, 30, 25, 0, -20, 25, 0); + * bezierVertex(0, 30, 0, 10, -25, 0, -20, -30, 0); + * endShape(); + * + * // Draw the second moon. + * beginShape(); + * vertex(-20, -30, -20); + * bezierVertex(30, -50, -20, 30, 25, -20, -20, 25, -20); + * bezierVertex(0, 30, -20, 10, -25, -20, -20, -30, -20); + * endShape(); + * } + * + *
+ */ + + /** + * @method bezierVertex + * @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 anchor point. + * @chainable + */ + fn.bezierVertex = function(...args) { + p5._validateParameters('bezierVertex', args); + if (this._renderer.isP3D) { + this._renderer.bezierVertex(...args); + } else { + if (vertices.length === 0) { + p5._friendlyError( + 'vertex() must be used once before calling bezierVertex()', + 'bezierVertex' + ); + } else { + isBezier = true; + const vert = []; + for (let i = 0; i < args.length; i++) { + vert[i] = args[i]; + } + vert.isVert = false; + if (isContour) { + contourVertices.push(vert); + } else { + vertices.push(vert); + } + } + } + return this; + }; + + /** + * Adds a spline curve segment to a custom shape. + * + * `curveVertex()` adds a curved segment to custom shapes. The spline curves + * it creates are defined like those made by the + * curve() function. `curveVertex()` must be called + * between the beginShape() and + * endShape() functions. + * + * Spline curves can form shapes and curves that slope gently. They’re like + * cables that are attached to a set of points. Splines are defined by two + * anchor points and two control points. `curveVertex()` must be called at + * least four times between + * beginShape() and + * endShape() in order to draw a curve: + * + * + * beginShape(); + * + * // Add the first control point. + * curveVertex(84, 91); + * + * // Add the anchor points to draw between. + * curveVertex(68, 19); + * curveVertex(21, 17); + * + * // Add the second control point. + * curveVertex(32, 91); + * + * endShape(); + * + * + * The code snippet above would only draw the curve between the anchor points, + * similar to the curve() function. The segments + * between the control and anchor points can be drawn by calling + * `curveVertex()` with the coordinates of the control points: + * + * + * beginShape(); + * + * // Add the first control point and draw a segment to it. + * curveVertex(84, 91); + * curveVertex(84, 91); + * + * // Add the anchor points to draw between. + * curveVertex(68, 19); + * curveVertex(21, 17); + * + * // Add the second control point. + * curveVertex(32, 91); + * + * // Uncomment the next line to draw the segment to the second control point. + * // curveVertex(32, 91); + * + * endShape(); + * + * + * The first two parameters, `x` and `y`, set the vertex’s location. For + * example, calling `curveVertex(10, 10)` adds a point to the curve at + * `(10, 10)`. + * + * Spline curves can also be drawn in 3D using WebGL mode. The 3D version of + * `curveVertex()` has three arguments because each point has x-, y-, and + * z-coordinates. By default, the vertex’s z-coordinate is set to 0. + * + * Note: `curveVertex()` won’t work when an argument is passed to + * beginShape(). + * + * @method curveVertex + * @param {Number} x x-coordinate of the vertex + * @param {Number} y y-coordinate of the vertex + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the shape. + * noFill(); + * strokeWeight(1); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the first control point. + * curveVertex(32, 91); + * + * // Add the anchor points. + * curveVertex(21, 17); + * curveVertex(68, 19); + * + * // Add the second control point. + * curveVertex(84, 91); + * + * // Stop drawing the shape. + * endShape(); + * + * // Style the anchor and control points. + * strokeWeight(5); + * + * // Draw the anchor points in black. + * stroke(0); + * point(21, 17); + * point(68, 19); + * + * // Draw the control points in red. + * stroke(255, 0, 0); + * point(32, 91); + * point(84, 91); + * + * describe( + * 'A black curve drawn on a gray background. The curve has black dots at its ends. Two red dots appear near the bottom of the canvas.' + * ); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the shape. + * noFill(); + * strokeWeight(1); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the first control point and draw a segment to it. + * curveVertex(32, 91); + * curveVertex(32, 91); + * + * // Add the anchor points. + * curveVertex(21, 17); + * curveVertex(68, 19); + * + * // Add the second control point. + * curveVertex(84, 91); + * + * // Stop drawing the shape. + * endShape(); + * + * // Style the anchor and control points. + * strokeWeight(5); + * + * // Draw the anchor points in black. + * stroke(0); + * point(21, 17); + * point(68, 19); + * + * // Draw the control points in red. + * stroke(255, 0, 0); + * point(32, 91); + * point(84, 91); + * + * describe( + * 'A black curve drawn on a gray background. The curve passes through one red dot and two black dots. Another red dot appears near the bottom of the canvas.' + * ); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the shape. + * noFill(); + * strokeWeight(1); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the first control point and draw a segment to it. + * curveVertex(32, 91); + * curveVertex(32, 91); + * + * // Add the anchor points. + * curveVertex(21, 17); + * curveVertex(68, 19); + * + * // Add the second control point and draw a segment to it. + * curveVertex(84, 91); + * curveVertex(84, 91); + * + * // Stop drawing the shape. + * endShape(); + * + * // Style the anchor and control points. + * strokeWeight(5); + * + * // Draw the anchor points in black. + * stroke(0); + * point(21, 17); + * point(68, 19); + * + * // Draw the control points in red. + * stroke(255, 0, 0); + * point(32, 91); + * point(84, 91); + * + * describe( + * 'A black U curve drawn upside down on a gray background. The curve passes from one red dot through two black dots and ends at another red dot.' + * ); + * } + * + *
+ * + *
+ * + * // Click the mouse near the red dot in the bottom-left corner + * // and drag to change the curve's shape. + * + * let x1 = 32; + * let y1 = 91; + * let isChanging = false; + * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * 'A black U curve drawn upside down on a gray background. The curve passes from one red dot through two black dots and ends at another red dot.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Style the shape. + * noFill(); + * stroke(0); + * strokeWeight(1); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the first control point and draw a segment to it. + * curveVertex(x1, y1); + * curveVertex(x1, y1); + * + * // Add the anchor points. + * curveVertex(21, 17); + * curveVertex(68, 19); + * + * // Add the second control point and draw a segment to it. + * curveVertex(84, 91); + * curveVertex(84, 91); + * + * // Stop drawing the shape. + * endShape(); + * + * // Style the anchor and control points. + * strokeWeight(5); + * + * // Draw the anchor points in black. + * stroke(0); + * point(21, 17); + * point(68, 19); + * + * // Draw the control points in red. + * stroke(255, 0, 0); + * point(x1, y1); + * point(84, 91); + * } + * + * // Start changing the first control point if the user clicks near it. + * function mousePressed() { + * if (dist(mouseX, mouseY, x1, y1) < 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) { + * x1 = mouseX; + * y1 = mouseY; + * } + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the first control point and draw a segment to it. + * curveVertex(32, 91); + * curveVertex(32, 91); + * + * // Add the anchor points. + * curveVertex(21, 17); + * curveVertex(68, 19); + * + * // Add the second control point. + * curveVertex(84, 91); + * curveVertex(84, 91); + * + * // Stop drawing the shape. + * endShape(); + * + * describe('A ghost shape drawn in white on a gray background.'); + * } + * + *
+ */ + + /** + * @method curveVertex + * @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(); + * curveVertex(-28, 41, 0); + * curveVertex(-28, 41, 0); + * curveVertex(-29, -33, 0); + * curveVertex(18, -31, 0); + * curveVertex(34, 41, 0); + * curveVertex(34, 41, 0); + * endShape(); + * + * // Draw the second ghost. + * noFill(); + * stroke('ghostwhite'); + * + * beginShape(); + * curveVertex(-28, 41, -20); + * curveVertex(-28, 41, -20); + * curveVertex(-29, -33, -20); + * curveVertex(18, -31, -20); + * curveVertex(34, 41, -20); + * curveVertex(34, 41, -20); + * endShape(); + * } + * + *
+ */ + fn.curveVertex = function(...args) { + p5._validateParameters('curveVertex', args); + if (this._renderer.isP3D) { + this._renderer.curveVertex(...args); + } else { + isCurve = true; + this.vertex(args[0], args[1]); + } + return this; + }; + + /** + * 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(). + * + * 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 + * @chainable + * + * @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(); + * + * // Stop drawing the shape. + * endShape(CLOSE); + * + * describe('A white square with a square hole in its center drawn on a gray background.'); + * } + * + *
+ * + *
+ * + * // 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(); + * + * // Stop drawing the shape. + * endShape(CLOSE); + * } + * + *
+ */ + fn.endContour = function() { + if (this._renderer.isP3D) { + return this; + } + + const vert = contourVertices[0].slice(); // copy all data + vert.isVert = contourVertices[0].isVert; + vert.moveTo = false; + contourVertices.push(vert); + + // prevent stray lines with multiple contours + if (isFirstContour) { + vertices.push(vertices[0]); + isFirstContour = false; + } + + for (let i = 0; i < contourVertices.length; i++) { + vertices.push(contourVertices[i]); + } + return this; + }; + + /** + * 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 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. + * + * 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(), + * quadraticVertex(), and/or + * curveVertex(). 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.' + * ); + * } + * + *
+ * + *
+ * + * // 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; + } + + if (this._renderer.isP3D) { + this._renderer.endShape( + mode, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind, + count + ); + } else { + if (count !== 1) { + console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + } + if (vertices.length === 0) { + return this; + } + if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { + return this; + } + + const closeShape = mode === constants.CLOSE; + + // if the shape is closed, the first element is also the last element + if (closeShape && !isContour) { + vertices.push(vertices[0]); + } + + this._renderer.endShape( + mode, + vertices, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind + ); + + // Reset some settings + isCurve = false; + isBezier = false; + isQuadratic = false; + isContour = false; + isFirstContour = true; + + // If the shape is closed, the first element was added as last element. + // We must remove it again to prevent the list of vertices from growing + // over successive calls to endShape(CLOSE) + if (closeShape) { + vertices.pop(); + } + } + return this; + }; + + /** + * Adds a quadratic Bézier curve segment to a custom shape. + * + * `quadraticVertex()` adds a curved segment to custom shapes. The Bézier + * curve segments it creates are similar to those made by the + * bezierVertex() function. + * `quadraticVertex()` must be called between the + * beginShape() and + * endShape() functions. The curved segment uses + * the previous vertex as the first anchor point, so there must be at least + * one call to vertex() before `quadraticVertex()` can + * be used. + * + * The first two parameters, `cx` and `cy`, set the curve’s control point. + * The control point "pulls" the curve towards its. + * + * The last two parameters, `x3`, and `y3`, 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 + * `bezierVertex()` has eight arguments because each point has x-, y-, and + * z-coordinates. + * + * Note: `quadraticVertex()` won’t work when an argument is passed to + * beginShape(). + * + * @method quadraticVertex + * @param {Number} cx x-coordinate of the control point. + * @param {Number} cy y-coordinate of the control point. + * @param {Number} x3 x-coordinate of the anchor point. + * @param {Number} y3 y-coordinate of the anchor point. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the curve. + * noFill(); + * + * // Draw the curve. + * beginShape(); + * vertex(20, 20); + * quadraticVertex(80, 20, 50, 50); + * endShape(); + * + * describe('A black curve drawn on a gray square. The curve starts at the top-left corner and ends at the center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Draw the curve. + * noFill(); + * beginShape(); + * vertex(20, 20); + * quadraticVertex(80, 20, 50, 50); + * endShape(); + * + * // Draw red lines from the anchor points to the control point. + * stroke(255, 0, 0); + * line(20, 20, 80, 20); + * line(50, 50, 80, 20); + * + * // Draw the anchor points in black. + * strokeWeight(5); + * stroke(0); + * point(20, 20); + * point(50, 50); + * + * // Draw the control point in red. + * stroke(255, 0, 0); + * point(80, 20); + * + * describe('A black curve that starts at the top-left corner and ends at the center. Its anchor and control points are marked with dots. Red lines connect both anchor points to the control point.'); + * } + * + *
+ * + *
+ * + * // 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 = 20; + * let isChanging = false; + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A black curve that starts at the top-left corner and ends at the center. Its anchor and control points are marked with dots. Red lines connect both anchor points to the control point.'); + * } + * + * function draw() { + * background(200); + * + * // Style the curve. + * noFill(); + * strokeWeight(1); + * stroke(0); + * + * // Draw the curve. + * beginShape(); + * vertex(20, 20); + * quadraticVertex(x2, y2, 50, 50); + * endShape(); + * + * // Draw red lines from the anchor points to the control point. + * stroke(255, 0, 0); + * line(20, 20, x2, y2); + * line(50, 50, x2, y2); + * + * // Draw the anchor points in black. + * strokeWeight(5); + * stroke(0); + * point(20, 20); + * point(50, 50); + * + * // Draw the control point in red. + * stroke(255, 0, 0); + * point(x2, y2); + * } + * + * // 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; + * } + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Start drawing the shape. + * beginShape(); + * + * // Add the curved segments. + * vertex(20, 20); + * quadraticVertex(80, 20, 50, 50); + * quadraticVertex(20, 80, 80, 80); + * + * // Add the straight segments. + * vertex(80, 10); + * vertex(20, 10); + * vertex(20, 20); + * + * // Stop drawing the shape. + * endShape(); + * + * describe('A white puzzle piece drawn on a gray background.'); + * } + * + *
+ * + *
+ * + * // Click the and drag the mouse to view the scene from a different angle. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white puzzle piece on a dark gray background. When the user clicks and drags the scene, the outline of a second puzzle piece is revealed.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the first puzzle piece. + * noStroke(); + * fill(255); + * + * // Draw the first puzzle piece. + * beginShape(); + * vertex(-30, -30, 0); + * quadraticVertex(30, -30, 0, 0, 0, 0); + * quadraticVertex(-30, 30, 0, 30, 30, 0); + * vertex(30, -40, 0); + * vertex(-30, -40, 0); + * vertex(-30, -30, 0); + * endShape(); + * + * // Style the second puzzle piece. + * stroke(255); + * noFill(); + * + * // Draw the second puzzle piece. + * beginShape(); + * vertex(-30, -30, -20); + * quadraticVertex(30, -30, -20, 0, 0, -20); + * quadraticVertex(-30, 30, -20, 30, 30, -20); + * vertex(30, -40, -20); + * vertex(-30, -40, -20); + * vertex(-30, -30, -20); + * endShape(); + * } + * + *
+ */ + + /** + * @method quadraticVertex + * @param {Number} cx + * @param {Number} cy + * @param {Number} cz z-coordinate of the control point. + * @param {Number} x3 + * @param {Number} y3 + * @param {Number} z3 z-coordinate of the anchor point. + */ + fn.quadraticVertex = function(...args) { + p5._validateParameters('quadraticVertex', args); + if (this._renderer.isP3D) { + this._renderer.quadraticVertex(...args); + } else { + //if we're drawing a contour, put the points into an + // array for inside drawing + if (this._contourInited) { + const pt = {}; + pt.x = args[0]; + pt.y = args[1]; + pt.x3 = args[2]; + pt.y3 = args[3]; + pt.type = constants.QUADRATIC; + this._contourVertices.push(pt); + + return this; + } + if (vertices.length > 0) { + isQuadratic = true; + const vert = []; + for (let i = 0; i < args.length; i++) { + vert[i] = args[i]; + } + vert.isVert = false; + if (isContour) { + contourVertices.push(vert); + } else { + vertices.push(vert); + } + } else { + p5._friendlyError( + 'vertex() must be used once before calling quadraticVertex()', + 'quadraticVertex' + ); + } + } + return this; + }; + + /** + * 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. + * @chainable + * + * @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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load an image to apply as a texture. + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * 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(); + * } + * + *
+ */ + /** + * @method vertex + * @param {Number} x + * @param {Number} y + * @param {Number} [z] z-coordinate of the vertex. Defaults to 0. + * @chainable + */ + /** + * @method vertex + * @param {Number} x + * @param {Number} y + * @param {Number} [z] + * @param {Number} [u] u-coordinate of the vertex's texture. Defaults to 0. + * @param {Number} [v] v-coordinate of the vertex's texture. Defaults to 0. + * @chainable + */ + fn.vertex = function(x, y, moveTo, u, v) { + if (this._renderer.isP3D) { + this._renderer.vertex(...arguments); + } else { + const vert = []; + vert.isVert = true; + vert[0] = x; + vert[1] = y; + vert[2] = 0; + vert[3] = 0; + vert[4] = 0; + vert[5] = this._renderer._getFill(); + vert[6] = this._renderer._getStroke(); + + if (moveTo) { + vert.moveTo = moveTo; + } + if (isContour) { + if (contourVertices.length === 0) { + vert.moveTo = true; + } + contourVertices.push(vert); + } else { + vertices.push(vert); + } + } + return this; + }; + + /** + * 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: + * + * + * 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(); + * } + * + *
+ * + *
+ * + * // 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(); + * } + * + *
+ * + *
+ * + * // 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; + }; +} + +export default vertex; + +if(typeof p5 !== 'undefined'){ + vertex(p5, p5.prototype); +} From db6e599c73f50257070cd6be2f08aca712e0c874 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 26 Sep 2024 13:20:20 +0100 Subject: [PATCH 18/55] Use new module API to attach methods to p5.Graphics --- src/core/main.js | 2 +- src/core/p5.Graphics.js | 81 +++++++++++++++++++++++++-------- src/core/p5.Renderer2D.js | 10 +--- src/image/loading_displaying.js | 4 +- 4 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index 9c8f2875ee..494143de4c 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -425,7 +425,7 @@ class p5 { } } -// attach constants to p5 prototype +// Attach constants to p5 prototype for (const k in constants) { p5.prototype[k] = constants[k]; } diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index a59ab7ff7d..28db13db9f 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -6,6 +6,14 @@ import p5 from './main'; import * as constants from './constants'; +import primitives2D from '../shape/2d_primitives'; +import attributes from '../shape/attributes'; +import curves from '../shape/curves'; +import vertex from '../shape/vertex'; +import setting from '../color/setting'; +import image from '../image/image'; +import loadingDisplaying from '../image/loading_displaying'; +import pixels from '../image/pixels'; /** * A class to describe a drawing surface that's separate from the main canvas. @@ -100,16 +108,16 @@ p5.Graphics = class Graphics { this._renderer = new p5.renderers[r](this._pInst, w, h, false, canvas); // Attach renderer methods - for(const p of Object.getOwnPropertyNames(p5.renderers[r].prototype)) { - if( - p !== 'constructor' && - p[0] !== '_' && - !(p in this) && - typeof this._renderer[p] === 'function' - ){ - this[p] = this._renderer[p].bind(this._renderer); - } - } + // for(const p of Object.getOwnPropertyNames(p5.renderers[r].prototype)) { + // if( + // p !== 'constructor' && + // p[0] !== '_' && + // !(p in this) && + // typeof this._renderer[p] === 'function' + // ){ + // this[p] = this._renderer[p].bind(this._renderer); + // } + // } // Attach renderer properties for (const p in this._renderer) { @@ -122,26 +130,48 @@ p5.Graphics = class Graphics { } // bind methods and props of p5 to the new object - for (const p in p5.prototype) { - if (!this[p]) { - // console.log(p); - if (typeof p5.prototype[p] === 'function') { - this[p] = p5.prototype[p].bind(this); - } else if(p !== 'deltaTime') { - this[p] = p5.prototype[p]; - } - } - } + // for (const p in p5.prototype) { + // if (!this[p]) { + // // console.log(p); + // if (typeof p5.prototype[p] === 'function') { + // this[p] = p5.prototype[p].bind(this); + // } else if(p !== 'deltaTime') { + // this[p] = p5.prototype[p]; + // } + // } + // } p5.prototype._initializeInstanceVariables.apply(this); this._renderer._applyDefaults(); + this._renderer.scale(this._renderer._pixelDensity, this._renderer._pixelDensity); return this; } + // NOTE: Temporary no op placeholder + static _validateParameters(){ + + } + get deltaTime(){ return this._pInst.deltaTime; } + // get canvas(){ + // return this._renderer.canvas; + // } + + // get drawingContext(){ + // return this._renderer.drawingContext; + // } + + // get width(){ + // return this._renderer.width; + // } + + // get height(){ + // return this._renderer.height; + // } + pixelDensity(val){ let returnValue; if (typeof val === 'number') { @@ -638,4 +668,15 @@ p5.Graphics = class Graphics { } }; +// Shapes +primitives2D(p5.Graphics, p5.Graphics.prototype); +attributes(p5.Graphics, p5.Graphics.prototype); +curves(p5.Graphics, p5.Graphics.prototype); +vertex(p5.Graphics, p5.Graphics.prototype); + +setting(p5.Graphics, p5.Graphics.prototype); +loadingDisplaying(p5.Graphics, p5.Graphics.prototype); +image(p5.Graphics, p5.Graphics.prototype); +pixels(p5.Graphics, p5.Graphics.prototype); + export default p5.Graphics; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 846f439ce8..459446bf41 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -343,15 +343,7 @@ class Renderer2D extends Renderer { if (this._isErasing) { this.blendMode(this._cachedBlendMode); } - // console.log(this.elt, cnv, - // s * sx, - // s * sy, - // s * sWidth, - // s * sHeight, - // dx, - // dy, - // dWidth, - // dHeight); + this.drawingContext.drawImage( cnv, s * sx, diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 2044478a8e..9fea1d4cac 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1401,8 +1401,8 @@ function loadingDisplaying(p5, fn){ * @param {p5.Image} The image to be tinted * @return {canvas} The resulting tinted canvas */ - fn._getTintedImageCanvas = - p5.Renderer2D.prototype._getTintedImageCanvas; + // fn._getTintedImageCanvas = + // p5.Renderer2D.prototype._getTintedImageCanvas; /** * Changes the location from which images are drawn when From 7cd3fb36ffc99410394dde3d351a504fad4c843a Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 27 Sep 2024 14:09:31 +0100 Subject: [PATCH 19/55] Attach more functions to p5.Graphics Move "push()" and "pop()" functions to core/transform module --- src/app.js | 3 +- src/core/p5.Graphics.js | 4 +- src/core/p5.Renderer.js | 4 + src/core/p5.Renderer2D.js | 1 + src/core/structure.js | 558 ------- src/core/transform.js | 3338 ++++++++++++++++++++++--------------- 6 files changed, 1960 insertions(+), 1948 deletions(-) diff --git a/src/app.js b/src/app.js index 8ee08375b5..3b8c365385 100644 --- a/src/app.js +++ b/src/app.js @@ -16,7 +16,8 @@ import './core/p5.Graphics'; import './core/p5.Renderer2D'; import './core/rendering'; import './core/structure'; -import './core/transform'; +import transform from './core/transform'; +p5.registerAddon(transform); import shape from './shape'; shape(p5); diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 28db13db9f..4416b4f0fd 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -14,6 +14,7 @@ import setting from '../color/setting'; import image from '../image/image'; import loadingDisplaying from '../image/loading_displaying'; import pixels from '../image/pixels'; +import transform from './transform'; /** * A class to describe a drawing surface that's separate from the main canvas. @@ -143,7 +144,6 @@ p5.Graphics = class Graphics { p5.prototype._initializeInstanceVariables.apply(this); this._renderer._applyDefaults(); - this._renderer.scale(this._renderer._pixelDensity, this._renderer._pixelDensity); return this; } @@ -679,4 +679,6 @@ loadingDisplaying(p5.Graphics, p5.Graphics.prototype); image(p5.Graphics, p5.Graphics.prototype); pixels(p5.Graphics, p5.Graphics.prototype); +transform(p5.Graphics, p5.Graphics.prototype); + export default p5.Graphics; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 29abe11a8f..0f9f8f795d 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -160,6 +160,10 @@ p5.Renderer = class Renderer { return region; } + scale(x, y){ + + } + textSize(s) { if (typeof s === 'number') { this.states.textSize = s; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 459446bf41..72fd47610e 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -65,6 +65,7 @@ class Renderer2D extends Renderer { // Get and store drawing context this.drawingContext = this.canvas.getContext('2d'); this._pInst.drawingContext = this.drawingContext; + this.scale(this._pixelDensity, this._pixelDensity); // Set and return p5.Element this.wrappedElt = new p5.Element(this.elt, this._pInst); diff --git a/src/core/structure.js b/src/core/structure.js index ff81ff4383..f614c47442 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -266,564 +266,6 @@ p5.prototype.isLooping = function() { return this._loop; }; -/** - * Begins a drawing group that contains its own styles and transformations. - * - * By default, styles such as fill() and - * transformations such as rotate() are applied to - * all drawing that follows. The `push()` and pop() - * functions can limit the effect of styles and transformations to a specific - * group of shapes, images, and text. For example, a group of shapes could be - * translated to follow the mouse without affecting the rest of the sketch: - * - * ```js - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the mouse's position. - * translate(mouseX, mouseY); - * - * // Style the face. - * noStroke(); - * fill('green'); - * - * // Draw the face. - * circle(0, 0, 60); - * - * // Style the eyes. - * fill('white'); - * - * // Draw the left eye. - * ellipse(-20, -20, 30, 20); - * - * // Draw the right eye. - * ellipse(20, -20, 30, 20); - * - * // End the drawing group. - * pop(); - * - * // Draw a bug. - * let x = random(0, 100); - * let y = random(0, 100); - * text('🦟', x, y); - * ``` - * - * In the code snippet above, the bug's position isn't affected by - * `translate(mouseX, mouseY)` because that transformation is contained - * between `push()` and pop(). The bug moves around - * the entire canvas as expected. - * - * Note: `push()` and pop() are always called as a - * pair. Both functions are required to begin and end a drawing group. - * - * `push()` and pop() can also be nested to create - * subgroups. For example, the code snippet above could be changed to give - * more detail to the frog’s eyes: - * - * ```js - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the mouse's position. - * translate(mouseX, mouseY); - * - * // Style the face. - * noStroke(); - * fill('green'); - * - * // Draw a face. - * circle(0, 0, 60); - * - * // Style the eyes. - * fill('white'); - * - * // Draw the left eye. - * push(); - * translate(-20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // Draw the right eye. - * push(); - * translate(20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // End the drawing group. - * pop(); - * - * // Draw a bug. - * let x = random(0, 100); - * let y = random(0, 100); - * text('🦟', x, y); - * ``` - * - * In this version, the code to draw each eye is contained between its own - * `push()` and pop() functions. Doing so makes it - * easier to add details in the correct part of a drawing. - * - * `push()` and pop() contain the effects of the - * following functions: - * - * - fill() - * - noFill() - * - noStroke() - * - stroke() - * - tint() - * - noTint() - * - strokeWeight() - * - strokeCap() - * - strokeJoin() - * - imageMode() - * - rectMode() - * - ellipseMode() - * - colorMode() - * - textAlign() - * - textFont() - * - textSize() - * - textLeading() - * - applyMatrix() - * - resetMatrix() - * - rotate() - * - scale() - * - shearX() - * - shearY() - * - translate() - * - * In WebGL mode, `push()` and pop() contain the - * effects of a few additional styles: - * - * - setCamera() - * - ambientLight() - * - directionalLight() - * - pointLight() texture() - * - specularMaterial() - * - shininess() - * - normalMaterial() - * - shader() - * - * @method push - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Draw the left circle. - * circle(25, 50, 20); - * - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the center. - * translate(50, 50); - * - * // Style the circle. - * strokeWeight(5); - * stroke('royalblue'); - * fill('orange'); - * - * // Draw the circle. - * circle(0, 0, 20); - * - * // End the drawing group. - * pop(); - * - * // Draw the right circle. - * circle(75, 50, 20); - * - * describe( - * 'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.' - * ); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Slow the frame rate. - * frameRate(24); - * - * describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.'); - * } - * - * function draw() { - * background(200); - * - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the mouse's position. - * translate(mouseX, mouseY); - * - * // Style the face. - * noStroke(); - * fill('green'); - * - * // Draw a face. - * circle(0, 0, 60); - * - * // Style the eyes. - * fill('white'); - * - * // Draw the left eye. - * push(); - * translate(-20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // Draw the right eye. - * push(); - * translate(20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // End the drawing group. - * pop(); - * - * // Draw a bug. - * let x = random(0, 100); - * let y = random(0, 100); - * text('🦟', x, y); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the red sphere. - * push(); - * translate(-25, 0, 0); - * noStroke(); - * directionalLight(255, 0, 0, 0, 0, -1); - * sphere(20); - * pop(); - * - * // Draw the blue sphere. - * push(); - * translate(25, 0, 0); - * strokeWeight(0.3); - * stroke(0, 0, 255); - * noFill(); - * sphere(20); - * pop(); - * } - * - *
- */ -p5.prototype.push = function() { - this._renderer.push(); -}; - -/** - * Ends a drawing group that contains its own styles and transformations. - * - * By default, styles such as fill() and - * transformations such as rotate() are applied to - * all drawing that follows. The push() and `pop()` - * functions can limit the effect of styles and transformations to a specific - * group of shapes, images, and text. For example, a group of shapes could be - * translated to follow the mouse without affecting the rest of the sketch: - * - * ```js - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the mouse's position. - * translate(mouseX, mouseY); - * - * // Style the face. - * noStroke(); - * fill('green'); - * - * // Draw the face. - * circle(0, 0, 60); - * - * // Style the eyes. - * fill('white'); - * - * // Draw the left eye. - * ellipse(-20, -20, 30, 20); - * - * // Draw the right eye. - * ellipse(20, -20, 30, 20); - * - * // End the drawing group. - * pop(); - * - * // Draw a bug. - * let x = random(0, 100); - * let y = random(0, 100); - * text('🦟', x, y); - * ``` - * - * In the code snippet above, the bug's position isn't affected by - * `translate(mouseX, mouseY)` because that transformation is contained - * between push() and `pop()`. The bug moves around - * the entire canvas as expected. - * - * Note: push() and `pop()` are always called as a - * pair. Both functions are required to begin and end a drawing group. - * - * push() and `pop()` can also be nested to create - * subgroups. For example, the code snippet above could be changed to give - * more detail to the frog’s eyes: - * - * ```js - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the mouse's position. - * translate(mouseX, mouseY); - * - * // Style the face. - * noStroke(); - * fill('green'); - * - * // Draw a face. - * circle(0, 0, 60); - * - * // Style the eyes. - * fill('white'); - * - * // Draw the left eye. - * push(); - * translate(-20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // Draw the right eye. - * push(); - * translate(20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // End the drawing group. - * pop(); - * - * // Draw a bug. - * let x = random(0, 100); - * let y = random(0, 100); - * text('🦟', x, y); - * ``` - * - * In this version, the code to draw each eye is contained between its own - * push() and `pop()` functions. Doing so makes it - * easier to add details in the correct part of a drawing. - * - * push() and `pop()` contain the effects of the - * following functions: - * - * - fill() - * - noFill() - * - noStroke() - * - stroke() - * - tint() - * - noTint() - * - strokeWeight() - * - strokeCap() - * - strokeJoin() - * - imageMode() - * - rectMode() - * - ellipseMode() - * - colorMode() - * - textAlign() - * - textFont() - * - textSize() - * - textLeading() - * - applyMatrix() - * - resetMatrix() - * - rotate() - * - scale() - * - shearX() - * - shearY() - * - translate() - * - * In WebGL mode, push() and `pop()` contain the - * effects of a few additional styles: - * - * - setCamera() - * - ambientLight() - * - directionalLight() - * - pointLight() texture() - * - specularMaterial() - * - shininess() - * - normalMaterial() - * - shader() - * - * @method pop - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Draw the left circle. - * circle(25, 50, 20); - * - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the center. - * translate(50, 50); - * - * // Style the circle. - * strokeWeight(5); - * stroke('royalblue'); - * fill('orange'); - * - * // Draw the circle. - * circle(0, 0, 20); - * - * // End the drawing group. - * pop(); - * - * // Draw the right circle. - * circle(75, 50, 20); - * - * describe( - * 'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.' - * ); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Slow the frame rate. - * frameRate(24); - * - * describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.'); - * } - * - * function draw() { - * background(200); - * - * // Begin the drawing group. - * push(); - * - * // Translate the origin to the mouse's position. - * translate(mouseX, mouseY); - * - * // Style the face. - * noStroke(); - * fill('green'); - * - * // Draw a face. - * circle(0, 0, 60); - * - * // Style the eyes. - * fill('white'); - * - * // Draw the left eye. - * push(); - * translate(-20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // Draw the right eye. - * push(); - * translate(20, -20); - * ellipse(0, 0, 30, 20); - * fill('black'); - * circle(0, 0, 8); - * pop(); - * - * // End the drawing group. - * pop(); - * - * // Draw a bug. - * let x = random(0, 100); - * let y = random(0, 100); - * text('🦟', x, y); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the red sphere. - * push(); - * translate(-25, 0, 0); - * noStroke(); - * directionalLight(255, 0, 0, 0, 0, -1); - * sphere(20); - * pop(); - * - * // Draw the blue sphere. - * push(); - * translate(25, 0, 0); - * strokeWeight(0.3); - * stroke(0, 0, 255); - * noFill(); - * sphere(20); - * pop(); - * } - * - *
- */ -p5.prototype.pop = function() { - this._renderer.pop(); -}; - /** * Runs the code in draw() once. * diff --git a/src/core/transform.js b/src/core/transform.js index 911d074d1a..e6e33c47e9 100644 --- a/src/core/transform.js +++ b/src/core/transform.js @@ -6,1404 +6,1966 @@ * @requires constants */ -import p5 from './main'; +function transform(p5, fn){ + /** + * Applies a transformation matrix to the coordinate system. + * + * Transformations such as + * translate(), + * rotate(), and + * scale() + * use matrix-vector multiplication behind the scenes. A table of numbers, + * called a matrix, encodes each transformation. The values in the matrix + * then multiply each point on the canvas, which is represented by a vector. + * + * `applyMatrix()` allows for many transformations to be applied at once. See + * Wikipedia + * and MDN + * for more details about transformations. + * + * There are two ways to call `applyMatrix()` in two and three dimensions. + * + * In 2D mode, the parameters `a`, `b`, `c`, `d`, `e`, and `f`, correspond to + * elements in the following transformation matrix: + * + * > The transformation matrix used when applyMatrix is called in 2D mode. + * + * 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 transformation matrix used when applyMatrix is called in 3D mode. + * + * 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} arr an array containing the elements of the transformation matrix. Its length should be either 6 (2D) or 16 (3D). + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white circle on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Translate the origin to the center. + * applyMatrix(1, 0, 0, 1, 50, 50); + * + * // Draw the circle at coordinates (0, 0). + * circle(0, 0, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white circle on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Translate the origin to the center. + * let m = [1, 0, 0, 1, 50, 50]; + * applyMatrix(m); + * + * // Draw the circle at coordinates (0, 0). + * circle(0, 0, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe("A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right."); + * } + * + * function draw() { + * background(200); + * + * // Rotate the coordinate system 1/8 turn. + * let angle = QUARTER_PI; + * let ca = cos(angle); + * let sa = sin(angle); + * applyMatrix(ca, sa, -sa, ca, 0, 0); + * + * // Draw a rectangle at coordinates (50, 0). + * rect(50, 0, 40, 20); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * 'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Draw a square at (30, 20). + * square(30, 20, 40); + * + * // Scale the coordinate system by a factor of 0.5. + * applyMatrix(0.5, 0, 0, 0.5, 0, 0); + * + * // Draw a square at (30, 20). + * // It appears at (15, 10) after scaling. + * square(30, 20, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white quadrilateral on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Calculate the shear factor. + * let angle = QUARTER_PI; + * let shearFactor = 1 / tan(HALF_PI - angle); + * + * // Shear the coordinate system along the x-axis. + * applyMatrix(1, 0, shearFactor, 1, 0, 0); + * + * // Draw the square. + * square(0, 0, 50); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube rotates slowly against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system a little more each frame. + * let angle = frameCount * 0.01; + * let ca = cos(angle); + * let sa = sin(angle); + * applyMatrix(ca, 0, sa, 0, 0, 1, 0, 0, -sa, 0, ca, 0, 0, 0, 0, 1); + * + * // Draw a box. + * box(); + * } + * + *
+ */ + /** + * @method applyMatrix + * @param {Number} a an element of the transformation matrix. + * @param {Number} b an element of the transformation matrix. + * @param {Number} c an element of the transformation matrix. + * @param {Number} d an element of the transformation matrix. + * @param {Number} e an element of the transformation matrix. + * @param {Number} f an element of the transformation matrix. + * @chainable + */ + /** + * @method applyMatrix + * @param {Number} a + * @param {Number} b + * @param {Number} c + * @param {Number} d + * @param {Number} e + * @param {Number} f + * @param {Number} g an element of the transformation matrix. + * @param {Number} h an element of the transformation matrix. + * @param {Number} i an element of the transformation matrix. + * @param {Number} j an element of the transformation matrix. + * @param {Number} k an element of the transformation matrix. + * @param {Number} l an element of the transformation matrix. + * @param {Number} m an element of the transformation matrix. + * @param {Number} n an element of the transformation matrix. + * @param {Number} o an element of the transformation matrix. + * @param {Number} p an element of the transformation matrix. + * @chainable + */ + fn.applyMatrix = function(...args) { + let isTypedArray = args[0] instanceof Object.getPrototypeOf(Uint8Array); + if (Array.isArray(args[0]) || isTypedArray) { + this._renderer.applyMatrix(...args[0]); + } else { + this._renderer.applyMatrix(...args); + } + return this; + }; -/** - * Applies a transformation matrix to the coordinate system. - * - * Transformations such as - * translate(), - * rotate(), and - * scale() - * use matrix-vector multiplication behind the scenes. A table of numbers, - * called a matrix, encodes each transformation. The values in the matrix - * then multiply each point on the canvas, which is represented by a vector. - * - * `applyMatrix()` allows for many transformations to be applied at once. See - * Wikipedia - * and MDN - * for more details about transformations. - * - * There are two ways to call `applyMatrix()` in two and three dimensions. - * - * In 2D mode, the parameters `a`, `b`, `c`, `d`, `e`, and `f`, correspond to - * elements in the following transformation matrix: - * - * > The transformation matrix used when applyMatrix is called in 2D mode. - * - * 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 transformation matrix used when applyMatrix is called in 3D mode. - * - * 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} arr an array containing the elements of the transformation matrix. Its length should be either 6 (2D) or 16 (3D). - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white circle on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Translate the origin to the center. - * applyMatrix(1, 0, 0, 1, 50, 50); - * - * // Draw the circle at coordinates (0, 0). - * circle(0, 0, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white circle on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Translate the origin to the center. - * let m = [1, 0, 0, 1, 50, 50]; - * applyMatrix(m); - * - * // Draw the circle at coordinates (0, 0). - * circle(0, 0, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe("A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right."); - * } - * - * function draw() { - * background(200); - * - * // Rotate the coordinate system 1/8 turn. - * let angle = QUARTER_PI; - * let ca = cos(angle); - * let sa = sin(angle); - * applyMatrix(ca, sa, -sa, ca, 0, 0); - * - * // Draw a rectangle at coordinates (50, 0). - * rect(50, 0, 40, 20); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * 'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Draw a square at (30, 20). - * square(30, 20, 40); - * - * // Scale the coordinate system by a factor of 0.5. - * applyMatrix(0.5, 0, 0, 0.5, 0, 0); - * - * // Draw a square at (30, 20). - * // It appears at (15, 10) after scaling. - * square(30, 20, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white quadrilateral on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Calculate the shear factor. - * let angle = QUARTER_PI; - * let shearFactor = 1 / tan(HALF_PI - angle); - * - * // Shear the coordinate system along the x-axis. - * applyMatrix(1, 0, shearFactor, 1, 0, 0); - * - * // Draw the square. - * square(0, 0, 50); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube rotates slowly against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system a little more each frame. - * let angle = frameCount * 0.01; - * let ca = cos(angle); - * let sa = sin(angle); - * applyMatrix(ca, 0, sa, 0, 0, 1, 0, 0, -sa, 0, ca, 0, 0, 0, 0, 1); - * - * // Draw a box. - * box(); - * } - * - *
- */ -/** - * @method applyMatrix - * @param {Number} a an element of the transformation matrix. - * @param {Number} b an element of the transformation matrix. - * @param {Number} c an element of the transformation matrix. - * @param {Number} d an element of the transformation matrix. - * @param {Number} e an element of the transformation matrix. - * @param {Number} f an element of the transformation matrix. - * @chainable - */ -/** - * @method applyMatrix - * @param {Number} a - * @param {Number} b - * @param {Number} c - * @param {Number} d - * @param {Number} e - * @param {Number} f - * @param {Number} g an element of the transformation matrix. - * @param {Number} h an element of the transformation matrix. - * @param {Number} i an element of the transformation matrix. - * @param {Number} j an element of the transformation matrix. - * @param {Number} k an element of the transformation matrix. - * @param {Number} l an element of the transformation matrix. - * @param {Number} m an element of the transformation matrix. - * @param {Number} n an element of the transformation matrix. - * @param {Number} o an element of the transformation matrix. - * @param {Number} p an element of the transformation matrix. - * @chainable - */ -p5.prototype.applyMatrix = function(...args) { - let isTypedArray = args[0] instanceof Object.getPrototypeOf(Uint8Array); - if (Array.isArray(args[0]) || isTypedArray) { - this._renderer.applyMatrix(...args[0]); - } else { - this._renderer.applyMatrix(...args); - } - return this; -}; + /** + * Clears all transformations applied to the coordinate system. + * + * @method resetMatrix + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * 'Two circles drawn on a gray background. A blue circle is at the top-left and a red circle is at the bottom-right.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Translate the origin to the center. + * translate(50, 50); + * + * // Draw a blue circle at the coordinates (25, 25). + * fill('blue'); + * circle(25, 25, 20); + * + * // Clear all transformations. + * // The origin is now at the top-left corner. + * resetMatrix(); + * + * // Draw a red circle at the coordinates (25, 25). + * fill('red'); + * circle(25, 25, 20); + * } + * + *
+ */ + fn.resetMatrix = function() { + this._renderer.resetMatrix(); + return this; + }; -/** - * Clears all transformations applied to the coordinate system. - * - * @method resetMatrix - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * 'Two circles drawn on a gray background. A blue circle is at the top-left and a red circle is at the bottom-right.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Translate the origin to the center. - * translate(50, 50); - * - * // Draw a blue circle at the coordinates (25, 25). - * fill('blue'); - * circle(25, 25, 20); - * - * // Clear all transformations. - * // The origin is now at the top-left corner. - * resetMatrix(); - * - * // Draw a red circle at the coordinates (25, 25). - * fill('red'); - * circle(25, 25, 20); - * } - * - *
- */ -p5.prototype.resetMatrix = function() { - this._renderer.resetMatrix(); - return this; -}; + /** + * Rotates the coordinate system. + * + * By default, the positive x-axis points to the right and the positive y-axis + * points downward. The `rotate()` function changes this orientation by + * rotating the coordinate system about the origin. Everything drawn after + * `rotate()` is called will appear to be rotated. + * + * The first parameter, `angle`, is the amount to rotate. For example, calling + * `rotate(1)` rotates the coordinate system clockwise 1 radian which is + * nearly 57˚. `rotate()` interprets angle values using the current + * angleMode(). + * + * The second parameter, `axis`, is optional. It's used to orient 3D rotations + * in WebGL mode. If a p5.Vector is passed, as in + * `rotate(QUARTER_PI, myVector)`, then the coordinate system will rotate + * `QUARTER_PI` radians about `myVector`. If an array of vector components is + * passed, as in `rotate(QUARTER_PI, [1, 0, 0])`, then the coordinate system + * will rotate `QUARTER_PI` radians about a vector with the components + * `[1, 0, 0]`. + * + * By default, transformations accumulate. For example, calling `rotate(1)` + * twice has the same effect as calling `rotate(2)` once. 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 + * `rotate(1)` inside the draw() function won't cause + * shapes to spin. + * + * @method rotate + * @param {Number} angle angle of rotation in the current angleMode(). + * @param {p5.Vector|Number[]} [axis] axis to rotate about in 3D. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." + * ); + * } + * + * function draw() { + * background(200); + * + * // Rotate the coordinate system 1/8 turn. + * rotate(QUARTER_PI); + * + * // Draw a rectangle at coordinates (50, 0). + * rect(50, 0, 40, 20); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." + * ); + * } + * + * function draw() { + * background(200); + * + * // Rotate the coordinate system 1/16 turn. + * rotate(QUARTER_PI / 2); + * + * // Rotate the coordinate system another 1/16 turn. + * rotate(QUARTER_PI / 2); + * + * // Draw a rectangle at coordinates (50, 0). + * rect(50, 0, 40, 20); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Use degrees. + * angleMode(DEGREES); + * + * describe( + * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." + * ); + * } + * + * function draw() { + * background(200); + * + * // Rotate the coordinate system 1/8 turn. + * rotate(45); + * + * // Draw a rectangle at coordinates (50, 0). + * rect(50, 0, 40, 20); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * 'A white rectangle on a gray background. The rectangle rotates slowly about the top-left corner. It disappears and reappears periodically.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Rotate the coordinate system a little more each frame. + * let angle = frameCount * 0.01; + * rotate(angle); + * + * // Draw a rectangle at coordinates (50, 0). + * rect(50, 0, 40, 20); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe("A cube on a gray background. The cube's front face points to the top-right."); + * } + * + * function draw() { + * background(200); + * + * // Rotate the coordinate system 1/8 turn about + * // the axis [1, 1, 0]. + * let axis = createVector(1, 1, 0); + * rotate(QUARTER_PI, axis); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe("A cube on a gray background. The cube's front face points to the top-right."); + * } + * + * function draw() { + * background(200); + * + * // Rotate the coordinate system 1/8 turn about + * // the axis [1, 1, 0]. + * let axis = [1, 1, 0]; + * rotate(QUARTER_PI, axis); + * + * // Draw a box. + * box(); + * } + * + *
+ */ + fn.rotate = function(angle, axis) { + p5._validateParameters('rotate', arguments); + this._renderer.rotate(this._toRadians(angle), axis); + return this; + }; -/** - * Rotates the coordinate system. - * - * By default, the positive x-axis points to the right and the positive y-axis - * points downward. The `rotate()` function changes this orientation by - * rotating the coordinate system about the origin. Everything drawn after - * `rotate()` is called will appear to be rotated. - * - * The first parameter, `angle`, is the amount to rotate. For example, calling - * `rotate(1)` rotates the coordinate system clockwise 1 radian which is - * nearly 57˚. `rotate()` interprets angle values using the current - * angleMode(). - * - * The second parameter, `axis`, is optional. It's used to orient 3D rotations - * in WebGL mode. If a p5.Vector is passed, as in - * `rotate(QUARTER_PI, myVector)`, then the coordinate system will rotate - * `QUARTER_PI` radians about `myVector`. If an array of vector components is - * passed, as in `rotate(QUARTER_PI, [1, 0, 0])`, then the coordinate system - * will rotate `QUARTER_PI` radians about a vector with the components - * `[1, 0, 0]`. - * - * By default, transformations accumulate. For example, calling `rotate(1)` - * twice has the same effect as calling `rotate(2)` once. 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 - * `rotate(1)` inside the draw() function won't cause - * shapes to spin. - * - * @method rotate - * @param {Number} angle angle of rotation in the current angleMode(). - * @param {p5.Vector|Number[]} [axis] axis to rotate about in 3D. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." - * ); - * } - * - * function draw() { - * background(200); - * - * // Rotate the coordinate system 1/8 turn. - * rotate(QUARTER_PI); - * - * // Draw a rectangle at coordinates (50, 0). - * rect(50, 0, 40, 20); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." - * ); - * } - * - * function draw() { - * background(200); - * - * // Rotate the coordinate system 1/16 turn. - * rotate(QUARTER_PI / 2); - * - * // Rotate the coordinate system another 1/16 turn. - * rotate(QUARTER_PI / 2); - * - * // Draw a rectangle at coordinates (50, 0). - * rect(50, 0, 40, 20); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Use degrees. - * angleMode(DEGREES); - * - * describe( - * "A white rectangle on a gray background. The rectangle's long axis runs from top-left to bottom-right." - * ); - * } - * - * function draw() { - * background(200); - * - * // Rotate the coordinate system 1/8 turn. - * rotate(45); - * - * // Draw a rectangle at coordinates (50, 0). - * rect(50, 0, 40, 20); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * 'A white rectangle on a gray background. The rectangle rotates slowly about the top-left corner. It disappears and reappears periodically.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Rotate the coordinate system a little more each frame. - * let angle = frameCount * 0.01; - * rotate(angle); - * - * // Draw a rectangle at coordinates (50, 0). - * rect(50, 0, 40, 20); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe("A cube on a gray background. The cube's front face points to the top-right."); - * } - * - * function draw() { - * background(200); - * - * // Rotate the coordinate system 1/8 turn about - * // the axis [1, 1, 0]. - * let axis = createVector(1, 1, 0); - * rotate(QUARTER_PI, axis); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe("A cube on a gray background. The cube's front face points to the top-right."); - * } - * - * function draw() { - * background(200); - * - * // Rotate the coordinate system 1/8 turn about - * // the axis [1, 1, 0]. - * let axis = [1, 1, 0]; - * rotate(QUARTER_PI, axis); - * - * // Draw a box. - * box(); - * } - * - *
- */ -p5.prototype.rotate = function(angle, axis) { - p5._validateParameters('rotate', arguments); - this._renderer.rotate(this._toRadians(angle), axis); - return this; -}; + /** + * Rotates the coordinate system about the x-axis in WebGL mode. + * + * The parameter, `angle`, is the amount to rotate. For example, calling + * `rotateX(1)` rotates the coordinate system about the x-axis by 1 radian. + * `rotateX()` interprets angle values using the current + * angleMode(). + * + * By default, transformations accumulate. For example, calling `rotateX(1)` + * twice has the same effect as calling `rotateX(2)` once. 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 + * `rotateX(1)` inside the draw() function won't cause + * shapes to spin. + * + * @method rotateX + * @param {Number} angle angle of rotation in the current angleMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/8 turn. + * rotateX(QUARTER_PI); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/16 turn. + * rotateX(QUARTER_PI / 2); + * + * // Rotate the coordinate system 1/16 turn. + * rotateX(QUARTER_PI / 2); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Use degrees. + * angleMode(DEGREES); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/8 turn. + * rotateX(45); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube rotates slowly against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system a little more each frame. + * let angle = frameCount * 0.01; + * rotateX(angle); + * + * // Draw a box. + * box(); + * } + * + *
+ */ + fn.rotateX = function(angle) { + this._assert3d('rotateX'); + p5._validateParameters('rotateX', arguments); + this._renderer.rotateX(this._toRadians(angle)); + return this; + }; -/** - * Rotates the coordinate system about the x-axis in WebGL mode. - * - * The parameter, `angle`, is the amount to rotate. For example, calling - * `rotateX(1)` rotates the coordinate system about the x-axis by 1 radian. - * `rotateX()` interprets angle values using the current - * angleMode(). - * - * By default, transformations accumulate. For example, calling `rotateX(1)` - * twice has the same effect as calling `rotateX(2)` once. 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 - * `rotateX(1)` inside the draw() function won't cause - * shapes to spin. - * - * @method rotateX - * @param {Number} angle angle of rotation in the current angleMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/8 turn. - * rotateX(QUARTER_PI); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/16 turn. - * rotateX(QUARTER_PI / 2); - * - * // Rotate the coordinate system 1/16 turn. - * rotateX(QUARTER_PI / 2); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Use degrees. - * angleMode(DEGREES); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/8 turn. - * rotateX(45); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube rotates slowly against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system a little more each frame. - * let angle = frameCount * 0.01; - * rotateX(angle); - * - * // Draw a box. - * box(); - * } - * - *
- */ -p5.prototype.rotateX = function(angle) { - this._assert3d('rotateX'); - p5._validateParameters('rotateX', arguments); - this._renderer.rotateX(this._toRadians(angle)); - return this; -}; + /** + * Rotates the coordinate system about the y-axis in WebGL mode. + * + * The parameter, `angle`, is the amount to rotate. For example, calling + * `rotateY(1)` rotates the coordinate system about the y-axis by 1 radian. + * `rotateY()` interprets angle values using the current + * angleMode(). + * + * By default, transformations accumulate. For example, calling `rotateY(1)` + * twice has the same effect as calling `rotateY(2)` once. 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 + * `rotateY(1)` inside the draw() function won't cause + * shapes to spin. + * + * @method rotateY + * @param {Number} angle angle of rotation in the current angleMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/8 turn. + * rotateY(QUARTER_PI); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/16 turn. + * rotateY(QUARTER_PI / 2); + * + * // Rotate the coordinate system 1/16 turn. + * rotateY(QUARTER_PI / 2); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Use degrees. + * angleMode(DEGREES); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/8 turn. + * rotateY(45); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube rotates slowly against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system a little more each frame. + * let angle = frameCount * 0.01; + * rotateY(angle); + * + * // Draw a box. + * box(); + * } + * + *
+ */ + fn.rotateY = function(angle) { + this._assert3d('rotateY'); + p5._validateParameters('rotateY', arguments); + this._renderer.rotateY(this._toRadians(angle)); + return this; + }; -/** - * Rotates the coordinate system about the y-axis in WebGL mode. - * - * The parameter, `angle`, is the amount to rotate. For example, calling - * `rotateY(1)` rotates the coordinate system about the y-axis by 1 radian. - * `rotateY()` interprets angle values using the current - * angleMode(). - * - * By default, transformations accumulate. For example, calling `rotateY(1)` - * twice has the same effect as calling `rotateY(2)` once. 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 - * `rotateY(1)` inside the draw() function won't cause - * shapes to spin. - * - * @method rotateY - * @param {Number} angle angle of rotation in the current angleMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/8 turn. - * rotateY(QUARTER_PI); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/16 turn. - * rotateY(QUARTER_PI / 2); - * - * // Rotate the coordinate system 1/16 turn. - * rotateY(QUARTER_PI / 2); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Use degrees. - * angleMode(DEGREES); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/8 turn. - * rotateY(45); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube rotates slowly against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system a little more each frame. - * let angle = frameCount * 0.01; - * rotateY(angle); - * - * // Draw a box. - * box(); - * } - * - *
- */ -p5.prototype.rotateY = function(angle) { - this._assert3d('rotateY'); - p5._validateParameters('rotateY', arguments); - this._renderer.rotateY(this._toRadians(angle)); - return this; -}; + /** + * Rotates the coordinate system about the z-axis in WebGL mode. + * + * The parameter, `angle`, is the amount to rotate. For example, calling + * `rotateZ(1)` rotates the coordinate system about the z-axis by 1 radian. + * `rotateZ()` interprets angle values using the current + * angleMode(). + * + * By default, transformations accumulate. For example, calling `rotateZ(1)` + * twice has the same effect as calling `rotateZ(2)` once. 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 + * `rotateZ(1)` inside the draw() function won't cause + * shapes to spin. + * + * @method rotateZ + * @param {Number} angle angle of rotation in the current angleMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/8 turn. + * rotateZ(QUARTER_PI); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/16 turn. + * rotateZ(QUARTER_PI / 2); + * + * // Rotate the coordinate system 1/16 turn. + * rotateZ(QUARTER_PI / 2); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Use degrees. + * angleMode(DEGREES); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system 1/8 turn. + * rotateZ(45); + * + * // Draw a box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube rotates slowly against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rotate the coordinate system a little more each frame. + * let angle = frameCount * 0.01; + * rotateZ(angle); + * + * // Draw a box. + * box(); + * } + * + *
+ */ + fn.rotateZ = function(angle) { + this._assert3d('rotateZ'); + p5._validateParameters('rotateZ', arguments); + this._renderer.rotateZ(this._toRadians(angle)); + return this; + }; -/** - * Rotates the coordinate system about the z-axis in WebGL mode. - * - * The parameter, `angle`, is the amount to rotate. For example, calling - * `rotateZ(1)` rotates the coordinate system about the z-axis by 1 radian. - * `rotateZ()` interprets angle values using the current - * angleMode(). - * - * By default, transformations accumulate. For example, calling `rotateZ(1)` - * twice has the same effect as calling `rotateZ(2)` once. 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 - * `rotateZ(1)` inside the draw() function won't cause - * shapes to spin. - * - * @method rotateZ - * @param {Number} angle angle of rotation in the current angleMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/8 turn. - * rotateZ(QUARTER_PI); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/16 turn. - * rotateZ(QUARTER_PI / 2); - * - * // Rotate the coordinate system 1/16 turn. - * rotateZ(QUARTER_PI / 2); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Use degrees. - * angleMode(DEGREES); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system 1/8 turn. - * rotateZ(45); - * - * // Draw a box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube rotates slowly against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rotate the coordinate system a little more each frame. - * let angle = frameCount * 0.01; - * rotateZ(angle); - * - * // Draw a box. - * box(); - * } - * - *
- */ -p5.prototype.rotateZ = function(angle) { - this._assert3d('rotateZ'); - p5._validateParameters('rotateZ', arguments); - this._renderer.rotateZ(this._toRadians(angle)); - return this; -}; + /** + * Scales the coordinate system. + * + * By default, shapes are drawn at their original scale. A rectangle that's 50 + * pixels wide appears to take up half the width of a 100 pixel-wide canvas. + * The `scale()` function can shrink or stretch the coordinate system so that + * shapes appear at different sizes. There are two ways to call `scale()` with + * parameters that set the scale factor(s). + * + * The first way to call `scale()` uses numbers to set the amount of scaling. + * The first parameter, `s`, sets the amount to scale each axis. For example, + * calling `scale(2)` stretches the x-, y-, and z-axes by a factor of 2. The + * next two parameters, `y` and `z`, are optional. They set the amount to + * scale the y- and z-axes. For example, calling `scale(2, 0.5, 1)` stretches + * the x-axis by a factor of 2, shrinks the y-axis by a factor of 0.5, and + * leaves the z-axis unchanged. + * + * The second way to call `scale()` uses a p5.Vector + * object to set the scale factors. For example, calling `scale(myVector)` + * uses the x-, y-, and z-components of `myVector` to set the amount of + * scaling along the x-, y-, and z-axes. Doing so is the same as calling + * `scale(myVector.x, myVector.y, myVector.z)`. + * + * By default, transformations accumulate. For example, calling `scale(1)` + * twice has the same effect as calling `scale(2)` once. 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 + * `scale(2)` inside the draw() function won't cause + * shapes to grow continuously. + * + * @method scale + * @param {Number|p5.Vector|Number[]} s amount to scale along the positive x-axis. + * @param {Number} [y] amount to scale along the positive y-axis. Defaults to `s`. + * @param {Number} [z] amount to scale along the positive z-axis. Defaults to `y`. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * 'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Draw a square at (30, 20). + * square(30, 20, 40); + * + * // Scale the coordinate system by a factor of 0.5. + * scale(0.5); + * + * // Draw a square at (30, 20). + * // It appears at (15, 10) after scaling. + * square(30, 20, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A rectangle and a square drawn in white on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Draw a square at (30, 20). + * square(30, 20, 40); + * + * // Scale the coordinate system by factors of + * // 0.5 along the x-axis and + * // 1.3 along the y-axis. + * scale(0.5, 1.3); + * + * // Draw a square at (30, 20). + * // It appears as a rectangle at (15, 26) after scaling. + * square(30, 20, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A rectangle and a square drawn in white on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Draw a square at (30, 20). + * square(30, 20, 40); + * + * // Create a p5.Vector object. + * let v = createVector(0.5, 1.3); + * + * // Scale the coordinate system by factors of + * // 0.5 along the x-axis and + * // 1.3 along the y-axis. + * scale(v); + * + * // Draw a square at (30, 20). + * // It appears as a rectangle at (15, 26) after scaling. + * square(30, 20, 40); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'A red box and a blue box drawn on a gray background. The red box appears embedded in the blue box.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the spheres. + * noStroke(); + * + * // Draw the red sphere. + * fill('red'); + * box(); + * + * // Scale the coordinate system by factors of + * // 0.5 along the x-axis and + * // 1.3 along the y-axis and + * // 2 along the z-axis. + * scale(0.5, 1.3, 2); + * + * // Draw the blue sphere. + * fill('blue'); + * box(); + * } + * + *
+ */ + /** + * @method scale + * @param {p5.Vector|Number[]} scales vector whose components should be used to scale. + * @chainable + */ + fn.scale = function(x, y, z) { + p5._validateParameters('scale', arguments); + // Only check for Vector argument type if Vector is available + if (x instanceof p5.Vector) { + const v = x; + x = v.x; + y = v.y; + z = v.z; + } else if (Array.isArray(x)) { + const rg = x; + x = rg[0]; + y = rg[1]; + z = rg[2] || 1; + } + if (isNaN(y)) { + y = z = x; + } else if (isNaN(z)) { + z = 1; + } -/** - * Scales the coordinate system. - * - * By default, shapes are drawn at their original scale. A rectangle that's 50 - * pixels wide appears to take up half the width of a 100 pixel-wide canvas. - * The `scale()` function can shrink or stretch the coordinate system so that - * shapes appear at different sizes. There are two ways to call `scale()` with - * parameters that set the scale factor(s). - * - * The first way to call `scale()` uses numbers to set the amount of scaling. - * The first parameter, `s`, sets the amount to scale each axis. For example, - * calling `scale(2)` stretches the x-, y-, and z-axes by a factor of 2. The - * next two parameters, `y` and `z`, are optional. They set the amount to - * scale the y- and z-axes. For example, calling `scale(2, 0.5, 1)` stretches - * the x-axis by a factor of 2, shrinks the y-axis by a factor of 0.5, and - * leaves the z-axis unchanged. - * - * The second way to call `scale()` uses a p5.Vector - * object to set the scale factors. For example, calling `scale(myVector)` - * uses the x-, y-, and z-components of `myVector` to set the amount of - * scaling along the x-, y-, and z-axes. Doing so is the same as calling - * `scale(myVector.x, myVector.y, myVector.z)`. - * - * By default, transformations accumulate. For example, calling `scale(1)` - * twice has the same effect as calling `scale(2)` once. 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 - * `scale(2)` inside the draw() function won't cause - * shapes to grow continuously. - * - * @method scale - * @param {Number|p5.Vector|Number[]} s amount to scale along the positive x-axis. - * @param {Number} [y] amount to scale along the positive y-axis. Defaults to `s`. - * @param {Number} [z] amount to scale along the positive z-axis. Defaults to `y`. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * 'Two white squares on a gray background. The larger square appears at the top-center. The smaller square appears at the top-left.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Draw a square at (30, 20). - * square(30, 20, 40); - * - * // Scale the coordinate system by a factor of 0.5. - * scale(0.5); - * - * // Draw a square at (30, 20). - * // It appears at (15, 10) after scaling. - * square(30, 20, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A rectangle and a square drawn in white on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Draw a square at (30, 20). - * square(30, 20, 40); - * - * // Scale the coordinate system by factors of - * // 0.5 along the x-axis and - * // 1.3 along the y-axis. - * scale(0.5, 1.3); - * - * // Draw a square at (30, 20). - * // It appears as a rectangle at (15, 26) after scaling. - * square(30, 20, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A rectangle and a square drawn in white on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Draw a square at (30, 20). - * square(30, 20, 40); - * - * // Create a p5.Vector object. - * let v = createVector(0.5, 1.3); - * - * // Scale the coordinate system by factors of - * // 0.5 along the x-axis and - * // 1.3 along the y-axis. - * scale(v); - * - * // Draw a square at (30, 20). - * // It appears as a rectangle at (15, 26) after scaling. - * square(30, 20, 40); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'A red box and a blue box drawn on a gray background. The red box appears embedded in the blue box.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the spheres. - * noStroke(); - * - * // Draw the red sphere. - * fill('red'); - * box(); - * - * // Scale the coordinate system by factors of - * // 0.5 along the x-axis and - * // 1.3 along the y-axis and - * // 2 along the z-axis. - * scale(0.5, 1.3, 2); - * - * // Draw the blue sphere. - * fill('blue'); - * box(); - * } - * - *
- */ -/** - * @method scale - * @param {p5.Vector|Number[]} scales vector whose components should be used to scale. - * @chainable - */ -p5.prototype.scale = function(x, y, z) { - p5._validateParameters('scale', arguments); - // Only check for Vector argument type if Vector is available - if (x instanceof p5.Vector) { - const v = x; - x = v.x; - y = v.y; - z = v.z; - } else if (Array.isArray(x)) { - const rg = x; - x = rg[0]; - y = rg[1]; - z = rg[2] || 1; - } - if (isNaN(y)) { - y = z = x; - } else if (isNaN(z)) { - z = 1; - } + this._renderer.scale(x, y, z); - this._renderer.scale(x, y, z); + return this; + }; - return this; -}; + /** + * Shears the x-axis so that shapes appear skewed. + * + * By default, the x- and y-axes are perpendicular. The `shearX()` function + * transforms the coordinate system so that x-coordinates are translated while + * y-coordinates are fixed. + * + * The first parameter, `angle`, is the amount to shear. For example, calling + * `shearX(1)` transforms all x-coordinates using the formula + * `x = x + y * tan(angle)`. `shearX()` interprets angle values using the + * current angleMode(). + * + * By default, transformations accumulate. For example, calling + * `shearX(1)` twice has the same effect as calling `shearX(2)` once. 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 + * `shearX(1)` inside the draw() function won't + * cause shapes to shear continuously. + * + * @method shearX + * @param {Number} angle angle to shear by in the current angleMode(). + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white quadrilateral on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Shear the coordinate system along the x-axis. + * shearX(QUARTER_PI); + * + * // Draw the square. + * square(0, 0, 50); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Use degrees. + * angleMode(DEGREES); + * + * describe('A white quadrilateral on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Shear the coordinate system along the x-axis. + * shearX(45); + * + * // Draw the square. + * square(0, 0, 50); + * } + * + *
+ */ + fn.shearX = function(angle) { + p5._validateParameters('shearX', arguments); + const rad = this._toRadians(angle); + this._renderer.applyMatrix(1, 0, Math.tan(rad), 1, 0, 0); + return this; + }; -/** - * Shears the x-axis so that shapes appear skewed. - * - * By default, the x- and y-axes are perpendicular. The `shearX()` function - * transforms the coordinate system so that x-coordinates are translated while - * y-coordinates are fixed. - * - * The first parameter, `angle`, is the amount to shear. For example, calling - * `shearX(1)` transforms all x-coordinates using the formula - * `x = x + y * tan(angle)`. `shearX()` interprets angle values using the - * current angleMode(). - * - * By default, transformations accumulate. For example, calling - * `shearX(1)` twice has the same effect as calling `shearX(2)` once. 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 - * `shearX(1)` inside the draw() function won't - * cause shapes to shear continuously. - * - * @method shearX - * @param {Number} angle angle to shear by in the current angleMode(). - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white quadrilateral on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Shear the coordinate system along the x-axis. - * shearX(QUARTER_PI); - * - * // Draw the square. - * square(0, 0, 50); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Use degrees. - * angleMode(DEGREES); - * - * describe('A white quadrilateral on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Shear the coordinate system along the x-axis. - * shearX(45); - * - * // Draw the square. - * square(0, 0, 50); - * } - * - *
- */ -p5.prototype.shearX = function(angle) { - p5._validateParameters('shearX', arguments); - const rad = this._toRadians(angle); - this._renderer.applyMatrix(1, 0, Math.tan(rad), 1, 0, 0); - return this; -}; + /** + * Shears the y-axis so that shapes appear skewed. + * + * By default, the x- and y-axes are perpendicular. The `shearY()` function + * transforms the coordinate system so that y-coordinates are translated while + * x-coordinates are fixed. + * + * The first parameter, `angle`, is the amount to shear. For example, calling + * `shearY(1)` transforms all y-coordinates using the formula + * `y = y + x * tan(angle)`. `shearY()` interprets angle values using the + * current angleMode(). + * + * By default, transformations accumulate. For example, calling + * `shearY(1)` twice has the same effect as calling `shearY(2)` once. 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 + * `shearY(1)` inside the draw() function won't + * cause shapes to shear continuously. + * + * @method shearY + * @param {Number} angle angle to shear by in the current angleMode(). + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white quadrilateral on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Shear the coordinate system along the x-axis. + * shearY(QUARTER_PI); + * + * // Draw the square. + * square(0, 0, 50); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Use degrees. + * angleMode(DEGREES); + * + * describe('A white quadrilateral on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Shear the coordinate system along the x-axis. + * shearY(45); + * + * // Draw the square. + * square(0, 0, 50); + * } + * + *
+ */ + fn.shearY = function(angle) { + p5._validateParameters('shearY', arguments); + const rad = this._toRadians(angle); + this._renderer.applyMatrix(1, Math.tan(rad), 0, 1, 0, 0); + return this; + }; -/** - * Shears the y-axis so that shapes appear skewed. - * - * By default, the x- and y-axes are perpendicular. The `shearY()` function - * transforms the coordinate system so that y-coordinates are translated while - * x-coordinates are fixed. - * - * The first parameter, `angle`, is the amount to shear. For example, calling - * `shearY(1)` transforms all y-coordinates using the formula - * `y = y + x * tan(angle)`. `shearY()` interprets angle values using the - * current angleMode(). - * - * By default, transformations accumulate. For example, calling - * `shearY(1)` twice has the same effect as calling `shearY(2)` once. 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 - * `shearY(1)` inside the draw() function won't - * cause shapes to shear continuously. - * - * @method shearY - * @param {Number} angle angle to shear by in the current angleMode(). - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white quadrilateral on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Shear the coordinate system along the x-axis. - * shearY(QUARTER_PI); - * - * // Draw the square. - * square(0, 0, 50); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Use degrees. - * angleMode(DEGREES); - * - * describe('A white quadrilateral on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Shear the coordinate system along the x-axis. - * shearY(45); - * - * // Draw the square. - * square(0, 0, 50); - * } - * - *
- */ -p5.prototype.shearY = function(angle) { - p5._validateParameters('shearY', arguments); - const rad = this._toRadians(angle); - this._renderer.applyMatrix(1, Math.tan(rad), 0, 1, 0, 0); - return this; -}; + /** + * Translates the coordinate system. + * + * By default, the origin `(0, 0)` is at the sketch's top-left corner in 2D + * mode and center in WebGL mode. The `translate()` function shifts the origin + * to a different position. Everything drawn after `translate()` is called + * will appear to be shifted. There are two ways to call `translate()` with + * parameters that set the origin's position. + * + * The first way to call `translate()` uses numbers to set the amount of + * translation. The first two parameters, `x` and `y`, set the amount to + * translate along the positive x- and y-axes. For example, calling + * `translate(20, 30)` translates the origin 20 pixels along the x-axis and 30 + * pixels along the y-axis. The third parameter, `z`, is optional. It sets the + * amount to translate along the positive z-axis. For example, calling + * `translate(20, 30, 40)` translates the origin 20 pixels along the x-axis, + * 30 pixels along the y-axis, and 40 pixels along the z-axis. + * + * The second way to call `translate()` uses a + * p5.Vector object to set the amount of + * translation. For example, calling `translate(myVector)` uses the x-, y-, + * and z-components of `myVector` to set the amount to translate along the x-, + * y-, and z-axes. Doing so is the same as calling + * `translate(myVector.x, myVector.y, myVector.z)`. + * + * By default, transformations accumulate. For example, calling + * `translate(10, 0)` twice has the same effect as calling + * `translate(20, 0)` once. 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 + * `translate(10, 0)` inside the draw() function won't + * cause shapes to move continuously. + * + * @method translate + * @param {Number} x amount to translate along the positive x-axis. + * @param {Number} y amount to translate along the positive y-axis. + * @param {Number} [z] amount to translate along the positive z-axis. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white circle on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Translate the origin to the center. + * translate(50, 50); + * + * // Draw a circle at coordinates (0, 0). + * circle(0, 0, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * 'Two circles drawn on a gray background. The blue circle on the right overlaps the red circle at the center.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Translate the origin to the center. + * translate(50, 50); + * + * // Draw the red circle. + * fill('red'); + * circle(0, 0, 40); + * + * // Translate the origin to the right. + * translate(25, 0); + * + * // Draw the blue circle. + * fill('blue'); + * circle(0, 0, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white circle moves slowly from left to right on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Calculate the x-coordinate. + * let x = frameCount * 0.2; + * + * // Translate the origin. + * translate(x, 50); + * + * // Draw a circle at coordinates (0, 0). + * circle(0, 0, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white circle on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Create a p5.Vector object. + * let v = createVector(50, 50); + * + * // Translate the origin by the vector. + * translate(v); + * + * // Draw a circle at coordinates (0, 0). + * circle(0, 0, 40); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'Two spheres sitting side-by-side on gray background. The sphere at the center is red. The sphere on the right is blue.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Turn on the lights. + * lights(); + * + * // Style the spheres. + * noStroke(); + * + * // Draw the red sphere. + * fill('red'); + * sphere(10); + * + * // Translate the origin to the right. + * translate(30, 0, 0); + * + * // Draw the blue sphere. + * fill('blue'); + * sphere(10); + * } + * + *
+ */ + /** + * @method translate + * @param {p5.Vector} vector vector by which to translate. + * @chainable + */ + fn.translate = function(x, y, z) { + p5._validateParameters('translate', arguments); + if (this._renderer.isP3D) { + this._renderer.translate(x, y, z); + } else { + this._renderer.translate(x, y); + } + return this; + }; -/** - * Translates the coordinate system. - * - * By default, the origin `(0, 0)` is at the sketch's top-left corner in 2D - * mode and center in WebGL mode. The `translate()` function shifts the origin - * to a different position. Everything drawn after `translate()` is called - * will appear to be shifted. There are two ways to call `translate()` with - * parameters that set the origin's position. - * - * The first way to call `translate()` uses numbers to set the amount of - * translation. The first two parameters, `x` and `y`, set the amount to - * translate along the positive x- and y-axes. For example, calling - * `translate(20, 30)` translates the origin 20 pixels along the x-axis and 30 - * pixels along the y-axis. The third parameter, `z`, is optional. It sets the - * amount to translate along the positive z-axis. For example, calling - * `translate(20, 30, 40)` translates the origin 20 pixels along the x-axis, - * 30 pixels along the y-axis, and 40 pixels along the z-axis. - * - * The second way to call `translate()` uses a - * p5.Vector object to set the amount of - * translation. For example, calling `translate(myVector)` uses the x-, y-, - * and z-components of `myVector` to set the amount to translate along the x-, - * y-, and z-axes. Doing so is the same as calling - * `translate(myVector.x, myVector.y, myVector.z)`. - * - * By default, transformations accumulate. For example, calling - * `translate(10, 0)` twice has the same effect as calling - * `translate(20, 0)` once. 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 - * `translate(10, 0)` inside the draw() function won't - * cause shapes to move continuously. - * - * @method translate - * @param {Number} x amount to translate along the positive x-axis. - * @param {Number} y amount to translate along the positive y-axis. - * @param {Number} [z] amount to translate along the positive z-axis. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white circle on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Translate the origin to the center. - * translate(50, 50); - * - * // Draw a circle at coordinates (0, 0). - * circle(0, 0, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * 'Two circles drawn on a gray background. The blue circle on the right overlaps the red circle at the center.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Translate the origin to the center. - * translate(50, 50); - * - * // Draw the red circle. - * fill('red'); - * circle(0, 0, 40); - * - * // Translate the origin to the right. - * translate(25, 0); - * - * // Draw the blue circle. - * fill('blue'); - * circle(0, 0, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white circle moves slowly from left to right on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Calculate the x-coordinate. - * let x = frameCount * 0.2; - * - * // Translate the origin. - * translate(x, 50); - * - * // Draw a circle at coordinates (0, 0). - * circle(0, 0, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white circle on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Create a p5.Vector object. - * let v = createVector(50, 50); - * - * // Translate the origin by the vector. - * translate(v); - * - * // Draw a circle at coordinates (0, 0). - * circle(0, 0, 40); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'Two spheres sitting side-by-side on gray background. The sphere at the center is red. The sphere on the right is blue.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Turn on the lights. - * lights(); - * - * // Style the spheres. - * noStroke(); - * - * // Draw the red sphere. - * fill('red'); - * sphere(10); - * - * // Translate the origin to the right. - * translate(30, 0, 0); - * - * // Draw the blue sphere. - * fill('blue'); - * sphere(10); - * } - * - *
- */ -/** - * @method translate - * @param {p5.Vector} vector vector by which to translate. - * @chainable - */ -p5.prototype.translate = function(x, y, z) { - p5._validateParameters('translate', arguments); - if (this._renderer.isP3D) { - this._renderer.translate(x, y, z); - } else { - this._renderer.translate(x, y); - } - return this; -}; + /** + * Begins a drawing group that contains its own styles and transformations. + * + * By default, styles such as fill() and + * transformations such as rotate() are applied to + * all drawing that follows. The `push()` and pop() + * functions can limit the effect of styles and transformations to a specific + * group of shapes, images, and text. For example, a group of shapes could be + * translated to follow the mouse without affecting the rest of the sketch: + * + * ```js + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the mouse's position. + * translate(mouseX, mouseY); + * + * // Style the face. + * noStroke(); + * fill('green'); + * + * // Draw the face. + * circle(0, 0, 60); + * + * // Style the eyes. + * fill('white'); + * + * // Draw the left eye. + * ellipse(-20, -20, 30, 20); + * + * // Draw the right eye. + * ellipse(20, -20, 30, 20); + * + * // End the drawing group. + * pop(); + * + * // Draw a bug. + * let x = random(0, 100); + * let y = random(0, 100); + * text('🦟', x, y); + * ``` + * + * In the code snippet above, the bug's position isn't affected by + * `translate(mouseX, mouseY)` because that transformation is contained + * between `push()` and pop(). The bug moves around + * the entire canvas as expected. + * + * Note: `push()` and pop() are always called as a + * pair. Both functions are required to begin and end a drawing group. + * + * `push()` and pop() can also be nested to create + * subgroups. For example, the code snippet above could be changed to give + * more detail to the frog’s eyes: + * + * ```js + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the mouse's position. + * translate(mouseX, mouseY); + * + * // Style the face. + * noStroke(); + * fill('green'); + * + * // Draw a face. + * circle(0, 0, 60); + * + * // Style the eyes. + * fill('white'); + * + * // Draw the left eye. + * push(); + * translate(-20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // Draw the right eye. + * push(); + * translate(20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // End the drawing group. + * pop(); + * + * // Draw a bug. + * let x = random(0, 100); + * let y = random(0, 100); + * text('🦟', x, y); + * ``` + * + * In this version, the code to draw each eye is contained between its own + * `push()` and pop() functions. Doing so makes it + * easier to add details in the correct part of a drawing. + * + * `push()` and pop() contain the effects of the + * following functions: + * + * - fill() + * - noFill() + * - noStroke() + * - stroke() + * - tint() + * - noTint() + * - strokeWeight() + * - strokeCap() + * - strokeJoin() + * - imageMode() + * - rectMode() + * - ellipseMode() + * - colorMode() + * - textAlign() + * - textFont() + * - textSize() + * - textLeading() + * - applyMatrix() + * - resetMatrix() + * - rotate() + * - scale() + * - shearX() + * - shearY() + * - translate() + * + * In WebGL mode, `push()` and pop() contain the + * effects of a few additional styles: + * + * - setCamera() + * - ambientLight() + * - directionalLight() + * - pointLight() texture() + * - specularMaterial() + * - shininess() + * - normalMaterial() + * - shader() + * + * @method push + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Draw the left circle. + * circle(25, 50, 20); + * + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the center. + * translate(50, 50); + * + * // Style the circle. + * strokeWeight(5); + * stroke('royalblue'); + * fill('orange'); + * + * // Draw the circle. + * circle(0, 0, 20); + * + * // End the drawing group. + * pop(); + * + * // Draw the right circle. + * circle(75, 50, 20); + * + * describe( + * 'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.' + * ); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Slow the frame rate. + * frameRate(24); + * + * describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.'); + * } + * + * function draw() { + * background(200); + * + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the mouse's position. + * translate(mouseX, mouseY); + * + * // Style the face. + * noStroke(); + * fill('green'); + * + * // Draw a face. + * circle(0, 0, 60); + * + * // Style the eyes. + * fill('white'); + * + * // Draw the left eye. + * push(); + * translate(-20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // Draw the right eye. + * push(); + * translate(20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // End the drawing group. + * pop(); + * + * // Draw a bug. + * let x = random(0, 100); + * let y = random(0, 100); + * text('🦟', x, y); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the red sphere. + * push(); + * translate(-25, 0, 0); + * noStroke(); + * directionalLight(255, 0, 0, 0, 0, -1); + * sphere(20); + * pop(); + * + * // Draw the blue sphere. + * push(); + * translate(25, 0, 0); + * strokeWeight(0.3); + * stroke(0, 0, 255); + * noFill(); + * sphere(20); + * pop(); + * } + * + *
+ */ + fn.push = function() { + this._renderer.push(); + }; + + /** + * Ends a drawing group that contains its own styles and transformations. + * + * By default, styles such as fill() and + * transformations such as rotate() are applied to + * all drawing that follows. The push() and `pop()` + * functions can limit the effect of styles and transformations to a specific + * group of shapes, images, and text. For example, a group of shapes could be + * translated to follow the mouse without affecting the rest of the sketch: + * + * ```js + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the mouse's position. + * translate(mouseX, mouseY); + * + * // Style the face. + * noStroke(); + * fill('green'); + * + * // Draw the face. + * circle(0, 0, 60); + * + * // Style the eyes. + * fill('white'); + * + * // Draw the left eye. + * ellipse(-20, -20, 30, 20); + * + * // Draw the right eye. + * ellipse(20, -20, 30, 20); + * + * // End the drawing group. + * pop(); + * + * // Draw a bug. + * let x = random(0, 100); + * let y = random(0, 100); + * text('🦟', x, y); + * ``` + * + * In the code snippet above, the bug's position isn't affected by + * `translate(mouseX, mouseY)` because that transformation is contained + * between push() and `pop()`. The bug moves around + * the entire canvas as expected. + * + * Note: push() and `pop()` are always called as a + * pair. Both functions are required to begin and end a drawing group. + * + * push() and `pop()` can also be nested to create + * subgroups. For example, the code snippet above could be changed to give + * more detail to the frog’s eyes: + * + * ```js + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the mouse's position. + * translate(mouseX, mouseY); + * + * // Style the face. + * noStroke(); + * fill('green'); + * + * // Draw a face. + * circle(0, 0, 60); + * + * // Style the eyes. + * fill('white'); + * + * // Draw the left eye. + * push(); + * translate(-20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // Draw the right eye. + * push(); + * translate(20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // End the drawing group. + * pop(); + * + * // Draw a bug. + * let x = random(0, 100); + * let y = random(0, 100); + * text('🦟', x, y); + * ``` + * + * In this version, the code to draw each eye is contained between its own + * push() and `pop()` functions. Doing so makes it + * easier to add details in the correct part of a drawing. + * + * push() and `pop()` contain the effects of the + * following functions: + * + * - fill() + * - noFill() + * - noStroke() + * - stroke() + * - tint() + * - noTint() + * - strokeWeight() + * - strokeCap() + * - strokeJoin() + * - imageMode() + * - rectMode() + * - ellipseMode() + * - colorMode() + * - textAlign() + * - textFont() + * - textSize() + * - textLeading() + * - applyMatrix() + * - resetMatrix() + * - rotate() + * - scale() + * - shearX() + * - shearY() + * - translate() + * + * In WebGL mode, push() and `pop()` contain the + * effects of a few additional styles: + * + * - setCamera() + * - ambientLight() + * - directionalLight() + * - pointLight() texture() + * - specularMaterial() + * - shininess() + * - normalMaterial() + * - shader() + * + * @method pop + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Draw the left circle. + * circle(25, 50, 20); + * + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the center. + * translate(50, 50); + * + * // Style the circle. + * strokeWeight(5); + * stroke('royalblue'); + * fill('orange'); + * + * // Draw the circle. + * circle(0, 0, 20); + * + * // End the drawing group. + * pop(); + * + * // Draw the right circle. + * circle(75, 50, 20); + * + * describe( + * 'Three circles drawn in a row on a gray background. The left and right circles are white with thin, black borders. The middle circle is orange with a thick, blue border.' + * ); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Slow the frame rate. + * frameRate(24); + * + * describe('A mosquito buzzes in front of a green frog. The frog follows the mouse as the user moves.'); + * } + * + * function draw() { + * background(200); + * + * // Begin the drawing group. + * push(); + * + * // Translate the origin to the mouse's position. + * translate(mouseX, mouseY); + * + * // Style the face. + * noStroke(); + * fill('green'); + * + * // Draw a face. + * circle(0, 0, 60); + * + * // Style the eyes. + * fill('white'); + * + * // Draw the left eye. + * push(); + * translate(-20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // Draw the right eye. + * push(); + * translate(20, -20); + * ellipse(0, 0, 30, 20); + * fill('black'); + * circle(0, 0, 8); + * pop(); + * + * // End the drawing group. + * pop(); + * + * // Draw a bug. + * let x = random(0, 100); + * let y = random(0, 100); + * text('🦟', x, y); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'Two spheres drawn on a gray background. The sphere on the left is red and lit from the front. The sphere on the right is a blue wireframe.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the red sphere. + * push(); + * translate(-25, 0, 0); + * noStroke(); + * directionalLight(255, 0, 0, 0, 0, -1); + * sphere(20); + * pop(); + * + * // Draw the blue sphere. + * push(); + * translate(25, 0, 0); + * strokeWeight(0.3); + * stroke(0, 0, 255); + * noFill(); + * sphere(20); + * pop(); + * } + * + *
+ */ + fn.pop = function() { + this._renderer.pop(); + }; +} + +export default transform; -export default p5; +if(typeof p5 !== 'undefined'){ + transform(p5, p5.prototype); +} From 104bd325f09c52fd592c1f3c240b73d164c0bf8a Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 28 Sep 2024 10:41:32 +0100 Subject: [PATCH 20/55] Move blendMode() to color/setting module --- src/color/setting.js | 476 ++++++++++++++++++++++++++++++++++++++++++ src/core/rendering.js | 476 ------------------------------------------ 2 files changed, 476 insertions(+), 476 deletions(-) diff --git a/src/color/setting.js b/src/color/setting.js index 97546501e9..6c2f5cb72d 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -1713,6 +1713,482 @@ function setting(p5, fn){ 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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 === constants.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 = constants.BLEND; + } + this._renderer.blendMode(mode); + }; } export default setting; diff --git a/src/core/rendering.js b/src/core/rendering.js index 291eab3db6..bfee521cad 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -626,482 +626,6 @@ p5.prototype.clearDepth = function (depth) { this._renderer.clearDepth(depth); }; -/** - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.blendMode = function (mode) { - p5._validateParameters('blendMode', arguments); - if (mode === constants.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 = constants.BLEND; - } - this._renderer.blendMode(mode); -}; - /** * A system variable that provides direct access to the sketch's * `<canvas>` element. From 27326fe99098b1d3212c7cf858f37f4b8e283a64 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 11 Oct 2024 12:50:13 +0100 Subject: [PATCH 21/55] Convert renderers to use new syntax modules --- src/app.js | 14 - src/core/environment.js | 2427 +++++++------- src/core/main.js | 16 +- src/core/p5.Renderer.js | 871 ++--- src/core/p5.Renderer2D.js | 2486 +++++++------- src/core/rendering.js | 1333 ++++---- src/core/structure.js | 1132 +++---- src/webgl/index.js | 6 + src/webgl/p5.RendererGL.Immediate.js | 1197 +++---- src/webgl/p5.RendererGL.Retained.js | 549 ++-- src/webgl/p5.RendererGL.js | 4497 +++++++++++++------------- src/webgl/p5.Texture.js | 8 +- 12 files changed, 7287 insertions(+), 7249 deletions(-) diff --git a/src/app.js b/src/app.js index f1e9dadbf0..d9fe8da11b 100644 --- a/src/app.js +++ b/src/app.js @@ -1,23 +1,13 @@ // core import p5 from './core/main'; -import './core/constants'; -import './core/environment'; import './core/friendly_errors/stacktrace'; import './core/friendly_errors/validate_params'; import './core/friendly_errors/file_errors'; import './core/friendly_errors/fes_core'; import './core/friendly_errors/sketch_reader'; -import './core/helpers'; -// import './core/legacy'; -// import './core/preload'; import './core/p5.Element'; import './core/p5.Graphics'; -// import './core/p5.Renderer'; -import './core/p5.Renderer2D'; import './core/rendering'; -import './core/structure'; -import transform from './core/transform'; -p5.registerAddon(transform); import shape from './shape'; shape(p5); @@ -69,10 +59,6 @@ utilities(p5); // webgl import webgl from './webgl'; webgl(p5); -import './webgl/p5.RendererGL.Immediate'; -import './webgl/p5.RendererGL'; -import './webgl/p5.RendererGL.Retained'; -import './webgl/p5.Texture'; import './core/init'; diff --git a/src/core/environment.js b/src/core/environment.js index ab7f6d46d7..f0eca64248 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -6,1260 +6,1265 @@ * @requires constants */ -import p5 from './main'; import * as C from './constants'; -const standardCursors = [C.ARROW, C.CROSS, C.HAND, C.MOVE, C.TEXT, C.WAIT]; +function environment(p5, fn){ + const standardCursors = [C.ARROW, C.CROSS, C.HAND, C.MOVE, C.TEXT, C.WAIT]; -p5.prototype._frameRate = 0; -p5.prototype._lastFrameTime = window.performance.now(); -p5.prototype._targetFrameRate = 60; + fn._frameRate = 0; + fn._lastFrameTime = window.performance.now(); + fn._targetFrameRate = 60; -const _windowPrint = window.print; -let windowPrintDisabled = false; + const _windowPrint = window.print; + let windowPrintDisabled = false; -/** - * Displays text in the web browser's console. - * - * `print()` is helpful for printing values while debugging. Each call to - * `print()` creates a new line of text. - * - * Note: Call `print('\n')` to print a blank line. Calling `print()` without - * an argument opens the browser's dialog for printing documents. - * - * @method print - * @param {Any} contents content to print to the console. - * @example - *
- * - * function setup() { - * // Prints "hello, world" to the console. - * print('hello, world'); - * } - * - *
- * - *
- * - * function setup() { - * let name = 'ada'; - * // Prints "hello, ada" to the console. - * print(`hello, ${name}`); - * } - * - *
- */ -p5.prototype.print = function(...args) { - if (!args.length) { - if (!windowPrintDisabled) { - _windowPrint(); - if ( - window.confirm( - 'You just tried to print the webpage. Do you want to prevent this from running again?' - ) - ) { - windowPrintDisabled = true; + /** + * Displays text in the web browser's console. + * + * `print()` is helpful for printing values while debugging. Each call to + * `print()` creates a new line of text. + * + * Note: Call `print('\n')` to print a blank line. Calling `print()` without + * an argument opens the browser's dialog for printing documents. + * + * @method print + * @param {Any} contents content to print to the console. + * @example + *
+ * + * function setup() { + * // Prints "hello, world" to the console. + * print('hello, world'); + * } + * + *
+ * + *
+ * + * function setup() { + * let name = 'ada'; + * // Prints "hello, ada" to the console. + * print(`hello, ${name}`); + * } + * + *
+ */ + fn.print = function(...args) { + if (!args.length) { + if (!windowPrintDisabled) { + _windowPrint(); + if ( + window.confirm( + 'You just tried to print the webpage. Do you want to prevent this from running again?' + ) + ) { + windowPrintDisabled = true; + } } + } else { + console.log(...args); } - } else { - console.log(...args); - } -}; + }; -/** - * A `Number` variable that tracks the number of frames drawn since the sketch - * started. - * - * `frameCount`'s value is 0 inside setup(). It - * increments by 1 each time the code in draw() - * finishes executing. - * - * @property {Integer} frameCount - * @readOnly - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Display the value of - * // frameCount. - * textSize(30); - * textAlign(CENTER, CENTER); - * text(frameCount, 50, 50); - * - * describe('The number 0 written in black in the middle of a gray square.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Set the frameRate to 30. - * frameRate(30); - * - * textSize(30); - * textAlign(CENTER, CENTER); - * - * describe('A number written in black in the middle of a gray square. Its value increases rapidly.'); - * } - * - * function draw() { - * background(200); - * - * // Display the value of - * // frameCount. - * text(frameCount, 50, 50); - * } - * - *
- */ -p5.prototype.frameCount = 0; + /** + * A `Number` variable that tracks the number of frames drawn since the sketch + * started. + * + * `frameCount`'s value is 0 inside setup(). It + * increments by 1 each time the code in draw() + * finishes executing. + * + * @property {Integer} frameCount + * @readOnly + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Display the value of + * // frameCount. + * textSize(30); + * textAlign(CENTER, CENTER); + * text(frameCount, 50, 50); + * + * describe('The number 0 written in black in the middle of a gray square.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Set the frameRate to 30. + * frameRate(30); + * + * textSize(30); + * textAlign(CENTER, CENTER); + * + * describe('A number written in black in the middle of a gray square. Its value increases rapidly.'); + * } + * + * function draw() { + * background(200); + * + * // Display the value of + * // frameCount. + * text(frameCount, 50, 50); + * } + * + *
+ */ + fn.frameCount = 0; -/** - * A `Number` variable that tracks the number of milliseconds it took to draw - * the last frame. - * - * `deltaTime` contains the amount of time it took - * draw() to execute during the previous frame. It's - * useful for simulating physics. - * - * @property {Integer} deltaTime - * @readOnly - * @example - *
- * - * let x = 0; - * let speed = 0.05; - * - * function setup() { - * createCanvas(100, 100); - * - * // Set the frameRate to 30. - * frameRate(30); - * - * describe('A white circle moves from left to right on a gray background. It reappears on the left side when it reaches the right side.'); - * } - * - * function draw() { - * background(200); - * - * // Use deltaTime to calculate - * // a change in position. - * let deltaX = speed * deltaTime; - * - * // Update the x variable. - * x += deltaX; - * - * // Reset x to 0 if it's - * // greater than 100. - * if (x > 100) { - * x = 0; - * } - * - * // Use x to set the circle's - * // position. - * circle(x, 50, 20); - * } - * - *
- */ -p5.prototype.deltaTime = 0; + /** + * A `Number` variable that tracks the number of milliseconds it took to draw + * the last frame. + * + * `deltaTime` contains the amount of time it took + * draw() to execute during the previous frame. It's + * useful for simulating physics. + * + * @property {Integer} deltaTime + * @readOnly + * @example + *
+ * + * let x = 0; + * let speed = 0.05; + * + * function setup() { + * createCanvas(100, 100); + * + * // Set the frameRate to 30. + * frameRate(30); + * + * describe('A white circle moves from left to right on a gray background. It reappears on the left side when it reaches the right side.'); + * } + * + * function draw() { + * background(200); + * + * // Use deltaTime to calculate + * // a change in position. + * let deltaX = speed * deltaTime; + * + * // Update the x variable. + * x += deltaX; + * + * // Reset x to 0 if it's + * // greater than 100. + * if (x > 100) { + * x = 0; + * } + * + * // Use x to set the circle's + * // position. + * circle(x, 50, 20); + * } + * + *
+ */ + fn.deltaTime = 0; -/** - * A `Boolean` variable that's `true` if the browser is focused and `false` if - * not. - * - * Note: The browser window can only receive input if it's focused. - * - * @property {Boolean} focused - * @readOnly - * @example - *
- * - * // Open this example in two separate browser - * // windows placed side-by-side to demonstrate. - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A square changes color from green to red when the browser window is out of focus.'); - * } - * - * function draw() { - * // Change the background color - * // when the browser window - * // goes in/out of focus. - * if (focused === true) { - * background(0, 255, 0); - * } else { - * background(255, 0, 0); - * } - * } - * - *
- */ -p5.prototype.focused = document.hasFocus(); + /** + * A `Boolean` variable that's `true` if the browser is focused and `false` if + * not. + * + * Note: The browser window can only receive input if it's focused. + * + * @property {Boolean} focused + * @readOnly + * @example + *
+ * + * // Open this example in two separate browser + * // windows placed side-by-side to demonstrate. + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A square changes color from green to red when the browser window is out of focus.'); + * } + * + * function draw() { + * // Change the background color + * // when the browser window + * // goes in/out of focus. + * if (focused === true) { + * background(0, 255, 0); + * } else { + * background(255, 0, 0); + * } + * } + * + *
+ */ + fn.focused = document.hasFocus(); -/** - * Changes the cursor's appearance. - * - * The first parameter, `type`, sets the type of cursor to display. The - * built-in options are `ARROW`, `CROSS`, `HAND`, `MOVE`, `TEXT`, and `WAIT`. - * `cursor()` also recognizes standard CSS cursor properties passed as - * strings: `'help'`, `'wait'`, `'crosshair'`, `'not-allowed'`, `'zoom-in'`, - * and `'grab'`. If the path to an image is passed, as in - * `cursor('assets/target.png')`, then the image will be used as the cursor. - * Images must be in .cur, .gif, .jpg, .jpeg, or .png format and should be at most 32 by 32 pixels large. - * - * The parameters `x` and `y` are optional. If an image is used for the - * cursor, `x` and `y` set the location pointed to within the image. They are - * both 0 by default, so the cursor points to the image's top-left corner. `x` - * and `y` must be less than the image's width and height, respectively. - * - * @method cursor - * @param {(ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String)} type Built-in: either ARROW, CROSS, HAND, MOVE, TEXT, or WAIT. - * Native CSS properties: 'grab', 'progress', and so on. - * Path to cursor image. - * @param {Number} [x] horizontal active spot of the cursor. - * @param {Number} [y] vertical active spot of the cursor. - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A gray square. The cursor appears as crosshairs.'); - * } - * - * function draw() { - * background(200); - * - * // Set the cursor to crosshairs: + - * cursor(CROSS); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A gray square divided into quadrants. The cursor image changes when the mouse moves to each quadrant.'); - * } - * - * function draw() { - * background(200); - * - * // Divide the canvas into quadrants. - * line(50, 0, 50, 100); - * line(0, 50, 100, 50); - * - * // Change cursor based on mouse position. - * if (mouseX < 50 && mouseY < 50) { - * cursor(CROSS); - * } else if (mouseX > 50 && mouseY < 50) { - * cursor('progress'); - * } else if (mouseX > 50 && mouseY > 50) { - * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16'); - * } else { - * cursor('grab'); - * } - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('An image of three purple curves follows the mouse. The image shifts when the mouse is pressed.'); - * } - * - * function draw() { - * background(200); - * - * // Change the cursor's active spot - * // when the mouse is pressed. - * if (mouseIsPressed === true) { - * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16', 8, 8); - * } else { - * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16'); - * } - * } - * - *
- */ -p5.prototype.cursor = function(type, x, y) { - let cursor = 'auto'; - const canvas = this._curElement.elt; - if (standardCursors.includes(type)) { - // Standard css cursor - cursor = type; - } else if (typeof type === 'string') { - let coords = ''; - if (x && y && (typeof x === 'number' && typeof y === 'number')) { - // Note that x and y values must be unit-less positive integers < 32 - // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor - coords = `${x} ${y}`; - } - if ( - type.substring(0, 7) === 'http://' || - type.substring(0, 8) === 'https://' - ) { - // Image (absolute url) - cursor = `url(${type}) ${coords}, auto`; - } else if (/\.(cur|jpg|jpeg|gif|png|CUR|JPG|JPEG|GIF|PNG)$/.test(type)) { - // Image file (relative path) - Separated for performance reasons - cursor = `url(${type}) ${coords}, auto`; - } else { - // Any valid string for the css cursor property + /** + * Changes the cursor's appearance. + * + * The first parameter, `type`, sets the type of cursor to display. The + * built-in options are `ARROW`, `CROSS`, `HAND`, `MOVE`, `TEXT`, and `WAIT`. + * `cursor()` also recognizes standard CSS cursor properties passed as + * strings: `'help'`, `'wait'`, `'crosshair'`, `'not-allowed'`, `'zoom-in'`, + * and `'grab'`. If the path to an image is passed, as in + * `cursor('assets/target.png')`, then the image will be used as the cursor. + * Images must be in .cur, .gif, .jpg, .jpeg, or .png format and should be at most 32 by 32 pixels large. + * + * The parameters `x` and `y` are optional. If an image is used for the + * cursor, `x` and `y` set the location pointed to within the image. They are + * both 0 by default, so the cursor points to the image's top-left corner. `x` + * and `y` must be less than the image's width and height, respectively. + * + * @method cursor + * @param {(ARROW|CROSS|HAND|MOVE|TEXT|WAIT|String)} type Built-in: either ARROW, CROSS, HAND, MOVE, TEXT, or WAIT. + * Native CSS properties: 'grab', 'progress', and so on. + * Path to cursor image. + * @param {Number} [x] horizontal active spot of the cursor. + * @param {Number} [y] vertical active spot of the cursor. + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A gray square. The cursor appears as crosshairs.'); + * } + * + * function draw() { + * background(200); + * + * // Set the cursor to crosshairs: + + * cursor(CROSS); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A gray square divided into quadrants. The cursor image changes when the mouse moves to each quadrant.'); + * } + * + * function draw() { + * background(200); + * + * // Divide the canvas into quadrants. + * line(50, 0, 50, 100); + * line(0, 50, 100, 50); + * + * // Change cursor based on mouse position. + * if (mouseX < 50 && mouseY < 50) { + * cursor(CROSS); + * } else if (mouseX > 50 && mouseY < 50) { + * cursor('progress'); + * } else if (mouseX > 50 && mouseY > 50) { + * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16'); + * } else { + * cursor('grab'); + * } + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('An image of three purple curves follows the mouse. The image shifts when the mouse is pressed.'); + * } + * + * function draw() { + * background(200); + * + * // Change the cursor's active spot + * // when the mouse is pressed. + * if (mouseIsPressed === true) { + * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16', 8, 8); + * } else { + * cursor('https://avatars0.githubusercontent.com/u/1617169?s=16'); + * } + * } + * + *
+ */ + fn.cursor = function(type, x, y) { + let cursor = 'auto'; + const canvas = this._curElement.elt; + if (standardCursors.includes(type)) { + // Standard css cursor cursor = type; + } else if (typeof type === 'string') { + let coords = ''; + if (x && y && (typeof x === 'number' && typeof y === 'number')) { + // Note that x and y values must be unit-less positive integers < 32 + // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor + coords = `${x} ${y}`; + } + if ( + type.substring(0, 7) === 'http://' || + type.substring(0, 8) === 'https://' + ) { + // Image (absolute url) + cursor = `url(${type}) ${coords}, auto`; + } else if (/\.(cur|jpg|jpeg|gif|png|CUR|JPG|JPEG|GIF|PNG)$/.test(type)) { + // Image file (relative path) - Separated for performance reasons + cursor = `url(${type}) ${coords}, auto`; + } else { + // Any valid string for the css cursor property + cursor = type; + } } - } - canvas.style.cursor = cursor; -}; + canvas.style.cursor = cursor; + }; -/** - * Sets the number of frames to draw per second. - * - * Calling `frameRate()` with one numeric argument, as in `frameRate(30)`, - * attempts to draw 30 frames per second (FPS). The target frame rate may not - * be achieved depending on the sketch's processing needs. Most computers - * default to a frame rate of 60 FPS. Frame rates of 24 FPS and above are - * fast enough for smooth animations. - * - * Calling `frameRate()` without an argument returns the current frame rate. - * The value returned is an approximation. - * - * @method frameRate - * @param {Number} fps number of frames to draw per second. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white circle on a gray background. The circle moves from left to right in a loop. It slows down when the mouse is pressed.'); - * } - * - * function draw() { - * background(200); - * - * // Set the x variable based - * // on the current frameCount. - * let x = frameCount % 100; - * - * // If the mouse is pressed, - * // decrease the frame rate. - * if (mouseIsPressed === true) { - * frameRate(10); - * } else { - * frameRate(60); - * } - * - * // Use x to set the circle's - * // position. - * circle(x, 50, 20); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A number written in black on a gray background. The number decreases when the mouse is pressed.'); - * } - * - * function draw() { - * background(200); - * - * // If the mouse is pressed, do lots - * // of math to slow down drawing. - * if (mouseIsPressed === true) { - * for (let i = 0; i < 1000000; i += 1) { - * random(); - * } - * } - * - * // Get the current frame rate - * // and display it. - * let fps = frameRate(); - * text(fps, 50, 50); - * } - * - *
- */ -/** - * @method frameRate - * @return {Number} current frame rate. - */ -p5.prototype.frameRate = function(fps) { - p5._validateParameters('frameRate', arguments); - if (typeof fps !== 'number' || fps < 0) { - return this._frameRate; - } else { - this._targetFrameRate = fps; - if (fps === 0) { - this._frameRate = fps; + /** + * Sets the number of frames to draw per second. + * + * Calling `frameRate()` with one numeric argument, as in `frameRate(30)`, + * attempts to draw 30 frames per second (FPS). The target frame rate may not + * be achieved depending on the sketch's processing needs. Most computers + * default to a frame rate of 60 FPS. Frame rates of 24 FPS and above are + * fast enough for smooth animations. + * + * Calling `frameRate()` without an argument returns the current frame rate. + * The value returned is an approximation. + * + * @method frameRate + * @param {Number} fps number of frames to draw per second. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white circle on a gray background. The circle moves from left to right in a loop. It slows down when the mouse is pressed.'); + * } + * + * function draw() { + * background(200); + * + * // Set the x variable based + * // on the current frameCount. + * let x = frameCount % 100; + * + * // If the mouse is pressed, + * // decrease the frame rate. + * if (mouseIsPressed === true) { + * frameRate(10); + * } else { + * frameRate(60); + * } + * + * // Use x to set the circle's + * // position. + * circle(x, 50, 20); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A number written in black on a gray background. The number decreases when the mouse is pressed.'); + * } + * + * function draw() { + * background(200); + * + * // If the mouse is pressed, do lots + * // of math to slow down drawing. + * if (mouseIsPressed === true) { + * for (let i = 0; i < 1000000; i += 1) { + * random(); + * } + * } + * + * // Get the current frame rate + * // and display it. + * let fps = frameRate(); + * text(fps, 50, 50); + * } + * + *
+ */ + /** + * @method frameRate + * @return {Number} current frame rate. + */ + fn.frameRate = function(fps) { + p5._validateParameters('frameRate', arguments); + if (typeof fps !== 'number' || fps < 0) { + return this._frameRate; + } else { + this._targetFrameRate = fps; + if (fps === 0) { + this._frameRate = fps; + } + return this; } - return this; - } -}; + }; -/** - * Returns the current framerate. - * - * @private - * @return {Number} current frameRate - */ -p5.prototype.getFrameRate = function() { - return this.frameRate(); -}; + /** + * Returns the current framerate. + * + * @private + * @return {Number} current frameRate + */ + fn.getFrameRate = function() { + return this.frameRate(); + }; -/** - * Specifies the number of frames to be displayed every second. For example, - * the function call frameRate(30) will attempt to refresh 30 times a second. - * If the processor is not fast enough to maintain the specified rate, the - * frame rate will not be achieved. Setting the frame rate within setup() is - * recommended. The default rate is 60 frames per second. - * - * Calling `frameRate()` with no arguments returns the current frame rate. - * - * @private - * @param {Number} [fps] number of frames to be displayed every second - */ -p5.prototype.setFrameRate = function(fps) { - return this.frameRate(fps); -}; + /** + * Specifies the number of frames to be displayed every second. For example, + * the function call frameRate(30) will attempt to refresh 30 times a second. + * If the processor is not fast enough to maintain the specified rate, the + * frame rate will not be achieved. Setting the frame rate within setup() is + * recommended. The default rate is 60 frames per second. + * + * Calling `frameRate()` with no arguments returns the current frame rate. + * + * @private + * @param {Number} [fps] number of frames to be displayed every second + */ + fn.setFrameRate = function(fps) { + return this.frameRate(fps); + }; -/** - * Returns the target frame rate. - * - * The value is either the system frame rate or the last value passed to - * frameRate(). - * - * @method getTargetFrameRate - * @return {Number} _targetFrameRate - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('The number 20 written in black on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Set the frame rate to 20. - * frameRate(20); - * - * // Get the target frame rate and - * // display it. - * let fps = getTargetFrameRate(); - * text(fps, 43, 54); - * } - * - *
- */ -p5.prototype.getTargetFrameRate = function() { - return this._targetFrameRate; -}; + /** + * Returns the target frame rate. + * + * The value is either the system frame rate or the last value passed to + * frameRate(). + * + * @method getTargetFrameRate + * @return {Number} _targetFrameRate + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('The number 20 written in black on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Set the frame rate to 20. + * frameRate(20); + * + * // Get the target frame rate and + * // display it. + * let fps = getTargetFrameRate(); + * text(fps, 43, 54); + * } + * + *
+ */ + fn.getTargetFrameRate = function() { + return this._targetFrameRate; + }; -/** - * Hides the cursor from view. - * - * @method noCursor - * @example - *
- * - * function setup() { - * // Hide the cursor. - * noCursor(); - * } - * - * function draw() { - * background(200); - * - * circle(mouseX, mouseY, 10); - * - * describe('A white circle on a gray background. The circle follows the mouse as it moves. The cursor is hidden.'); - * } - * - *
- */ -p5.prototype.noCursor = function() { - this._curElement.elt.style.cursor = 'none'; -}; + /** + * Hides the cursor from view. + * + * @method noCursor + * @example + *
+ * + * function setup() { + * // Hide the cursor. + * noCursor(); + * } + * + * function draw() { + * background(200); + * + * circle(mouseX, mouseY, 10); + * + * describe('A white circle on a gray background. The circle follows the mouse as it moves. The cursor is hidden.'); + * } + * + *
+ */ + fn.noCursor = function() { + this._curElement.elt.style.cursor = 'none'; + }; -/** - * A `String` variable with the WebGL version in use. - * - * `webglVersion`'s value equals one of the following string constants: - * - * - `WEBGL2` whose value is `'webgl2'`, - * - `WEBGL` whose value is `'webgl'`, or - * - `P2D` whose value is `'p2d'`. This is the default for 2D sketches. - * - * See setAttributes() for ways to set the - * WebGL version. - * - * @property {(WEBGL|WEBGL2)} webglVersion - * @readOnly - * @example - *
- * - * function setup() { - * background(200); - * - * // Display the current WebGL version. - * text(webglVersion, 42, 54); - * - * describe('The text "p2d" written in black on a gray background.'); - * } - * - *
- * - *
- * - * let font; - * - * function preload() { - * // Load a font to use. - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * // Create a canvas using WEBGL mode. - * createCanvas(100, 50, WEBGL); - * background(200); - * - * // Display the current WebGL version. - * fill(0); - * textFont(font); - * text(webglVersion, -15, 5); - * - * describe('The text "webgl2" written in black on a gray background.'); - * } - * - *
- * - *
- * - * let font; - * - * function preload() { - * // Load a font to use. - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * // Create a canvas using WEBGL mode. - * createCanvas(100, 50, WEBGL); - * - * // Set WebGL to version 1. - * setAttributes({ version: 1 }); - * - * background(200); - * - * // Display the current WebGL version. - * fill(0); - * textFont(font); - * text(webglVersion, -14, 5); - * - * describe('The text "webgl" written in black on a gray background.'); - * } - * - *
- */ -p5.prototype.webglVersion = C.P2D; + /** + * A `String` variable with the WebGL version in use. + * + * `webglVersion`'s value equals one of the following string constants: + * + * - `WEBGL2` whose value is `'webgl2'`, + * - `WEBGL` whose value is `'webgl'`, or + * - `P2D` whose value is `'p2d'`. This is the default for 2D sketches. + * + * See setAttributes() for ways to set the + * WebGL version. + * + * @property {(WEBGL|WEBGL2)} webglVersion + * @readOnly + * @example + *
+ * + * function setup() { + * background(200); + * + * // Display the current WebGL version. + * text(webglVersion, 42, 54); + * + * describe('The text "p2d" written in black on a gray background.'); + * } + * + *
+ * + *
+ * + * let font; + * + * function preload() { + * // Load a font to use. + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * // Create a canvas using WEBGL mode. + * createCanvas(100, 50, WEBGL); + * background(200); + * + * // Display the current WebGL version. + * fill(0); + * textFont(font); + * text(webglVersion, -15, 5); + * + * describe('The text "webgl2" written in black on a gray background.'); + * } + * + *
+ * + *
+ * + * let font; + * + * function preload() { + * // Load a font to use. + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * // Create a canvas using WEBGL mode. + * createCanvas(100, 50, WEBGL); + * + * // Set WebGL to version 1. + * setAttributes({ version: 1 }); + * + * background(200); + * + * // Display the current WebGL version. + * fill(0); + * textFont(font); + * text(webglVersion, -14, 5); + * + * describe('The text "webgl" written in black on a gray background.'); + * } + * + *
+ */ + fn.webglVersion = C.P2D; -/** - * A `Number` variable that stores the width of the screen display. - * - * `displayWidth` is useful for running full-screen programs. Its value - * depends on the current pixelDensity(). - * - * Note: The actual screen width can be computed as - * `displayWidth * pixelDensity()`. - * - * @property {Number} displayWidth - * @readOnly - * @example - *
- * - * function setup() { - * // Set the canvas' width and height - * // using the display's dimensions. - * createCanvas(displayWidth, displayHeight); - * - * background(200); - * - * describe('A gray canvas that is the same size as the display.'); - * } - * - *
- * - * @alt - * This example does not render anything. - */ -p5.prototype.displayWidth = screen.width; + /** + * A `Number` variable that stores the width of the screen display. + * + * `displayWidth` is useful for running full-screen programs. Its value + * depends on the current pixelDensity(). + * + * Note: The actual screen width can be computed as + * `displayWidth * pixelDensity()`. + * + * @property {Number} displayWidth + * @readOnly + * @example + *
+ * + * function setup() { + * // Set the canvas' width and height + * // using the display's dimensions. + * createCanvas(displayWidth, displayHeight); + * + * background(200); + * + * describe('A gray canvas that is the same size as the display.'); + * } + * + *
+ * + * @alt + * This example does not render anything. + */ + fn.displayWidth = screen.width; -/** - * A `Number` variable that stores the height of the screen display. - * - * `displayHeight` is useful for running full-screen programs. Its value - * depends on the current pixelDensity(). - * - * Note: The actual screen height can be computed as - * `displayHeight * pixelDensity()`. - * - * @property {Number} displayHeight - * @readOnly - * @example - *
- * - * function setup() { - * // Set the canvas' width and height - * // using the display's dimensions. - * createCanvas(displayWidth, displayHeight); - * - * background(200); - * - * describe('A gray canvas that is the same size as the display.'); - * } - * - *
- * - * @alt - * This example does not render anything. - */ -p5.prototype.displayHeight = screen.height; + /** + * A `Number` variable that stores the height of the screen display. + * + * `displayHeight` is useful for running full-screen programs. Its value + * depends on the current pixelDensity(). + * + * Note: The actual screen height can be computed as + * `displayHeight * pixelDensity()`. + * + * @property {Number} displayHeight + * @readOnly + * @example + *
+ * + * function setup() { + * // Set the canvas' width and height + * // using the display's dimensions. + * createCanvas(displayWidth, displayHeight); + * + * background(200); + * + * describe('A gray canvas that is the same size as the display.'); + * } + * + *
+ * + * @alt + * This example does not render anything. + */ + fn.displayHeight = screen.height; -/** - * A `Number` variable that stores the width of the browser's viewport. - * - * The layout viewport - * is the area within the browser that's available for drawing. - * - * @property {Number} windowWidth - * @readOnly - * @example - *
- * - * function setup() { - * // Set the canvas' width and height - * // using the browser's dimensions. - * createCanvas(windowWidth, windowHeight); - * - * background(200); - * - * describe('A gray canvas that takes up the entire browser window.'); - * } - * - *
- * - * @alt - * This example does not render anything. - */ -p5.prototype.windowWidth = 0; + /** + * A `Number` variable that stores the width of the browser's viewport. + * + * The layout viewport + * is the area within the browser that's available for drawing. + * + * @property {Number} windowWidth + * @readOnly + * @example + *
+ * + * function setup() { + * // Set the canvas' width and height + * // using the browser's dimensions. + * createCanvas(windowWidth, windowHeight); + * + * background(200); + * + * describe('A gray canvas that takes up the entire browser window.'); + * } + * + *
+ * + * @alt + * This example does not render anything. + */ + fn.windowWidth = 0; -/** - * A `Number` variable that stores the height of the browser's viewport. - * - * The layout viewport - * is the area within the browser that's available for drawing. - * - * @property {Number} windowHeight - * @readOnly - * @example - *
- * - * function setup() { - * // Set the canvas' width and height - * // using the browser's dimensions. - * createCanvas(windowWidth, windowHeight); - * - * background(200); - * - * describe('A gray canvas that takes up the entire browser window.'); - * } - * - *
- * - * @alt - * This example does not render anything. - */ -p5.prototype.windowHeight = 0; + /** + * A `Number` variable that stores the height of the browser's viewport. + * + * The layout viewport + * is the area within the browser that's available for drawing. + * + * @property {Number} windowHeight + * @readOnly + * @example + *
+ * + * function setup() { + * // Set the canvas' width and height + * // using the browser's dimensions. + * createCanvas(windowWidth, windowHeight); + * + * background(200); + * + * describe('A gray canvas that takes up the entire browser window.'); + * } + * + *
+ * + * @alt + * This example does not render anything. + */ + fn.windowHeight = 0; -/** - * A function that's called when the browser window is resized. - * - * Code placed in the body of `windowResized()` will run when the - * browser window's size changes. It's a good place to call - * resizeCanvas() or make other - * adjustments to accommodate the new window size. - * - * The `event` parameter is optional. If added to the function declaration, it - * can be used for debugging or other purposes. - * - * @method windowResized - * @param {UIEvent} [event] optional resize Event. - * @example - *
- * - * function setup() { - * createCanvas(windowWidth, windowHeight); - * - * describe('A gray canvas with a white circle at its center. The canvas takes up the entire browser window. It changes size to match the browser window.'); - * } - * - * function draw() { - * background(200); - * - * // Draw a circle at the center. - * circle(width / 2, height / 2, 50); - * } - * - * // Resize the canvas when the - * // browser's size changes. - * function windowResized() { - * resizeCanvas(windowWidth, windowHeight); - * } - * - *
- * @alt - * This example does not render anything. - * - *
- * - * function setup() { - * createCanvas(windowWidth, windowHeight); - * } - * - * function draw() { - * background(200); - * - * describe('A gray canvas that takes up the entire browser window. It changes size to match the browser window.'); - * } - * - * function windowResized(event) { - * // Resize the canvas when the - * // browser's size changes. - * resizeCanvas(windowWidth, windowHeight); - * - * // Print the resize event to the console for debugging. - * print(event); - * } - * - *
- * @alt - * This example does not render anything. - */ -p5.prototype._onresize = function(e) { - this.windowWidth = getWindowWidth(); - this.windowHeight = getWindowHeight(); - const context = this._isGlobal ? window : this; - let executeDefault; - if (typeof context.windowResized === 'function') { - executeDefault = context.windowResized(e); - if (executeDefault !== undefined && !executeDefault) { - e.preventDefault(); + /** + * A function that's called when the browser window is resized. + * + * Code placed in the body of `windowResized()` will run when the + * browser window's size changes. It's a good place to call + * resizeCanvas() or make other + * adjustments to accommodate the new window size. + * + * The `event` parameter is optional. If added to the function declaration, it + * can be used for debugging or other purposes. + * + * @method windowResized + * @param {UIEvent} [event] optional resize Event. + * @example + *
+ * + * function setup() { + * createCanvas(windowWidth, windowHeight); + * + * describe('A gray canvas with a white circle at its center. The canvas takes up the entire browser window. It changes size to match the browser window.'); + * } + * + * function draw() { + * background(200); + * + * // Draw a circle at the center. + * circle(width / 2, height / 2, 50); + * } + * + * // Resize the canvas when the + * // browser's size changes. + * function windowResized() { + * resizeCanvas(windowWidth, windowHeight); + * } + * + *
+ * @alt + * This example does not render anything. + * + *
+ * + * function setup() { + * createCanvas(windowWidth, windowHeight); + * } + * + * function draw() { + * background(200); + * + * describe('A gray canvas that takes up the entire browser window. It changes size to match the browser window.'); + * } + * + * function windowResized(event) { + * // Resize the canvas when the + * // browser's size changes. + * resizeCanvas(windowWidth, windowHeight); + * + * // Print the resize event to the console for debugging. + * print(event); + * } + * + *
+ * @alt + * This example does not render anything. + */ + fn._onresize = function(e) { + this.windowWidth = getWindowWidth(); + this.windowHeight = getWindowHeight(); + const context = this._isGlobal ? window : this; + let executeDefault; + if (typeof context.windowResized === 'function') { + executeDefault = context.windowResized(e); + if (executeDefault !== undefined && !executeDefault) { + e.preventDefault(); + } } - } -}; + }; -function getWindowWidth() { - return ( - window.innerWidth || - (document.documentElement && document.documentElement.clientWidth) || - (document.body && document.body.clientWidth) || - 0 - ); -} + function getWindowWidth() { + return ( + window.innerWidth || + (document.documentElement && document.documentElement.clientWidth) || + (document.body && document.body.clientWidth) || + 0 + ); + } -function getWindowHeight() { - return ( - window.innerHeight || - (document.documentElement && document.documentElement.clientHeight) || - (document.body && document.body.clientHeight) || - 0 - ); -} + function getWindowHeight() { + return ( + window.innerHeight || + (document.documentElement && document.documentElement.clientHeight) || + (document.body && document.body.clientHeight) || + 0 + ); + } -/** - * Called upon each p5 instantiation instead of module import due to the - * possibility of the window being resized when no sketch is active. - */ -p5.prototype._updateWindowSize = function() { - this.windowWidth = getWindowWidth(); - this.windowHeight = getWindowHeight(); -}; + /** + * Called upon each p5 instantiation instead of module import due to the + * possibility of the window being resized when no sketch is active. + */ + fn._updateWindowSize = function() { + this.windowWidth = getWindowWidth(); + this.windowHeight = getWindowHeight(); + }; -/** - * A `Number` variable that stores the width of the canvas in pixels. - * - * `width`'s default value is 100. Calling - * createCanvas() or - * resizeCanvas() changes the value of - * `width`. Calling noCanvas() sets its value to - * 0. - * - * @example - *
- * - * function setup() { - * background(200); - * - * // Display the canvas' width. - * text(width, 42, 54); - * - * describe('The number 100 written in black on a gray square.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(50, 100); - * - * background(200); - * - * // Display the canvas' width. - * text(width, 21, 54); - * - * describe('The number 50 written in black on a gray rectangle.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Display the canvas' width. - * text(width, 42, 54); - * - * describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.'); - * } - * - * // If the mouse is pressed, reisze - * // the canvas and display its new - * // width. - * function mousePressed() { - * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { - * resizeCanvas(50, 100); - * background(200); - * text(width, 21, 54); - * } - * } - * - *
- * - * @property {Number} width - * @readOnly - */ -Object.defineProperty(p5.prototype, 'width', { - get(){ - return this._renderer.width; - } -}); + /** + * A `Number` variable that stores the width of the canvas in pixels. + * + * `width`'s default value is 100. Calling + * createCanvas() or + * resizeCanvas() changes the value of + * `width`. Calling noCanvas() sets its value to + * 0. + * + * @example + *
+ * + * function setup() { + * background(200); + * + * // Display the canvas' width. + * text(width, 42, 54); + * + * describe('The number 100 written in black on a gray square.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(50, 100); + * + * background(200); + * + * // Display the canvas' width. + * text(width, 21, 54); + * + * describe('The number 50 written in black on a gray rectangle.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Display the canvas' width. + * text(width, 42, 54); + * + * describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.'); + * } + * + * // If the mouse is pressed, reisze + * // the canvas and display its new + * // width. + * function mousePressed() { + * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { + * resizeCanvas(50, 100); + * background(200); + * text(width, 21, 54); + * } + * } + * + *
+ * + * @property {Number} width + * @readOnly + */ + Object.defineProperty(fn, 'width', { + get(){ + return this._renderer.width; + } + }); -/** - * A `Number` variable that stores the height of the canvas in pixels. - * - * `height`'s default value is 100. Calling - * createCanvas() or - * resizeCanvas() changes the value of - * `height`. Calling noCanvas() sets its value to - * 0. - * - * @example - *
- * - * function setup() { - * background(200); - * - * // Display the canvas' height. - * text(height, 42, 54); - * - * describe('The number 100 written in black on a gray square.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 50); - * - * background(200); - * - * // Display the canvas' height. - * text(height, 42, 27); - * - * describe('The number 50 written in black on a gray rectangle.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Display the canvas' height. - * text(height, 42, 54); - * - * describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.'); - * } - * - * // If the mouse is pressed, reisze - * // the canvas and display its new - * // height. - * function mousePressed() { - * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { - * resizeCanvas(100, 50); - * background(200); - * text(height, 42, 27); - * } - * } - * - *
- * - * @property {Number} height - * @readOnly - */ -Object.defineProperty(p5.prototype, 'height', { - get(){ - return this._renderer.height; - } -}); + /** + * A `Number` variable that stores the height of the canvas in pixels. + * + * `height`'s default value is 100. Calling + * createCanvas() or + * resizeCanvas() changes the value of + * `height`. Calling noCanvas() sets its value to + * 0. + * + * @example + *
+ * + * function setup() { + * background(200); + * + * // Display the canvas' height. + * text(height, 42, 54); + * + * describe('The number 100 written in black on a gray square.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 50); + * + * background(200); + * + * // Display the canvas' height. + * text(height, 42, 27); + * + * describe('The number 50 written in black on a gray rectangle.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Display the canvas' height. + * text(height, 42, 54); + * + * describe('The number 100 written in black on a gray square. When the mouse is pressed, the square becomes a rectangle and the number becomes 50.'); + * } + * + * // If the mouse is pressed, reisze + * // the canvas and display its new + * // height. + * function mousePressed() { + * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { + * resizeCanvas(100, 50); + * background(200); + * text(height, 42, 27); + * } + * } + * + *
+ * + * @property {Number} height + * @readOnly + */ + Object.defineProperty(fn, 'height', { + get(){ + return this._renderer.height; + } + }); -/** - * Toggles full-screen mode or returns the current mode. - * - * Calling `fullscreen(true)` makes the sketch full-screen. Calling - * `fullscreen(false)` makes the sketch its original size. - * - * Calling `fullscreen()` without an argument returns `true` if the sketch - * is in full-screen mode and `false` if not. - * - * Note: Due to browser restrictions, `fullscreen()` can only be called with - * user input such as a mouse press. - * - * @method fullscreen - * @param {Boolean} [val] whether the sketch should be in fullscreen mode. - * @return {Boolean} current fullscreen state. - * @example - *
- * - * function setup() { - * background(200); - * - * describe('A gray canvas that switches between default and full-screen display when clicked.'); - * } - * - * // If the mouse is pressed, - * // toggle full-screen mode. - * function mousePressed() { - * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { - * let fs = fullscreen(); - * fullscreen(!fs); - * } - * } - * - *
- */ -p5.prototype.fullscreen = function(val) { - p5._validateParameters('fullscreen', arguments); - // no arguments, return fullscreen or not - if (typeof val === 'undefined') { - return ( - document.fullscreenElement || - document.webkitFullscreenElement || - document.mozFullScreenElement || - document.msFullscreenElement - ); - } else { - // otherwise set to fullscreen or not - if (val) { - launchFullscreen(document.documentElement); + /** + * Toggles full-screen mode or returns the current mode. + * + * Calling `fullscreen(true)` makes the sketch full-screen. Calling + * `fullscreen(false)` makes the sketch its original size. + * + * Calling `fullscreen()` without an argument returns `true` if the sketch + * is in full-screen mode and `false` if not. + * + * Note: Due to browser restrictions, `fullscreen()` can only be called with + * user input such as a mouse press. + * + * @method fullscreen + * @param {Boolean} [val] whether the sketch should be in fullscreen mode. + * @return {Boolean} current fullscreen state. + * @example + *
+ * + * function setup() { + * background(200); + * + * describe('A gray canvas that switches between default and full-screen display when clicked.'); + * } + * + * // If the mouse is pressed, + * // toggle full-screen mode. + * function mousePressed() { + * if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) { + * let fs = fullscreen(); + * fullscreen(!fs); + * } + * } + * + *
+ */ + fn.fullscreen = function(val) { + p5._validateParameters('fullscreen', arguments); + // no arguments, return fullscreen or not + if (typeof val === 'undefined') { + return ( + document.fullscreenElement || + document.webkitFullscreenElement || + document.mozFullScreenElement || + document.msFullscreenElement + ); } else { - exitFullscreen(); + // otherwise set to fullscreen or not + if (val) { + launchFullscreen(document.documentElement); + } else { + exitFullscreen(); + } } - } -}; + }; -/** - * Sets the pixel density or returns the current density. - * - * Computer displays are grids of little lights called pixels. A - * display's pixel density describes how many pixels it packs into an - * area. Displays with smaller pixels have a higher pixel density and create - * sharper images. - * - * `pixelDensity()` sets the pixel scaling for high pixel density displays. - * By default, the pixel density is set to match the display's density. - * Calling `pixelDensity(1)` turn this off. - * - * Calling `pixelDensity()` without an argument returns the current pixel - * density. - * - * @method pixelDensity - * @param {Number} [val] desired pixel density. - * @chainable - * @example - *
- * - * function setup() { - * // Set the pixel density to 1. - * pixelDensity(1); - * - * // Create a canvas and draw - * // a circle. - * createCanvas(100, 100); - * background(200); - * circle(50, 50, 70); - * - * describe('A fuzzy white circle on a gray canvas.'); - * } - * - *
- * - *
- * - * function setup() { - * // Set the pixel density to 3. - * pixelDensity(3); - * - * // Create a canvas, paint the - * // background, and draw a - * // circle. - * createCanvas(100, 100); - * background(200); - * circle(50, 50, 70); - * - * describe('A sharp white circle on a gray canvas.'); - * } - * - *
- */ -/** - * @method pixelDensity - * @returns {Number} current pixel density of the sketch. - */ -p5.prototype.pixelDensity = function(val) { - p5._validateParameters('pixelDensity', arguments); - let returnValue; - if (typeof val === 'number') { - if (val !== this._renderer._pixelDensity) { - this._renderer._pixelDensity = val; + /** + * Sets the pixel density or returns the current density. + * + * Computer displays are grids of little lights called pixels. A + * display's pixel density describes how many pixels it packs into an + * area. Displays with smaller pixels have a higher pixel density and create + * sharper images. + * + * `pixelDensity()` sets the pixel scaling for high pixel density displays. + * By default, the pixel density is set to match the display's density. + * Calling `pixelDensity(1)` turn this off. + * + * Calling `pixelDensity()` without an argument returns the current pixel + * density. + * + * @method pixelDensity + * @param {Number} [val] desired pixel density. + * @chainable + * @example + *
+ * + * function setup() { + * // Set the pixel density to 1. + * pixelDensity(1); + * + * // Create a canvas and draw + * // a circle. + * createCanvas(100, 100); + * background(200); + * circle(50, 50, 70); + * + * describe('A fuzzy white circle on a gray canvas.'); + * } + * + *
+ * + *
+ * + * function setup() { + * // Set the pixel density to 3. + * pixelDensity(3); + * + * // Create a canvas, paint the + * // background, and draw a + * // circle. + * createCanvas(100, 100); + * background(200); + * circle(50, 50, 70); + * + * describe('A sharp white circle on a gray canvas.'); + * } + * + *
+ */ + /** + * @method pixelDensity + * @returns {Number} current pixel density of the sketch. + */ + fn.pixelDensity = function(val) { + p5._validateParameters('pixelDensity', arguments); + let returnValue; + if (typeof val === 'number') { + if (val !== this._renderer._pixelDensity) { + this._renderer._pixelDensity = val; + } + returnValue = this; + this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas + } else { + returnValue = this._renderer._pixelDensity; } - returnValue = this; - this.resizeCanvas(this.width, this.height, true); // as a side effect, it will clear the canvas - } else { - returnValue = this._renderer._pixelDensity; - } - return returnValue; -}; + return returnValue; + }; -/** - * Returns the display's current pixel density. - * - * @method displayDensity - * @returns {Number} current pixel density of the display. - * @example - *
- * - * function setup() { - * // Set the pixel density to 1. - * pixelDensity(1); - * - * // Create a canvas and draw - * // a circle. - * createCanvas(100, 100); - * background(200); - * circle(50, 50, 70); - * - * describe('A fuzzy white circle drawn on a gray background. The circle becomes sharper when the mouse is pressed.'); - * } - * - * function mousePressed() { - * // Get the current display density. - * let d = displayDensity(); - * - * // Use the display density to set - * // the sketch's pixel density. - * pixelDensity(d); - * - * // Paint the background and - * // draw a circle. - * background(200); - * circle(50, 50, 70); - * } - * - *
- */ -p5.prototype.displayDensity = () => window.devicePixelRatio; + /** + * Returns the display's current pixel density. + * + * @method displayDensity + * @returns {Number} current pixel density of the display. + * @example + *
+ * + * function setup() { + * // Set the pixel density to 1. + * pixelDensity(1); + * + * // Create a canvas and draw + * // a circle. + * createCanvas(100, 100); + * background(200); + * circle(50, 50, 70); + * + * describe('A fuzzy white circle drawn on a gray background. The circle becomes sharper when the mouse is pressed.'); + * } + * + * function mousePressed() { + * // Get the current display density. + * let d = displayDensity(); + * + * // Use the display density to set + * // the sketch's pixel density. + * pixelDensity(d); + * + * // Paint the background and + * // draw a circle. + * background(200); + * circle(50, 50, 70); + * } + * + *
+ */ + fn.displayDensity = () => window.devicePixelRatio; -function launchFullscreen(element) { - const enabled = - document.fullscreenEnabled || - document.webkitFullscreenEnabled || - document.mozFullScreenEnabled || - document.msFullscreenEnabled; - if (!enabled) { - throw new Error('Fullscreen not enabled in this browser.'); - } - if (element.requestFullscreen) { - element.requestFullscreen(); - } else if (element.mozRequestFullScreen) { - element.mozRequestFullScreen(); - } else if (element.webkitRequestFullscreen) { - element.webkitRequestFullscreen(); - } else if (element.msRequestFullscreen) { - element.msRequestFullscreen(); + function launchFullscreen(element) { + const enabled = + document.fullscreenEnabled || + document.webkitFullscreenEnabled || + document.mozFullScreenEnabled || + document.msFullscreenEnabled; + if (!enabled) { + throw new Error('Fullscreen not enabled in this browser.'); + } + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } } -} -function exitFullscreen() { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); + function exitFullscreen() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } } -} -/** - * Returns the sketch's current - * URL - * as a `String`. - * - * @method getURL - * @return {String} url - * @example - *
- * - * function setup() { - * background(200); - * - * // Get the sketch's URL - * // and display it. - * let url = getURL(); - * textWrap(CHAR); - * text(url, 0, 40, 100); - * - * describe('The URL "https://p5js.org/reference/#/p5/getURL" written in black on a gray background.'); - * } - * - *
- */ -p5.prototype.getURL = () => location.href; + /** + * Returns the sketch's current + * URL + * as a `String`. + * + * @method getURL + * @return {String} url + * @example + *
+ * + * function setup() { + * background(200); + * + * // Get the sketch's URL + * // and display it. + * let url = getURL(); + * textWrap(CHAR); + * text(url, 0, 40, 100); + * + * describe('The URL "https://p5js.org/reference/#/p5/getURL" written in black on a gray background.'); + * } + * + *
+ */ + fn.getURL = () => location.href; -/** - * Returns the current - * URL - * path as an `Array` of `String`s. - * - * For example, consider a sketch hosted at the URL - * `https://example.com/sketchbook`. Calling `getURLPath()` returns - * `['sketchbook']`. For a sketch hosted at the URL - * `https://example.com/sketchbook/monday`, `getURLPath()` returns - * `['sketchbook', 'monday']`. - * - * @method getURLPath - * @return {String[]} path components. - * @example - *
- * - * function setup() { - * background(200); - * - * // Get the sketch's URL path - * // and display the first - * // part. - * let path = getURLPath(); - * text(path[0], 25, 54); - * - * describe('The word "reference" written in black on a gray background.'); - * } - * - *
- */ -p5.prototype.getURLPath = () => - location.pathname.split('/').filter(v => v !== ''); + /** + * Returns the current + * URL + * path as an `Array` of `String`s. + * + * For example, consider a sketch hosted at the URL + * `https://example.com/sketchbook`. Calling `getURLPath()` returns + * `['sketchbook']`. For a sketch hosted at the URL + * `https://example.com/sketchbook/monday`, `getURLPath()` returns + * `['sketchbook', 'monday']`. + * + * @method getURLPath + * @return {String[]} path components. + * @example + *
+ * + * function setup() { + * background(200); + * + * // Get the sketch's URL path + * // and display the first + * // part. + * let path = getURLPath(); + * text(path[0], 25, 54); + * + * describe('The word "reference" written in black on a gray background.'); + * } + * + *
+ */ + fn.getURLPath = () => + location.pathname.split('/').filter(v => v !== ''); -/** - * Returns the current - * URL parameters - * in an `Object`. - * - * For example, calling `getURLParams()` in a sketch hosted at the URL - * `http://p5js.org?year=2014&month=May&day=15` returns - * `{ year: 2014, month: 'May', day: 15 }`. - * - * @method getURLParams - * @return {Object} URL params - * @example - *
- * - * // Imagine this sketch is hosted at the following URL: - * // https://p5js.org?year=2014&month=May&day=15 - * - * function setup() { - * background(200); - * - * // Get the sketch's URL - * // parameters and display - * // them. - * let params = getURLParams(); - * text(params.day, 10, 20); - * text(params.month, 10, 40); - * text(params.year, 10, 60); - * - * describe('The text "15", "May", and "2014" written in black on separate lines.'); - * } - * - *
- * - * @alt - * This example does not render anything. - */ -p5.prototype.getURLParams = function() { - const re = /[?&]([^&=]+)(?:[&=])([^&=]+)/gim; - let m; - const v = {}; - while ((m = re.exec(location.search)) != null) { - if (m.index === re.lastIndex) { - re.lastIndex++; + /** + * Returns the current + * URL parameters + * in an `Object`. + * + * For example, calling `getURLParams()` in a sketch hosted at the URL + * `http://p5js.org?year=2014&month=May&day=15` returns + * `{ year: 2014, month: 'May', day: 15 }`. + * + * @method getURLParams + * @return {Object} URL params + * @example + *
+ * + * // Imagine this sketch is hosted at the following URL: + * // https://p5js.org?year=2014&month=May&day=15 + * + * function setup() { + * background(200); + * + * // Get the sketch's URL + * // parameters and display + * // them. + * let params = getURLParams(); + * text(params.day, 10, 20); + * text(params.month, 10, 40); + * text(params.year, 10, 60); + * + * describe('The text "15", "May", and "2014" written in black on separate lines.'); + * } + * + *
+ * + * @alt + * This example does not render anything. + */ + fn.getURLParams = function() { + const re = /[?&]([^&=]+)(?:[&=])([^&=]+)/gim; + let m; + const v = {}; + while ((m = re.exec(location.search)) != null) { + if (m.index === re.lastIndex) { + re.lastIndex++; + } + v[m[1]] = m[2]; } - v[m[1]] = m[2]; - } - return v; -}; + return v; + }; +} + +export default environment; -export default p5; +if(typeof p5 !== 'undefined'){ + environment(p5, p5.prototype); +} \ No newline at end of file diff --git a/src/core/main.js b/src/core/main.js index 494143de4c..279df9a421 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -5,8 +5,8 @@ * @requires constants */ -// Core needs the PVariables object import * as constants from './constants'; + /** * This is the p5 instance constructor. * @@ -658,4 +658,18 @@ for (const k in constants) { */ p5.disableFriendlyErrors = false; +import transform from './transform'; +import structure from './structure'; +import environment from './environment'; +import rendering from './rendering'; +import renderer from './p5.Renderer'; +import renderer2D from './p5.Renderer2D'; + +p5.registerAddon(transform); +p5.registerAddon(structure); +p5.registerAddon(environment); +p5.registerAddon(rendering); +p5.registerAddon(renderer); +p5.registerAddon(renderer2D); + export default p5; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 0f9f8f795d..0ac35bb2b4 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -4,375 +4,434 @@ * @for p5 */ -import p5 from './main'; import * as constants from '../core/constants'; -/** - * 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 - */ -p5.Renderer = class Renderer { - constructor(pInst, w, h, isMainCanvas) { - this._pInst = this._pixelsState = pInst; - this._isMainCanvas = isMainCanvas; - this.pixels = []; - this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; +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 + */ + p5.Renderer = class Renderer { + constructor(pInst, w, h, isMainCanvas) { + this._pInst = this._pixelsState = pInst; + this._isMainCanvas = isMainCanvas; + this.pixels = []; + this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; - this.width = w; - this.height = h; + this.width = w; + this.height = h; - this._events = {}; + this._events = {}; - if (isMainCanvas) { - this._isMainCanvas = true; - } + if (isMainCanvas) { + this._isMainCanvas = true; + } - // Renderer state machine - this.states = { - doStroke: true, - strokeSet: false, - doFill: true, - fillSet: false, - tint: null, - imageMode: constants.CORNER, - rectMode: constants.CORNER, - ellipseMode: constants.CENTER, - textFont: 'sans-serif', - textLeading: 15, - leadingSet: false, - textSize: 12, - textAlign: constants.LEFT, - textBaseline: constants.BASELINE, - textStyle: constants.NORMAL, - textWrap: constants.WORD - }; - this._pushPopStack = []; - // NOTE: can use the length of the push pop stack instead - this._pushPopDepth = 0; - - this._clipping = false; - this._clipInvert = false; - this._curveTightness = 0; - } + // Renderer state machine + this.states = { + doStroke: true, + strokeSet: false, + doFill: true, + fillSet: false, + tint: null, + imageMode: constants.CORNER, + rectMode: constants.CORNER, + ellipseMode: constants.CENTER, + textFont: 'sans-serif', + textLeading: 15, + leadingSet: false, + textSize: 12, + textAlign: constants.LEFT, + textBaseline: constants.BASELINE, + textStyle: constants.NORMAL, + textWrap: constants.WORD + }; + this._pushPopStack = []; + // NOTE: can use the length of the push pop stack instead + this._pushPopDepth = 0; - remove() { + this._clipping = false; + this._clipInvert = false; + this._curveTightness = 0; + } - } + remove() { + + } - pixelDensity(val){ - let returnValue; - if (typeof val === 'number') { - if (val !== this._pixelDensity) { - this._pixelDensity = val; + 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; } - returnValue = this; - this.resize(this.width, this.height); - } else { - returnValue = this._pixelDensity; + return returnValue; } - return returnValue; - } - // Makes a shallow copy of the current states - // and push it into the push pop stack - push() { - this._pushPopDepth++; - const currentStates = Object.assign({}, this.states); - // Clone properties that support it - for (const key in currentStates) { - if (currentStates[key] instanceof Array) { - currentStates[key] = currentStates[key].slice(); - } else if (currentStates[key] && currentStates[key].clone instanceof Function) { - currentStates[key] = currentStates[key].clone(); + // Makes a shallow copy of the current states + // and push it into the push pop stack + push() { + this._pushPopDepth++; + const currentStates = Object.assign({}, this.states); + // Clone properties that support it + for (const key in currentStates) { + if (currentStates[key] instanceof Array) { + currentStates[key] = currentStates[key].slice(); + } else if (currentStates[key] && currentStates[key].clone instanceof Function) { + currentStates[key] = currentStates[key].clone(); + } } + this._pushPopStack.push(currentStates); + return currentStates; } - this._pushPopStack.push(currentStates); - return currentStates; - } - // Pop the previous states out of the push pop stack and - // assign it back to the current state - pop() { - this._pushPopDepth--; - Object.assign(this.states, this._pushPopStack.pop()); - } + // Pop the previous states out of the push pop stack and + // assign it back to the current state + pop() { + this._pushPopDepth--; + Object.assign(this.states, this._pushPopStack.pop()); + } - 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()?"); + 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; } - 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?"); + 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; } - this._clipping = false; - } - /** - * Resize our canvas element. - */ - resize(w, h) { - this.width = w; - this.height = h; - } + /** + * Resize our canvas element. + */ + resize(w, h) { + this.width = w; + this.height = h; + } - get(x, y, w, h) { - const pixelsState = this._pixelsState; - const pd = this._pixelDensity; - const canvas = this.canvas; + get(x, y, w, h) { + const pixelsState = this._pixelsState; + const pd = this._pixelDensity; + const canvas = this.canvas; - if (typeof x === 'undefined' && typeof y === 'undefined') { - // get() - x = y = 0; - w = pixelsState.width; - h = pixelsState.height; - } else { - x *= pd; - y *= pd; + if (typeof x === 'undefined' && typeof y === 'undefined') { + // get() + x = y = 0; + w = pixelsState.width; + h = pixelsState.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]; - } + 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); + return this._getPixel(x, y); + } + // get(x,y,w,h) } - // get(x,y,w,h) - } - - const region = new p5.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; - } + const region = new p5.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); - scale(x, y){ + return region; + } - } + scale(x, y){ - textSize(s) { - if (typeof s === 'number') { - this.states.textSize = s; - if (!this.states.leadingSet) { - // only use a default value if not previously set (#5181) - this.states.textLeading = s * constants._DEFAULT_LEADMULT; - } - return this._applyTextProperties(); } - return this.states.textSize; - } + textSize(s) { + if (typeof s === 'number') { + this.states.textSize = s; + if (!this.states.leadingSet) { + // only use a default value if not previously set (#5181) + this.states.textLeading = s * constants._DEFAULT_LEADMULT; + } + return this._applyTextProperties(); + } - textLeading (l) { - if (typeof l === 'number') { - this.states.leadingSet = true; - this.states.textLeading = l; - return this._pInst; + return this.states.textSize; } - return this.states.textLeading; - } - - textStyle (s) { - if (s) { - if ( - s === constants.NORMAL || - s === constants.ITALIC || - s === constants.BOLD || - s === constants.BOLDITALIC - ) { - this.states.textStyle = s; + textLeading (l) { + if (typeof l === 'number') { + this.states.leadingSet = true; + this.states.textLeading = l; + return this._pInst; } - return this._applyTextProperties(); + return this.states.textLeading; } - return this.states.textStyle; - } + textStyle (s) { + if (s) { + if ( + s === constants.NORMAL || + s === constants.ITALIC || + s === constants.BOLD || + s === constants.BOLDITALIC + ) { + this.states.textStyle = s; + } - textAscent () { - if (this.states.textAscent === null) { - this._updateTextMetrics(); - } - return this.states.textAscent; - } + return this._applyTextProperties(); + } - textDescent () { - if (this.states.textDescent === null) { - this._updateTextMetrics(); + return this.states.textStyle; } - return this.states.textDescent; - } - - textAlign (h, v) { - if (typeof h !== 'undefined') { - this.states.textAlign = h; - if (typeof v !== 'undefined') { - this.states.textBaseline = v; + textAscent () { + if (this.states.textAscent === null) { + this._updateTextMetrics(); } + return this.states.textAscent; + } - return this._applyTextProperties(); - } else { - return { - horizontal: this.states.textAlign, - vertical: this.states.textBaseline - }; + textDescent () { + if (this.states.textDescent === null) { + this._updateTextMetrics(); + } + return this.states.textDescent; } - } - textWrap (wrapStyle) { - this.states.textWrap = wrapStyle; - return this.states.textWrap; - } + textAlign (h, v) { + if (typeof h !== 'undefined') { + this.states.textAlign = h; - text(str, x, y, maxWidth, maxHeight) { - const p = this._pInst; - const textWrapStyle = this.states.textWrap; - - let lines; - let line; - let testLine; - let testWidth; - let words; - let chars; - let shiftedY; - let finalMaxHeight = Number.MAX_VALUE; - // fix for #5785 (top of bounding box) - let finalMinHeight = y; - - if (!(this.states.doFill || this.states.doStroke)) { - return; - } + if (typeof v !== 'undefined') { + this.states.textBaseline = v; + } - if (typeof str === 'undefined') { - return; - } else if (typeof str !== 'string') { - str = str.toString(); + return this._applyTextProperties(); + } else { + return { + horizontal: this.states.textAlign, + vertical: this.states.textBaseline + }; + } } - // Replaces tabs with double-spaces and splits string on any line - // breaks present in the original string - str = str.replace(/(\t)/g, ' '); - lines = str.split('\n'); + textWrap (wrapStyle) { + this.states.textWrap = wrapStyle; + return this.states.textWrap; + } - if (typeof maxWidth !== 'undefined') { - if (this.states.rectMode === constants.CENTER) { - x -= maxWidth / 2; + text(str, x, y, maxWidth, maxHeight) { + const p = this._pInst; + const textWrapStyle = this.states.textWrap; + + let lines; + let line; + let testLine; + let testWidth; + let words; + let chars; + let shiftedY; + let finalMaxHeight = Number.MAX_VALUE; + // fix for #5785 (top of bounding box) + let finalMinHeight = y; + + if (!(this.states.doFill || this.states.doStroke)) { + return; } - switch (this.states.textAlign) { - case constants.CENTER: - x += maxWidth / 2; - break; - case constants.RIGHT: - x += maxWidth; - break; + if (typeof str === 'undefined') { + return; + } else if (typeof str !== 'string') { + str = str.toString(); } - if (typeof maxHeight !== 'undefined') { + // Replaces tabs with double-spaces and splits string on any line + // breaks present in the original string + str = str.replace(/(\t)/g, ' '); + lines = str.split('\n'); + + if (typeof maxWidth !== 'undefined') { if (this.states.rectMode === constants.CENTER) { - y -= maxHeight / 2; - finalMinHeight -= maxHeight / 2; + x -= maxWidth / 2; } - let originalY = y; - let ascent = p.textAscent(); - - switch (this.states.textBaseline) { - case constants.BOTTOM: - shiftedY = y + maxHeight; - y = Math.max(shiftedY, y); - // fix for #5785 (top of bounding box) - finalMinHeight += ascent; - break; + switch (this.states.textAlign) { case constants.CENTER: - shiftedY = y + maxHeight / 2; - y = Math.max(shiftedY, y); - // fix for #5785 (top of bounding box) - finalMinHeight += ascent / 2; + x += maxWidth / 2; + break; + case constants.RIGHT: + x += maxWidth; break; } - // remember the max-allowed y-position for any line (fix to #928) - finalMaxHeight = y + maxHeight - ascent; + if (typeof maxHeight !== 'undefined') { + if (this.states.rectMode === constants.CENTER) { + y -= maxHeight / 2; + finalMinHeight -= maxHeight / 2; + } - // fix for #5785 (bottom of bounding box) - if (this.states.textBaseline === constants.CENTER) { - finalMaxHeight = originalY + maxHeight - ascent / 2; - } - } else { - // no text-height specified, show warning for BOTTOM / CENTER - if (this.states.textBaseline === constants.BOTTOM || - this.states.textBaseline === constants.CENTER) { - // use rectHeight as an approximation for text height - let rectHeight = p.textSize() * this.states.textLeading; - finalMinHeight = y - rectHeight / 2; - finalMaxHeight = y + rectHeight / 2; + let originalY = y; + let ascent = p.textAscent(); + + switch (this.states.textBaseline) { + case constants.BOTTOM: + shiftedY = y + maxHeight; + y = Math.max(shiftedY, y); + // fix for #5785 (top of bounding box) + finalMinHeight += ascent; + break; + case constants.CENTER: + shiftedY = y + maxHeight / 2; + y = Math.max(shiftedY, y); + // fix for #5785 (top of bounding box) + finalMinHeight += ascent / 2; + break; + } + + // remember the max-allowed y-position for any line (fix to #928) + finalMaxHeight = y + maxHeight - ascent; + + // fix for #5785 (bottom of bounding box) + if (this.states.textBaseline === constants.CENTER) { + finalMaxHeight = originalY + maxHeight - ascent / 2; + } + } else { + // no text-height specified, show warning for BOTTOM / CENTER + if (this.states.textBaseline === constants.BOTTOM || + this.states.textBaseline === constants.CENTER) { + // use rectHeight as an approximation for text height + let rectHeight = p.textSize() * this.states.textLeading; + finalMinHeight = y - rectHeight / 2; + finalMaxHeight = y + rectHeight / 2; + } } - } - // Render lines of text according to settings of textWrap - // Splits lines at spaces, for loop adds one word + space - // at a time and tests length with next word added - if (textWrapStyle === constants.WORD) { - let nlines = []; - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - words = lines[lineIndex].split(' '); - for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { - testLine = `${line + words[wordIndex]}` + ' '; - testWidth = this.textWidth(testLine); - if (testWidth > maxWidth && line.length > 0) { - nlines.push(line); - line = `${words[wordIndex]}` + ' '; - } else { - line = testLine; + // Render lines of text according to settings of textWrap + // Splits lines at spaces, for loop adds one word + space + // at a time and tests length with next word added + if (textWrapStyle === constants.WORD) { + let nlines = []; + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + words = lines[lineIndex].split(' '); + for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { + testLine = `${line + words[wordIndex]}` + ' '; + testWidth = this.textWidth(testLine); + if (testWidth > maxWidth && line.length > 0) { + nlines.push(line); + line = `${words[wordIndex]}` + ' '; + } else { + line = testLine; + } } + nlines.push(line); } - nlines.push(line); - } - let offset = 0; - if (this.states.textBaseline === constants.CENTER) { - offset = (nlines.length - 1) * p.textLeading() / 2; - } else if (this.states.textBaseline === constants.BOTTOM) { - offset = (nlines.length - 1) * p.textLeading(); - } + let offset = 0; + if (this.states.textBaseline === constants.CENTER) { + offset = (nlines.length - 1) * p.textLeading() / 2; + } else if (this.states.textBaseline === constants.BOTTOM) { + offset = (nlines.length - 1) * p.textLeading(); + } + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + words = lines[lineIndex].split(' '); + for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { + testLine = `${line + words[wordIndex]}` + ' '; + testWidth = this.textWidth(testLine); + if (testWidth > maxWidth && line.length > 0) { + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); + line = `${words[wordIndex]}` + ' '; + y += p.textLeading(); + } else { + line = testLine; + } + } + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); + y += p.textLeading(); + } + } else { + let nlines = []; + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + chars = lines[lineIndex].split(''); + for (let charIndex = 0; charIndex < chars.length; charIndex++) { + testLine = `${line + chars[charIndex]}`; + testWidth = this.textWidth(testLine); + if (testWidth <= maxWidth) { + line += chars[charIndex]; + } else if (testWidth > maxWidth && line.length > 0) { + nlines.push(line); + line = `${chars[charIndex]}`; + } + } + } + + nlines.push(line); + let offset = 0; + if (this.states.textBaseline === constants.CENTER) { + offset = (nlines.length - 1) * p.textLeading() / 2; + } else if (this.states.textBaseline === constants.BOTTOM) { + offset = (nlines.length - 1) * p.textLeading(); + } - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - words = lines[lineIndex].split(' '); - for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { - testLine = `${line + words[wordIndex]}` + ' '; - testWidth = this.textWidth(testLine); - if (testWidth > maxWidth && line.length > 0) { - this._renderText( - p, - line.trim(), - x, - y - offset, - finalMaxHeight, - finalMinHeight - ); - line = `${words[wordIndex]}` + ' '; - y += p.textLeading(); - } else { - line = testLine; + // Splits lines at characters, for loop adds one char at a time + // and tests length with next char added + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + chars = lines[lineIndex].split(''); + for (let charIndex = 0; charIndex < chars.length; charIndex++) { + testLine = `${line + chars[charIndex]}`; + testWidth = this.textWidth(testLine); + if (testWidth <= maxWidth) { + line += chars[charIndex]; + } else if (testWidth > maxWidth && line.length > 0) { + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); + y += p.textLeading(); + line = `${chars[charIndex]}`; + } } } this._renderText( @@ -386,165 +445,107 @@ p5.Renderer = class Renderer { y += p.textLeading(); } } else { - let nlines = []; - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - chars = lines[lineIndex].split(''); - for (let charIndex = 0; charIndex < chars.length; charIndex++) { - testLine = `${line + chars[charIndex]}`; - testWidth = this.textWidth(testLine); - if (testWidth <= maxWidth) { - line += chars[charIndex]; - } else if (testWidth > maxWidth && line.length > 0) { - nlines.push(line); - line = `${chars[charIndex]}`; - } - } - } - - nlines.push(line); + // Offset to account for vertically centering multiple lines of text - no + // need to adjust anything for vertical align top or baseline let offset = 0; if (this.states.textBaseline === constants.CENTER) { - offset = (nlines.length - 1) * p.textLeading() / 2; + offset = (lines.length - 1) * p.textLeading() / 2; } else if (this.states.textBaseline === constants.BOTTOM) { - offset = (nlines.length - 1) * p.textLeading(); + offset = (lines.length - 1) * p.textLeading(); } - // Splits lines at characters, for loop adds one char at a time - // and tests length with next char added - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - chars = lines[lineIndex].split(''); - for (let charIndex = 0; charIndex < chars.length; charIndex++) { - testLine = `${line + chars[charIndex]}`; - testWidth = this.textWidth(testLine); - if (testWidth <= maxWidth) { - line += chars[charIndex]; - } else if (testWidth > maxWidth && line.length > 0) { - this._renderText( - p, - line.trim(), - x, - y - offset, - finalMaxHeight, - finalMinHeight - ); - y += p.textLeading(); - line = `${chars[charIndex]}`; - } - } + // Renders lines of text at any line breaks present in the original string + for (let i = 0; i < lines.length; i++) { + this._renderText( + p, + lines[i], + x, + y - offset, + finalMaxHeight, + finalMinHeight - offset + ); + y += p.textLeading(); } - this._renderText( - p, - line.trim(), - x, - y - offset, - finalMaxHeight, - finalMinHeight - ); - y += p.textLeading(); - } - } else { - // Offset to account for vertically centering multiple lines of text - no - // need to adjust anything for vertical align top or baseline - let offset = 0; - if (this.states.textBaseline === constants.CENTER) { - offset = (lines.length - 1) * p.textLeading() / 2; - } else if (this.states.textBaseline === constants.BOTTOM) { - offset = (lines.length - 1) * p.textLeading(); } - // Renders lines of text at any line breaks present in the original string - for (let i = 0; i < lines.length; i++) { - this._renderText( - p, - lines[i], - x, - y - offset, - finalMaxHeight, - finalMinHeight - offset - ); - y += p.textLeading(); - } + return p; } - return p; - } - - _applyDefaults() { - return this; - } - - /** - * Helper function to check font type (system or otf) - */ - _isOpenType(f = this.states.textFont) { - return typeof f === 'object' && f.font && f.font.supported; - } - - _updateTextMetrics() { - if (this._isOpenType()) { - this.states.textAscent = this.states.textFont._textAscent(); - this.states.textDescent = this.states.textFont._textDescent(); + _applyDefaults() { return this; } - // Adapted from http://stackoverflow.com/a/25355178 - const text = document.createElement('span'); - text.style.fontFamily = this.states.textFont; - text.style.fontSize = `${this.states.textSize}px`; - text.innerHTML = 'ABCjgq|'; + /** + * Helper function to check font type (system or otf) + */ + _isOpenType(f = this.states.textFont) { + return typeof f === 'object' && f.font && f.font.supported; + } - const block = document.createElement('div'); - block.style.display = 'inline-block'; - block.style.width = '1px'; - block.style.height = '0px'; + _updateTextMetrics() { + if (this._isOpenType()) { + this.states.textAscent = this.states.textFont._textAscent(); + this.states.textDescent = this.states.textFont._textDescent(); + return this; + } - const container = document.createElement('div'); - container.appendChild(text); - container.appendChild(block); + // Adapted from http://stackoverflow.com/a/25355178 + const text = document.createElement('span'); + text.style.fontFamily = this.states.textFont; + text.style.fontSize = `${this.states.textSize}px`; + text.innerHTML = 'ABCjgq|'; - container.style.height = '0px'; - container.style.overflow = 'hidden'; - document.body.appendChild(container); + const block = document.createElement('div'); + block.style.display = 'inline-block'; + block.style.width = '1px'; + block.style.height = '0px'; - block.style.verticalAlign = 'baseline'; - let blockOffset = calculateOffset(block); - let textOffset = calculateOffset(text); - const ascent = blockOffset[1] - textOffset[1]; + const container = document.createElement('div'); + container.appendChild(text); + container.appendChild(block); - block.style.verticalAlign = 'bottom'; - blockOffset = calculateOffset(block); - textOffset = calculateOffset(text); - const height = blockOffset[1] - textOffset[1]; - const descent = height - ascent; + container.style.height = '0px'; + container.style.overflow = 'hidden'; + document.body.appendChild(container); - document.body.removeChild(container); + block.style.verticalAlign = 'baseline'; + let blockOffset = calculateOffset(block); + let textOffset = calculateOffset(text); + const ascent = blockOffset[1] - textOffset[1]; - this.states.textAscent = ascent; - this.states.textDescent = descent; + block.style.verticalAlign = 'bottom'; + blockOffset = calculateOffset(block); + textOffset = calculateOffset(text); + const height = blockOffset[1] - textOffset[1]; + const descent = height - ascent; - return this; - } -}; + document.body.removeChild(container); -/** - * Helper fxn to measure ascent and descent. - * Adapted from http://stackoverflow.com/a/25355178 - */ -function calculateOffset(object) { - let currentLeft = 0, - currentTop = 0; - if (object.offsetParent) { - do { + this.states.textAscent = ascent; + this.states.textDescent = descent; + + return this; + } + }; + + /** + * Helper fxn to measure ascent and descent. + * Adapted from http://stackoverflow.com/a/25355178 + */ + function calculateOffset(object) { + let currentLeft = 0, + currentTop = 0; + if (object.offsetParent) { + do { + currentLeft += object.offsetLeft; + currentTop += object.offsetTop; + } while ((object = object.offsetParent)); + } else { currentLeft += object.offsetLeft; currentTop += object.offsetTop; - } while ((object = object.offsetParent)); - } else { - currentLeft += object.offsetLeft; - currentTop += object.offsetTop; + } + return [currentLeft, currentTop]; } - return [currentLeft, currentTop]; } -export default p5.Renderer; +export default renderer; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 72fd47610e..967f2501f4 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1,1477 +1,1477 @@ -import p5 from './main'; import * as constants from './constants'; -import Renderer from './p5.Renderer'; - -const styleEmpty = 'rgba(0,0,0,0)'; -// const alphaThreshold = 0.00125; // minimum visible - -/** - * p5.Renderer2D - * The 2D graphics canvas renderer class. - * extends p5.Renderer - * @private - */ -class Renderer2D extends Renderer { - constructor(pInst, w, h, isMainCanvas, elt) { - super(pInst, w, h, isMainCanvas); - - this.canvas = this.elt = elt || document.createElement('canvas'); - - if (isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } +function renderer2D(p5, fn){ + const styleEmpty = 'rgba(0,0,0,0)'; + // const alphaThreshold = 0.00125; // minimum visible + + /** + * p5.Renderer2D + * The 2D graphics canvas renderer class. + * extends p5.Renderer + * @private + */ + class Renderer2D extends p5.Renderer { + constructor(pInst, w, h, isMainCanvas, elt) { + super(pInst, w, h, isMainCanvas); + + this.canvas = this.elt = elt || document.createElement('canvas'); + + if (isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } - this.elt.id = 'defaultCanvas0'; - this.elt.classList.add('p5Canvas'); + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); - // Extend renderer with methods of p5.Element with getters - // this.wrappedElt = new p5.Element(elt, pInst); - for (const p of Object.getOwnPropertyNames(p5.Element.prototype)) { - if (p !== 'constructor' && p[0] !== '_') { - Object.defineProperty(this, p, { - get() { - return this.wrappedElt[p]; - } - }) + // Extend renderer with methods of p5.Element with getters + // this.wrappedElt = new p5.Element(elt, pInst); + for (const p of Object.getOwnPropertyNames(p5.Element.prototype)) { + if (p !== 'constructor' && p[0] !== '_') { + Object.defineProperty(this, p, { + get() { + return this.wrappedElt[p]; + } + }) + } } - } - // Set canvas size - this.elt.width = w * this._pixelDensity; - this.elt.height = h * this._pixelDensity; - this.elt.style.width = `${w}px`; - this.elt.style.height = `${h}px`; - - // Attach canvas element to DOM - if (this._pInst._userNode) { - // user input node case - this._pInst._userNode.appendChild(this.elt); - } else { - //create main element - if (document.getElementsByTagName('main').length === 0) { - let m = document.createElement('main'); - document.body.appendChild(m); - } - //append canvas to main - document.getElementsByTagName('main')[0].appendChild(this.elt); - } + // Set canvas size + this.elt.width = w * this._pixelDensity; + this.elt.height = h * this._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; - // Get and store drawing context - this.drawingContext = this.canvas.getContext('2d'); - this._pInst.drawingContext = this.drawingContext; - this.scale(this._pixelDensity, this._pixelDensity); + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName('main').length === 0) { + let m = document.createElement('main'); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName('main')[0].appendChild(this.elt); + } - // Set and return p5.Element - this.wrappedElt = new p5.Element(this.elt, this._pInst); - } + // Get and store drawing context + this.drawingContext = this.canvas.getContext('2d'); + this._pInst.drawingContext = this.drawingContext; + this.scale(this._pixelDensity, this._pixelDensity); - remove(){ - this.wrappedElt.remove(); - this.wrappedElt = null; - this.canvas = null; - this.elt = null; - } + // Set and return p5.Element + this.wrappedElt = new p5.Element(this.elt, this._pInst); + } - getFilterGraphicsLayer() { - // create hidden webgl renderer if it doesn't exist - if (!this.filterGraphicsLayer) { - const pInst = this._pInst; - - // create secondary layer - this.filterGraphicsLayer = - new p5.Graphics( - this.width, - this.height, - constants.WEBGL, - pInst - ); + remove(){ + this.wrappedElt.remove(); + this.wrappedElt = null; + this.canvas = null; + this.elt = null; } - if ( - this.filterGraphicsLayer.width !== this.width || - this.filterGraphicsLayer.height !== this.height - ) { - // Resize the graphics layer - this.filterGraphicsLayer.resizeCanvas(this.width, this.height); + + getFilterGraphicsLayer() { + // create hidden webgl renderer if it doesn't exist + if (!this.filterGraphicsLayer) { + const pInst = this._pInst; + + // create secondary layer + this.filterGraphicsLayer = + new p5.Graphics( + this.width, + this.height, + constants.WEBGL, + pInst + ); + } + if ( + this.filterGraphicsLayer.width !== this.width || + this.filterGraphicsLayer.height !== this.height + ) { + // Resize the graphics layer + this.filterGraphicsLayer.resizeCanvas(this.width, this.height); + } + if ( + this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() + ) { + this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); + } + return this.filterGraphicsLayer; } - if ( - this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() - ) { - this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); + + _applyDefaults() { + this._cachedFillStyle = this._cachedStrokeStyle = undefined; + this._cachedBlendMode = constants.BLEND; + this._setFill(constants._DEFAULT_FILL); + this._setStroke(constants._DEFAULT_STROKE); + this.drawingContext.lineCap = constants.ROUND; + this.drawingContext.font = 'normal 12px sans-serif'; } - return this.filterGraphicsLayer; - } - _applyDefaults() { - this._cachedFillStyle = this._cachedStrokeStyle = undefined; - this._cachedBlendMode = constants.BLEND; - this._setFill(constants._DEFAULT_FILL); - this._setStroke(constants._DEFAULT_STROKE); - this.drawingContext.lineCap = constants.ROUND; - this.drawingContext.font = 'normal 12px sans-serif'; - } + resize(w, h) { + super.resize(w, h); + + // save canvas properties + const props = {}; + for (const key in this.drawingContext) { + const val = this.drawingContext[key]; + if (typeof val !== 'object' && typeof val !== 'function') { + props[key] = val; + } + } - resize(w, h) { - super.resize(w, h); + this.canvas.width = w * this._pixelDensity; + this.canvas.height = h * this._pixelDensity; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; + this.drawingContext.scale( + this._pixelDensity, + this._pixelDensity + ); - // save canvas properties - const props = {}; - for (const key in this.drawingContext) { - const val = this.drawingContext[key]; - if (typeof val !== 'object' && typeof val !== 'function') { - props[key] = val; + // reset canvas properties + for (const savedKey in props) { + try { + this.drawingContext[savedKey] = props[savedKey]; + } catch (err) { + // ignore read-only property errors + } } } - this.canvas.width = w * this._pixelDensity; - this.canvas.height = h * this._pixelDensity; - this.canvas.style.width = `${w}px`; - this.canvas.style.height = `${h}px`; - this.drawingContext.scale( - this._pixelDensity, - this._pixelDensity - ); - - // reset canvas properties - for (const savedKey in props) { - try { - this.drawingContext[savedKey] = props[savedKey]; - } catch (err) { - // ignore read-only property errors + ////////////////////////////////////////////// + // COLOR | Setting + ////////////////////////////////////////////// + + background(...args) { + this.drawingContext.save(); + this.resetMatrix(); + + if (args[0] instanceof p5.Image) { + if (args[1] >= 0) { + // set transparency of background + const img = args[0]; + this.drawingContext.globalAlpha = args[1] / 255; + this._pInst.image(img, 0, 0, this.width, this.height); + } else { + this._pInst.image(args[0], 0, 0, this.width, this.height); + } + } else { + const curFill = this._getFill(); + // create background rect + const color = this._pInst.color(...args); + + //accessible Outputs + if (this._pInst._addAccsOutput()) { + this._pInst._accsBackground(color.levels); + } + + const newFill = color.toString(); + this._setFill(newFill); + + if (this._isErasing) { + this.blendMode(this._cachedBlendMode); + } + + this.drawingContext.fillRect(0, 0, this.width, this.height); + // reset fill + this._setFill(curFill); + + if (this._isErasing) { + this._pInst.erase(); + } } + this.drawingContext.restore(); } - } - ////////////////////////////////////////////// - // COLOR | Setting - ////////////////////////////////////////////// + clear() { + this.drawingContext.save(); + this.resetMatrix(); + this.drawingContext.clearRect(0, 0, this.width, this.height); + this.drawingContext.restore(); + } - background(...args) { - this.drawingContext.save(); - this.resetMatrix(); + fill(...args) { + const color = this._pInst.color(...args); + this._setFill(color.toString()); - if (args[0] instanceof p5.Image) { - if (args[1] >= 0) { - // set transparency of background - const img = args[0]; - this.drawingContext.globalAlpha = args[1] / 255; - this._pInst.image(img, 0, 0, this.width, this.height); - } else { - this._pInst.image(args[0], 0, 0, this.width, this.height); + //accessible Outputs + if (this._pInst._addAccsOutput()) { + this._pInst._accsCanvasColors('fill', color.levels); } - } else { - const curFill = this._getFill(); - // create background rect + } + + stroke(...args) { const color = this._pInst.color(...args); + this._setStroke(color.toString()); //accessible Outputs if (this._pInst._addAccsOutput()) { - this._pInst._accsBackground(color.levels); + this._pInst._accsCanvasColors('stroke', color.levels); } + } - const newFill = color.toString(); - this._setFill(newFill); + erase(opacityFill, opacityStroke) { + if (!this._isErasing) { + // cache the fill style + this._cachedFillStyle = this.drawingContext.fillStyle; + const newFill = this._pInst.color(255, opacityFill).toString(); + this.drawingContext.fillStyle = newFill; - if (this._isErasing) { - this.blendMode(this._cachedBlendMode); - } + // cache the stroke style + this._cachedStrokeStyle = this.drawingContext.strokeStyle; + const newStroke = this._pInst.color(255, opacityStroke).toString(); + this.drawingContext.strokeStyle = newStroke; - this.drawingContext.fillRect(0, 0, this.width, this.height); - // reset fill - this._setFill(curFill); + // cache blendMode + const tempBlendMode = this._cachedBlendMode; + this.blendMode(constants.REMOVE); + this._cachedBlendMode = tempBlendMode; - if (this._isErasing) { - this._pInst.erase(); + this._isErasing = true; } } - this.drawingContext.restore(); - } - - clear() { - this.drawingContext.save(); - this.resetMatrix(); - this.drawingContext.clearRect(0, 0, this.width, this.height); - this.drawingContext.restore(); - } - fill(...args) { - const color = this._pInst.color(...args); - this._setFill(color.toString()); + noErase() { + if (this._isErasing) { + this.drawingContext.fillStyle = this._cachedFillStyle; + this.drawingContext.strokeStyle = this._cachedStrokeStyle; - //accessible Outputs - if (this._pInst._addAccsOutput()) { - this._pInst._accsCanvasColors('fill', color.levels); + this.blendMode(this._cachedBlendMode); + this._isErasing = false; + } } - } - stroke(...args) { - const color = this._pInst.color(...args); - this._setStroke(color.toString()); + beginClip(options = {}) { + super.beginClip(options); - //accessible Outputs - if (this._pInst._addAccsOutput()) { - this._pInst._accsCanvasColors('stroke', color.levels); - } - } - - erase(opacityFill, opacityStroke) { - if (!this._isErasing) { // cache the fill style this._cachedFillStyle = this.drawingContext.fillStyle; - const newFill = this._pInst.color(255, opacityFill).toString(); + const newFill = this._pInst.color(255, 0).toString(); this.drawingContext.fillStyle = newFill; // cache the stroke style this._cachedStrokeStyle = this.drawingContext.strokeStyle; - const newStroke = this._pInst.color(255, opacityStroke).toString(); + const newStroke = this._pInst.color(255, 0).toString(); this.drawingContext.strokeStyle = newStroke; // cache blendMode const tempBlendMode = this._cachedBlendMode; - this.blendMode(constants.REMOVE); + this.blendMode(constants.BLEND); this._cachedBlendMode = tempBlendMode; - this._isErasing = true; + // Start a new path. Everything from here on out should become part of this + // one path so that we can clip to the whole thing. + this.drawingContext.beginPath(); + + if (this._clipInvert) { + // Slight hack: draw a big rectangle over everything with reverse winding + // order. This is hopefully large enough to cover most things. + this.drawingContext.moveTo( + -2 * this.width, + -2 * this.height + ); + this.drawingContext.lineTo( + -2 * this.width, + 2 * this.height + ); + this.drawingContext.lineTo( + 2 * this.width, + 2 * this.height + ); + this.drawingContext.lineTo( + 2 * this.width, + -2 * this.height + ); + this.drawingContext.closePath(); + } } - } - noErase() { - if (this._isErasing) { + endClip() { + this._doFillStrokeClose(); + this.drawingContext.clip(); + + super.endClip(); + this.drawingContext.fillStyle = this._cachedFillStyle; this.drawingContext.strokeStyle = this._cachedStrokeStyle; this.blendMode(this._cachedBlendMode); - this._isErasing = false; - } - } - - beginClip(options = {}) { - super.beginClip(options); - - // cache the fill style - this._cachedFillStyle = this.drawingContext.fillStyle; - const newFill = this._pInst.color(255, 0).toString(); - this.drawingContext.fillStyle = newFill; - - // cache the stroke style - this._cachedStrokeStyle = this.drawingContext.strokeStyle; - const newStroke = this._pInst.color(255, 0).toString(); - this.drawingContext.strokeStyle = newStroke; - - // cache blendMode - const tempBlendMode = this._cachedBlendMode; - this.blendMode(constants.BLEND); - this._cachedBlendMode = tempBlendMode; - - // Start a new path. Everything from here on out should become part of this - // one path so that we can clip to the whole thing. - this.drawingContext.beginPath(); - - if (this._clipInvert) { - // Slight hack: draw a big rectangle over everything with reverse winding - // order. This is hopefully large enough to cover most things. - this.drawingContext.moveTo( - -2 * this.width, - -2 * this.height - ); - this.drawingContext.lineTo( - -2 * this.width, - 2 * this.height - ); - this.drawingContext.lineTo( - 2 * this.width, - 2 * this.height - ); - this.drawingContext.lineTo( - 2 * this.width, - -2 * this.height - ); - this.drawingContext.closePath(); } - } - - endClip() { - this._doFillStrokeClose(); - this.drawingContext.clip(); - - super.endClip(); - this.drawingContext.fillStyle = this._cachedFillStyle; - this.drawingContext.strokeStyle = this._cachedStrokeStyle; + ////////////////////////////////////////////// + // IMAGE | Loading & Displaying + ////////////////////////////////////////////// + + image( + img, + sx, + sy, + sWidth, + sHeight, + dx, + dy, + dWidth, + dHeight + ) { + let cnv; + if (img.gifProperties) { + img._animateGif(this._pInst); + } - this.blendMode(this._cachedBlendMode); - } + try { + if (p5.MediaElement && img instanceof p5.MediaElement) { + img._ensureCanvas(); + } + if (this.states.tint && img.canvas) { + cnv = this._getTintedImageCanvas(img); + } + if (!cnv) { + cnv = img.canvas || img.elt; + } + let s = 1; + if (img.width && img.width > 0) { + s = cnv.width / img.width; + } + if (this._isErasing) { + this.blendMode(this._cachedBlendMode); + } - ////////////////////////////////////////////// - // IMAGE | Loading & Displaying - ////////////////////////////////////////////// - - image( - img, - sx, - sy, - sWidth, - sHeight, - dx, - dy, - dWidth, - dHeight - ) { - let cnv; - if (img.gifProperties) { - img._animateGif(this._pInst); + this.drawingContext.drawImage( + cnv, + s * sx, + s * sy, + s * sWidth, + s * sHeight, + dx, + dy, + dWidth, + dHeight + ); + if (this._isErasing) { + this._pInst.erase(); + } + } catch (e) { + if (e.name !== 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + } } - try { - if (p5.MediaElement && img instanceof p5.MediaElement) { - img._ensureCanvas(); - } - if (this.states.tint && img.canvas) { - cnv = this._getTintedImageCanvas(img); + _getTintedImageCanvas(img) { + if (!img.canvas) { + return img; } - if (!cnv) { - cnv = img.canvas || img.elt; + + if (!img.tintCanvas) { + // Once an image has been tinted, keep its tint canvas + // around so we don't need to re-incur the cost of + // creating a new one for each tint + img.tintCanvas = document.createElement('canvas'); } - let s = 1; - if (img.width && img.width > 0) { - s = cnv.width / img.width; + + // Keep the size of the tint canvas up-to-date + if (img.tintCanvas.width !== img.canvas.width) { + img.tintCanvas.width = img.canvas.width; } - if (this._isErasing) { - this.blendMode(this._cachedBlendMode); + if (img.tintCanvas.height !== img.canvas.height) { + img.tintCanvas.height = img.canvas.height; } - this.drawingContext.drawImage( - cnv, - s * sx, - s * sy, - s * sWidth, - s * sHeight, - dx, - dy, - dWidth, - dHeight - ); - if (this._isErasing) { - this._pInst.erase(); - } - } catch (e) { - if (e.name !== 'NS_ERROR_NOT_AVAILABLE') { - throw e; + // Goal: multiply the r,g,b,a values of the source by + // the r,g,b,a values of the tint color + const ctx = img.tintCanvas.getContext('2d'); + + ctx.save(); + ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); + + if (this.states.tint[0] < 255 || this.states.tint[1] < 255 || this.states.tint[2] < 255) { + // Color tint: we need to use the multiply blend mode to change the colors. + // However, the canvas implementation of this destroys the alpha channel of + // the image. To accommodate, we first get a version of the image with full + // opacity everywhere, tint using multiply, and then use the destination-in + // blend mode to restore the alpha channel again. + + // Start with the original image + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode makes everything opaque but forces the luma to match + // the original image again + ctx.globalCompositeOperation = 'luminosity'; + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode forces the hue and chroma to match the original image. + // After this we should have the original again, but with full opacity. + ctx.globalCompositeOperation = 'color'; + ctx.drawImage(img.canvas, 0, 0); + + // Apply color tint + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = `rgb(${this.states.tint.slice(0, 3).join(', ')})`; + ctx.fillRect(0, 0, img.canvas.width, img.canvas.height); + + // Replace the alpha channel with the original alpha * the alpha tint + ctx.globalCompositeOperation = 'destination-in'; + ctx.globalAlpha = this.states.tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); + } else { + // If we only need to change the alpha, we can skip all the extra work! + ctx.globalAlpha = this.states.tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); } - } - } - _getTintedImageCanvas(img) { - if (!img.canvas) { - return img; + ctx.restore(); + return img.tintCanvas; + } + + ////////////////////////////////////////////// + // IMAGE | Pixels + ////////////////////////////////////////////// + + blendMode(mode) { + if (mode === constants.SUBTRACT) { + console.warn('blendMode(SUBTRACT) only works in WEBGL mode.'); + } else if ( + mode === constants.BLEND || + mode === constants.REMOVE || + mode === constants.DARKEST || + mode === constants.LIGHTEST || + mode === constants.DIFFERENCE || + mode === constants.MULTIPLY || + mode === constants.EXCLUSION || + mode === constants.SCREEN || + mode === constants.REPLACE || + mode === constants.OVERLAY || + mode === constants.HARD_LIGHT || + mode === constants.SOFT_LIGHT || + mode === constants.DODGE || + mode === constants.BURN || + mode === constants.ADD + ) { + this._cachedBlendMode = mode; + this.drawingContext.globalCompositeOperation = mode; + } else { + throw new Error(`Mode ${mode} not recognized.`); + } } - if (!img.tintCanvas) { - // Once an image has been tinted, keep its tint canvas - // around so we don't need to re-incur the cost of - // creating a new one for each tint - img.tintCanvas = document.createElement('canvas'); - } + blend(...args) { + const currBlend = this.drawingContext.globalCompositeOperation; + const blendMode = args[args.length - 1]; - // Keep the size of the tint canvas up-to-date - if (img.tintCanvas.width !== img.canvas.width) { - img.tintCanvas.width = img.canvas.width; - } - if (img.tintCanvas.height !== img.canvas.height) { - img.tintCanvas.height = img.canvas.height; - } + const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1); - // Goal: multiply the r,g,b,a values of the source by - // the r,g,b,a values of the tint color - const ctx = img.tintCanvas.getContext('2d'); - - ctx.save(); - ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); - - if (this.states.tint[0] < 255 || this.states.tint[1] < 255 || this.states.tint[2] < 255) { - // Color tint: we need to use the multiply blend mode to change the colors. - // However, the canvas implementation of this destroys the alpha channel of - // the image. To accommodate, we first get a version of the image with full - // opacity everywhere, tint using multiply, and then use the destination-in - // blend mode to restore the alpha channel again. - - // Start with the original image - ctx.drawImage(img.canvas, 0, 0); - - // This blend mode makes everything opaque but forces the luma to match - // the original image again - ctx.globalCompositeOperation = 'luminosity'; - ctx.drawImage(img.canvas, 0, 0); - - // This blend mode forces the hue and chroma to match the original image. - // After this we should have the original again, but with full opacity. - ctx.globalCompositeOperation = 'color'; - ctx.drawImage(img.canvas, 0, 0); - - // Apply color tint - ctx.globalCompositeOperation = 'multiply'; - ctx.fillStyle = `rgb(${this.states.tint.slice(0, 3).join(', ')})`; - ctx.fillRect(0, 0, img.canvas.width, img.canvas.height); - - // Replace the alpha channel with the original alpha * the alpha tint - ctx.globalCompositeOperation = 'destination-in'; - ctx.globalAlpha = this.states.tint[3] / 255; - ctx.drawImage(img.canvas, 0, 0); - } else { - // If we only need to change the alpha, we can skip all the extra work! - ctx.globalAlpha = this.states.tint[3] / 255; - ctx.drawImage(img.canvas, 0, 0); - } + this.drawingContext.globalCompositeOperation = blendMode; - ctx.restore(); - return img.tintCanvas; - } + fn.copy.apply(this, copyArgs); - ////////////////////////////////////////////// - // IMAGE | Pixels - ////////////////////////////////////////////// - - blendMode(mode) { - if (mode === constants.SUBTRACT) { - console.warn('blendMode(SUBTRACT) only works in WEBGL mode.'); - } else if ( - mode === constants.BLEND || - mode === constants.REMOVE || - mode === constants.DARKEST || - mode === constants.LIGHTEST || - mode === constants.DIFFERENCE || - mode === constants.MULTIPLY || - mode === constants.EXCLUSION || - mode === constants.SCREEN || - mode === constants.REPLACE || - mode === constants.OVERLAY || - mode === constants.HARD_LIGHT || - mode === constants.SOFT_LIGHT || - mode === constants.DODGE || - mode === constants.BURN || - mode === constants.ADD - ) { - this._cachedBlendMode = mode; - this.drawingContext.globalCompositeOperation = mode; - } else { - throw new Error(`Mode ${mode} not recognized.`); + this.drawingContext.globalCompositeOperation = currBlend; } - } - - blend(...args) { - const currBlend = this.drawingContext.globalCompositeOperation; - const blendMode = args[args.length - 1]; - - const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1); - - this.drawingContext.globalCompositeOperation = blendMode; - p5.prototype.copy.apply(this, copyArgs); + // p5.Renderer2D.prototype.get = p5.Renderer.prototype.get; + // .get() is not overridden - this.drawingContext.globalCompositeOperation = currBlend; - } + // x,y are canvas-relative (pre-scaled by _pixelDensity) + _getPixel(x, y) { + let imageData, index; + imageData = this.drawingContext.getImageData(x, y, 1, 1).data; + index = 0; + return [ + imageData[index + 0], + imageData[index + 1], + imageData[index + 2], + imageData[index + 3] + ]; + } - // p5.Renderer2D.prototype.get = p5.Renderer.prototype.get; - // .get() is not overridden - - // x,y are canvas-relative (pre-scaled by _pixelDensity) - _getPixel(x, y) { - let imageData, index; - imageData = this.drawingContext.getImageData(x, y, 1, 1).data; - index = 0; - return [ - imageData[index + 0], - imageData[index + 1], - imageData[index + 2], - imageData[index + 3] - ]; - } + loadPixels() { + const pixelsState = this._pixelsState; // if called by p5.Image - loadPixels() { - const pixelsState = this._pixelsState; // if called by p5.Image - - const pd = this._pixelDensity; - const w = this.width * pd; - const h = this.height * pd; - const imageData = this.drawingContext.getImageData(0, 0, w, h); - // @todo this should actually set pixels per object, so diff buffers can - // have diff pixel arrays. - pixelsState.imageData = imageData; - this.pixels = pixelsState.pixels = imageData.data; - } + const pd = this._pixelDensity; + const w = this.width * pd; + const h = this.height * pd; + const imageData = this.drawingContext.getImageData(0, 0, w, h); + // @todo this should actually set pixels per object, so diff buffers can + // have diff pixel arrays. + pixelsState.imageData = imageData; + this.pixels = pixelsState.pixels = imageData.data; + } - set(x, y, imgOrCol) { - // round down to get integer numbers - x = Math.floor(x); - y = Math.floor(y); - const pixelsState = this._pixelsState; - if (imgOrCol instanceof p5.Image) { - this.drawingContext.save(); - this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); - this.drawingContext.scale( - this._pixelDensity, - this._pixelDensity - ); - this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); - this.drawingContext.drawImage(imgOrCol.canvas, x, y); - this.drawingContext.restore(); - } else { - let r = 0, - g = 0, - b = 0, - a = 0; - let idx = - 4 * - (y * - this._pixelDensity * - (this.width * this._pixelDensity) + - x * this._pixelDensity); - if (!pixelsState.imageData) { - pixelsState.loadPixels(); - } - if (typeof imgOrCol === 'number') { - if (idx < pixelsState.pixels.length) { - r = imgOrCol; - g = imgOrCol; - b = imgOrCol; - a = 255; - //this.updatePixels.call(this); - } - } else if (Array.isArray(imgOrCol)) { - if (imgOrCol.length < 4) { - throw new Error('pixel array must be of the form [R, G, B, A]'); - } - if (idx < pixelsState.pixels.length) { - r = imgOrCol[0]; - g = imgOrCol[1]; - b = imgOrCol[2]; - a = imgOrCol[3]; - //this.updatePixels.call(this); + set(x, y, imgOrCol) { + // round down to get integer numbers + x = Math.floor(x); + y = Math.floor(y); + const pixelsState = this._pixelsState; + if (imgOrCol instanceof p5.Image) { + this.drawingContext.save(); + this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); + this.drawingContext.scale( + this._pixelDensity, + this._pixelDensity + ); + this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); + this.drawingContext.drawImage(imgOrCol.canvas, x, y); + this.drawingContext.restore(); + } else { + let r = 0, + g = 0, + b = 0, + a = 0; + let idx = + 4 * + (y * + this._pixelDensity * + (this.width * this._pixelDensity) + + x * this._pixelDensity); + if (!pixelsState.imageData) { + pixelsState.loadPixels(); } - } else if (imgOrCol instanceof p5.Color) { - if (idx < pixelsState.pixels.length) { - r = imgOrCol.levels[0]; - g = imgOrCol.levels[1]; - b = imgOrCol.levels[2]; - a = imgOrCol.levels[3]; - //this.updatePixels.call(this); + if (typeof imgOrCol === 'number') { + if (idx < pixelsState.pixels.length) { + r = imgOrCol; + g = imgOrCol; + b = imgOrCol; + a = 255; + //this.updatePixels.call(this); + } + } else if (Array.isArray(imgOrCol)) { + if (imgOrCol.length < 4) { + throw new Error('pixel array must be of the form [R, G, B, A]'); + } + if (idx < pixelsState.pixels.length) { + r = imgOrCol[0]; + g = imgOrCol[1]; + b = imgOrCol[2]; + a = imgOrCol[3]; + //this.updatePixels.call(this); + } + } else if (imgOrCol instanceof p5.Color) { + if (idx < pixelsState.pixels.length) { + r = imgOrCol.levels[0]; + g = imgOrCol.levels[1]; + b = imgOrCol.levels[2]; + a = imgOrCol.levels[3]; + //this.updatePixels.call(this); + } } - } - // loop over pixelDensity * pixelDensity - for (let i = 0; i < this._pixelDensity; i++) { - for (let j = 0; j < this._pixelDensity; j++) { - // loop over - idx = - 4 * - ((y * this._pixelDensity + j) * - this.width * - this._pixelDensity + - (x * this._pixelDensity + i)); - pixelsState.pixels[idx] = r; - pixelsState.pixels[idx + 1] = g; - pixelsState.pixels[idx + 2] = b; - pixelsState.pixels[idx + 3] = a; + // loop over pixelDensity * pixelDensity + for (let i = 0; i < this._pixelDensity; i++) { + for (let j = 0; j < this._pixelDensity; j++) { + // loop over + idx = + 4 * + ((y * this._pixelDensity + j) * + this.width * + this._pixelDensity + + (x * this._pixelDensity + i)); + pixelsState.pixels[idx] = r; + pixelsState.pixels[idx + 1] = g; + pixelsState.pixels[idx + 2] = b; + pixelsState.pixels[idx + 3] = a; + } } } } - } - - updatePixels(x, y, w, h) { - const pixelsState = this._pixelsState; - const pd = this._pixelDensity; - if ( - x === undefined && - y === undefined && - w === undefined && - h === undefined - ) { - x = 0; - y = 0; - w = this.width; - h = this.height; - } - x *= pd; - y *= pd; - w *= pd; - h *= pd; - - if (this.gifProperties) { - this.gifProperties.frames[this.gifProperties.displayIndex].image = - pixelsState.imageData; - } - - this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); - } - ////////////////////////////////////////////// - // SHAPE | 2D Primitives - ////////////////////////////////////////////// - - /** - * Generate a cubic Bezier representing an arc on the unit circle of total - * angle `size` radians, beginning `start` radians above the x-axis. Up to - * four of these curves are combined to make a full arc. - * - * See ecridge.com/bezier.pdf for an explanation of the method. - */ - _acuteArcToBezier( - start, - size - ) { - // Evaluate constants. - const alpha = size / 2.0, - cos_alpha = Math.cos(alpha), - sin_alpha = Math.sin(alpha), - cot_alpha = 1.0 / Math.tan(alpha), - // This is how far the arc needs to be rotated. - phi = start + alpha, - cos_phi = Math.cos(phi), - sin_phi = Math.sin(phi), - lambda = (4.0 - cos_alpha) / 3.0, - mu = sin_alpha + (cos_alpha - lambda) * cot_alpha; - - // Return rotated waypoints. - return { - ax: Math.cos(start).toFixed(7), - ay: Math.sin(start).toFixed(7), - bx: (lambda * cos_phi + mu * sin_phi).toFixed(7), - by: (lambda * sin_phi - mu * cos_phi).toFixed(7), - cx: (lambda * cos_phi - mu * sin_phi).toFixed(7), - cy: (lambda * sin_phi + mu * cos_phi).toFixed(7), - dx: Math.cos(start + size).toFixed(7), - dy: Math.sin(start + size).toFixed(7) - }; - } + updatePixels(x, y, w, h) { + const pixelsState = this._pixelsState; + const pd = this._pixelDensity; + if ( + x === undefined && + y === undefined && + w === undefined && + h === undefined + ) { + x = 0; + y = 0; + w = this.width; + h = this.height; + } + x *= pd; + y *= pd; + w *= pd; + h *= pd; + + if (this.gifProperties) { + this.gifProperties.frames[this.gifProperties.displayIndex].image = + pixelsState.imageData; + } - /* - * This function requires that: - * - * 0 <= start < TWO_PI - * - * start <= stop < start + TWO_PI - */ - arc(x, y, w, h, start, stop, mode) { - const ctx = this.drawingContext; - const rx = w / 2.0; - const ry = h / 2.0; - const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. - let arcToDraw = 0; - const curves = []; - - x += rx; - y += ry; - - // Create curves - while (stop - start >= epsilon) { - arcToDraw = Math.min(stop - start, constants.HALF_PI); - curves.push(this._acuteArcToBezier(start, arcToDraw)); - start += arcToDraw; + this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); } - // Fill curves - if (this.states.doFill) { - if (!this._clipping) ctx.beginPath(); - curves.forEach((curve, index) => { - if (index === 0) { - ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); - } - /* eslint-disable indent */ - ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, - x + curve.cx * rx, y + curve.cy * ry, - x + curve.dx * rx, y + curve.dy * ry); - /* eslint-enable indent */ - }); - if (mode === constants.PIE || mode == null) { - ctx.lineTo(x, y); + ////////////////////////////////////////////// + // SHAPE | 2D Primitives + ////////////////////////////////////////////// + + /** + * Generate a cubic Bezier representing an arc on the unit circle of total + * angle `size` radians, beginning `start` radians above the x-axis. Up to + * four of these curves are combined to make a full arc. + * + * See ecridge.com/bezier.pdf for an explanation of the method. + */ + _acuteArcToBezier( + start, + size + ) { + // Evaluate constants. + const alpha = size / 2.0, + cos_alpha = Math.cos(alpha), + sin_alpha = Math.sin(alpha), + cot_alpha = 1.0 / Math.tan(alpha), + // This is how far the arc needs to be rotated. + phi = start + alpha, + cos_phi = Math.cos(phi), + sin_phi = Math.sin(phi), + lambda = (4.0 - cos_alpha) / 3.0, + mu = sin_alpha + (cos_alpha - lambda) * cot_alpha; + + // Return rotated waypoints. + return { + ax: Math.cos(start).toFixed(7), + ay: Math.sin(start).toFixed(7), + bx: (lambda * cos_phi + mu * sin_phi).toFixed(7), + by: (lambda * sin_phi - mu * cos_phi).toFixed(7), + cx: (lambda * cos_phi - mu * sin_phi).toFixed(7), + cy: (lambda * sin_phi + mu * cos_phi).toFixed(7), + dx: Math.cos(start + size).toFixed(7), + dy: Math.sin(start + size).toFixed(7) + }; + } + + /* + * This function requires that: + * + * 0 <= start < TWO_PI + * + * start <= stop < start + TWO_PI + */ + arc(x, y, w, h, start, stop, mode) { + const ctx = this.drawingContext; + const rx = w / 2.0; + const ry = h / 2.0; + const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. + let arcToDraw = 0; + const curves = []; + + x += rx; + y += ry; + + // Create curves + while (stop - start >= epsilon) { + arcToDraw = Math.min(stop - start, constants.HALF_PI); + curves.push(this._acuteArcToBezier(start, arcToDraw)); + start += arcToDraw; } - ctx.closePath(); - if (!this._clipping) ctx.fill(); - } - // Stroke curves - if (this.states.doStroke) { - if (!this._clipping) ctx.beginPath(); - curves.forEach((curve, index) => { - if (index === 0) { - ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); + // Fill curves + if (this.states.doFill) { + if (!this._clipping) ctx.beginPath(); + curves.forEach((curve, index) => { + if (index === 0) { + ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); + } + /* eslint-disable indent */ + ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, + x + curve.cx * rx, y + curve.cy * ry, + x + curve.dx * rx, y + curve.dy * ry); + /* eslint-enable indent */ + }); + if (mode === constants.PIE || mode == null) { + ctx.lineTo(x, y); } - /* eslint-disable indent */ - ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, - x + curve.cx * rx, y + curve.cy * ry, - x + curve.dx * rx, y + curve.dy * ry); - /* eslint-enable indent */ - }); - if (mode === constants.PIE) { - ctx.lineTo(x, y); - ctx.closePath(); - } else if (mode === constants.CHORD) { ctx.closePath(); + if (!this._clipping) ctx.fill(); } - if (!this._clipping) ctx.stroke(); - } - return this; - } - ellipse(args) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - const x = parseFloat(args[0]), - y = parseFloat(args[1]), - w = parseFloat(args[2]), - h = parseFloat(args[3]); - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { - return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { - return this; + // Stroke curves + if (this.states.doStroke) { + if (!this._clipping) ctx.beginPath(); + curves.forEach((curve, index) => { + if (index === 0) { + ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); + } + /* eslint-disable indent */ + ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, + x + curve.cx * rx, y + curve.cy * ry, + x + curve.dx * rx, y + curve.dy * ry); + /* eslint-enable indent */ + }); + if (mode === constants.PIE) { + ctx.lineTo(x, y); + ctx.closePath(); + } else if (mode === constants.CHORD) { + ctx.closePath(); + } + if (!this._clipping) ctx.stroke(); } + return this; } - const centerX = x + w / 2, - centerY = y + h / 2, - radiusX = w / 2, - radiusY = h / 2; - if (!this._clipping) ctx.beginPath(); - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); - - if (!this._clipping && doFill) { - ctx.fill(); - } - if (!this._clipping && doStroke) { - ctx.stroke(); - } - } + ellipse(args) { + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + const x = parseFloat(args[0]), + y = parseFloat(args[1]), + w = parseFloat(args[2]), + h = parseFloat(args[3]); + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { + return this; + } + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { + return this; + } + } + const centerX = x + w / 2, + centerY = y + h / 2, + radiusX = w / 2, + radiusY = h / 2; + if (!this._clipping) ctx.beginPath(); - line(x1, y1, x2, y2) { - const ctx = this.drawingContext; - if (!this.states.doStroke) { - return this; - } else if (this._getStroke() === styleEmpty) { - return this; - } - if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - return this; - } + ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); - point(x, y) { - const ctx = this.drawingContext; - if (!this.states.doStroke) { - return this; - } else if (this._getStroke() === styleEmpty) { - return this; - } - const s = this._getStroke(); - const f = this._getFill(); - if (!this._clipping) { - // swapping fill color to stroke and back after for correct point rendering - this._setFill(s); - } - if (!this._clipping) ctx.beginPath(); - ctx.arc(x, y, ctx.lineWidth / 2, 0, constants.TWO_PI, false); - if (!this._clipping) { - ctx.fill(); - this._setFill(f); + if (!this._clipping && doFill) { + ctx.fill(); + } + if (!this._clipping && doStroke) { + ctx.stroke(); + } } - } - quad(x1, y1, x2, y2, x3, y3, x4, y4) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { + line(x1, y1, x2, y2) { + const ctx = this.drawingContext; + if (!this.states.doStroke) { return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { + } else if (this._getStroke() === styleEmpty) { return this; } - } - if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.lineTo(x3, y3); - ctx.lineTo(x4, y4); - ctx.closePath(); - if (!this._clipping && doFill) { - ctx.fill(); - } - if (!this._clipping && doStroke) { + if (!this._clipping) ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); ctx.stroke(); + return this; } - return this; - } - rect(args) { - const x = args[0]; - const y = args[1]; - const w = args[2]; - const h = args[3]; - let tl = args[4]; - let tr = args[5]; - let br = args[6]; - let bl = args[7]; - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { + point(x, y) { + const ctx = this.drawingContext; + if (!this.states.doStroke) { return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { + } else if (this._getStroke() === styleEmpty) { return this; } - } - if (!this._clipping) ctx.beginPath(); - - if (typeof tl === 'undefined') { - // No rounded corners - ctx.rect(x, y, w, h); - } else { - // At least one rounded corner - // Set defaults when not specified - if (typeof tr === 'undefined') { - tr = tl; - } - if (typeof br === 'undefined') { - br = tr; + const s = this._getStroke(); + const f = this._getFill(); + if (!this._clipping) { + // swapping fill color to stroke and back after for correct point rendering + this._setFill(s); } - if (typeof bl === 'undefined') { - bl = br; + if (!this._clipping) ctx.beginPath(); + ctx.arc(x, y, ctx.lineWidth / 2, 0, constants.TWO_PI, false); + if (!this._clipping) { + ctx.fill(); + this._setFill(f); } + } - // corner rounding must always be positive - const absW = Math.abs(w); - const absH = Math.abs(h); - const hw = absW / 2; - const hh = absH / 2; - - // Clip radii - if (absW < 2 * tl) { - tl = hw; - } - if (absH < 2 * tl) { - tl = hh; + quad(x1, y1, x2, y2, x3, y3, x4, y4) { + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { + return this; + } + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { + return this; + } } - if (absW < 2 * tr) { - tr = hw; + if (!this._clipping) ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineTo(x3, y3); + ctx.lineTo(x4, y4); + ctx.closePath(); + if (!this._clipping && doFill) { + ctx.fill(); } - if (absH < 2 * tr) { - tr = hh; + if (!this._clipping && doStroke) { + ctx.stroke(); } - if (absW < 2 * br) { - br = hw; + return this; + } + + rect(args) { + const x = args[0]; + const y = args[1]; + const w = args[2]; + const h = args[3]; + let tl = args[4]; + let tr = args[5]; + let br = args[6]; + let bl = args[7]; + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { + return this; + } + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { + return this; + } } - if (absH < 2 * br) { - br = hh; + if (!this._clipping) ctx.beginPath(); + + if (typeof tl === 'undefined') { + // No rounded corners + ctx.rect(x, y, w, h); + } else { + // At least one rounded corner + // Set defaults when not specified + if (typeof tr === 'undefined') { + tr = tl; + } + if (typeof br === 'undefined') { + br = tr; + } + if (typeof bl === 'undefined') { + bl = br; + } + + // corner rounding must always be positive + const absW = Math.abs(w); + const absH = Math.abs(h); + const hw = absW / 2; + const hh = absH / 2; + + // Clip radii + if (absW < 2 * tl) { + tl = hw; + } + if (absH < 2 * tl) { + tl = hh; + } + if (absW < 2 * tr) { + tr = hw; + } + if (absH < 2 * tr) { + tr = hh; + } + if (absW < 2 * br) { + br = hw; + } + if (absH < 2 * br) { + br = hh; + } + if (absW < 2 * bl) { + bl = hw; + } + if (absH < 2 * bl) { + bl = hh; + } + + // Draw shape + if (!this._clipping) ctx.beginPath(); + ctx.moveTo(x + tl, y); + ctx.arcTo(x + w, y, x + w, y + h, tr); + ctx.arcTo(x + w, y + h, x, y + h, br); + ctx.arcTo(x, y + h, x, y, bl); + ctx.arcTo(x, y, x + w, y, tl); + ctx.closePath(); } - if (absW < 2 * bl) { - bl = hw; + if (!this._clipping && this.states.doFill) { + ctx.fill(); } - if (absH < 2 * bl) { - bl = hh; + if (!this._clipping && this.states.doStroke) { + ctx.stroke(); } + return this; + } - // Draw shape + + triangle(args) { + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + const x1 = args[0], + y1 = args[1]; + const x2 = args[2], + y2 = args[3]; + const x3 = args[4], + y3 = args[5]; + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { + return this; + } + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { + return this; + } + } if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x + tl, y); - ctx.arcTo(x + w, y, x + w, y + h, tr); - ctx.arcTo(x + w, y + h, x, y + h, br); - ctx.arcTo(x, y + h, x, y, bl); - ctx.arcTo(x, y, x + w, y, tl); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineTo(x3, y3); ctx.closePath(); + if (!this._clipping && doFill) { + ctx.fill(); + } + if (!this._clipping && doStroke) { + ctx.stroke(); + } } - if (!this._clipping && this.states.doFill) { - ctx.fill(); - } - if (!this._clipping && this.states.doStroke) { - ctx.stroke(); - } - return this; - } - - triangle(args) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - const x1 = args[0], - y1 = args[1]; - const x2 = args[2], - y2 = args[3]; - const x3 = args[4], - y3 = args[5]; - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { + endShape( + mode, + vertices, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind + ) { + if (vertices.length === 0) { return this; } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { + if (!this.states.doStroke && !this.states.doFill) { return this; } - } - if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.lineTo(x3, y3); - ctx.closePath(); - if (!this._clipping && doFill) { - ctx.fill(); - } - if (!this._clipping && doStroke) { - ctx.stroke(); - } - } - - endShape( - mode, - vertices, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind - ) { - if (vertices.length === 0) { - return this; - } - if (!this.states.doStroke && !this.states.doFill) { - return this; - } - const closeShape = mode === constants.CLOSE; - let v; - if (closeShape && !isContour) { - vertices.push(vertices[0]); - } - let i, j; - const numVerts = vertices.length; - if (isCurve && shapeKind === null) { - if (numVerts > 3) { - const b = [], - s = 1 - this._curveTightness; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[1][0], vertices[1][1]); - for (i = 1; i + 2 < numVerts; i++) { - v = vertices[i]; - b[0] = [v[0], v[1]]; - b[1] = [ - v[0] + (s * vertices[i + 1][0] - s * vertices[i - 1][0]) / 6, - v[1] + (s * vertices[i + 1][1] - s * vertices[i - 1][1]) / 6 - ]; - b[2] = [ - vertices[i + 1][0] + - (s * vertices[i][0] - s * vertices[i + 2][0]) / 6, - vertices[i + 1][1] + - (s * vertices[i][1] - s * vertices[i + 2][1]) / 6 - ]; - b[3] = [vertices[i + 1][0], vertices[i + 1][1]]; - this.drawingContext.bezierCurveTo( - b[1][0], - b[1][1], - b[2][0], - b[2][1], - b[3][0], - b[3][1] - ); - } - if (closeShape) { - this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); - } - this._doFillStrokeClose(closeShape); + const closeShape = mode === constants.CLOSE; + let v; + if (closeShape && !isContour) { + vertices.push(vertices[0]); } - } else if ( - isBezier && - shapeKind === null - ) { - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 0; i < numVerts; i++) { - if (vertices[i].isVert) { - if (vertices[i].moveTo) { - this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); - } else { - this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); + let i, j; + const numVerts = vertices.length; + if (isCurve && shapeKind === null) { + if (numVerts > 3) { + const b = [], + s = 1 - this._curveTightness; + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(vertices[1][0], vertices[1][1]); + for (i = 1; i + 2 < numVerts; i++) { + v = vertices[i]; + b[0] = [v[0], v[1]]; + b[1] = [ + v[0] + (s * vertices[i + 1][0] - s * vertices[i - 1][0]) / 6, + v[1] + (s * vertices[i + 1][1] - s * vertices[i - 1][1]) / 6 + ]; + b[2] = [ + vertices[i + 1][0] + + (s * vertices[i][0] - s * vertices[i + 2][0]) / 6, + vertices[i + 1][1] + + (s * vertices[i][1] - s * vertices[i + 2][1]) / 6 + ]; + b[3] = [vertices[i + 1][0], vertices[i + 1][1]]; + this.drawingContext.bezierCurveTo( + b[1][0], + b[1][1], + b[2][0], + b[2][1], + b[3][0], + b[3][1] + ); } - } else { - this.drawingContext.bezierCurveTo( - vertices[i][0], - vertices[i][1], - vertices[i][2], - vertices[i][3], - vertices[i][4], - vertices[i][5] - ); - } - } - this._doFillStrokeClose(closeShape); - } else if ( - isQuadratic && - shapeKind === null - ) { - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 0; i < numVerts; i++) { - if (vertices[i].isVert) { - if (vertices[i].moveTo) { - this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); - } else { - this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); + if (closeShape) { + this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); } - } else { - this.drawingContext.quadraticCurveTo( - vertices[i][0], - vertices[i][1], - vertices[i][2], - vertices[i][3] - ); + this._doFillStrokeClose(closeShape); } - } - this._doFillStrokeClose(closeShape); - } else { - if (shapeKind === constants.POINTS) { + } else if ( + isBezier && + shapeKind === null + ) { + if (!this._clipping) this.drawingContext.beginPath(); for (i = 0; i < numVerts; i++) { - v = vertices[i]; - if (this.states.doStroke) { - this._pInst.stroke(v[6]); - } - this._pInst.point(v[0], v[1]); - } - } else if (shapeKind === constants.LINES) { - for (i = 0; i + 1 < numVerts; i += 2) { - v = vertices[i]; - if (this.states.doStroke) { - this._pInst.stroke(vertices[i + 1][6]); + if (vertices[i].isVert) { + if (vertices[i].moveTo) { + this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); + } else { + this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); + } + } else { + this.drawingContext.bezierCurveTo( + vertices[i][0], + vertices[i][1], + vertices[i][2], + vertices[i][3], + vertices[i][4], + vertices[i][5] + ); } - this._pInst.line(v[0], v[1], vertices[i + 1][0], vertices[i + 1][1]); } - } else if (shapeKind === constants.TRIANGLES) { - for (i = 0; i + 2 < numVerts; i += 3) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(v[0], v[1]); - this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); - this.drawingContext.closePath(); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 2][5]); - this.drawingContext.fill(); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 2][6]); - this.drawingContext.stroke(); + this._doFillStrokeClose(closeShape); + } else if ( + isQuadratic && + shapeKind === null + ) { + if (!this._clipping) this.drawingContext.beginPath(); + for (i = 0; i < numVerts; i++) { + if (vertices[i].isVert) { + if (vertices[i].moveTo) { + this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); + } else { + this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); + } + } else { + this.drawingContext.quadraticCurveTo( + vertices[i][0], + vertices[i][1], + vertices[i][2], + vertices[i][3] + ); } } - } else if (shapeKind === constants.TRIANGLE_STRIP) { - for (i = 0; i + 1 < numVerts; i++) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo(v[0], v[1]); - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 1][6]); + this._doFillStrokeClose(closeShape); + } else { + if (shapeKind === constants.POINTS) { + for (i = 0; i < numVerts; i++) { + v = vertices[i]; + if (this.states.doStroke) { + this._pInst.stroke(v[6]); + } + this._pInst.point(v[0], v[1]); } - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 1][5]); + } else if (shapeKind === constants.LINES) { + for (i = 0; i + 1 < numVerts; i += 2) { + v = vertices[i]; + if (this.states.doStroke) { + this._pInst.stroke(vertices[i + 1][6]); + } + this._pInst.line(v[0], v[1], vertices[i + 1][0], vertices[i + 1][1]); } - if (i + 2 < numVerts) { + } else if (shapeKind === constants.TRIANGLES) { + for (i = 0; i + 2 < numVerts; i += 3) { + v = vertices[i]; + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(v[0], v[1]); + this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); + this.drawingContext.closePath(); + if (!this._clipping && this.states.doFill) { + this._pInst.fill(vertices[i + 2][5]); + this.drawingContext.fill(); + } if (!this._clipping && this.states.doStroke) { this._pInst.stroke(vertices[i + 2][6]); + this.drawingContext.stroke(); + } + } + } else if (shapeKind === constants.TRIANGLE_STRIP) { + for (i = 0; i + 1 < numVerts; i++) { + v = vertices[i]; + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(vertices[i + 1][0], vertices[i + 1][1]); + this.drawingContext.lineTo(v[0], v[1]); + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(vertices[i + 1][6]); } if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 2][5]); + this._pInst.fill(vertices[i + 1][5]); } + if (i + 2 < numVerts) { + this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(vertices[i + 2][6]); + } + if (!this._clipping && this.states.doFill) { + this._pInst.fill(vertices[i + 2][5]); + } + } + this._doFillStrokeClose(closeShape); } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.TRIANGLE_FAN) { - if (numVerts > 2) { - // For performance reasons, try to batch as many of the - // fill and stroke calls as possible. - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 2; i < numVerts; i++) { + } else if (shapeKind === constants.TRIANGLE_FAN) { + if (numVerts > 2) { + // For performance reasons, try to batch as many of the + // fill and stroke calls as possible. + if (!this._clipping) this.drawingContext.beginPath(); + for (i = 2; i < numVerts; i++) { + v = vertices[i]; + this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); + this.drawingContext.lineTo(vertices[i - 1][0], vertices[i - 1][1]); + this.drawingContext.lineTo(v[0], v[1]); + this.drawingContext.lineTo(vertices[0][0], vertices[0][1]); + // If the next colour is going to be different, stroke / fill now + if (i < numVerts - 1) { + if ( + (this.states.doFill && v[5] !== vertices[i + 1][5]) || + (this.states.doStroke && v[6] !== vertices[i + 1][6]) + ) { + if (!this._clipping && this.states.doFill) { + this._pInst.fill(v[5]); + this.drawingContext.fill(); + this._pInst.fill(vertices[i + 1][5]); + } + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(v[6]); + this.drawingContext.stroke(); + this._pInst.stroke(vertices[i + 1][6]); + } + this.drawingContext.closePath(); + if (!this._clipping) this.drawingContext.beginPath(); // Begin the next one + } + } + } + this._doFillStrokeClose(closeShape); + } + } else if (shapeKind === constants.QUADS) { + for (i = 0; i + 3 < numVerts; i += 4) { v = vertices[i]; - this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); - this.drawingContext.lineTo(vertices[i - 1][0], vertices[i - 1][1]); + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(v[0], v[1]); + for (j = 1; j < 4; j++) { + this.drawingContext.lineTo(vertices[i + j][0], vertices[i + j][1]); + } this.drawingContext.lineTo(v[0], v[1]); - this.drawingContext.lineTo(vertices[0][0], vertices[0][1]); - // If the next colour is going to be different, stroke / fill now - if (i < numVerts - 1) { - if ( - (this.states.doFill && v[5] !== vertices[i + 1][5]) || - (this.states.doStroke && v[6] !== vertices[i + 1][6]) - ) { + if (!this._clipping && this.states.doFill) { + this._pInst.fill(vertices[i + 3][5]); + } + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(vertices[i + 3][6]); + } + this._doFillStrokeClose(closeShape); + } + } else if (shapeKind === constants.QUAD_STRIP) { + if (numVerts > 3) { + for (i = 0; i + 1 < numVerts; i += 2) { + v = vertices[i]; + if (!this._clipping) this.drawingContext.beginPath(); + if (i + 3 < numVerts) { + this.drawingContext.moveTo( + vertices[i + 2][0], vertices[i + 2][1]); + this.drawingContext.lineTo(v[0], v[1]); + this.drawingContext.lineTo( + vertices[i + 1][0], vertices[i + 1][1]); + this.drawingContext.lineTo( + vertices[i + 3][0], vertices[i + 3][1]); if (!this._clipping && this.states.doFill) { - this._pInst.fill(v[5]); - this.drawingContext.fill(); - this._pInst.fill(vertices[i + 1][5]); + this._pInst.fill(vertices[i + 3][5]); } if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(v[6]); - this.drawingContext.stroke(); - this._pInst.stroke(vertices[i + 1][6]); + this._pInst.stroke(vertices[i + 3][6]); } - this.drawingContext.closePath(); - if (!this._clipping) this.drawingContext.beginPath(); // Begin the next one + } else { + this.drawingContext.moveTo(v[0], v[1]); + this.drawingContext.lineTo( + vertices[i + 1][0], vertices[i + 1][1]); } + this._doFillStrokeClose(closeShape); } } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.QUADS) { - for (i = 0; i + 3 < numVerts; i += 4) { - v = vertices[i]; + } else { if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(v[0], v[1]); - for (j = 1; j < 4; j++) { - this.drawingContext.lineTo(vertices[i + j][0], vertices[i + j][1]); - } - this.drawingContext.lineTo(v[0], v[1]); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 3][5]); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 3][6]); - } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.QUAD_STRIP) { - if (numVerts > 3) { - for (i = 0; i + 1 < numVerts; i += 2) { + this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); + for (i = 1; i < numVerts; i++) { v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - if (i + 3 < numVerts) { - this.drawingContext.moveTo( - vertices[i + 2][0], vertices[i + 2][1]); - this.drawingContext.lineTo(v[0], v[1]); - this.drawingContext.lineTo( - vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo( - vertices[i + 3][0], vertices[i + 3][1]); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 3][5]); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 3][6]); + if (v.isVert) { + if (v.moveTo) { + if (closeShape) this.drawingContext.closePath(); + this.drawingContext.moveTo(v[0], v[1]); + } else { + this.drawingContext.lineTo(v[0], v[1]); } - } else { - this.drawingContext.moveTo(v[0], v[1]); - this.drawingContext.lineTo( - vertices[i + 1][0], vertices[i + 1][1]); - } - this._doFillStrokeClose(closeShape); - } - } - } else { - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); - for (i = 1; i < numVerts; i++) { - v = vertices[i]; - if (v.isVert) { - if (v.moveTo) { - if (closeShape) this.drawingContext.closePath(); - this.drawingContext.moveTo(v[0], v[1]); - } else { - this.drawingContext.lineTo(v[0], v[1]); } } + this._doFillStrokeClose(closeShape); } - this._doFillStrokeClose(closeShape); } + isCurve = false; + isBezier = false; + isQuadratic = false; + isContour = false; + if (closeShape) { + vertices.pop(); + } + + return this; } - isCurve = false; - isBezier = false; - isQuadratic = false; - isContour = false; - if (closeShape) { - vertices.pop(); + ////////////////////////////////////////////// + // SHAPE | Attributes + ////////////////////////////////////////////// + + strokeCap(cap) { + if ( + cap === constants.ROUND || + cap === constants.SQUARE || + cap === constants.PROJECT + ) { + this.drawingContext.lineCap = cap; + } + return this; } - return this; - } - ////////////////////////////////////////////// - // SHAPE | Attributes - ////////////////////////////////////////////// - - strokeCap(cap) { - if ( - cap === constants.ROUND || - cap === constants.SQUARE || - cap === constants.PROJECT - ) { - this.drawingContext.lineCap = cap; + strokeJoin(join) { + if ( + join === constants.ROUND || + join === constants.BEVEL || + join === constants.MITER + ) { + this.drawingContext.lineJoin = join; + } + return this; } - return this; - } - strokeJoin(join) { - if ( - join === constants.ROUND || - join === constants.BEVEL || - join === constants.MITER - ) { - this.drawingContext.lineJoin = join; + strokeWeight(w) { + if (typeof w === 'undefined' || w === 0) { + // hack because lineWidth 0 doesn't work + this.drawingContext.lineWidth = 0.0001; + } else { + this.drawingContext.lineWidth = w; + } + return this; } - return this; - } - strokeWeight(w) { - if (typeof w === 'undefined' || w === 0) { - // hack because lineWidth 0 doesn't work - this.drawingContext.lineWidth = 0.0001; - } else { - this.drawingContext.lineWidth = w; + _getFill() { + if (!this._cachedFillStyle) { + this._cachedFillStyle = this.drawingContext.fillStyle; + } + return this._cachedFillStyle; } - return this; - } - _getFill() { - if (!this._cachedFillStyle) { - this._cachedFillStyle = this.drawingContext.fillStyle; + _setFill(fillStyle) { + if (fillStyle !== this._cachedFillStyle) { + this.drawingContext.fillStyle = fillStyle; + // console.log('here', this.drawingContext.fillStyle); + // console.trace(); + this._cachedFillStyle = fillStyle; + } } - return this._cachedFillStyle; - } - _setFill(fillStyle) { - if (fillStyle !== this._cachedFillStyle) { - this.drawingContext.fillStyle = fillStyle; - // console.log('here', this.drawingContext.fillStyle); - // console.trace(); - this._cachedFillStyle = fillStyle; + _getStroke() { + if (!this._cachedStrokeStyle) { + this._cachedStrokeStyle = this.drawingContext.strokeStyle; + } + return this._cachedStrokeStyle; } - } - _getStroke() { - if (!this._cachedStrokeStyle) { - this._cachedStrokeStyle = this.drawingContext.strokeStyle; + _setStroke(strokeStyle) { + if (strokeStyle !== this._cachedStrokeStyle) { + this.drawingContext.strokeStyle = strokeStyle; + this._cachedStrokeStyle = strokeStyle; + } } - return this._cachedStrokeStyle; - } - _setStroke(strokeStyle) { - if (strokeStyle !== this._cachedStrokeStyle) { - this.drawingContext.strokeStyle = strokeStyle; - this._cachedStrokeStyle = strokeStyle; + ////////////////////////////////////////////// + // SHAPE | Curves + ////////////////////////////////////////////// + bezier(x1, y1, x2, y2, x3, y3, x4, y4) { + this._pInst.beginShape(); + this._pInst.vertex(x1, y1); + this._pInst.bezierVertex(x2, y2, x3, y3, x4, y4); + this._pInst.endShape(); + return this; } - } - - ////////////////////////////////////////////// - // SHAPE | Curves - ////////////////////////////////////////////// - bezier(x1, y1, x2, y2, x3, y3, x4, y4) { - this._pInst.beginShape(); - this._pInst.vertex(x1, y1); - this._pInst.bezierVertex(x2, y2, x3, y3, x4, y4); - this._pInst.endShape(); - return this; - } - curve(x1, y1, x2, y2, x3, y3, x4, y4) { - this._pInst.beginShape(); - this._pInst.curveVertex(x1, y1); - this._pInst.curveVertex(x2, y2); - this._pInst.curveVertex(x3, y3); - this._pInst.curveVertex(x4, y4); - this._pInst.endShape(); - return this; - } + curve(x1, y1, x2, y2, x3, y3, x4, y4) { + this._pInst.beginShape(); + this._pInst.curveVertex(x1, y1); + this._pInst.curveVertex(x2, y2); + this._pInst.curveVertex(x3, y3); + this._pInst.curveVertex(x4, y4); + this._pInst.endShape(); + return this; + } - ////////////////////////////////////////////// - // SHAPE | Vertex - ////////////////////////////////////////////// + ////////////////////////////////////////////// + // SHAPE | Vertex + ////////////////////////////////////////////// - _doFillStrokeClose(closeShape) { - if (closeShape) { - this.drawingContext.closePath(); - } - if (!this._clipping && this.states.doFill) { - this.drawingContext.fill(); - } - if (!this._clipping && this.states.doStroke) { - this.drawingContext.stroke(); + _doFillStrokeClose(closeShape) { + if (closeShape) { + this.drawingContext.closePath(); + } + if (!this._clipping && this.states.doFill) { + this.drawingContext.fill(); + } + if (!this._clipping && this.states.doStroke) { + this.drawingContext.stroke(); + } } - } - ////////////////////////////////////////////// - // TRANSFORM - ////////////////////////////////////////////// + ////////////////////////////////////////////// + // TRANSFORM + ////////////////////////////////////////////// - applyMatrix(a, b, c, d, e, f) { - this.drawingContext.transform(a, b, c, d, e, f); - } + applyMatrix(a, b, c, d, e, f) { + this.drawingContext.transform(a, b, c, d, e, f); + } - resetMatrix() { - this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); - this.drawingContext.scale( - this._pixelDensity, - this._pixelDensity - ); - return this; - } + resetMatrix() { + this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); + this.drawingContext.scale( + this._pixelDensity, + this._pixelDensity + ); + return this; + } - rotate(rad) { - this.drawingContext.rotate(rad); - } + rotate(rad) { + this.drawingContext.rotate(rad); + } - scale(x, y) { - this.drawingContext.scale(x, y); - return this; - } + scale(x, y) { + this.drawingContext.scale(x, y); + return this; + } - translate(x, y) { - // support passing a vector as the 1st parameter - if (x instanceof p5.Vector) { - y = x.y; - x = x.x; + translate(x, y) { + // support passing a vector as the 1st parameter + if (x instanceof p5.Vector) { + y = x.y; + x = x.x; + } + this.drawingContext.translate(x, y); + return this; } - this.drawingContext.translate(x, y); - return this; - } - ////////////////////////////////////////////// - // TYPOGRAPHY - // - ////////////////////////////////////////////// + ////////////////////////////////////////////// + // TYPOGRAPHY + // + ////////////////////////////////////////////// - _renderText(p, line, x, y, maxY, minY) { - if (y < minY || y >= maxY) { - return; // don't render lines beyond our minY/maxY bounds (see #5785) - } + _renderText(p, line, x, y, maxY, minY) { + if (y < minY || y >= maxY) { + return; // don't render lines beyond our minY/maxY bounds (see #5785) + } - p.push(); // fix to #803 + p.push(); // fix to #803 - if (!this._isOpenType()) { - // a system/browser font + if (!this._isOpenType()) { + // a system/browser font - // no stroke unless specified by user - if (this.states.doStroke && this.states.strokeSet) { - this.drawingContext.strokeText(line, x, y); - } + // no stroke unless specified by user + if (this.states.doStroke && this.states.strokeSet) { + this.drawingContext.strokeText(line, x, y); + } - if (!this._clipping && this.states.doFill) { - // if fill hasn't been set by user, use default text fill - if (!this.states.fillSet) { - this._setFill(constants._DEFAULT_TEXT_FILL); + if (!this._clipping && this.states.doFill) { + // if fill hasn't been set by user, use default text fill + if (!this.states.fillSet) { + this._setFill(constants._DEFAULT_TEXT_FILL); + } + + this.drawingContext.fillText(line, x, y); } + } else { + // an opentype font, let it handle the rendering - this.drawingContext.fillText(line, x, y); + this.states.textFont._renderPath(line, x, y, { renderer: this }); } - } else { - // an opentype font, let it handle the rendering - this.states.textFont._renderPath(line, x, y, { renderer: this }); + p.pop(); + return p; } - p.pop(); - return p; - } + textWidth(s) { + if (this._isOpenType()) { + return this.states.textFont._textWidth(s, this.states.textSize); + } - textWidth(s) { - if (this._isOpenType()) { - return this.states.textFont._textWidth(s, this.states.textSize); + return this.drawingContext.measureText(s).width; } - return this.drawingContext.measureText(s).width; - } + text(str, x, y, maxWidth, maxHeight) { + let baselineHacked; - text(str, x, y, maxWidth, maxHeight) { - let baselineHacked; + // baselineHacked: (HACK) + // A temporary fix to conform to Processing's implementation + // of BASELINE vertical alignment in a bounding box - // baselineHacked: (HACK) - // A temporary fix to conform to Processing's implementation - // of BASELINE vertical alignment in a bounding box + if (typeof maxWidth !== 'undefined') { + if (this.drawingContext.textBaseline === constants.BASELINE) { + baselineHacked = true; + this.drawingContext.textBaseline = constants.TOP; + } + } - if (typeof maxWidth !== 'undefined') { - if (this.drawingContext.textBaseline === constants.BASELINE) { - baselineHacked = true; - this.drawingContext.textBaseline = constants.TOP; + const p = fn.prototype.text.apply(this, arguments); + + if (baselineHacked) { + this.drawingContext.textBaseline = constants.BASELINE; } + + return p; } - const p = Renderer.prototype.text.apply(this, arguments); + _applyTextProperties() { + let font; + const p = this._pInst; - if (baselineHacked) { - this.drawingContext.textBaseline = constants.BASELINE; - } + this.states.textAscent = null; + this.states.textDescent = null; - return p; - } + font = this.states.textFont; - _applyTextProperties() { - let font; - const p = this._pInst; + if (this._isOpenType()) { + font = this.states.textFont.font.familyName; + this.states.textStyle = this._textFont.font.styleName; + } - this.states.textAscent = null; - this.states.textDescent = null; + let fontNameString = font || 'sans-serif'; + if (/\s/.exec(fontNameString)) { + // If the name includes spaces, surround in quotes + fontNameString = `"${fontNameString}"`; + } + this.drawingContext.font = `${this.states.textStyle || 'normal'} ${this.states.textSize || + 12}px ${fontNameString}`; - font = this.states.textFont; + this.drawingContext.textAlign = this.states.textAlign; + if (this.states.textBaseline === constants.CENTER) { + this.drawingContext.textBaseline = constants._CTX_MIDDLE; + } else { + this.drawingContext.textBaseline = this.states.textBaseline; + } - if (this._isOpenType()) { - font = this.states.textFont.font.familyName; - this.states.textStyle = this._textFont.font.styleName; + return p; } - let fontNameString = font || 'sans-serif'; - if (/\s/.exec(fontNameString)) { - // If the name includes spaces, surround in quotes - fontNameString = `"${fontNameString}"`; - } - this.drawingContext.font = `${this.states.textStyle || 'normal'} ${this.states.textSize || - 12}px ${fontNameString}`; - - this.drawingContext.textAlign = this.states.textAlign; - if (this.states.textBaseline === constants.CENTER) { - this.drawingContext.textBaseline = constants._CTX_MIDDLE; - } else { - this.drawingContext.textBaseline = this.states.textBaseline; - } + ////////////////////////////////////////////// + // STRUCTURE + ////////////////////////////////////////////// - return p; - } + // a push() operation is in progress. + // the renderer should return a 'style' object that it wishes to + // store on the push stack. + // derived renderers should call the base class' push() method + // to fetch the base style object. + push() { + this.drawingContext.save(); - ////////////////////////////////////////////// - // STRUCTURE - ////////////////////////////////////////////// + // get the base renderer style + return super.push(); + } - // a push() operation is in progress. - // the renderer should return a 'style' object that it wishes to - // store on the push stack. - // derived renderers should call the base class' push() method - // to fetch the base style object. - push() { - this.drawingContext.save(); + // a pop() operation is in progress + // the renderer is passed the 'style' object that it returned + // from its push() method. + // derived renderers should pass this object to their base + // class' pop method + pop(style) { + this.drawingContext.restore(); + // Re-cache the fill / stroke state + this._cachedFillStyle = this.drawingContext.fillStyle; + this._cachedStrokeStyle = this.drawingContext.strokeStyle; - // get the base renderer style - return super.push(); + super.pop(style); + } } - // a pop() operation is in progress - // the renderer is passed the 'style' object that it returned - // from its push() method. - // derived renderers should pass this object to their base - // class' pop method - pop(style) { - this.drawingContext.restore(); - // Re-cache the fill / stroke state - this._cachedFillStyle = this.drawingContext.fillStyle; - this._cachedStrokeStyle = this.drawingContext.strokeStyle; - - super.pop(style); - } + p5.Renderer2D = Renderer2D; + p5.renderers[constants.P2D] = Renderer2D; } -p5.Renderer2D = Renderer2D; - -export default p5.Renderer2D; +export default renderer2D; diff --git a/src/core/rendering.js b/src/core/rendering.js index bfee521cad..6f44f90a38 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -4,684 +4,683 @@ * @for p5 */ -import p5 from './main'; import * as constants from './constants'; -import './p5.Graphics'; -import Renderer2D from './p5.Renderer2D'; -import RendererGL from '../webgl/p5.RendererGL'; -let defaultId = 'defaultCanvas0'; // this gets set again in createCanvas -const defaultClass = 'p5Canvas'; -// Attach renderers object to p5 class, new renderer can be similarly attached -const renderers = p5.renderers = { - [constants.P2D]: Renderer2D, - [constants.WEBGL]: RendererGL, - [constants.WEBGL2]: RendererGL -}; -/** - * Creates a canvas element on the web page. - * - * `createCanvas()` creates the main drawing canvas for a sketch. It should - * only be called once at the beginning of setup(). - * Calling `createCanvas()` more than once causes unpredictable behavior. - * - * The first two parameters, `width` and `height`, are optional. They set the - * dimensions of the canvas and the values of the - * width and height system - * variables. For example, calling `createCanvas(900, 500)` creates a canvas - * that's 900×500 pixels. By default, `width` and `height` are both 100. - * - * The third parameter is also optional. If either of the constants `P2D` or - * `WEBGL` is passed, as in `createCanvas(900, 500, WEBGL)`, then it will set - * the sketch's rendering mode. If an existing - * HTMLCanvasElement - * is passed, as in `createCanvas(900, 500, myCanvas)`, then it will be used - * by the sketch. - * - * The fourth parameter is also optional. If an existing - * HTMLCanvasElement - * is passed, as in `createCanvas(900, 500, WEBGL, myCanvas)`, then it will be - * used by the sketch. - * - * Note: In WebGL mode, the canvas will use a WebGL2 context if it's supported - * by the browser. Check the webglVersion - * system variable to check what version is being used, or call - * `setAttributes({ version: 1 })` to create a WebGL1 context. - * - * @method createCanvas - * @param {Number} [width] width of the canvas. Defaults to 100. - * @param {Number} [height] height of the canvas. Defaults to 100. - * @param {(P2D|WEBGL)} [renderer] either P2D or WEBGL. Defaults to `P2D`. - * @param {HTMLCanvasElement} [canvas] existing canvas element that should be used for the sketch. - * @return {p5.Renderer} new `p5.Renderer` that holds the canvas. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Draw a diagonal line. - * line(0, 0, width, height); - * - * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 50); - * - * background(200); - * - * // Draw a diagonal line. - * line(0, 0, width, height); - * - * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); - * } - * - *
- * - *
- * - * // Use WebGL mode. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Draw a diagonal line. - * line(-width / 2, -height / 2, width / 2, height / 2); - * - * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Render object. - * let cnv = createCanvas(50, 50); - * - * // Position the canvas. - * cnv.position(10, 20); - * - * background(200); - * - * // Draw a diagonal line. - * line(0, 0, width, height); - * - * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); - * } - * - *
- */ -/** - * @method createCanvas - * @param {Number} [width] - * @param {Number} [height] - * @param {HTMLCanvasElement} [canvas] - * @return {p5.Renderer} - */ -p5.prototype.createCanvas = function (w, h, renderer, ...args) { - p5._validateParameters('createCanvas', arguments); - //optional: renderer, otherwise defaults to p2d +function rendering(p5, fn){ + let defaultId = 'defaultCanvas0'; // this gets set again in createCanvas + const defaultClass = 'p5Canvas'; + // Extend additional renderers object to p5 class, new renderer can be similarly attached + const renderers = p5.renderers = {}; - let selectedRenderer = constants.P2D - // Check third argument whether it is renderer constants - if(Reflect.ownKeys(renderers).includes(renderer)){ - selectedRenderer = renderer; - }else{ - args.unshift(renderer); - } + /** + * Creates a canvas element on the web page. + * + * `createCanvas()` creates the main drawing canvas for a sketch. It should + * only be called once at the beginning of setup(). + * Calling `createCanvas()` more than once causes unpredictable behavior. + * + * The first two parameters, `width` and `height`, are optional. They set the + * dimensions of the canvas and the values of the + * width and height system + * variables. For example, calling `createCanvas(900, 500)` creates a canvas + * that's 900×500 pixels. By default, `width` and `height` are both 100. + * + * The third parameter is also optional. If either of the constants `P2D` or + * `WEBGL` is passed, as in `createCanvas(900, 500, WEBGL)`, then it will set + * the sketch's rendering mode. If an existing + * HTMLCanvasElement + * is passed, as in `createCanvas(900, 500, myCanvas)`, then it will be used + * by the sketch. + * + * The fourth parameter is also optional. If an existing + * HTMLCanvasElement + * is passed, as in `createCanvas(900, 500, WEBGL, myCanvas)`, then it will be + * used by the sketch. + * + * Note: In WebGL mode, the canvas will use a WebGL2 context if it's supported + * by the browser. Check the webglVersion + * system variable to check what version is being used, or call + * `setAttributes({ version: 1 })` to create a WebGL1 context. + * + * @method createCanvas + * @param {Number} [width] width of the canvas. Defaults to 100. + * @param {Number} [height] height of the canvas. Defaults to 100. + * @param {(P2D|WEBGL)} [renderer] either P2D or WEBGL. Defaults to `P2D`. + * @param {HTMLCanvasElement} [canvas] existing canvas element that should be used for the sketch. + * @return {p5.Renderer} new `p5.Renderer` that holds the canvas. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Draw a diagonal line. + * line(0, 0, width, height); + * + * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 50); + * + * background(200); + * + * // Draw a diagonal line. + * line(0, 0, width, height); + * + * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); + * } + * + *
+ * + *
+ * + * // Use WebGL mode. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Draw a diagonal line. + * line(-width / 2, -height / 2, width / 2, height / 2); + * + * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Render object. + * let cnv = createCanvas(50, 50); + * + * // Position the canvas. + * cnv.position(10, 20); + * + * background(200); + * + * // Draw a diagonal line. + * line(0, 0, width, height); + * + * describe('A diagonal line drawn from top-left to bottom-right on a gray background.'); + * } + * + *
+ */ + /** + * @method createCanvas + * @param {Number} [width] + * @param {Number} [height] + * @param {HTMLCanvasElement} [canvas] + * @return {p5.Renderer} + */ + p5.prototype.createCanvas = function (w, h, renderer, ...args) { + p5._validateParameters('createCanvas', arguments); + //optional: renderer, otherwise defaults to p2d - // Init our graphics renderer - if(this._renderer) this._renderer.remove(); - this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args); - this._defaultGraphicsCreated = true; - this._elements.push(this._renderer); - this._renderer._applyDefaults(); - return this._renderer; -}; + let selectedRenderer = constants.P2D + // Check third argument whether it is renderer constants + if(Reflect.ownKeys(renderers).includes(renderer)){ + selectedRenderer = renderer; + }else{ + args.unshift(renderer); + } -/** - * Resizes the canvas to a given width and height. - * - * `resizeCanvas()` immediately clears the canvas and calls - * redraw(). It's common to call `resizeCanvas()` - * within the body of windowResized() like - * so: - * - * ```js - * function windowResized() { - * resizeCanvas(windowWidth, windowHeight); - * } - * ``` - * - * The first two parameters, `width` and `height`, set the dimensions of the - * canvas. They also the values of the width and - * height system variables. For example, calling - * `resizeCanvas(300, 500)` resizes the canvas to 300×500 pixels, then sets - * width to 300 and - * height 500. - * - * The third parameter, `noRedraw`, is optional. If `true` is passed, as in - * `resizeCanvas(300, 500, true)`, then the canvas will be canvas to 300×500 - * pixels but the redraw() function won't be called - * immediately. By default, redraw() is called - * immediately when `resizeCanvas()` finishes executing. - * - * @method resizeCanvas - * @param {Number} width width of the canvas. - * @param {Number} height height of the canvas. - * @param {Boolean} [noRedraw] whether to delay calling - * redraw(). Defaults - * to `false`. - * - * @example - *
- * - * // Double-click to resize the canvas. - * - * function setup() { - * createCanvas(100, 100); - * - * describe( - * 'A white circle drawn on a gray background. The canvas shrinks by half the first time the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Draw a circle at the center of the canvas. - * circle(width / 2, height / 2, 20); - * } - * - * // Resize the canvas when the user double-clicks. - * function doubleClicked() { - * resizeCanvas(50, 50); - * } - * - *
- * - *
- * - * // Resize the web browser to change the canvas size. - * - * function setup() { - * createCanvas(windowWidth, windowHeight); - * - * describe('A white circle drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Draw a circle at the center of the canvas. - * circle(width / 2, height / 2, 20); - * } - * - * // Always resize the canvas to fill the browser window. - * function windowResized() { - * resizeCanvas(windowWidth, windowHeight); - * } - * - *
- */ -p5.prototype.resizeCanvas = function (w, h, noRedraw) { - p5._validateParameters('resizeCanvas', arguments); - if (this._renderer) { - // Make sure width and height are updated before the renderer resizes so - // that framebuffers updated from the resize read the correct size - this._renderer.resize(w, h); + // Init our graphics renderer + if(this._renderer) this._renderer.remove(); + this._renderer = new renderers[selectedRenderer](this, w, h, true, ...args); + this._defaultGraphicsCreated = true; + this._elements.push(this._renderer); + this._renderer._applyDefaults(); + return this._renderer; + }; + + /** + * Resizes the canvas to a given width and height. + * + * `resizeCanvas()` immediately clears the canvas and calls + * redraw(). It's common to call `resizeCanvas()` + * within the body of windowResized() like + * so: + * + * ```js + * function windowResized() { + * resizeCanvas(windowWidth, windowHeight); + * } + * ``` + * + * The first two parameters, `width` and `height`, set the dimensions of the + * canvas. They also the values of the width and + * height system variables. For example, calling + * `resizeCanvas(300, 500)` resizes the canvas to 300×500 pixels, then sets + * width to 300 and + * height 500. + * + * The third parameter, `noRedraw`, is optional. If `true` is passed, as in + * `resizeCanvas(300, 500, true)`, then the canvas will be canvas to 300×500 + * pixels but the redraw() function won't be called + * immediately. By default, redraw() is called + * immediately when `resizeCanvas()` finishes executing. + * + * @method resizeCanvas + * @param {Number} width width of the canvas. + * @param {Number} height height of the canvas. + * @param {Boolean} [noRedraw] whether to delay calling + * redraw(). Defaults + * to `false`. + * + * @example + *
+ * + * // Double-click to resize the canvas. + * + * function setup() { + * createCanvas(100, 100); + * + * describe( + * 'A white circle drawn on a gray background. The canvas shrinks by half the first time the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Draw a circle at the center of the canvas. + * circle(width / 2, height / 2, 20); + * } + * + * // Resize the canvas when the user double-clicks. + * function doubleClicked() { + * resizeCanvas(50, 50); + * } + * + *
+ * + *
+ * + * // Resize the web browser to change the canvas size. + * + * function setup() { + * createCanvas(windowWidth, windowHeight); + * + * describe('A white circle drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Draw a circle at the center of the canvas. + * circle(width / 2, height / 2, 20); + * } + * + * // Always resize the canvas to fill the browser window. + * function windowResized() { + * resizeCanvas(windowWidth, windowHeight); + * } + * + *
+ */ + p5.prototype.resizeCanvas = function (w, h, noRedraw) { + p5._validateParameters('resizeCanvas', arguments); + if (this._renderer) { + // Make sure width and height are updated before the renderer resizes so + // that framebuffers updated from the resize read the correct size + this._renderer.resize(w, h); - if (!noRedraw) { - this.redraw(); + if (!noRedraw) { + this.redraw(); + } + } + //accessible Outputs + if (this._addAccsOutput()) { + this._updateAccsOutput(); } - } - //accessible Outputs - if (this._addAccsOutput()) { - this._updateAccsOutput(); - } -}; + }; -/** - * Removes the default canvas. - * - * By default, a 100×100 pixels canvas is created without needing to call - * createCanvas(). `noCanvas()` removes the - * default canvas for sketches that don't need it. - * - * @method noCanvas - * - * @example - *
- * - * function setup() { - * noCanvas(); - * } - * - *
- */ -p5.prototype.noCanvas = function () { - if (this.canvas) { - this.canvas.parentNode.removeChild(this.canvas); - } -}; + /** + * Removes the default canvas. + * + * By default, a 100×100 pixels canvas is created without needing to call + * createCanvas(). `noCanvas()` removes the + * default canvas for sketches that don't need it. + * + * @method noCanvas + * + * @example + *
+ * + * function setup() { + * noCanvas(); + * } + * + *
+ */ + p5.prototype.noCanvas = function () { + if (this.canvas) { + this.canvas.parentNode.removeChild(this.canvas); + } + }; -/** - * Creates a p5.Graphics object. - * - * `createGraphics()` creates an offscreen drawing canvas (graphics buffer) - * and returns it as a p5.Graphics object. Drawing - * to a separate graphics buffer can be helpful for performance and for - * organizing code. - * - * The first two parameters, `width` and `height`, are optional. They set the - * dimensions of the p5.Graphics object. For - * example, calling `createGraphics(900, 500)` creates a graphics buffer - * that's 900×500 pixels. - * - * The third parameter is also optional. If either of the constants `P2D` or - * `WEBGL` is passed, as in `createGraphics(900, 500, WEBGL)`, then it will set - * the p5.Graphics object's rendering mode. If an - * existing - * HTMLCanvasElement - * is passed, as in `createGraphics(900, 500, myCanvas)`, then it will be used - * by the graphics buffer. - * - * The fourth parameter is also optional. If an existing - * HTMLCanvasElement - * is passed, as in `createGraphics(900, 500, WEBGL, myCanvas)`, then it will be - * used by the graphics buffer. - * - * Note: In WebGL mode, the p5.Graphics object - * will use a WebGL2 context if it's supported by the browser. Check the - * webglVersion system variable to check what - * version is being used, or call `setAttributes({ version: 1 })` to create a - * WebGL1 context. - * - * @method createGraphics - * @param {Number} width width of the graphics buffer. - * @param {Number} height height of the graphics buffer. - * @param {(P2D|WEBGL)} [renderer] either P2D or WEBGL. Defaults to P2D. - * @param {HTMLCanvasElement} [canvas] existing canvas element that should be - * used for the graphics buffer.. - * @return {p5.Graphics} new graphics buffer. - * - * @example - *
- * - * // Double-click to draw the contents of the graphics buffer. - * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create the p5.Graphics object. - * pg = createGraphics(50, 50); - * - * // Draw to the graphics buffer. - * pg.background(100); - * pg.circle(pg.width / 2, pg.height / 2, 20); - * - * describe('A gray square. A smaller, darker square with a white circle at its center appears when the user double-clicks.'); - * } - * - * // Display the graphics buffer when the user double-clicks. - * function doubleClicked() { - * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { - * image(pg, 25, 25); - * } - * } - * - *
- * - *
- * - * // Double-click to draw the contents of the graphics buffer. - * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create the p5.Graphics object in WebGL mode. - * pg = createGraphics(50, 50, WEBGL); - * - * // Draw to the graphics buffer. - * pg.background(100); - * pg.lights(); - * pg.noStroke(); - * pg.rotateX(QUARTER_PI); - * pg.rotateY(QUARTER_PI); - * pg.torus(15, 5); - * - * describe('A gray square. A smaller, darker square with a white torus at its center appears when the user double-clicks.'); - * } - * - * // Display the graphics buffer when the user double-clicks. - * function doubleClicked() { - * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { - * image(pg, 25, 25); - * } - * } - * - *
- */ -/** - * @method createGraphics - * @param {Number} width - * @param {Number} height - * @param {HTMLCanvasElement} [canvas] - * @return {p5.Graphics} - */ -p5.prototype.createGraphics = function (w, h, ...args) { /** - * args[0] is expected to be renderer - * args[1] is expected to be canvas - */ - if (args[0] instanceof HTMLCanvasElement) { - args[1] = args[0]; - args[0] = constants.P2D; - } - p5._validateParameters('createGraphics', arguments); - return new p5.Graphics(w, h, args[0], this, args[1]); -}; + * Creates a p5.Graphics object. + * + * `createGraphics()` creates an offscreen drawing canvas (graphics buffer) + * and returns it as a p5.Graphics object. Drawing + * to a separate graphics buffer can be helpful for performance and for + * organizing code. + * + * The first two parameters, `width` and `height`, are optional. They set the + * dimensions of the p5.Graphics object. For + * example, calling `createGraphics(900, 500)` creates a graphics buffer + * that's 900×500 pixels. + * + * The third parameter is also optional. If either of the constants `P2D` or + * `WEBGL` is passed, as in `createGraphics(900, 500, WEBGL)`, then it will set + * the p5.Graphics object's rendering mode. If an + * existing + * HTMLCanvasElement + * is passed, as in `createGraphics(900, 500, myCanvas)`, then it will be used + * by the graphics buffer. + * + * The fourth parameter is also optional. If an existing + * HTMLCanvasElement + * is passed, as in `createGraphics(900, 500, WEBGL, myCanvas)`, then it will be + * used by the graphics buffer. + * + * Note: In WebGL mode, the p5.Graphics object + * will use a WebGL2 context if it's supported by the browser. Check the + * webglVersion system variable to check what + * version is being used, or call `setAttributes({ version: 1 })` to create a + * WebGL1 context. + * + * @method createGraphics + * @param {Number} width width of the graphics buffer. + * @param {Number} height height of the graphics buffer. + * @param {(P2D|WEBGL)} [renderer] either P2D or WEBGL. Defaults to P2D. + * @param {HTMLCanvasElement} [canvas] existing canvas element that should be + * used for the graphics buffer.. + * @return {p5.Graphics} new graphics buffer. + * + * @example + *
+ * + * // Double-click to draw the contents of the graphics buffer. + * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create the p5.Graphics object. + * pg = createGraphics(50, 50); + * + * // Draw to the graphics buffer. + * pg.background(100); + * pg.circle(pg.width / 2, pg.height / 2, 20); + * + * describe('A gray square. A smaller, darker square with a white circle at its center appears when the user double-clicks.'); + * } + * + * // Display the graphics buffer when the user double-clicks. + * function doubleClicked() { + * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { + * image(pg, 25, 25); + * } + * } + * + *
+ * + *
+ * + * // Double-click to draw the contents of the graphics buffer. + * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create the p5.Graphics object in WebGL mode. + * pg = createGraphics(50, 50, WEBGL); + * + * // Draw to the graphics buffer. + * pg.background(100); + * pg.lights(); + * pg.noStroke(); + * pg.rotateX(QUARTER_PI); + * pg.rotateY(QUARTER_PI); + * pg.torus(15, 5); + * + * describe('A gray square. A smaller, darker square with a white torus at its center appears when the user double-clicks.'); + * } + * + * // Display the graphics buffer when the user double-clicks. + * function doubleClicked() { + * if (mouseX > 0 && mouseX < 100 && mouseY > 0 && mouseY < 100) { + * image(pg, 25, 25); + * } + * } + * + *
+ */ + /** + * @method createGraphics + * @param {Number} width + * @param {Number} height + * @param {HTMLCanvasElement} [canvas] + * @return {p5.Graphics} + */ + p5.prototype.createGraphics = function (w, h, ...args) { + /** + * args[0] is expected to be renderer + * args[1] is expected to be canvas + */ + if (args[0] instanceof HTMLCanvasElement) { + args[1] = args[0]; + args[0] = constants.P2D; + } + p5._validateParameters('createGraphics', arguments); + return new p5.Graphics(w, h, args[0], this, args[1]); + }; -/** - * Creates and a new p5.Framebuffer object. - * - * p5.Framebuffer objects are separate drawing - * surfaces that can be used as textures in WebGL mode. They're similar to - * p5.Graphics objects and generally run much - * faster when used as textures. - * - * The parameter, `options`, is optional. An object can be passed to configure - * the p5.Framebuffer object. The available - * properties are: - * - * - `format`: data format of the texture, either `UNSIGNED_BYTE`, `FLOAT`, or `HALF_FLOAT`. Default is `UNSIGNED_BYTE`. - * - `channels`: whether to store `RGB` or `RGBA` color channels. Default is to match the main canvas which is `RGBA`. - * - `depth`: whether to include a depth buffer. Default is `true`. - * - `depthFormat`: data format of depth information, either `UNSIGNED_INT` or `FLOAT`. Default is `FLOAT`. - * - `stencil`: whether to include a stencil buffer for masking. `depth` must be `true` for this feature to work. Defaults to the value of `depth` which is `true`. - * - `antialias`: whether to perform anti-aliasing. If set to `true`, as in `{ antialias: true }`, 2 samples will be used by default. The number of samples can also be set, as in `{ antialias: 4 }`. Default is to match setAttributes() which is `false` (`true` in Safari). - * - `width`: width of the p5.Framebuffer object. Default is to always match the main canvas width. - * - `height`: height of the p5.Framebuffer object. Default is to always match the main canvas height. - * - `density`: pixel density of the p5.Framebuffer object. Default is to always match the main canvas pixel density. - * - `textureFiltering`: how to read values from the p5.Framebuffer object. Either `LINEAR` (nearby pixels will be interpolated) or `NEAREST` (no interpolation). Generally, use `LINEAR` when using the texture as an image and `NEAREST` if reading the texture as data. Default is `LINEAR`. - * - * If the `width`, `height`, or `density` attributes are set, they won't automatically match the main canvas and must be changed manually. - * - * Note: `createFramebuffer()` can only be used in WebGL mode. - * - * @method createFramebuffer - * @param {Object} [options] configuration options. - * @return {p5.Framebuffer} new framebuffer. - * - * @example - *
- * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * describe('A grid of white toruses rotating against a dark gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Start drawing to the p5.Framebuffer object. - * myBuffer.begin(); - * - * // Clear the drawing surface. - * clear(); - * - * // Turn on the lights. - * lights(); - * - * // Rotate the coordinate system. - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * - * // Style the torus. - * noStroke(); - * - * // Draw the torus. - * torus(20); - * - * // Stop drawing to the p5.Framebuffer object. - * myBuffer.end(); - * - * // Iterate from left to right. - * for (let x = -50; x < 50; x += 25) { - * // Iterate from top to bottom. - * for (let y = -50; y < 50; y += 25) { - * // Draw the p5.Framebuffer object to the canvas. - * image(myBuffer, x, y, 25, 25); - * } - * } - * } - * - *
- * - *
- * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create an options object. - * let options = { width: 25, height: 25 }; - * - * // Create a p5.Framebuffer object. - * // Use options for configuration. - * myBuffer = createFramebuffer(options); - * - * describe('A grid of white toruses rotating against a dark gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Start drawing to the p5.Framebuffer object. - * myBuffer.begin(); - * - * // Clear the drawing surface. - * clear(); - * - * // Turn on the lights. - * lights(); - * - * // Rotate the coordinate system. - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * - * // Style the torus. - * noStroke(); - * - * // Draw the torus. - * torus(5, 2.5); - * - * // Stop drawing to the p5.Framebuffer object. - * myBuffer.end(); - * - * // Iterate from left to right. - * for (let x = -50; x < 50; x += 25) { - * // Iterate from top to bottom. - * for (let y = -50; y < 50; y += 25) { - * // Draw the p5.Framebuffer object to the canvas. - * image(myBuffer, x, y); - * } - * } - * } - * - *
- */ -p5.prototype.createFramebuffer = function (options) { - return new p5.Framebuffer(this, options); -}; + /** + * Creates and a new p5.Framebuffer object. + * + * p5.Framebuffer objects are separate drawing + * surfaces that can be used as textures in WebGL mode. They're similar to + * p5.Graphics objects and generally run much + * faster when used as textures. + * + * The parameter, `options`, is optional. An object can be passed to configure + * the p5.Framebuffer object. The available + * properties are: + * + * - `format`: data format of the texture, either `UNSIGNED_BYTE`, `FLOAT`, or `HALF_FLOAT`. Default is `UNSIGNED_BYTE`. + * - `channels`: whether to store `RGB` or `RGBA` color channels. Default is to match the main canvas which is `RGBA`. + * - `depth`: whether to include a depth buffer. Default is `true`. + * - `depthFormat`: data format of depth information, either `UNSIGNED_INT` or `FLOAT`. Default is `FLOAT`. + * - `stencil`: whether to include a stencil buffer for masking. `depth` must be `true` for this feature to work. Defaults to the value of `depth` which is `true`. + * - `antialias`: whether to perform anti-aliasing. If set to `true`, as in `{ antialias: true }`, 2 samples will be used by default. The number of samples can also be set, as in `{ antialias: 4 }`. Default is to match setAttributes() which is `false` (`true` in Safari). + * - `width`: width of the p5.Framebuffer object. Default is to always match the main canvas width. + * - `height`: height of the p5.Framebuffer object. Default is to always match the main canvas height. + * - `density`: pixel density of the p5.Framebuffer object. Default is to always match the main canvas pixel density. + * - `textureFiltering`: how to read values from the p5.Framebuffer object. Either `LINEAR` (nearby pixels will be interpolated) or `NEAREST` (no interpolation). Generally, use `LINEAR` when using the texture as an image and `NEAREST` if reading the texture as data. Default is `LINEAR`. + * + * If the `width`, `height`, or `density` attributes are set, they won't automatically match the main canvas and must be changed manually. + * + * Note: `createFramebuffer()` can only be used in WebGL mode. + * + * @method createFramebuffer + * @param {Object} [options] configuration options. + * @return {p5.Framebuffer} new framebuffer. + * + * @example + *
+ * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * describe('A grid of white toruses rotating against a dark gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Start drawing to the p5.Framebuffer object. + * myBuffer.begin(); + * + * // Clear the drawing surface. + * clear(); + * + * // Turn on the lights. + * lights(); + * + * // Rotate the coordinate system. + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * + * // Style the torus. + * noStroke(); + * + * // Draw the torus. + * torus(20); + * + * // Stop drawing to the p5.Framebuffer object. + * myBuffer.end(); + * + * // Iterate from left to right. + * for (let x = -50; x < 50; x += 25) { + * // Iterate from top to bottom. + * for (let y = -50; y < 50; y += 25) { + * // Draw the p5.Framebuffer object to the canvas. + * image(myBuffer, x, y, 25, 25); + * } + * } + * } + * + *
+ * + *
+ * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create an options object. + * let options = { width: 25, height: 25 }; + * + * // Create a p5.Framebuffer object. + * // Use options for configuration. + * myBuffer = createFramebuffer(options); + * + * describe('A grid of white toruses rotating against a dark gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Start drawing to the p5.Framebuffer object. + * myBuffer.begin(); + * + * // Clear the drawing surface. + * clear(); + * + * // Turn on the lights. + * lights(); + * + * // Rotate the coordinate system. + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * + * // Style the torus. + * noStroke(); + * + * // Draw the torus. + * torus(5, 2.5); + * + * // Stop drawing to the p5.Framebuffer object. + * myBuffer.end(); + * + * // Iterate from left to right. + * for (let x = -50; x < 50; x += 25) { + * // Iterate from top to bottom. + * for (let y = -50; y < 50; y += 25) { + * // Draw the p5.Framebuffer object to the canvas. + * image(myBuffer, x, y); + * } + * } + * } + * + *
+ */ + p5.prototype.createFramebuffer = function (options) { + return new p5.Framebuffer(this, options); + }; -/** - * Clears the depth buffer in WebGL mode. - * - * `clearDepth()` clears information about how far objects are from the camera - * in 3D space. This information is stored in an object called the - * *depth buffer*. Clearing the depth buffer ensures new objects aren't drawn - * behind old ones. Doing so can be useful for feedback effects in which the - * previous frame serves as the background for the current frame. - * - * The parameter, `depth`, is optional. If a number is passed, as in - * `clearDepth(0.5)`, it determines the range of objects to clear from the - * depth buffer. 0 doesn't clear any depth information, 0.5 clears depth - * information halfway between the near and far clipping planes, and 1 clears - * depth information all the way to the far clipping plane. By default, - * `depth` is 1. - * - * Note: `clearDepth()` can only be used in WebGL mode. - * - * @method clearDepth - * @param {Number} [depth] amount of the depth buffer to clear between 0 - * (none) and 1 (far clipping plane). Defaults to 1. - * - * @example - *
- * - * let previous; - * let current; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Framebuffer objects. - * previous = createFramebuffer({ format: FLOAT }); - * current = createFramebuffer({ format: FLOAT }); - * - * describe( - * 'A multicolor box drifts from side to side on a white background. It leaves a trail that fades over time.' - * ); - * } - * - * function draw() { - * // Swap the previous p5.Framebuffer and the - * // current one so it can be used as a texture. - * [previous, current] = [current, previous]; - * - * // Start drawing to the current p5.Framebuffer. - * current.begin(); - * - * // Paint the background. - * background(255); - * - * // Draw the previous p5.Framebuffer. - * // Clear the depth buffer so the previous - * // frame doesn't block the current one. - * push(); - * tint(255, 250); - * image(previous, -50, -50); - * clearDepth(); - * pop(); - * - * // Draw the box on top of the previous frame. - * push(); - * let x = 25 * sin(frameCount * 0.01); - * let y = 25 * sin(frameCount * 0.02); - * translate(x, y, 0); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * normalMaterial(); - * box(12); - * pop(); - * - * // Stop drawing to the current p5.Framebuffer. - * current.end(); - * - * // Display the current p5.Framebuffer. - * image(current, -50, -50); - * } - * - *
- */ -p5.prototype.clearDepth = function (depth) { - this._assert3d('clearDepth'); - this._renderer.clearDepth(depth); -}; + /** + * Clears the depth buffer in WebGL mode. + * + * `clearDepth()` clears information about how far objects are from the camera + * in 3D space. This information is stored in an object called the + * *depth buffer*. Clearing the depth buffer ensures new objects aren't drawn + * behind old ones. Doing so can be useful for feedback effects in which the + * previous frame serves as the background for the current frame. + * + * The parameter, `depth`, is optional. If a number is passed, as in + * `clearDepth(0.5)`, it determines the range of objects to clear from the + * depth buffer. 0 doesn't clear any depth information, 0.5 clears depth + * information halfway between the near and far clipping planes, and 1 clears + * depth information all the way to the far clipping plane. By default, + * `depth` is 1. + * + * Note: `clearDepth()` can only be used in WebGL mode. + * + * @method clearDepth + * @param {Number} [depth] amount of the depth buffer to clear between 0 + * (none) and 1 (far clipping plane). Defaults to 1. + * + * @example + *
+ * + * let previous; + * let current; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Framebuffer objects. + * previous = createFramebuffer({ format: FLOAT }); + * current = createFramebuffer({ format: FLOAT }); + * + * describe( + * 'A multicolor box drifts from side to side on a white background. It leaves a trail that fades over time.' + * ); + * } + * + * function draw() { + * // Swap the previous p5.Framebuffer and the + * // current one so it can be used as a texture. + * [previous, current] = [current, previous]; + * + * // Start drawing to the current p5.Framebuffer. + * current.begin(); + * + * // Paint the background. + * background(255); + * + * // Draw the previous p5.Framebuffer. + * // Clear the depth buffer so the previous + * // frame doesn't block the current one. + * push(); + * tint(255, 250); + * image(previous, -50, -50); + * clearDepth(); + * pop(); + * + * // Draw the box on top of the previous frame. + * push(); + * let x = 25 * sin(frameCount * 0.01); + * let y = 25 * sin(frameCount * 0.02); + * translate(x, y, 0); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * normalMaterial(); + * box(12); + * pop(); + * + * // Stop drawing to the current p5.Framebuffer. + * current.end(); + * + * // Display the current p5.Framebuffer. + * image(current, -50, -50); + * } + * + *
+ */ + p5.prototype.clearDepth = function (depth) { + this._assert3d('clearDepth'); + this._renderer.clearDepth(depth); + }; -/** - * A system variable that provides direct access to the sketch's - * `<canvas>` element. - * - * The `<canvas>` element provides many specialized features that aren't - * included in the p5.js library. The `drawingContext` system variable - * provides access to these features by exposing the sketch's - * CanvasRenderingContext2D - * object. - * - * @property drawingContext - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the circle using shadows. - * drawingContext.shadowOffsetX = 5; - * drawingContext.shadowOffsetY = -5; - * drawingContext.shadowBlur = 10; - * drawingContext.shadowColor = 'black'; - * - * // Draw the circle. - * circle(50, 50, 40); - * - * describe("A white circle on a gray background. The circle's edges are shadowy."); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background('skyblue'); - * - * // Style the circle using a color gradient. - * let myGradient = drawingContext.createRadialGradient(50, 50, 3, 50, 50, 40); - * myGradient.addColorStop(0, 'yellow'); - * myGradient.addColorStop(0.6, 'orangered'); - * myGradient.addColorStop(1, 'yellow'); - * drawingContext.fillStyle = myGradient; - * drawingContext.strokeStyle = 'rgba(0, 0, 0, 0)'; - * - * // Draw the circle. - * circle(50, 50, 40); - * - * describe('A fiery sun drawn on a light blue background.'); - * } - * - *
- */ + /** + * A system variable that provides direct access to the sketch's + * `<canvas>` element. + * + * The `<canvas>` element provides many specialized features that aren't + * included in the p5.js library. The `drawingContext` system variable + * provides access to these features by exposing the sketch's + * CanvasRenderingContext2D + * object. + * + * @property drawingContext + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the circle using shadows. + * drawingContext.shadowOffsetX = 5; + * drawingContext.shadowOffsetY = -5; + * drawingContext.shadowBlur = 10; + * drawingContext.shadowColor = 'black'; + * + * // Draw the circle. + * circle(50, 50, 40); + * + * describe("A white circle on a gray background. The circle's edges are shadowy."); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background('skyblue'); + * + * // Style the circle using a color gradient. + * let myGradient = drawingContext.createRadialGradient(50, 50, 3, 50, 50, 40); + * myGradient.addColorStop(0, 'yellow'); + * myGradient.addColorStop(0.6, 'orangered'); + * myGradient.addColorStop(1, 'yellow'); + * drawingContext.fillStyle = myGradient; + * drawingContext.strokeStyle = 'rgba(0, 0, 0, 0)'; + * + * // Draw the circle. + * circle(50, 50, 40); + * + * describe('A fiery sun drawn on a light blue background.'); + * } + * + *
+ */ +} + +export default rendering; -export default p5; +if(typeof p5 !== 'undefined'){ + rendering(p5, p5.prototype); +} \ No newline at end of file diff --git a/src/core/structure.js b/src/core/structure.js index f614c47442..e675cd0517 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -5,576 +5,582 @@ * @requires core */ -import p5 from './main'; -/** - * Stops the code in draw() from running repeatedly. - * - * By default, draw() tries to run 60 times per - * second. Calling `noLoop()` stops draw() from - * repeating. The draw loop can be restarted by calling - * loop(). draw() can be run - * once by calling redraw(). - * - * The isLooping() function can be used to check - * whether a sketch is looping, as in `isLooping() === true`. - * - * @method noLoop - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Turn off the draw loop. - * noLoop(); - * - * describe('A white half-circle on the left edge of a gray square.'); - * } - * - * function draw() { - * background(200); - * - * // Calculate the circle's x-coordinate. - * let x = frameCount; - * - * // Draw the circle. - * // Normally, the circle would move from left to right. - * circle(x, 50, 20); - * } - * - *
- * - *
- * - * // Double-click to stop the draw loop. - * - * function setup() { - * createCanvas(100, 100); - * - * // Slow the frame rate. - * frameRate(5); - * - * describe('A white circle moves randomly on a gray background. It stops moving when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Calculate the circle's coordinates. - * let x = random(0, 100); - * let y = random(0, 100); - * - * // Draw the circle. - * // Normally, the circle would move from left to right. - * circle(x, y, 20); - * } - * - * // Stop the draw loop when the user double-clicks. - * function doubleClicked() { - * noLoop(); - * } - * - *
- * - *
- * - * let startButton; - * let stopButton; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create the button elements and place them - * // beneath the canvas. - * startButton = createButton('▶'); - * startButton.position(0, 100); - * startButton.size(50, 20); - * stopButton = createButton('◾'); - * stopButton.position(50, 100); - * stopButton.size(50, 20); - * - * // Set functions to call when the buttons are pressed. - * startButton.mousePressed(loop); - * stopButton.mousePressed(noLoop); - * - * // Slow the frame rate. - * frameRate(5); - * - * describe( - * 'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Calculate the circle's coordinates. - * let x = random(0, 100); - * let y = random(0, 100); - * - * // Draw the circle. - * // Normally, the circle would move from left to right. - * circle(x, y, 20); - * } - * - *
- */ -p5.prototype.noLoop = function() { - this._loop = false; -}; +function structure(p5, fn){ + /** + * Stops the code in draw() from running repeatedly. + * + * By default, draw() tries to run 60 times per + * second. Calling `noLoop()` stops draw() from + * repeating. The draw loop can be restarted by calling + * loop(). draw() can be run + * once by calling redraw(). + * + * The isLooping() function can be used to check + * whether a sketch is looping, as in `isLooping() === true`. + * + * @method noLoop + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Turn off the draw loop. + * noLoop(); + * + * describe('A white half-circle on the left edge of a gray square.'); + * } + * + * function draw() { + * background(200); + * + * // Calculate the circle's x-coordinate. + * let x = frameCount; + * + * // Draw the circle. + * // Normally, the circle would move from left to right. + * circle(x, 50, 20); + * } + * + *
+ * + *
+ * + * // Double-click to stop the draw loop. + * + * function setup() { + * createCanvas(100, 100); + * + * // Slow the frame rate. + * frameRate(5); + * + * describe('A white circle moves randomly on a gray background. It stops moving when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Calculate the circle's coordinates. + * let x = random(0, 100); + * let y = random(0, 100); + * + * // Draw the circle. + * // Normally, the circle would move from left to right. + * circle(x, y, 20); + * } + * + * // Stop the draw loop when the user double-clicks. + * function doubleClicked() { + * noLoop(); + * } + * + *
+ * + *
+ * + * let startButton; + * let stopButton; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create the button elements and place them + * // beneath the canvas. + * startButton = createButton('▶'); + * startButton.position(0, 100); + * startButton.size(50, 20); + * stopButton = createButton('◾'); + * stopButton.position(50, 100); + * stopButton.size(50, 20); + * + * // Set functions to call when the buttons are pressed. + * startButton.mousePressed(loop); + * stopButton.mousePressed(noLoop); + * + * // Slow the frame rate. + * frameRate(5); + * + * describe( + * 'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Calculate the circle's coordinates. + * let x = random(0, 100); + * let y = random(0, 100); + * + * // Draw the circle. + * // Normally, the circle would move from left to right. + * circle(x, y, 20); + * } + * + *
+ */ + fn.noLoop = function() { + this._loop = false; + }; -/** - * Resumes the draw loop after noLoop() has been - * called. - * - * By default, draw() tries to run 60 times per - * second. Calling noLoop() stops - * draw() from repeating. The draw loop can be - * restarted by calling `loop()`. - * - * The isLooping() function can be used to check - * whether a sketch is looping, as in `isLooping() === true`. - * - * @method loop - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Turn off the draw loop. - * noLoop(); - * - * describe( - * 'A white half-circle on the left edge of a gray square. The circle starts moving to the right when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Calculate the circle's x-coordinate. - * let x = frameCount; - * - * // Draw the circle. - * circle(x, 50, 20); - * } - * - * // Resume the draw loop when the user double-clicks. - * function doubleClicked() { - * loop(); - * } - * - *
- * - *
- * - * let startButton; - * let stopButton; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create the button elements and place them - * // beneath the canvas. - * startButton = createButton('▶'); - * startButton.position(0, 100); - * startButton.size(50, 20); - * stopButton = createButton('◾'); - * stopButton.position(50, 100); - * stopButton.size(50, 20); - * - * // Set functions to call when the buttons are pressed. - * startButton.mousePressed(loop); - * stopButton.mousePressed(noLoop); - * - * // Slow the frame rate. - * frameRate(5); - * - * describe( - * 'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Calculate the circle's coordinates. - * let x = random(0, 100); - * let y = random(0, 100); - * - * // Draw the circle. - * // Normally, the circle would move from left to right. - * circle(x, y, 20); - * } - * - *
- */ -p5.prototype.loop = function() { - if (!this._loop) { - this._loop = true; - if (this._setupDone) { - this._draw(); + /** + * Resumes the draw loop after noLoop() has been + * called. + * + * By default, draw() tries to run 60 times per + * second. Calling noLoop() stops + * draw() from repeating. The draw loop can be + * restarted by calling `loop()`. + * + * The isLooping() function can be used to check + * whether a sketch is looping, as in `isLooping() === true`. + * + * @method loop + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Turn off the draw loop. + * noLoop(); + * + * describe( + * 'A white half-circle on the left edge of a gray square. The circle starts moving to the right when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Calculate the circle's x-coordinate. + * let x = frameCount; + * + * // Draw the circle. + * circle(x, 50, 20); + * } + * + * // Resume the draw loop when the user double-clicks. + * function doubleClicked() { + * loop(); + * } + * + *
+ * + *
+ * + * let startButton; + * let stopButton; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create the button elements and place them + * // beneath the canvas. + * startButton = createButton('▶'); + * startButton.position(0, 100); + * startButton.size(50, 20); + * stopButton = createButton('◾'); + * stopButton.position(50, 100); + * stopButton.size(50, 20); + * + * // Set functions to call when the buttons are pressed. + * startButton.mousePressed(loop); + * stopButton.mousePressed(noLoop); + * + * // Slow the frame rate. + * frameRate(5); + * + * describe( + * 'A white circle moves randomly on a gray background. Play and stop buttons are shown beneath the canvas. The circle stops or starts moving when the user presses a button.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Calculate the circle's coordinates. + * let x = random(0, 100); + * let y = random(0, 100); + * + * // Draw the circle. + * // Normally, the circle would move from left to right. + * circle(x, y, 20); + * } + * + *
+ */ + fn.loop = function() { + if (!this._loop) { + this._loop = true; + if (this._setupDone) { + this._draw(); + } } - } -}; + }; -/** - * Returns `true` if the draw loop is running and `false` if not. - * - * By default, draw() tries to run 60 times per - * second. Calling noLoop() stops - * draw() from repeating. The draw loop can be - * restarted by calling loop(). - * - * The `isLooping()` function can be used to check whether a sketch is - * looping, as in `isLooping() === true`. - * - * @method isLooping - * @returns {boolean} - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A white circle drawn against a gray background. When the user double-clicks, the circle stops or resumes following the mouse.'); - * } - * - * function draw() { - * background(200); - * - * // Draw the circle at the mouse's position. - * circle(mouseX, mouseY, 20); - * } - * - * // Toggle the draw loop when the user double-clicks. - * function doubleClicked() { - * if (isLooping() === true) { - * noLoop(); - * } else { - * loop(); - * } - * } - * - *
- */ -p5.prototype.isLooping = function() { - return this._loop; -}; + /** + * Returns `true` if the draw loop is running and `false` if not. + * + * By default, draw() tries to run 60 times per + * second. Calling noLoop() stops + * draw() from repeating. The draw loop can be + * restarted by calling loop(). + * + * The `isLooping()` function can be used to check whether a sketch is + * looping, as in `isLooping() === true`. + * + * @method isLooping + * @returns {boolean} + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A white circle drawn against a gray background. When the user double-clicks, the circle stops or resumes following the mouse.'); + * } + * + * function draw() { + * background(200); + * + * // Draw the circle at the mouse's position. + * circle(mouseX, mouseY, 20); + * } + * + * // Toggle the draw loop when the user double-clicks. + * function doubleClicked() { + * if (isLooping() === true) { + * noLoop(); + * } else { + * loop(); + * } + * } + * + *
+ */ + fn.isLooping = function() { + return this._loop; + }; -/** - * Runs the code in draw() once. - * - * By default, draw() tries to run 60 times per - * second. Calling noLoop() stops - * draw() from repeating. Calling `redraw()` will - * execute the code in the draw() function a set - * number of times. - * - * The parameter, `n`, is optional. If a number is passed, as in `redraw(5)`, - * then the draw loop will run the given number of times. By default, `n` is - * 1. - * - * @method redraw - * @param {Integer} [n] number of times to run draw(). Defaults to 1. - * - * @example - *
- * - * // Double-click the canvas to move the circle. - * - * let x = 0; - * - * function setup() { - * createCanvas(100, 100); - * - * // Turn off the draw loop. - * noLoop(); - * - * describe( - * 'A white half-circle on the left edge of a gray square. The circle moves a little to the right when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Draw the circle. - * circle(x, 50, 20); - * - * // Increment x. - * x += 5; - * } - * - * // Run the draw loop when the user double-clicks. - * function doubleClicked() { - * redraw(); - * } - * - *
- * - *
- * - * // Double-click the canvas to move the circle. - * - * let x = 0; - * - * function setup() { - * createCanvas(100, 100); - * - * // Turn off the draw loop. - * noLoop(); - * - * describe( - * 'A white half-circle on the left edge of a gray square. The circle hops to the right when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Draw the circle. - * circle(x, 50, 20); - * - * // Increment x. - * x += 5; - * } - * - * // Run the draw loop three times when the user double-clicks. - * function doubleClicked() { - * redraw(3); - * } - * - *
- */ -p5.prototype.redraw = async function(n) { - if (this._inUserDraw || !this._setupDone) { - return; - } - - let numberOfRedraws = parseInt(n); - if (isNaN(numberOfRedraws) || numberOfRedraws < 1) { - numberOfRedraws = 1; - } + /** + * Runs the code in draw() once. + * + * By default, draw() tries to run 60 times per + * second. Calling noLoop() stops + * draw() from repeating. Calling `redraw()` will + * execute the code in the draw() function a set + * number of times. + * + * The parameter, `n`, is optional. If a number is passed, as in `redraw(5)`, + * then the draw loop will run the given number of times. By default, `n` is + * 1. + * + * @method redraw + * @param {Integer} [n] number of times to run draw(). Defaults to 1. + * + * @example + *
+ * + * // Double-click the canvas to move the circle. + * + * let x = 0; + * + * function setup() { + * createCanvas(100, 100); + * + * // Turn off the draw loop. + * noLoop(); + * + * describe( + * 'A white half-circle on the left edge of a gray square. The circle moves a little to the right when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Draw the circle. + * circle(x, 50, 20); + * + * // Increment x. + * x += 5; + * } + * + * // Run the draw loop when the user double-clicks. + * function doubleClicked() { + * redraw(); + * } + * + *
+ * + *
+ * + * // Double-click the canvas to move the circle. + * + * let x = 0; + * + * function setup() { + * createCanvas(100, 100); + * + * // Turn off the draw loop. + * noLoop(); + * + * describe( + * 'A white half-circle on the left edge of a gray square. The circle hops to the right when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Draw the circle. + * circle(x, 50, 20); + * + * // Increment x. + * x += 5; + * } + * + * // Run the draw loop three times when the user double-clicks. + * function doubleClicked() { + * redraw(3); + * } + * + *
+ */ + fn.redraw = async function(n) { + if (this._inUserDraw || !this._setupDone) { + return; + } - const context = this._isGlobal ? window : this; - if (typeof context.draw === 'function') { - if (typeof context.setup === 'undefined') { - context.scale(context._pixelDensity, context._pixelDensity); + let numberOfRedraws = parseInt(n); + if (isNaN(numberOfRedraws) || numberOfRedraws < 1) { + numberOfRedraws = 1; } - for (let idxRedraw = 0; idxRedraw < numberOfRedraws; idxRedraw++) { - context.resetMatrix(); - if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { - this._updateAccsOutput(); - } - if (this._renderer.isP3D) { - this._renderer._update(); + + const context = this._isGlobal ? window : this; + if (typeof context.draw === 'function') { + if (typeof context.setup === 'undefined') { + context.scale(context._pixelDensity, context._pixelDensity); } - this.frameCount = context.frameCount + 1; - await this._runLifecycleHook('predraw'); - this._inUserDraw = true; - try { - await context.draw(); - } finally { - this._inUserDraw = false; + for (let idxRedraw = 0; idxRedraw < numberOfRedraws; idxRedraw++) { + context.resetMatrix(); + if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { + this._updateAccsOutput(); + } + if (this._renderer.isP3D) { + this._renderer._update(); + } + this.frameCount = context.frameCount + 1; + await this._runLifecycleHook('predraw'); + this._inUserDraw = true; + try { + await context.draw(); + } finally { + this._inUserDraw = false; + } + await this._runLifecycleHook('postdraw'); } - await this._runLifecycleHook('postdraw'); } - } -}; + }; -/** - * Creates a new sketch in "instance" mode. - * - * All p5.js sketches are instances of the `p5` class. Put another way, all - * p5.js sketches are objects with methods including `pInst.setup()`, - * `pInst.draw()`, `pInst.circle()`, and `pInst.fill()`. By default, sketches - * run in "global mode" to hide some of this complexity. - * - * In global mode, a default instance of the `p5` class is created - * automatically. The default `p5` instance searches the web page's source - * code for declarations of system functions such as `setup()`, `draw()`, - * and `mousePressed()`, then attaches those functions to itself as methods. - * Calling a function such as `circle()` in global mode actually calls the - * default `p5` object's `pInst.circle()` method. - * - * It's often helpful to isolate the code within sketches from the rest of the - * code on a web page. Two common use cases are web pages that use other - * JavaScript libraries and web pages with multiple sketches. "Instance mode" - * makes it easy to support both of these scenarios. - * - * Instance mode sketches support the same API as global mode sketches. They - * use a function to bundle, or encapsulate, an entire sketch. The function - * containing the sketch is then passed to the `p5()` constructor. - * - * The first parameter, `sketch`, is a function that contains the sketch. For - * example, the statement `new p5(mySketch)` would create a new instance mode - * sketch from a function named `mySketch`. The function should have one - * parameter, `p`, that's a `p5` object. - * - * The second parameter, `node`, is optional. If a string is passed, as in - * `new p5(mySketch, 'sketch-one')` the new instance mode sketch will become a - * child of the HTML element with the id `sketch-one`. If an HTML element is - * passed, as in `new p5(mySketch, myElement)`, then the new instance mode - * sketch will become a child of the `Element` object called `myElement`. - * - * @method p5 - * @param {Object} sketch function containing the sketch. - * @param {String|HTMLElement} node ID or reference to the HTML element that will contain the sketch. - * - * @example - *
- * - * // Declare the function containing the sketch. - * function sketch(p) { - * - * // Declare the setup() method. - * p.setup = function () { - * p.createCanvas(100, 100); - * - * p.describe('A white circle drawn on a gray background.'); - * }; - * - * // Declare the draw() method. - * p.draw = function () { - * p.background(200); - * - * // Draw the circle. - * p.circle(50, 50, 20); - * }; - * } - * - * // Initialize the sketch. - * new p5(sketch); - * - *
- * - *
- * - * // Declare the function containing the sketch. - * function sketch(p) { - * // Create the sketch's variables within its scope. - * let x = 50; - * let y = 50; - * - * // Declare the setup() method. - * p.setup = function () { - * p.createCanvas(100, 100); - * - * p.describe('A white circle moves randomly on a gray background.'); - * }; - * - * // Declare the draw() method. - * p.draw = function () { - * p.background(200); - * - * // Update x and y. - * x += p.random(-1, 1); - * y += p.random(-1, 1); - * - * // Draw the circle. - * p.circle(x, y, 20); - * }; - * } - * - * // Initialize the sketch. - * new p5(sketch); - * - *
- * - *
- * - * // Declare the function containing the sketch. - * function sketch(p) { - * - * // Declare the setup() method. - * p.setup = function () { - * p.createCanvas(100, 100); - * - * p.describe('A white circle drawn on a gray background.'); - * }; - * - * // Declare the draw() method. - * p.draw = function () { - * p.background(200); - * - * // Draw the circle. - * p.circle(50, 50, 20); - * }; - * } - * - * // Select the web page's body element. - * let body = document.querySelector('body'); - * - * // Initialize the sketch and attach it to the web page's body. - * new p5(sketch, body); - * - *
- * - *
- * - * // Declare the function containing the sketch. - * function sketch(p) { - * - * // Declare the setup() method. - * p.setup = function () { - * p.createCanvas(100, 100); - * - * p.describe( - * 'A white circle drawn on a gray background. The circle follows the mouse as the user moves.' - * ); - * }; - * - * // Declare the draw() method. - * p.draw = function () { - * p.background(200); - * - * // Draw the circle. - * p.circle(p.mouseX, p.mouseY, 20); - * }; - * } - * - * // Initialize the sketch. - * new p5(sketch); - * - *
- * - *
- * - * // Declare the function containing the sketch. - * function sketch(p) { - * - * // Declare the setup() method. - * p.setup = function () { - * p.createCanvas(100, 100); - * - * p.describe( - * 'A white circle drawn on a gray background. The circle follows the mouse as the user moves. The circle becomes black when the user double-clicks.' - * ); - * }; - * - * // Declare the draw() method. - * p.draw = function () { - * p.background(200); - * - * // Draw the circle. - * p.circle(p.mouseX, p.mouseY, 20); - * }; - * - * // Declare the doubleClicked() method. - * p.doubleClicked = function () { - * // Change the fill color when the user double-clicks. - * p.fill(0); - * }; - * } - * - * // Initialize the sketch. - * new p5(sketch); - * - *
- */ -export default p5; + /** + * Creates a new sketch in "instance" mode. + * + * All p5.js sketches are instances of the `p5` class. Put another way, all + * p5.js sketches are objects with methods including `pInst.setup()`, + * `pInst.draw()`, `pInst.circle()`, and `pInst.fill()`. By default, sketches + * run in "global mode" to hide some of this complexity. + * + * In global mode, a default instance of the `p5` class is created + * automatically. The default `p5` instance searches the web page's source + * code for declarations of system functions such as `setup()`, `draw()`, + * and `mousePressed()`, then attaches those functions to itself as methods. + * Calling a function such as `circle()` in global mode actually calls the + * default `p5` object's `pInst.circle()` method. + * + * It's often helpful to isolate the code within sketches from the rest of the + * code on a web page. Two common use cases are web pages that use other + * JavaScript libraries and web pages with multiple sketches. "Instance mode" + * makes it easy to support both of these scenarios. + * + * Instance mode sketches support the same API as global mode sketches. They + * use a function to bundle, or encapsulate, an entire sketch. The function + * containing the sketch is then passed to the `p5()` constructor. + * + * The first parameter, `sketch`, is a function that contains the sketch. For + * example, the statement `new p5(mySketch)` would create a new instance mode + * sketch from a function named `mySketch`. The function should have one + * parameter, `p`, that's a `p5` object. + * + * The second parameter, `node`, is optional. If a string is passed, as in + * `new p5(mySketch, 'sketch-one')` the new instance mode sketch will become a + * child of the HTML element with the id `sketch-one`. If an HTML element is + * passed, as in `new p5(mySketch, myElement)`, then the new instance mode + * sketch will become a child of the `Element` object called `myElement`. + * + * @method p5 + * @param {Object} sketch function containing the sketch. + * @param {String|HTMLElement} node ID or reference to the HTML element that will contain the sketch. + * + * @example + *
+ * + * // Declare the function containing the sketch. + * function sketch(p) { + * + * // Declare the setup() method. + * p.setup = function () { + * p.createCanvas(100, 100); + * + * p.describe('A white circle drawn on a gray background.'); + * }; + * + * // Declare the draw() method. + * p.draw = function () { + * p.background(200); + * + * // Draw the circle. + * p.circle(50, 50, 20); + * }; + * } + * + * // Initialize the sketch. + * new p5(sketch); + * + *
+ * + *
+ * + * // Declare the function containing the sketch. + * function sketch(p) { + * // Create the sketch's variables within its scope. + * let x = 50; + * let y = 50; + * + * // Declare the setup() method. + * p.setup = function () { + * p.createCanvas(100, 100); + * + * p.describe('A white circle moves randomly on a gray background.'); + * }; + * + * // Declare the draw() method. + * p.draw = function () { + * p.background(200); + * + * // Update x and y. + * x += p.random(-1, 1); + * y += p.random(-1, 1); + * + * // Draw the circle. + * p.circle(x, y, 20); + * }; + * } + * + * // Initialize the sketch. + * new p5(sketch); + * + *
+ * + *
+ * + * // Declare the function containing the sketch. + * function sketch(p) { + * + * // Declare the setup() method. + * p.setup = function () { + * p.createCanvas(100, 100); + * + * p.describe('A white circle drawn on a gray background.'); + * }; + * + * // Declare the draw() method. + * p.draw = function () { + * p.background(200); + * + * // Draw the circle. + * p.circle(50, 50, 20); + * }; + * } + * + * // Select the web page's body element. + * let body = document.querySelector('body'); + * + * // Initialize the sketch and attach it to the web page's body. + * new p5(sketch, body); + * + *
+ * + *
+ * + * // Declare the function containing the sketch. + * function sketch(p) { + * + * // Declare the setup() method. + * p.setup = function () { + * p.createCanvas(100, 100); + * + * p.describe( + * 'A white circle drawn on a gray background. The circle follows the mouse as the user moves.' + * ); + * }; + * + * // Declare the draw() method. + * p.draw = function () { + * p.background(200); + * + * // Draw the circle. + * p.circle(p.mouseX, p.mouseY, 20); + * }; + * } + * + * // Initialize the sketch. + * new p5(sketch); + * + *
+ * + *
+ * + * // Declare the function containing the sketch. + * function sketch(p) { + * + * // Declare the setup() method. + * p.setup = function () { + * p.createCanvas(100, 100); + * + * p.describe( + * 'A white circle drawn on a gray background. The circle follows the mouse as the user moves. The circle becomes black when the user double-clicks.' + * ); + * }; + * + * // Declare the draw() method. + * p.draw = function () { + * p.background(200); + * + * // Draw the circle. + * p.circle(p.mouseX, p.mouseY, 20); + * }; + * + * // Declare the doubleClicked() method. + * p.doubleClicked = function () { + * // Change the fill color when the user double-clicks. + * p.fill(0); + * }; + * } + * + * // Initialize the sketch. + * new p5(sketch); + * + *
+ */ +} + +export default structure; + +if(typeof p5 !== 'undefined'){ + structure(p5, p5.prototype); +} \ No newline at end of file diff --git a/src/webgl/index.js b/src/webgl/index.js index cd56d77f9e..74ed708fad 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -13,8 +13,12 @@ import dataArray from './p5.DataArray'; import shader from './p5.Shader'; import camera from './p5.Camera'; import texture from './p5.Texture'; +import rendererGL from './p5.RendererGL'; +import rendererGLImmediate from './p5.RendererGL.Immediate'; +import rendererGLRetained from './p5.RendererGL.Retained'; export default function(p5){ + rendererGL(p5, p5.prototype); primitives3D(p5, p5.prototype); interaction(p5, p5.prototype); light(p5, p5.prototype); @@ -30,4 +34,6 @@ export default function(p5){ dataArray(p5, p5.prototype); shader(p5, p5.prototype); texture(p5, p5.prototype); + rendererGLImmediate(p5, p5.prototype); + rendererGLRetained(p5, p5.prototype); } diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index c031e48851..19013aa255 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -11,650 +11,655 @@ * however, Immediate Mode is useful for sketching quick * geometric ideas. */ -import p5 from '../core/main'; import * as constants from '../core/constants'; -/** - * Begin shape drawing. This is a helpful way of generating - * custom shapes quickly. However in WEBGL mode, application - * performance will likely drop as a result of too many calls to - * beginShape() / endShape(). As a high performance alternative, - * please use p5.js geometry primitives. - * @private - * @method beginShape - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, - * and TESS(WEBGL only) - * @chainable - */ -p5.RendererGL.prototype.beginShape = function(mode) { - this.immediateMode.shapeMode = - mode !== undefined ? mode : constants.TESS; - if (this._useUserVertexProperties === true){ - this._resetUserVertexProperties(); - } - this.immediateMode.geometry.reset(); - this.immediateMode.contourIndices = []; - return this; -}; - -p5.RendererGL.prototype.immediateBufferStrides = { - vertices: 1, - vertexNormals: 1, - vertexColors: 4, - vertexStrokeColors: 4, - uvs: 2 -}; - -p5.RendererGL.prototype.beginContour = function() { - if (this.immediateMode.shapeMode !== constants.TESS) { - throw new Error('WebGL mode can only use contours with beginShape(TESS).'); - } - this.immediateMode.contourIndices.push( - this.immediateMode.geometry.vertices.length - ); -}; - -/** - * adds a vertex to be drawn in a custom Shape. - * @private - * @method vertex - * @param {Number} x x-coordinate of vertex - * @param {Number} y y-coordinate of vertex - * @param {Number} z z-coordinate of vertex - * @chainable - * @TODO implement handling of p5.Vector args - */ -p5.RendererGL.prototype.vertex = function(x, y) { - // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn - // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra - // work to convert QUAD_STRIP here, since the only difference is in how edges - // are rendered.) - if (this.immediateMode.shapeMode === constants.QUADS) { - // A finished quad turned into triangles should leave 6 vertices in the - // buffer: - // 0--3 0 3--5 - // | | --> | \ \ | - // 1--2 1--2 4 - // When vertex index 3 is being added, add the necessary duplicates. - if (this.immediateMode.geometry.vertices.length % 6 === 3) { - for (const key in this.immediateBufferStrides) { - const stride = this.immediateBufferStrides[key]; - const buffer = this.immediateMode.geometry[key]; - buffer.push( - ...buffer.slice( - buffer.length - 3 * stride, - buffer.length - 2 * stride - ), - ...buffer.slice(buffer.length - stride, buffer.length) - ); +function rendererGLImmediate(p5, fn){ + /** + * Begin shape drawing. This is a helpful way of generating + * custom shapes quickly. However in WEBGL mode, application + * performance will likely drop as a result of too many calls to + * beginShape() / endShape(). As a high performance alternative, + * please use p5.js geometry primitives. + * @private + * @method beginShape + * @param {Number} mode webgl primitives mode. beginShape supports the + * following modes: + * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, + * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, + * and TESS(WEBGL only) + * @chainable + */ + p5.RendererGL.prototype.beginShape = function(mode) { + this.immediateMode.shapeMode = + mode !== undefined ? mode : constants.TESS; + if (this._useUserVertexProperties === true){ + this._resetUserVertexProperties(); + } + this.immediateMode.geometry.reset(); + this.immediateMode.contourIndices = []; + return this; + }; + + p5.RendererGL.prototype.immediateBufferStrides = { + vertices: 1, + vertexNormals: 1, + vertexColors: 4, + vertexStrokeColors: 4, + uvs: 2 + }; + + p5.RendererGL.prototype.beginContour = function() { + if (this.immediateMode.shapeMode !== constants.TESS) { + throw new Error('WebGL mode can only use contours with beginShape(TESS).'); + } + this.immediateMode.contourIndices.push( + this.immediateMode.geometry.vertices.length + ); + }; + + /** + * adds a vertex to be drawn in a custom Shape. + * @private + * @method vertex + * @param {Number} x x-coordinate of vertex + * @param {Number} y y-coordinate of vertex + * @param {Number} z z-coordinate of vertex + * @chainable + * @TODO implement handling of p5.Vector args + */ + p5.RendererGL.prototype.vertex = function(x, y) { + // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn + // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra + // work to convert QUAD_STRIP here, since the only difference is in how edges + // are rendered.) + if (this.immediateMode.shapeMode === constants.QUADS) { + // A finished quad turned into triangles should leave 6 vertices in the + // buffer: + // 0--3 0 3--5 + // | | --> | \ \ | + // 1--2 1--2 4 + // When vertex index 3 is being added, add the necessary duplicates. + if (this.immediateMode.geometry.vertices.length % 6 === 3) { + for (const key in this.immediateBufferStrides) { + const stride = this.immediateBufferStrides[key]; + const buffer = this.immediateMode.geometry[key]; + buffer.push( + ...buffer.slice( + buffer.length - 3 * stride, + buffer.length - 2 * stride + ), + ...buffer.slice(buffer.length - stride, buffer.length) + ); + } } } - } - - 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]; - } - const vert = new p5.Vector(x, y, z); - this.immediateMode.geometry.vertices.push(vert); - this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); - - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const geom = this.immediateMode.geometry; - const prop = geom.userVertexProperties[propName]; - const verts = geom.vertices; - if (prop.getSrcArray().length === 0 && verts.length > 1) { - const numMissingValues = prop.getDataSize() * (verts.length - 1); - const missingValues = Array(numMissingValues).fill(0); - prop.pushDirect(missingValues); - } - prop.pushCurrentData(); - } - - const vertexColor = this.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; - this.immediateMode.geometry.vertexColors.push( - vertexColor[0], - vertexColor[1], - vertexColor[2], - vertexColor[3] - ); - const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; - this.immediateMode.geometry.vertexStrokeColors.push( - lineVertexColor[0], - lineVertexColor[1], - lineVertexColor[2], - lineVertexColor[3] - ); - - if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { - if (this.states._tex !== null) { - if (this.states._tex.width > 0 && this.states._tex.height > 0) { - u /= this.states._tex.width; - v /= this.states._tex.height; + + 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]; + } + const vert = new p5.Vector(x, y, z); + this.immediateMode.geometry.vertices.push(vert); + this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); + + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const geom = this.immediateMode.geometry; + const prop = geom.userVertexProperties[propName]; + const verts = geom.vertices; + if (prop.getSrcArray().length === 0 && verts.length > 1) { + const numMissingValues = prop.getDataSize() * (verts.length - 1); + const missingValues = Array(numMissingValues).fill(0); + prop.pushDirect(missingValues); } - } else if ( - this.states.userFillShader !== undefined || - this.states.userStrokeShader !== undefined || - this.states.userPointShader !== undefined - ) { - // Do nothing if user-defined shaders are present - } else if ( - this.states._tex === null && - arguments.length >= 4 - ) { - // Only throw this warning if custom uv's have been provided - console.warn( - 'You must first call texture() before using' + - ' vertex() with image based u and v coordinates' - ); + prop.pushCurrentData(); } - } - this.immediateMode.geometry.uvs.push(u, v); + const vertexColor = this.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; + this.immediateMode.geometry.vertexColors.push( + vertexColor[0], + vertexColor[1], + vertexColor[2], + vertexColor[3] + ); + const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; + this.immediateMode.geometry.vertexStrokeColors.push( + lineVertexColor[0], + lineVertexColor[1], + lineVertexColor[2], + lineVertexColor[3] + ); - this.immediateMode._bezierVertex[0] = x; - this.immediateMode._bezierVertex[1] = y; - this.immediateMode._bezierVertex[2] = z; + if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { + if (this.states._tex !== null) { + if (this.states._tex.width > 0 && this.states._tex.height > 0) { + u /= this.states._tex.width; + v /= this.states._tex.height; + } + } else if ( + this.states.userFillShader !== undefined || + this.states.userStrokeShader !== undefined || + this.states.userPointShader !== undefined + ) { + // Do nothing if user-defined shaders are present + } else if ( + this.states._tex === null && + arguments.length >= 4 + ) { + // Only throw this warning if custom uv's have been provided + console.warn( + 'You must first call texture() before using' + + ' vertex() with image based u and v coordinates' + ); + } + } - this.immediateMode._quadraticVertex[0] = x; - this.immediateMode._quadraticVertex[1] = y; - this.immediateMode._quadraticVertex[2] = z; + this.immediateMode.geometry.uvs.push(u, v); - return this; -}; + this.immediateMode._bezierVertex[0] = x; + this.immediateMode._bezierVertex[1] = y; + this.immediateMode._bezierVertex[2] = z; -p5.RendererGL.prototype.vertexProperty = function(propertyName, data){ - if(!this._useUserVertexProperties){ - this._useUserVertexProperties = true; - this.immediateMode.geometry.userVertexProperties = {}; - } - const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; - let prop; - if (propertyExists){ - prop = this.immediateMode.geometry.userVertexProperties[propertyName]; - } - else { - prop = this.immediateMode.geometry._userVertexPropertyHelper(propertyName, data); - this.tessyVertexSize += prop.getDataSize(); - this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); - this.immediateMode.buffers.user.push( - new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) - ); - } - prop.setCurrentData(data); -}; - -p5.RendererGL.prototype._resetUserVertexProperties = function(){ - const properties = this.immediateMode.geometry.userVertexProperties; - for (const propName in properties){ - const prop = properties[propName]; - delete this.immediateBufferStrides[propName]; - prop.delete(); - } - this._useUserVertexProperties = false; - this.tessyVertexSize = 12; - this.immediateMode.geometry.userVertexProperties = {}; - this.immediateMode.buffers.user = []; -}; + this.immediateMode._quadraticVertex[0] = x; + this.immediateMode._quadraticVertex[1] = y; + this.immediateMode._quadraticVertex[2] = z; -/** - * Sets the normal to use for subsequent vertices. - * @private - * @method normal - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @chainable - * - * @method normal - * @param {Vector} v - * @chainable - */ -p5.RendererGL.prototype.normal = function(xorv, y, z) { - if (xorv instanceof p5.Vector) { - this.states._currentNormal = xorv; - } else { - this.states._currentNormal = new p5.Vector(xorv, y, z); - } + return this; + }; - return this; -}; + p5.RendererGL.prototype.vertexProperty = function(propertyName, data){ + if(!this._useUserVertexProperties){ + this._useUserVertexProperties = true; + this.immediateMode.geometry.userVertexProperties = {}; + } + const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; + let prop; + if (propertyExists){ + prop = this.immediateMode.geometry.userVertexProperties[propertyName]; + } + else { + prop = this.immediateMode.geometry._userVertexPropertyHelper(propertyName, data); + this.tessyVertexSize += prop.getDataSize(); + this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); + this.immediateMode.buffers.user.push( + new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) + ); + } + prop.setCurrentData(data); + }; + + p5.RendererGL.prototype._resetUserVertexProperties = function(){ + const properties = this.immediateMode.geometry.userVertexProperties; + for (const propName in properties){ + const prop = properties[propName]; + delete this.immediateBufferStrides[propName]; + prop.delete(); + } + this._useUserVertexProperties = false; + this.tessyVertexSize = 12; + this.immediateMode.geometry.userVertexProperties = {}; + this.immediateMode.buffers.user = []; + }; + + /** + * Sets the normal to use for subsequent vertices. + * @private + * @method normal + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @chainable + * + * @method normal + * @param {Vector} v + * @chainable + */ + p5.RendererGL.prototype.normal = function(xorv, y, z) { + if (xorv instanceof p5.Vector) { + this.states._currentNormal = xorv; + } else { + this.states._currentNormal = new p5.Vector(xorv, y, z); + } -/** - * End shape drawing and render vertices to screen. - * @chainable - */ -p5.RendererGL.prototype.endShape = function( - mode, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind, - count = 1 -) { - if (this.immediateMode.shapeMode === constants.POINTS) { - this._drawPoints( - this.immediateMode.geometry.vertices, - this.immediateMode.buffers.point - ); return this; - } - // When we are drawing a shape then the shape mode is TESS, - // but in case of triangle we can skip the breaking into small triangle - // this can optimize performance by skipping the step of breaking it into triangles - if (this.immediateMode.geometry.vertices.length === 3 && - this.immediateMode.shapeMode === constants.TESS + }; + + /** + * End shape drawing and render vertices to screen. + * @chainable + */ + p5.RendererGL.prototype.endShape = function( + mode, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind, + count = 1 ) { - this.immediateMode.shapeMode === constants.TRIANGLES; - } - - this.isProcessingVertices = true; - this._processVertices(...arguments); - this.isProcessingVertices = false; - - // LINE_STRIP and LINES are not used for rendering, instead - // they only indicate a way to modify vertices during the _processVertices() step - let is_line = false; - if ( - this.immediateMode.shapeMode === constants.LINE_STRIP || - this.immediateMode.shapeMode === constants.LINES - ) { - this.immediateMode.shapeMode = constants.TRIANGLE_FAN; - is_line = true; - } - - // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we - // need to convert them to a supported format. In `vertex()`, we reformat - // the input data into the formats specified below. - if (this.immediateMode.shapeMode === constants.QUADS) { - this.immediateMode.shapeMode = constants.TRIANGLES; - } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { - this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; - } - - if (this.states.doFill && !is_line) { - if ( - !this.geometryBuilder && - this.immediateMode.geometry.vertices.length >= 3 + if (this.immediateMode.shapeMode === constants.POINTS) { + this._drawPoints( + this.immediateMode.geometry.vertices, + this.immediateMode.buffers.point + ); + return this; + } + // When we are drawing a shape then the shape mode is TESS, + // but in case of triangle we can skip the breaking into small triangle + // this can optimize performance by skipping the step of breaking it into triangles + if (this.immediateMode.geometry.vertices.length === 3 && + this.immediateMode.shapeMode === constants.TESS ) { - this._drawImmediateFill(count); + this.immediateMode.shapeMode === constants.TRIANGLES; } - } - if (this.states.doStroke) { + + this.isProcessingVertices = true; + this._processVertices(...arguments); + this.isProcessingVertices = false; + + // LINE_STRIP and LINES are not used for rendering, instead + // they only indicate a way to modify vertices during the _processVertices() step + let is_line = false; if ( - !this.geometryBuilder && - this.immediateMode.geometry.lineVertices.length >= 1 + this.immediateMode.shapeMode === constants.LINE_STRIP || + this.immediateMode.shapeMode === constants.LINES ) { - this._drawImmediateStroke(); + this.immediateMode.shapeMode = constants.TRIANGLE_FAN; + is_line = true; } - } - if (this.geometryBuilder) { - this.geometryBuilder.addImmediate(); - } + // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we + // need to convert them to a supported format. In `vertex()`, we reformat + // the input data into the formats specified below. + if (this.immediateMode.shapeMode === constants.QUADS) { + this.immediateMode.shapeMode = constants.TRIANGLES; + } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { + this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; + } - this.isBezier = false; - this.isQuadratic = false; - this.isCurve = false; - this.immediateMode._bezierVertex.length = 0; - this.immediateMode._quadraticVertex.length = 0; - this.immediateMode._curveVertex.length = 0; + if (this.states.doFill && !is_line) { + if ( + !this.geometryBuilder && + this.immediateMode.geometry.vertices.length >= 3 + ) { + this._drawImmediateFill(count); + } + } + if (this.states.doStroke) { + if ( + !this.geometryBuilder && + this.immediateMode.geometry.lineVertices.length >= 1 + ) { + this._drawImmediateStroke(); + } + } - return this; -}; + if (this.geometryBuilder) { + this.geometryBuilder.addImmediate(); + } -/** - * Called from endShape(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) - */ -p5.RendererGL.prototype._processVertices = function(mode) { - if (this.immediateMode.geometry.vertices.length === 0) return; - - const calculateStroke = this.states.doStroke; - const shouldClose = mode === constants.CLOSE; - if (calculateStroke) { - this.immediateMode.geometry.edges = this._calculateEdges( - this.immediateMode.shapeMode, - this.immediateMode.geometry.vertices, - shouldClose - ); - if (!this.geometryBuilder) { - this.immediateMode.geometry._edgesToVertices(); - } - } - // For hollow shapes, user must set mode to TESS - const convexShape = this.immediateMode.shapeMode === constants.TESS; - // If the shape has a contour, we have to re-triangulate to cut out the - // contour region - const hasContour = this.immediateMode.contourIndices.length > 0; - // We tesselate when drawing curves or convex shapes - const shouldTess = - this.states.doFill && - ( - this.isBezier || - this.isQuadratic || - this.isCurve || - convexShape || - hasContour - ) && - this.immediateMode.shapeMode !== constants.LINES; - - if (shouldTess) { - this._tesselateShape(); - } -}; + this.isBezier = false; + this.isQuadratic = false; + this.isCurve = false; + this.immediateMode._bezierVertex.length = 0; + this.immediateMode._quadraticVertex.length = 0; + this.immediateMode._curveVertex.length = 0; -/** - * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @returns {Number[]} indices for custom shape vertices indicating edges. - */ -p5.RendererGL.prototype._calculateEdges = function( - shapeMode, - verts, - shouldClose -) { - const res = []; - let i = 0; - const contourIndices = this.immediateMode.contourIndices.slice(); - let contourStart = 0; - switch (shapeMode) { - case constants.TRIANGLE_STRIP: - for (i = 0; i < verts.length - 2; i++) { - res.push([i, i + 1]); - res.push([i, i + 2]); - } - res.push([i, i + 1]); - break; - case constants.TRIANGLE_FAN: - for (i = 1; i < verts.length - 1; i++) { - res.push([0, i]); - res.push([i, i + 1]); - } - res.push([0, verts.length - 1]); - break; - case constants.TRIANGLES: - for (i = 0; i < verts.length - 2; i = i + 3) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 2, i]); - } - break; - case constants.LINES: - for (i = 0; i < verts.length - 1; i = i + 2) { - res.push([i, i + 1]); - } - break; - case constants.QUADS: - // Quads have been broken up into two triangles by `vertex()`: - // 0 3--5 - // | \ \ | - // 1--2 4 - for (i = 0; i < verts.length - 5; i += 6) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 3, i + 5]); - res.push([i + 4, i + 5]); + return this; + }; + + /** + * Called from endShape(). This function calculates the stroke vertices for custom shapes and + * tesselates shapes when applicable. + * @private + * @param {Number} mode webgl primitives mode. beginShape supports the + * following modes: + * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, + * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) + */ + p5.RendererGL.prototype._processVertices = function(mode) { + if (this.immediateMode.geometry.vertices.length === 0) return; + + const calculateStroke = this.states.doStroke; + const shouldClose = mode === constants.CLOSE; + if (calculateStroke) { + this.immediateMode.geometry.edges = this._calculateEdges( + this.immediateMode.shapeMode, + this.immediateMode.geometry.vertices, + shouldClose + ); + if (!this.geometryBuilder) { + this.immediateMode.geometry._edgesToVertices(); } - break; - case constants.QUAD_STRIP: - // 0---2---4 - // | | | - // 1---3---5 - for (i = 0; i < verts.length - 2; i += 2) { + } + // For hollow shapes, user must set mode to TESS + const convexShape = this.immediateMode.shapeMode === constants.TESS; + // If the shape has a contour, we have to re-triangulate to cut out the + // contour region + const hasContour = this.immediateMode.contourIndices.length > 0; + // We tesselate when drawing curves or convex shapes + const shouldTess = + this.states.doFill && + ( + this.isBezier || + this.isQuadratic || + this.isCurve || + convexShape || + hasContour + ) && + this.immediateMode.shapeMode !== constants.LINES; + + if (shouldTess) { + this._tesselateShape(); + } + }; + + /** + * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and + * tesselates shapes when applicable. + * @private + * @returns {Number[]} indices for custom shape vertices indicating edges. + */ + p5.RendererGL.prototype._calculateEdges = function( + shapeMode, + verts, + shouldClose + ) { + const res = []; + let i = 0; + const contourIndices = this.immediateMode.contourIndices.slice(); + let contourStart = 0; + switch (shapeMode) { + case constants.TRIANGLE_STRIP: + for (i = 0; i < verts.length - 2; i++) { + res.push([i, i + 1]); + res.push([i, i + 2]); + } res.push([i, i + 1]); - res.push([i, i + 2]); - res.push([i + 1, i + 3]); - } - res.push([i, i + 1]); - break; - default: - // TODO: handle contours in other modes too - for (i = 0; i < verts.length; i++) { - // Handle breaks between contours - if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { + break; + case constants.TRIANGLE_FAN: + for (i = 1; i < verts.length - 1; i++) { + res.push([0, i]); res.push([i, i + 1]); - } else { - if (shouldClose || contourStart) { - res.push([i, contourStart]); - } - if (contourIndices.length > 0) { - contourStart = contourIndices.shift(); + } + res.push([0, verts.length - 1]); + break; + case constants.TRIANGLES: + for (i = 0; i < verts.length - 2; i = i + 3) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 2, i]); + } + break; + case constants.LINES: + for (i = 0; i < verts.length - 1; i = i + 2) { + res.push([i, i + 1]); + } + break; + case constants.QUADS: + // Quads have been broken up into two triangles by `vertex()`: + // 0 3--5 + // | \ \ | + // 1--2 4 + for (i = 0; i < verts.length - 5; i += 6) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 3, i + 5]); + res.push([i + 4, i + 5]); + } + break; + case constants.QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + for (i = 0; i < verts.length - 2; i += 2) { + res.push([i, i + 1]); + res.push([i, i + 2]); + res.push([i + 1, i + 3]); + } + res.push([i, i + 1]); + break; + default: + // TODO: handle contours in other modes too + for (i = 0; i < verts.length; i++) { + // Handle breaks between contours + if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { + res.push([i, i + 1]); + } else { + if (shouldClose || contourStart) { + res.push([i, contourStart]); + } + if (contourIndices.length > 0) { + contourStart = contourIndices.shift(); + } } } + break; + } + if (shapeMode !== constants.TESS && shouldClose) { + res.push([verts.length - 1, 0]); + } + return res; + }; + + /** + * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. + * @private + */ + p5.RendererGL.prototype._tesselateShape = function() { + // TODO: handle non-TESS shape modes that have contours + this.immediateMode.shapeMode = constants.TRIANGLES; + const contours = [[]]; + for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { + if ( + this.immediateMode.contourIndices.length > 0 && + this.immediateMode.contourIndices[0] === i + ) { + this.immediateMode.contourIndices.shift(); + contours.push([]); } - break; - } - if (shapeMode !== constants.TESS && shouldClose) { - res.push([verts.length - 1, 0]); - } - return res; -}; - -/** - * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. - * @private - */ -p5.RendererGL.prototype._tesselateShape = function() { - // TODO: handle non-TESS shape modes that have contours - this.immediateMode.shapeMode = constants.TRIANGLES; - const contours = [[]]; - for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { - if ( - this.immediateMode.contourIndices.length > 0 && - this.immediateMode.contourIndices[0] === i - ) { - this.immediateMode.contourIndices.shift(); - contours.push([]); - } - contours[contours.length-1].push( - this.immediateMode.geometry.vertices[i].x, - this.immediateMode.geometry.vertices[i].y, - this.immediateMode.geometry.vertices[i].z, - this.immediateMode.geometry.uvs[i * 2], - this.immediateMode.geometry.uvs[i * 2 + 1], - this.immediateMode.geometry.vertexColors[i * 4], - this.immediateMode.geometry.vertexColors[i * 4 + 1], - this.immediateMode.geometry.vertexColors[i * 4 + 2], - this.immediateMode.geometry.vertexColors[i * 4 + 3], - this.immediateMode.geometry.vertexNormals[i].x, - this.immediateMode.geometry.vertexNormals[i].y, - this.immediateMode.geometry.vertexNormals[i].z - ); + contours[contours.length-1].push( + this.immediateMode.geometry.vertices[i].x, + this.immediateMode.geometry.vertices[i].y, + this.immediateMode.geometry.vertices[i].z, + this.immediateMode.geometry.uvs[i * 2], + this.immediateMode.geometry.uvs[i * 2 + 1], + this.immediateMode.geometry.vertexColors[i * 4], + this.immediateMode.geometry.vertexColors[i * 4 + 1], + this.immediateMode.geometry.vertexColors[i * 4 + 2], + this.immediateMode.geometry.vertexColors[i * 4 + 3], + this.immediateMode.geometry.vertexNormals[i].x, + this.immediateMode.geometry.vertexNormals[i].y, + this.immediateMode.geometry.vertexNormals[i].z + ); + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const prop = this.immediateMode.geometry.userVertexProperties[propName]; + const start = i * prop.getDataSize(); + const end = start + prop.getDataSize(); + const vals = prop.getSrcArray().slice(start, end); + contours[contours.length-1].push(...vals); + } + } + const polyTriangles = this._triangulate(contours); + const originalVertices = this.immediateMode.geometry.vertices; + this.immediateMode.geometry.vertices = []; + this.immediateMode.geometry.vertexNormals = []; + this.immediateMode.geometry.uvs = []; for (const propName in this.immediateMode.geometry.userVertexProperties){ const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const start = i * prop.getDataSize(); - const end = start + prop.getDataSize(); - const vals = prop.getSrcArray().slice(start, end); - contours[contours.length-1].push(...vals); - } - } - const polyTriangles = this._triangulate(contours); - const originalVertices = this.immediateMode.geometry.vertices; - this.immediateMode.geometry.vertices = []; - this.immediateMode.geometry.vertexNormals = []; - this.immediateMode.geometry.uvs = []; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - prop.resetSrcArray(); - } - const colors = []; - for ( - let j = 0, polyTriLength = polyTriangles.length; - j < polyTriLength; - j = j + this.tessyVertexSize - ) { - colors.push(...polyTriangles.slice(j + 5, j + 9)); - this.normal(...polyTriangles.slice(j + 9, j + 12)); - { - let offset = 12; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - const start = j + offset; - const end = start + size; - prop.setCurrentData(polyTriangles.slice(start, end)); - offset += size; + prop.resetSrcArray(); + } + const colors = []; + for ( + let j = 0, polyTriLength = polyTriangles.length; + j < polyTriLength; + j = j + this.tessyVertexSize + ) { + colors.push(...polyTriangles.slice(j + 5, j + 9)); + this.normal(...polyTriangles.slice(j + 9, j + 12)); + { + let offset = 12; + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const prop = this.immediateMode.geometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + const start = j + offset; + const end = start + size; + prop.setCurrentData(polyTriangles.slice(start, end)); + offset += size; + } } + this.vertex(...polyTriangles.slice(j, j + 5)); } - this.vertex(...polyTriangles.slice(j, j + 5)); - } - if (this.geometryBuilder) { - // Tesselating the face causes the indices of edge vertices to stop being - // correct. When rendering, this is not a problem, since _edgesToVertices - // will have been called before this, and edge vertex indices are no longer - // needed. However, the geometry builder still needs this information, so - // when one is active, we need to update the indices. - // - // We record index mappings in a Map so that once we have found a - // corresponding vertex, we don't need to loop to find it again. - const newIndex = new Map(); - this.immediateMode.geometry.edges = - this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => { - if (!newIndex.has(origIdx)) { - const orig = originalVertices[origIdx]; - let newVertIndex = this.immediateMode.geometry.vertices.findIndex( - v => - orig.x === v.x && - orig.y === v.y && - orig.z === v.z - ); - if (newVertIndex === -1) { - // The tesselation process didn't output a vertex with the exact - // coordinate as before, potentially due to numerical issues. This - // doesn't happen often, but in this case, pick the closest point - let closestDist = Infinity; - let closestIndex = 0; - for ( - let i = 0; - i < this.immediateMode.geometry.vertices.length; - i++ - ) { - const vert = this.immediateMode.geometry.vertices[i]; - const dX = orig.x - vert.x; - const dY = orig.y - vert.y; - const dZ = orig.z - vert.z; - const dist = dX*dX + dY*dY + dZ*dZ; - if (dist < closestDist) { - closestDist = dist; - closestIndex = i; + if (this.geometryBuilder) { + // Tesselating the face causes the indices of edge vertices to stop being + // correct. When rendering, this is not a problem, since _edgesToVertices + // will have been called before this, and edge vertex indices are no longer + // needed. However, the geometry builder still needs this information, so + // when one is active, we need to update the indices. + // + // We record index mappings in a Map so that once we have found a + // corresponding vertex, we don't need to loop to find it again. + const newIndex = new Map(); + this.immediateMode.geometry.edges = + this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => { + if (!newIndex.has(origIdx)) { + const orig = originalVertices[origIdx]; + let newVertIndex = this.immediateMode.geometry.vertices.findIndex( + v => + orig.x === v.x && + orig.y === v.y && + orig.z === v.z + ); + if (newVertIndex === -1) { + // The tesselation process didn't output a vertex with the exact + // coordinate as before, potentially due to numerical issues. This + // doesn't happen often, but in this case, pick the closest point + let closestDist = Infinity; + let closestIndex = 0; + for ( + let i = 0; + i < this.immediateMode.geometry.vertices.length; + i++ + ) { + const vert = this.immediateMode.geometry.vertices[i]; + const dX = orig.x - vert.x; + const dY = orig.y - vert.y; + const dZ = orig.z - vert.z; + const dist = dX*dX + dY*dY + dZ*dZ; + if (dist < closestDist) { + closestDist = dist; + closestIndex = i; + } } + newVertIndex = closestIndex; } - newVertIndex = closestIndex; + newIndex.set(origIdx, newVertIndex); } - newIndex.set(origIdx, newVertIndex); - } - return newIndex.get(origIdx); - })); - } - this.immediateMode.geometry.vertexColors = colors; -}; - -/** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. - * @private - */ -p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { - const gl = this.GL; - this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); + return newIndex.get(origIdx); + })); + } + this.immediateMode.geometry.vertexColors = colors; + }; - let shader; - shader = this._getImmediateFillShader(); + /** + * Called from endShape(). Responsible for calculating normals, setting shader uniforms, + * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. + * @private + */ + p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { + const gl = this.GL; + this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); - this._setFillUniforms(shader); + let shader; + shader = this._getImmediateFillShader(); - for (const buff of this.immediateMode.buffers.fill) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - shader.disableRemainingAttributes(); + this._setFillUniforms(shader); - this._applyColorBlend( - this.states.curFillColor, - this.immediateMode.geometry.hasFillTransparency() - ); + for (const buff of this.immediateMode.buffers.fill) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + shader.disableRemainingAttributes(); - if (count === 1) { - gl.drawArrays( - this.immediateMode.shapeMode, - 0, - this.immediateMode.geometry.vertices.length + this._applyColorBlend( + this.states.curFillColor, + this.immediateMode.geometry.hasFillTransparency() ); - } - else { - try { - gl.drawArraysInstanced( + + if (count === 1) { + gl.drawArrays( this.immediateMode.shapeMode, 0, - this.immediateMode.geometry.vertices.length, - count + this.immediateMode.geometry.vertices.length ); } - catch (e) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + else { + try { + gl.drawArraysInstanced( + this.immediateMode.shapeMode, + 0, + this.immediateMode.geometry.vertices.length, + count + ); + } + catch (e) { + console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + } + } + shader.unbindShader(); + }; + + /** + * Called from endShape(). Responsible for calculating normals, setting shader uniforms, + * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. + * @private + */ + p5.RendererGL.prototype._drawImmediateStroke = function() { + const gl = this.GL; + + this._useLineColor = + (this.immediateMode.geometry.vertexStrokeColors.length > 0); + + const shader = this._getImmediateStrokeShader(); + this._setStrokeUniforms(shader); + for (const buff of this.immediateMode.buffers.stroke) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); } - } - shader.unbindShader(); -}; + shader.disableRemainingAttributes(); + this._applyColorBlend( + this.states.curStrokeColor, + this.immediateMode.geometry.hasFillTransparency() + ); -/** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. - * @private - */ -p5.RendererGL.prototype._drawImmediateStroke = function() { - const gl = this.GL; - - this._useLineColor = - (this.immediateMode.geometry.vertexStrokeColors.length > 0); - - const shader = this._getImmediateStrokeShader(); - this._setStrokeUniforms(shader); - for (const buff of this.immediateMode.buffers.stroke) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - shader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - this.immediateMode.geometry.hasFillTransparency() - ); - - gl.drawArrays( - gl.TRIANGLES, - 0, - this.immediateMode.geometry.lineVertices.length / 3 - ); - shader.unbindShader(); -}; - -export default p5.RendererGL; + gl.drawArrays( + gl.TRIANGLES, + 0, + this.immediateMode.geometry.lineVertices.length / 3 + ); + shader.unbindShader(); + }; +} + +export default rendererGLImmediate; + +if(typeof p5 !== 'undefined'){ + rendererGLImmediate(p5, p5.prototype); +} \ No newline at end of file diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 57b661b6ce..10b5726336 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -1,295 +1,300 @@ //Retained Mode. The default mode for rendering 3D primitives //in WEBGL. -import p5 from '../core/main'; import * as constants from '../core/constants'; -/** - * @param {p5.Geometry} geometry The model whose resources will be freed - */ -p5.RendererGL.prototype.freeGeometry = function(geometry) { - if (!geometry.gid) { - console.warn('The model you passed to freeGeometry does not have an id!'); - return; - } - this._freeBuffers(geometry.gid); -}; - -/** - * _initBufferDefaults - * @private - * @description initializes buffer defaults. runs each time a new geometry is - * registered - * @param {String} gId key of the geometry object - * @returns {Object} a new buffer object - */ -p5.RendererGL.prototype._initBufferDefaults = function(gId) { - this._freeBuffers(gId); - - //@TODO remove this limit on hashes in retainedMode.geometry - if (Object.keys(this.retainedMode.geometry).length > 1000) { - const key = Object.keys(this.retainedMode.geometry)[0]; - this._freeBuffers(key); - } - - //create a new entry in our retainedMode.geometry - return (this.retainedMode.geometry[gId] = {}); -}; - -p5.RendererGL.prototype._freeBuffers = function(gId) { - const buffers = this.retainedMode.geometry[gId]; - if (!buffers) { - return; - } - - delete this.retainedMode.geometry[gId]; - - const gl = this.GL; - if (buffers.indexBuffer) { - gl.deleteBuffer(buffers.indexBuffer); - } - - function freeBuffers(defs) { - for (const def of defs) { - if (buffers[def.dst]) { - gl.deleteBuffer(buffers[def.dst]); - buffers[def.dst] = null; - } +function rendererGLRetained(p5, fn){ + /** + * @param {p5.Geometry} geometry The model whose resources will be freed + */ + p5.RendererGL.prototype.freeGeometry = function(geometry) { + if (!geometry.gid) { + console.warn('The model you passed to freeGeometry does not have an id!'); + return; } - } - - // free all the buffers - freeBuffers(this.retainedMode.buffers.stroke); - freeBuffers(this.retainedMode.buffers.fill); - freeBuffers(this.retainedMode.buffers.user); - this.retainedMode.buffers.user = []; -}; - -/** - * creates a buffers object that holds the WebGL render buffers - * for a geometry. - * @private - * @param {String} gId key of the geometry object - * @param {p5.Geometry} model contains geometry data - */ -p5.RendererGL.prototype.createBuffers = function(gId, model) { - const gl = this.GL; - //initialize the gl buffers for our geom groups - const buffers = this._initBufferDefaults(gId); - buffers.model = model; - - let indexBuffer = buffers.indexBuffer; - - if (model.faces.length) { - // allocate space for faces - if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); - const vals = p5.RendererGL.prototype._flatten(model.faces); - - // If any face references a vertex with an index greater than the maximum - // un-singed 16 bit integer, then we need to use a Uint32Array instead of a - // Uint16Array - const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); - let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; - this._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); - - // If we're using a Uint32Array for our indexBuffer we will need to pass a - // different enum value to WebGL draw triangles. This happens in - // the _drawElements function. - buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 - ? gl.UNSIGNED_INT - : gl.UNSIGNED_SHORT; - - // the vertex count is based on the number of faces - buffers.vertexCount = model.faces.length * 3; - } else { - // the index buffer is unused, remove it - if (indexBuffer) { - gl.deleteBuffer(indexBuffer); - buffers.indexBuffer = null; + this._freeBuffers(geometry.gid); + }; + + /** + * _initBufferDefaults + * @private + * @description initializes buffer defaults. runs each time a new geometry is + * registered + * @param {String} gId key of the geometry object + * @returns {Object} a new buffer object + */ + p5.RendererGL.prototype._initBufferDefaults = function(gId) { + this._freeBuffers(gId); + + //@TODO remove this limit on hashes in retainedMode.geometry + if (Object.keys(this.retainedMode.geometry).length > 1000) { + const key = Object.keys(this.retainedMode.geometry)[0]; + this._freeBuffers(key); } - // the vertex count comes directly from the model - buffers.vertexCount = model.vertices ? model.vertices.length : 0; - } - - buffers.lineVertexCount = model.lineVertices - ? model.lineVertices.length / 3 - : 0; - - for (const propName in model.userVertexProperties){ - const prop = model.userVertexProperties[propName]; - this.retainedMode.buffers.user.push( - new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) - ); - } - return buffers; -}; - -/** - * Draws buffers given a geometry key ID - * @private - * @param {String} gId ID in our geom hash - * @chainable - */ -p5.RendererGL.prototype.drawBuffers = function(gId) { - const gl = this.GL; - const geometry = this.retainedMode.geometry[gId]; - - if ( - !this.geometryBuilder && - this.states.doFill && - geometry.vertexCount > 0 - ) { - this._useVertexColor = (geometry.model.vertexColors.length > 0); - const fillShader = this._getRetainedFillShader(); - this._setFillUniforms(fillShader); - for (const buff of this.retainedMode.buffers.fill) { - buff._prepareBuffer(geometry, fillShader); + + //create a new entry in our retainedMode.geometry + return (this.retainedMode.geometry[gId] = {}); + }; + + p5.RendererGL.prototype._freeBuffers = function(gId) { + const buffers = this.retainedMode.geometry[gId]; + if (!buffers) { + return; } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + + delete this.retainedMode.geometry[gId]; + + const gl = this.GL; + if (buffers.indexBuffer) { + gl.deleteBuffer(buffers.indexBuffer); + } + + function freeBuffers(defs) { + for (const def of defs) { + if (buffers[def.dst]) { + gl.deleteBuffer(buffers[def.dst]); + buffers[def.dst] = null; + } } - buff._prepareBuffer(geometry, fillShader); } - fillShader.disableRemainingAttributes(); - if (geometry.indexBuffer) { - //vertex index buffer - this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + + // free all the buffers + freeBuffers(this.retainedMode.buffers.stroke); + freeBuffers(this.retainedMode.buffers.fill); + freeBuffers(this.retainedMode.buffers.user); + this.retainedMode.buffers.user = []; + }; + + /** + * creates a buffers object that holds the WebGL render buffers + * for a geometry. + * @private + * @param {String} gId key of the geometry object + * @param {p5.Geometry} model contains geometry data + */ + p5.RendererGL.prototype.createBuffers = function(gId, model) { + const gl = this.GL; + //initialize the gl buffers for our geom groups + const buffers = this._initBufferDefaults(gId); + buffers.model = model; + + let indexBuffer = buffers.indexBuffer; + + if (model.faces.length) { + // allocate space for faces + if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); + const vals = p5.RendererGL.prototype._flatten(model.faces); + + // If any face references a vertex with an index greater than the maximum + // un-singed 16 bit integer, then we need to use a Uint32Array instead of a + // Uint16Array + const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); + let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; + this._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); + + // If we're using a Uint32Array for our indexBuffer we will need to pass a + // different enum value to WebGL draw triangles. This happens in + // the _drawElements function. + buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 + ? gl.UNSIGNED_INT + : gl.UNSIGNED_SHORT; + + // the vertex count is based on the number of faces + buffers.vertexCount = model.faces.length * 3; + } else { + // the index buffer is unused, remove it + if (indexBuffer) { + gl.deleteBuffer(indexBuffer); + buffers.indexBuffer = null; + } + // the vertex count comes directly from the model + buffers.vertexCount = model.vertices ? model.vertices.length : 0; } - this._applyColorBlend( - this.states.curFillColor, - geometry.model.hasFillTransparency() - ); - this._drawElements(gl.TRIANGLES, gId); - fillShader.unbindShader(); - } - - if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { - this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); - const strokeShader = this._getRetainedStrokeShader(); - this._setStrokeUniforms(strokeShader); - for (const buff of this.retainedMode.buffers.stroke) { - buff._prepareBuffer(geometry, strokeShader); + + buffers.lineVertexCount = model.lineVertices + ? model.lineVertices.length / 3 + : 0; + + for (const propName in model.userVertexProperties){ + const prop = model.userVertexProperties[propName]; + this.retainedMode.buffers.user.push( + new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) + ); } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + return buffers; + }; + + /** + * Draws buffers given a geometry key ID + * @private + * @param {String} gId ID in our geom hash + * @chainable + */ + p5.RendererGL.prototype.drawBuffers = function(gId) { + const gl = this.GL; + const geometry = this.retainedMode.geometry[gId]; + + if ( + !this.geometryBuilder && + this.states.doFill && + geometry.vertexCount > 0 + ) { + this._useVertexColor = (geometry.model.vertexColors.length > 0); + const fillShader = this._getRetainedFillShader(); + this._setFillUniforms(fillShader); + for (const buff of this.retainedMode.buffers.fill) { + buff._prepareBuffer(geometry, fillShader); } - buff._prepareBuffer(geometry, strokeShader); + for (const buff of this.retainedMode.buffers.user){ + const prop = geometry.model.userVertexProperties[buff.attr]; + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } + buff._prepareBuffer(geometry, fillShader); + } + fillShader.disableRemainingAttributes(); + if (geometry.indexBuffer) { + //vertex index buffer + this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + } + this._applyColorBlend( + this.states.curFillColor, + geometry.model.hasFillTransparency() + ); + this._drawElements(gl.TRIANGLES, gId); + fillShader.unbindShader(); + } + + if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { + this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); + const strokeShader = this._getRetainedStrokeShader(); + this._setStrokeUniforms(strokeShader); + for (const buff of this.retainedMode.buffers.stroke) { + buff._prepareBuffer(geometry, strokeShader); + } + for (const buff of this.retainedMode.buffers.user){ + const prop = geometry.model.userVertexProperties[buff.attr]; + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } + buff._prepareBuffer(geometry, strokeShader); + } + strokeShader.disableRemainingAttributes(); + this._applyColorBlend( + this.states.curStrokeColor, + geometry.model.hasStrokeTransparency() + ); + this._drawArrays(gl.TRIANGLES, gId); + strokeShader.unbindShader(); + } + + if (this.geometryBuilder) { + this.geometryBuilder.addRetained(geometry); } - strokeShader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - geometry.model.hasStrokeTransparency() + + return this; + }; + + /** + * Calls drawBuffers() with a scaled model/view matrix. + * + * This is used by various 3d primitive methods (in primitives.js, eg. plane, + * box, torus, etc...) to allow caching of un-scaled geometries. Those + * geometries are generally created with unit-length dimensions, cached as + * such, and then scaled appropriately in this method prior to rendering. + * + * @private + * @method drawBuffersScaled + * @param {String} gId ID in our geom hash + * @param {Number} scaleX the amount to scale in the X direction + * @param {Number} scaleY the amount to scale in the Y direction + * @param {Number} scaleZ the amount to scale in the Z direction + */ + p5.RendererGL.prototype.drawBuffersScaled = function( + gId, + scaleX, + scaleY, + scaleZ + ) { + let originalModelMatrix = this.states.uModelMatrix.copy(); + try { + this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); + + this.drawBuffers(gId); + } finally { + + this.states.uModelMatrix = originalModelMatrix; + } + }; + p5.RendererGL.prototype._drawArrays = function(drawMode, gId) { + this.GL.drawArrays( + drawMode, + 0, + this.retainedMode.geometry[gId].lineVertexCount ); - this._drawArrays(gl.TRIANGLES, gId); - strokeShader.unbindShader(); - } - - if (this.geometryBuilder) { - this.geometryBuilder.addRetained(geometry); - } - - return this; -}; - -/** - * Calls drawBuffers() with a scaled model/view matrix. - * - * This is used by various 3d primitive methods (in primitives.js, eg. plane, - * box, torus, etc...) to allow caching of un-scaled geometries. Those - * geometries are generally created with unit-length dimensions, cached as - * such, and then scaled appropriately in this method prior to rendering. - * - * @private - * @method drawBuffersScaled - * @param {String} gId ID in our geom hash - * @param {Number} scaleX the amount to scale in the X direction - * @param {Number} scaleY the amount to scale in the Y direction - * @param {Number} scaleZ the amount to scale in the Z direction - */ -p5.RendererGL.prototype.drawBuffersScaled = function( - gId, - scaleX, - scaleY, - scaleZ -) { - let originalModelMatrix = this.states.uModelMatrix.copy(); - try { - this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); - - this.drawBuffers(gId); - } finally { - - this.states.uModelMatrix = originalModelMatrix; - } -}; -p5.RendererGL.prototype._drawArrays = function(drawMode, gId) { - this.GL.drawArrays( - drawMode, - 0, - this.retainedMode.geometry[gId].lineVertexCount - ); - return this; -}; - -p5.RendererGL.prototype._drawElements = function(drawMode, gId) { - const buffers = this.retainedMode.geometry[gId]; - const gl = this.GL; - // render the fill - if (buffers.indexBuffer) { - // If this model is using a Uint32Array we need to ensure the - // OES_element_index_uint WebGL extension is enabled. - if ( - this._pInst.webglVersion !== constants.WEBGL2 && - buffers.indexBufferType === gl.UNSIGNED_INT - ) { - if (!gl.getExtension('OES_element_index_uint')) { - throw new Error( - 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' - ); + return this; + }; + + p5.RendererGL.prototype._drawElements = function(drawMode, gId) { + const buffers = this.retainedMode.geometry[gId]; + const gl = this.GL; + // render the fill + if (buffers.indexBuffer) { + // If this model is using a Uint32Array we need to ensure the + // OES_element_index_uint WebGL extension is enabled. + if ( + this._pInst.webglVersion !== constants.WEBGL2 && + buffers.indexBufferType === gl.UNSIGNED_INT + ) { + if (!gl.getExtension('OES_element_index_uint')) { + throw new Error( + 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' + ); + } } + // we're drawing faces + gl.drawElements( + gl.TRIANGLES, + buffers.vertexCount, + buffers.indexBufferType, + 0 + ); + } else { + // drawing vertices + gl.drawArrays(drawMode || gl.TRIANGLES, 0, buffers.vertexCount); } - // we're drawing faces - gl.drawElements( - gl.TRIANGLES, - buffers.vertexCount, - buffers.indexBufferType, - 0 + }; + + p5.RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { + const gl = this.GL; + const pointShader = this._getImmediatePointShader(); + this._setPointUniforms(pointShader); + + this._bindBuffer( + vertexBuffer, + gl.ARRAY_BUFFER, + this._vToNArray(vertices), + Float32Array, + gl.STATIC_DRAW ); - } else { - // drawing vertices - gl.drawArrays(drawMode || gl.TRIANGLES, 0, buffers.vertexCount); - } -}; - -p5.RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { - const gl = this.GL; - const pointShader = this._getImmediatePointShader(); - this._setPointUniforms(pointShader); - this._bindBuffer( - vertexBuffer, - gl.ARRAY_BUFFER, - this._vToNArray(vertices), - Float32Array, - gl.STATIC_DRAW - ); + pointShader.enableAttrib(pointShader.attributes.aPosition, 3); - pointShader.enableAttrib(pointShader.attributes.aPosition, 3); + this._applyColorBlend(this.states.curStrokeColor); - this._applyColorBlend(this.states.curStrokeColor); + gl.drawArrays(gl.Points, 0, vertices.length); - gl.drawArrays(gl.Points, 0, vertices.length); + pointShader.unbindShader(); + }; +} - pointShader.unbindShader(); -}; +export default rendererGLRetained; -export default p5.RendererGL; +if(typeof p5 !== 'undefined'){ + rendererGLRetained(p5, p5.prototype); +} \ No newline at end of file diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 30bcd8b564..d55a68dd0d 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1,30 +1,6 @@ -import p5 from '../core/main'; import * as constants from '../core/constants'; import GeometryBuilder from './GeometryBuilder'; import libtess from 'libtess'; // Fixed with exporting module from libtess -import Renderer from '../core/p5.Renderer'; - -const STROKE_CAP_ENUM = {}; -const STROKE_JOIN_ENUM = {}; -let lineDefs = ''; -const defineStrokeCapEnum = function (key, val) { - lineDefs += `#define STROKE_CAP_${key} ${val}\n`; - STROKE_CAP_ENUM[constants[key]] = val; -}; -const defineStrokeJoinEnum = function (key, val) { - lineDefs += `#define STROKE_JOIN_${key} ${val}\n`; - STROKE_JOIN_ENUM[constants[key]] = val; -}; - - -// Define constants in line shaders for each type of cap/join, and also record -// the values in JS objects -defineStrokeCapEnum('ROUND', 0); -defineStrokeCapEnum('PROJECT', 1); -defineStrokeCapEnum('SQUARE', 2); -defineStrokeJoinEnum('ROUND', 0); -defineStrokeJoinEnum('MITER', 1); -defineStrokeJoinEnum('BEVEL', 2); import lightingShader from './shaders/lighting.glsl'; import webgl2CompatibilityShader from './shaders/webgl2Compatibility.glsl'; @@ -49,39 +25,6 @@ import imageLightVert from './shaders/imageLight.vert'; import imageLightDiffusedFrag from './shaders/imageLightDiffused.frag'; import imageLightSpecularFrag from './shaders/imageLightSpecular.frag'; -const defaultShaders = { - immediateVert, - vertexColorVert, - vertexColorFrag, - normalVert, - normalFrag, - basicFrag, - sphereMappingFrag, - lightVert: - lightingShader + - lightVert, - lightTextureFrag, - phongVert, - phongFrag: - lightingShader + - phongFrag, - fontVert, - fontFrag, - lineVert: - lineDefs + lineVert, - lineFrag: - lineDefs + lineFrag, - pointVert, - pointFrag, - imageLightVert, - imageLightDiffusedFrag, - imageLightSpecularFrag -}; -let sphereMapping = defaultShaders.sphereMappingFrag; -for (const key in defaultShaders) { - defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key]; -} - import filterGrayFrag from './shaders/filters/gray.frag'; import filterErodeFrag from './shaders/filters/erode.frag'; import filterDilateFrag from './shaders/filters/dilate.frag'; @@ -92,1705 +35,1754 @@ import filterInvertFrag from './shaders/filters/invert.frag'; import filterThresholdFrag from './shaders/filters/threshold.frag'; import filterShaderVert from './shaders/filters/default.vert'; -const filterShaderFrags = { - [constants.GRAY]: filterGrayFrag, - [constants.ERODE]: filterErodeFrag, - [constants.DILATE]: filterDilateFrag, - [constants.BLUR]: filterBlurFrag, - [constants.POSTERIZE]: filterPosterizeFrag, - [constants.OPAQUE]: filterOpaqueFrag, - [constants.INVERT]: filterInvertFrag, - [constants.THRESHOLD]: filterThresholdFrag -}; +function rendererGL(p5, fn){ + const STROKE_CAP_ENUM = {}; + const STROKE_JOIN_ENUM = {}; + let lineDefs = ''; + const defineStrokeCapEnum = function (key, val) { + lineDefs += `#define STROKE_CAP_${key} ${val}\n`; + STROKE_CAP_ENUM[constants[key]] = val; + }; + const defineStrokeJoinEnum = function (key, val) { + lineDefs += `#define STROKE_JOIN_${key} ${val}\n`; + STROKE_JOIN_ENUM[constants[key]] = val; + }; + + + // Define constants in line shaders for each type of cap/join, and also record + // the values in JS objects + defineStrokeCapEnum('ROUND', 0); + defineStrokeCapEnum('PROJECT', 1); + defineStrokeCapEnum('SQUARE', 2); + defineStrokeJoinEnum('ROUND', 0); + defineStrokeJoinEnum('MITER', 1); + defineStrokeJoinEnum('BEVEL', 2); + + const defaultShaders = { + immediateVert, + vertexColorVert, + vertexColorFrag, + normalVert, + normalFrag, + basicFrag, + sphereMappingFrag, + lightVert: + lightingShader + + lightVert, + lightTextureFrag, + phongVert, + phongFrag: + lightingShader + + phongFrag, + fontVert, + fontFrag, + lineVert: + lineDefs + lineVert, + lineFrag: + lineDefs + lineFrag, + pointVert, + pointFrag, + imageLightVert, + imageLightDiffusedFrag, + imageLightSpecularFrag + }; + let sphereMapping = defaultShaders.sphereMappingFrag; + for (const key in defaultShaders) { + defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key]; + } + + const filterShaderFrags = { + [constants.GRAY]: filterGrayFrag, + [constants.ERODE]: filterErodeFrag, + [constants.DILATE]: filterDilateFrag, + [constants.BLUR]: filterBlurFrag, + [constants.POSTERIZE]: filterPosterizeFrag, + [constants.OPAQUE]: filterOpaqueFrag, + [constants.INVERT]: filterInvertFrag, + [constants.THRESHOLD]: filterThresholdFrag + }; -/** - * @module Rendering - * @submodule Rendering - * @for p5 - */ -/** - * Set attributes for the WebGL Drawing context. - * This is a way of adjusting how the WebGL - * renderer works to fine-tune the display and performance. - * - * Note that this will reinitialize the drawing context - * if called after the WebGL canvas is made. - * - * If an object is passed as the parameter, all attributes - * not declared in the object will be set to defaults. - * - * The available attributes are: - *
- * alpha - indicates if the canvas contains an alpha buffer - * default is true - * - * depth - indicates whether the drawing buffer has a depth buffer - * of at least 16 bits - default is true - * - * stencil - indicates whether the drawing buffer has a stencil buffer - * of at least 8 bits - * - * antialias - indicates whether or not to perform anti-aliasing - * default is false (true in Safari) - * - * premultipliedAlpha - indicates that the page compositor will assume - * the drawing buffer contains colors with pre-multiplied alpha - * default is true - * - * preserveDrawingBuffer - if true the buffers will not be cleared and - * and will preserve their values until cleared or overwritten by author - * (note that p5 clears automatically on draw loop) - * default is true - * - * perPixelLighting - if true, per-pixel lighting will be used in the - * lighting shader otherwise per-vertex lighting is used. - * default is true. - * - * version - either 1 or 2, to specify which WebGL version to ask for. By - * default, WebGL 2 will be requested. If WebGL2 is not available, it will - * fall back to WebGL 1. You can check what version is used with by looking at - * the global `webglVersion` property. - * - * @method setAttributes - * @for p5 - * @param {String} key Name of attribute - * @param {Boolean} value New value of named attribute - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * } - * - * function draw() { - * background(255); - * push(); - * rotateZ(frameCount * 0.02); - * rotateX(frameCount * 0.02); - * rotateY(frameCount * 0.02); - * fill(0, 0, 0); - * box(50); - * pop(); - * } - * - *
- *
- * Now with the antialias attribute set to true. - *
- *
- * - * function setup() { - * setAttributes('antialias', true); - * createCanvas(100, 100, WEBGL); - * } - * - * function draw() { - * background(255); - * push(); - * rotateZ(frameCount * 0.02); - * rotateX(frameCount * 0.02); - * rotateY(frameCount * 0.02); - * fill(0, 0, 0); - * box(50); - * pop(); - * } - * - *
- * - *
- * - * // press the mouse button to disable perPixelLighting - * function setup() { - * createCanvas(100, 100, WEBGL); - * noStroke(); - * fill(255); - * } - * - * let lights = [ - * { c: '#f00', t: 1.12, p: 1.91, r: 0.2 }, - * { c: '#0f0', t: 1.21, p: 1.31, r: 0.2 }, - * { c: '#00f', t: 1.37, p: 1.57, r: 0.2 }, - * { c: '#ff0', t: 1.12, p: 1.91, r: 0.7 }, - * { c: '#0ff', t: 1.21, p: 1.31, r: 0.7 }, - * { c: '#f0f', t: 1.37, p: 1.57, r: 0.7 } - * ]; - * - * function draw() { - * let t = millis() / 1000 + 1000; - * background(0); - * directionalLight(color('#222'), 1, 1, 1); - * - * for (let i = 0; i < lights.length; i++) { - * let light = lights[i]; - * pointLight( - * color(light.c), - * p5.Vector.fromAngles(t * light.t, t * light.p, width * light.r) - * ); - * } - * - * specularMaterial(255); - * sphere(width * 0.1); - * - * rotateX(t * 0.77); - * rotateY(t * 0.83); - * rotateZ(t * 0.91); - * torus(width * 0.3, width * 0.07, 24, 10); - * } - * - * function mousePressed() { - * setAttributes('perPixelLighting', false); - * noStroke(); - * fill(255); - * } - * function mouseReleased() { - * setAttributes('perPixelLighting', true); - * noStroke(); - * fill(255); - * } - * - *
- * - * @alt a rotating cube with smoother edges - */ -/** - * @method setAttributes - * @for p5 - * @param {Object} obj object with key-value pairs - */ -p5.prototype.setAttributes = function (key, value) { - if (typeof this._glAttributes === 'undefined') { - console.log( - 'You are trying to use setAttributes on a p5.Graphics object ' + - 'that does not use a WEBGL renderer.' - ); - return; - } - let unchanged = true; - if (typeof value !== 'undefined') { - //first time modifying the attributes - if (this._glAttributes === null) { - this._glAttributes = {}; - } - if (this._glAttributes[key] !== value) { - //changing value of previously altered attribute - this._glAttributes[key] = value; - unchanged = false; - } - //setting all attributes with some change - } else if (key instanceof Object) { - if (this._glAttributes !== key) { - this._glAttributes = key; - unchanged = false; + /** + * @module Rendering + * @submodule Rendering + * @for p5 + */ + /** + * Set attributes for the WebGL Drawing context. + * This is a way of adjusting how the WebGL + * renderer works to fine-tune the display and performance. + * + * Note that this will reinitialize the drawing context + * if called after the WebGL canvas is made. + * + * If an object is passed as the parameter, all attributes + * not declared in the object will be set to defaults. + * + * The available attributes are: + *
+ * alpha - indicates if the canvas contains an alpha buffer + * default is true + * + * depth - indicates whether the drawing buffer has a depth buffer + * of at least 16 bits - default is true + * + * stencil - indicates whether the drawing buffer has a stencil buffer + * of at least 8 bits + * + * antialias - indicates whether or not to perform anti-aliasing + * default is false (true in Safari) + * + * premultipliedAlpha - indicates that the page compositor will assume + * the drawing buffer contains colors with pre-multiplied alpha + * default is true + * + * preserveDrawingBuffer - if true the buffers will not be cleared and + * and will preserve their values until cleared or overwritten by author + * (note that p5 clears automatically on draw loop) + * default is true + * + * perPixelLighting - if true, per-pixel lighting will be used in the + * lighting shader otherwise per-vertex lighting is used. + * default is true. + * + * version - either 1 or 2, to specify which WebGL version to ask for. By + * default, WebGL 2 will be requested. If WebGL2 is not available, it will + * fall back to WebGL 1. You can check what version is used with by looking at + * the global `webglVersion` property. + * + * @method setAttributes + * @for p5 + * @param {String} key Name of attribute + * @param {Boolean} value New value of named attribute + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * } + * + * function draw() { + * background(255); + * push(); + * rotateZ(frameCount * 0.02); + * rotateX(frameCount * 0.02); + * rotateY(frameCount * 0.02); + * fill(0, 0, 0); + * box(50); + * pop(); + * } + * + *
+ *
+ * Now with the antialias attribute set to true. + *
+ *
+ * + * function setup() { + * setAttributes('antialias', true); + * createCanvas(100, 100, WEBGL); + * } + * + * function draw() { + * background(255); + * push(); + * rotateZ(frameCount * 0.02); + * rotateX(frameCount * 0.02); + * rotateY(frameCount * 0.02); + * fill(0, 0, 0); + * box(50); + * pop(); + * } + * + *
+ * + *
+ * + * // press the mouse button to disable perPixelLighting + * function setup() { + * createCanvas(100, 100, WEBGL); + * noStroke(); + * fill(255); + * } + * + * let lights = [ + * { c: '#f00', t: 1.12, p: 1.91, r: 0.2 }, + * { c: '#0f0', t: 1.21, p: 1.31, r: 0.2 }, + * { c: '#00f', t: 1.37, p: 1.57, r: 0.2 }, + * { c: '#ff0', t: 1.12, p: 1.91, r: 0.7 }, + * { c: '#0ff', t: 1.21, p: 1.31, r: 0.7 }, + * { c: '#f0f', t: 1.37, p: 1.57, r: 0.7 } + * ]; + * + * function draw() { + * let t = millis() / 1000 + 1000; + * background(0); + * directionalLight(color('#222'), 1, 1, 1); + * + * for (let i = 0; i < lights.length; i++) { + * let light = lights[i]; + * pointLight( + * color(light.c), + * p5.Vector.fromAngles(t * light.t, t * light.p, width * light.r) + * ); + * } + * + * specularMaterial(255); + * sphere(width * 0.1); + * + * rotateX(t * 0.77); + * rotateY(t * 0.83); + * rotateZ(t * 0.91); + * torus(width * 0.3, width * 0.07, 24, 10); + * } + * + * function mousePressed() { + * setAttributes('perPixelLighting', false); + * noStroke(); + * fill(255); + * } + * function mouseReleased() { + * setAttributes('perPixelLighting', true); + * noStroke(); + * fill(255); + * } + * + *
+ * + * @alt a rotating cube with smoother edges + */ + /** + * @method setAttributes + * @for p5 + * @param {Object} obj object with key-value pairs + */ + fn.setAttributes = function (key, value) { + if (typeof this._glAttributes === 'undefined') { + console.log( + 'You are trying to use setAttributes on a p5.Graphics object ' + + 'that does not use a WEBGL renderer.' + ); + return; } - } - //@todo_FES - if (!this._renderer.isP3D || unchanged) { - return; - } - - if (!this._setupDone) { - for (const x in this._renderer.retainedMode.geometry) { - if (this._renderer.retainedMode.geometry.hasOwnProperty(x)) { - p5._friendlyError( - 'Sorry, Could not set the attributes, you need to call setAttributes() ' + - 'before calling the other drawing methods in setup()' - ); - return; + let unchanged = true; + if (typeof value !== 'undefined') { + //first time modifying the attributes + if (this._glAttributes === null) { + this._glAttributes = {}; + } + if (this._glAttributes[key] !== value) { + //changing value of previously altered attribute + this._glAttributes[key] = value; + unchanged = false; + } + //setting all attributes with some change + } else if (key instanceof Object) { + if (this._glAttributes !== key) { + this._glAttributes = key; + unchanged = false; } } - } - - this.push(); - this._renderer._resetContext(); - this.pop(); - - if (this._renderer.states.curCamera) { - this._renderer.states.curCamera._renderer = this._renderer; - } -}; -/** - * @private - * @param {Uint8Array|Float32Array|undefined} pixels An existing pixels array to reuse if the size is the same - * @param {WebGLRenderingContext} gl The WebGL context - * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read - * @param {Number} x The x coordiante to read, premultiplied by pixel density - * @param {Number} y The y coordiante to read, premultiplied by pixel density - * @param {Number} width The width in pixels to be read (factoring in pixel density) - * @param {Number} height The height in pixels to be read (factoring in pixel density) - * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read - * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT - * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about - * @returns {Uint8Array|Float32Array} pixels A pixels array with the current state of the - * WebGL context read into it - */ -export function readPixelsWebGL( - pixels, - gl, - framebuffer, - x, - y, - width, - height, - format, - type, - flipY -) { - // Record the currently bound framebuffer so we can go back to it after, and - // bind the framebuffer we want to read from - const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); - gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - - const channels = format === gl.RGBA ? 4 : 3; - - // Make a pixels buffer if it doesn't already exist - const len = width * height * channels; - const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; - if (!(pixels instanceof TypedArrayClass) || pixels.length !== len) { - pixels = new TypedArrayClass(len); - } + //@todo_FES + if (!this._renderer.isP3D || unchanged) { + return; + } - gl.readPixels( - x, - flipY ? (flipY - y - height) : y, - width, - height, - format, - type, - pixels - ); + if (!this._setupDone) { + for (const x in this._renderer.retainedMode.geometry) { + if (this._renderer.retainedMode.geometry.hasOwnProperty(x)) { + p5._friendlyError( + 'Sorry, Could not set the attributes, you need to call setAttributes() ' + + 'before calling the other drawing methods in setup()' + ); + return; + } + } + } - // Re-bind whatever was previously bound - gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); + this.push(); + this._renderer._resetContext(); + this.pop(); - if (flipY) { - // WebGL pixels are inverted compared to 2D pixels, so we have to flip - // the resulting rows. Adapted from https://stackoverflow.com/a/41973289 - const halfHeight = Math.floor(height / 2); - const tmpRow = new TypedArrayClass(width * channels); - for (let y = 0; y < halfHeight; y++) { - const topOffset = y * width * 4; - const bottomOffset = (height - y - 1) * width * 4; - tmpRow.set(pixels.subarray(topOffset, topOffset + width * 4)); - pixels.copyWithin(topOffset, bottomOffset, bottomOffset + width * 4); - pixels.set(tmpRow, bottomOffset); + if (this._renderer.states.curCamera) { + this._renderer.states.curCamera._renderer = this._renderer; } - } + }; - return pixels; -} + /** + * 3D graphics class + * @private + * @class p5.RendererGL + * @extends p5.Renderer + * @todo extend class to include public method for offscreen + * rendering (FBO). + */ + p5.RendererGL = class RendererGL extends p5.Renderer { + constructor(pInst, w, h, isMainCanvas, elt, attr) { + super(pInst, w, h, isMainCanvas); + + // Create new canvas + this.canvas = this.elt = elt || document.createElement('canvas'); + this._initContext(); + // This redundant property is useful in reminding you that you are + // interacting with WebGLRenderingContext, still worth considering future removal + this.GL = this.drawingContext; + this._pInst.drawingContext = this.drawingContext; + + if (this._isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); + + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; + + this.width = w; + this.height = h; + + // Set canvas size + this.elt.width = w * this._pixelDensity; + this.elt.height = h * this._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; + this._origViewport = { + width: this.GL.drawingBufferWidth, + height: this.GL.drawingBufferHeight + }; + this.viewport( + this._origViewport.width, + this._origViewport.height + ); -/** - * @private - * @param {WebGLRenderingContext} gl The WebGL context - * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read - * @param {Number} x The x coordinate to read, premultiplied by pixel density - * @param {Number} y The y coordinate to read, premultiplied by pixel density - * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read - * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT - * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about - * @returns {Number[]} pixels The channel data for the pixel at that location - */ -export function readPixelWebGL( - gl, - framebuffer, - x, - y, - format, - type, - flipY -) { - // Record the currently bound framebuffer so we can go back to it after, and - // bind the framebuffer we want to read from - const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); - gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName('main').length === 0) { + let m = document.createElement('main'); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName('main')[0].appendChild(this.elt); + } - const channels = format === gl.RGBA ? 4 : 3; - const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; - const pixels = new TypedArrayClass(channels); + this._setAttributeDefaults(pInst); + this.isP3D = true; //lets us know we're in 3d mode + + // When constructing a new p5.Geometry, this will represent the builder + this.geometryBuilder = undefined; + + // Push/pop state + this.states.uModelMatrix = new p5.Matrix(); + this.states.uViewMatrix = new p5.Matrix(); + this.states.uMVMatrix = new p5.Matrix(); + this.states.uPMatrix = new p5.Matrix(); + this.states.uNMatrix = new p5.Matrix('mat3'); + this.states.curMatrix = new p5.Matrix('mat3'); + + this.states.curCamera = new p5.Camera(this); + + this.states.enableLighting = false; + this.states.ambientLightColors = []; + this.states.specularColors = [1, 1, 1]; + this.states.directionalLightDirections = []; + this.states.directionalLightDiffuseColors = []; + this.states.directionalLightSpecularColors = []; + this.states.pointLightPositions = []; + this.states.pointLightDiffuseColors = []; + this.states.pointLightSpecularColors = []; + this.states.spotLightPositions = []; + this.states.spotLightDirections = []; + this.states.spotLightDiffuseColors = []; + this.states.spotLightSpecularColors = []; + this.states.spotLightAngle = []; + this.states.spotLightConc = []; + this.states.activeImageLight = null; + + this.states.curFillColor = [1, 1, 1, 1]; + this.states.curAmbientColor = [1, 1, 1, 1]; + this.states.curSpecularColor = [0, 0, 0, 0]; + this.states.curEmissiveColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 1]; + + this.states.curBlendMode = constants.BLEND; + + this.states._hasSetAmbient = false; + this.states._useSpecularMaterial = false; + this.states._useEmissiveMaterial = false; + this.states._useNormalMaterial = false; + this.states._useShininess = 1; + this.states._useMetalness = 0; + + this.states.tint = [255, 255, 255, 255]; + + this.states.constantAttenuation = 1; + this.states.linearAttenuation = 0; + this.states.quadraticAttenuation = 0; + + this.states._currentNormal = new p5.Vector(0, 0, 1); + + this.states.drawMode = constants.FILL; + + this.states._tex = null; + + // erasing + this._isErasing = false; - gl.readPixels( - x, flipY ? (flipY - y - 1) : y, 1, 1, - format, type, - pixels - ); + // clipping + this._clipDepths = []; + this._isClipApplied = false; + this._stencilTestOn = false; + + this.mixedAmbientLight = []; + this.mixedSpecularColor = []; + + // p5.framebuffer for this are calculated in getDiffusedTexture function + this.diffusedTextures = new Map(); + // p5.framebuffer for this are calculated in getSpecularTexture function + this.specularTextures = new Map(); + + this.preEraseBlend = undefined; + this._cachedBlendMode = undefined; + this._cachedFillStyle = [1, 1, 1, 1]; + this._cachedStrokeStyle = [0, 0, 0, 1]; + if (this.webglVersion === constants.WEBGL2) { + this.blendExt = this.GL; + } else { + this.blendExt = this.GL.getExtension('EXT_blend_minmax'); + } + this._isBlending = false; + + this._useLineColor = false; + this._useVertexColor = false; + + this.registerEnabled = new Set(); + + // Camera + this.states.curCamera._computeCameraDefaultSettings(); + this.states.curCamera._setDefaultCamera(); + + // FilterCamera + this.filterCamera = new p5.Camera(this); + this.filterCamera._computeCameraDefaultSettings(); + this.filterCamera._setDefaultCamera(); + // Information about the previous frame's touch object + // for executing orbitControl() + this.prevTouches = []; + // Velocity variable for use with orbitControl() + this.zoomVelocity = 0; + this.rotateVelocity = new p5.Vector(0, 0); + this.moveVelocity = new p5.Vector(0, 0); + // Flags for recording the state of zooming, rotation and moving + this.executeZoom = false; + this.executeRotateAndMove = false; + + this.states.specularShader = undefined; + this.sphereMapping = undefined; + this.states.diffusedShader = undefined; + this._defaultLightShader = undefined; + this._defaultImmediateModeShader = undefined; + this._defaultNormalShader = undefined; + this._defaultColorShader = undefined; + this._defaultPointShader = undefined; + + this.states.userFillShader = undefined; + this.states.userStrokeShader = undefined; + this.states.userPointShader = undefined; + + this._useUserVertexProperties = undefined; + + // Default drawing is done in Retained Mode + // Geometry and Material hashes stored here + this.retainedMode = { + geometry: {}, + buffers: { + stroke: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + ], + fill: [ + new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + //new BufferDef(3, 'vertexSpeculars', 'specularBuffer', 'aSpecularColor'), + new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + text: [ + new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + user:[] + } + }; - // Re-bind whatever was previously bound - gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); + // Immediate Mode + // Geometry and Material hashes stored here + this.immediateMode = { + geometry: new p5.Geometry(), + shapeMode: constants.TRIANGLE_FAN, + contourIndices: [], + _bezierVertex: [], + _quadraticVertex: [], + _curveVertex: [], + buffers: { + fill: [ + new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + stroke: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + ], + point: this.GL.createBuffer(), + user:[] + } + }; - return Array.from(pixels); -} -/** - * 3D graphics class - * @private - * @class p5.RendererGL - * @extends p5.Renderer - * @todo extend class to include public method for offscreen - * rendering (FBO). - */ -p5.RendererGL = class RendererGL extends Renderer { - constructor(pInst, w, h, isMainCanvas, elt, attr) { - super(pInst, w, h, isMainCanvas); - - // Create new canvas - this.canvas = this.elt = elt || document.createElement('canvas'); - this._initContext(); - // This redundant property is useful in reminding you that you are - // interacting with WebGLRenderingContext, still worth considering future removal - this.GL = this.drawingContext; - this._pInst.drawingContext = this.drawingContext; - - if (this._isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } - this.elt.id = 'defaultCanvas0'; - this.elt.classList.add('p5Canvas'); - - const dimensions = this._adjustDimensions(w, h); - w = dimensions.adjustedWidth; - h = dimensions.adjustedHeight; - - this.width = w; - this.height = h; - - // Set canvas size - this.elt.width = w * this._pixelDensity; - this.elt.height = h * this._pixelDensity; - this.elt.style.width = `${w}px`; - this.elt.style.height = `${h}px`; - this._origViewport = { - width: this.GL.drawingBufferWidth, - height: this.GL.drawingBufferHeight - }; - this.viewport( - this._origViewport.width, - this._origViewport.height - ); - - // Attach canvas element to DOM - if (this._pInst._userNode) { - // user input node case - this._pInst._userNode.appendChild(this.elt); - } else { - //create main element - if (document.getElementsByTagName('main').length === 0) { - let m = document.createElement('main'); - document.body.appendChild(m); - } - //append canvas to main - document.getElementsByTagName('main')[0].appendChild(this.elt); - } - - this._setAttributeDefaults(pInst); - this.isP3D = true; //lets us know we're in 3d mode - - // When constructing a new p5.Geometry, this will represent the builder - this.geometryBuilder = undefined; - - // Push/pop state - this.states.uModelMatrix = new p5.Matrix(); - this.states.uViewMatrix = new p5.Matrix(); - this.states.uMVMatrix = new p5.Matrix(); - this.states.uPMatrix = new p5.Matrix(); - this.states.uNMatrix = new p5.Matrix('mat3'); - this.states.curMatrix = new p5.Matrix('mat3'); - - this.states.curCamera = new p5.Camera(this); - - this.states.enableLighting = false; - this.states.ambientLightColors = []; - this.states.specularColors = [1, 1, 1]; - this.states.directionalLightDirections = []; - this.states.directionalLightDiffuseColors = []; - this.states.directionalLightSpecularColors = []; - this.states.pointLightPositions = []; - this.states.pointLightDiffuseColors = []; - this.states.pointLightSpecularColors = []; - this.states.spotLightPositions = []; - this.states.spotLightDirections = []; - this.states.spotLightDiffuseColors = []; - this.states.spotLightSpecularColors = []; - this.states.spotLightAngle = []; - this.states.spotLightConc = []; - this.states.activeImageLight = null; - - this.states.curFillColor = [1, 1, 1, 1]; - this.states.curAmbientColor = [1, 1, 1, 1]; - this.states.curSpecularColor = [0, 0, 0, 0]; - this.states.curEmissiveColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 1]; - - this.states.curBlendMode = constants.BLEND; - - this.states._hasSetAmbient = false; - this.states._useSpecularMaterial = false; - this.states._useEmissiveMaterial = false; - this.states._useNormalMaterial = false; - this.states._useShininess = 1; - this.states._useMetalness = 0; - - this.states.tint = [255, 255, 255, 255]; - - this.states.constantAttenuation = 1; - this.states.linearAttenuation = 0; - this.states.quadraticAttenuation = 0; - - this.states._currentNormal = new p5.Vector(0, 0, 1); - - this.states.drawMode = constants.FILL; - - this.states._tex = null; - - // erasing - this._isErasing = false; - - // clipping - this._clipDepths = []; - this._isClipApplied = false; - this._stencilTestOn = false; - - this.mixedAmbientLight = []; - this.mixedSpecularColor = []; - - // p5.framebuffer for this are calculated in getDiffusedTexture function - this.diffusedTextures = new Map(); - // p5.framebuffer for this are calculated in getSpecularTexture function - this.specularTextures = new Map(); - - this.preEraseBlend = undefined; - this._cachedBlendMode = undefined; - this._cachedFillStyle = [1, 1, 1, 1]; - this._cachedStrokeStyle = [0, 0, 0, 1]; - if (this.webglVersion === constants.WEBGL2) { - this.blendExt = this.GL; - } else { - this.blendExt = this.GL.getExtension('EXT_blend_minmax'); - } - this._isBlending = false; - - this._useLineColor = false; - this._useVertexColor = false; - - this.registerEnabled = new Set(); - - // Camera - this.states.curCamera._computeCameraDefaultSettings(); - this.states.curCamera._setDefaultCamera(); - - // FilterCamera - this.filterCamera = new p5.Camera(this); - this.filterCamera._computeCameraDefaultSettings(); - this.filterCamera._setDefaultCamera(); - // Information about the previous frame's touch object - // for executing orbitControl() - this.prevTouches = []; - // Velocity variable for use with orbitControl() - this.zoomVelocity = 0; - this.rotateVelocity = new p5.Vector(0, 0); - this.moveVelocity = new p5.Vector(0, 0); - // Flags for recording the state of zooming, rotation and moving - this.executeZoom = false; - this.executeRotateAndMove = false; - - this.states.specularShader = undefined; - this.sphereMapping = undefined; - this.states.diffusedShader = undefined; - this._defaultLightShader = undefined; - this._defaultImmediateModeShader = undefined; - this._defaultNormalShader = undefined; - this._defaultColorShader = undefined; - this._defaultPointShader = undefined; - - this.states.userFillShader = undefined; - this.states.userStrokeShader = undefined; - this.states.userPointShader = undefined; - - this._useUserVertexProperties = undefined; - - // Default drawing is done in Retained Mode - // Geometry and Material hashes stored here - this.retainedMode = { - geometry: {}, - buffers: { - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - fill: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - //new BufferDef(3, 'vertexSpeculars', 'specularBuffer', 'aSpecularColor'), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - text: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - user:[] - } - }; - - // Immediate Mode - // Geometry and Material hashes stored here - this.immediateMode = { - geometry: new p5.Geometry(), - shapeMode: constants.TRIANGLE_FAN, - contourIndices: [], - _bezierVertex: [], - _quadraticVertex: [], - _curveVertex: [], - buffers: { - fill: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - point: this.GL.createBuffer(), - user:[] - } - }; - - this.pointSize = 5.0; //default point size - this.curStrokeWeight = 1; - this.curStrokeCap = constants.ROUND; - this.curStrokeJoin = constants.ROUND; - - // map of texture sources to textures created in this gl context via this.getTexture(src) - this.textures = new Map(); - - // set of framebuffers in use - this.framebuffers = new Set(); - // stack of active framebuffers - this.activeFramebuffers = []; - - // for post processing step - this.states.filterShader = undefined; - this.filterLayer = undefined; - this.filterLayerTemp = undefined; - this.defaultFilterShaders = {}; - - this.textureMode = constants.IMAGE; - // default wrap settings - this.textureWrapX = constants.CLAMP; - this.textureWrapY = constants.CLAMP; - this.states._tex = null; - this._curveTightness = 6; - - // lookUpTable for coefficients needed to be calculated for bezierVertex, same are used for curveVertex - this._lookUpTableBezier = []; - // lookUpTable for coefficients needed to be calculated for quadraticVertex - this._lookUpTableQuadratic = []; - - // current curveDetail in the Bezier lookUpTable - this._lutBezierDetail = 0; - // current curveDetail in the Quadratic lookUpTable - this._lutQuadraticDetail = 0; - - // Used to distinguish between user calls to vertex() and internal calls - this.isProcessingVertices = false; - this._tessy = this._initTessy(); - - this.fontInfos = {}; - - this._curShader = undefined; - } + this.pointSize = 5.0; //default point size + this.curStrokeWeight = 1; + this.curStrokeCap = constants.ROUND; + this.curStrokeJoin = constants.ROUND; + + // map of texture sources to textures created in this gl context via this.getTexture(src) + this.textures = new Map(); + + // set of framebuffers in use + this.framebuffers = new Set(); + // stack of active framebuffers + this.activeFramebuffers = []; + + // for post processing step + this.states.filterShader = undefined; + this.filterLayer = undefined; + this.filterLayerTemp = undefined; + this.defaultFilterShaders = {}; + + this.textureMode = constants.IMAGE; + // default wrap settings + this.textureWrapX = constants.CLAMP; + this.textureWrapY = constants.CLAMP; + this.states._tex = null; + this._curveTightness = 6; + + // lookUpTable for coefficients needed to be calculated for bezierVertex, same are used for curveVertex + this._lookUpTableBezier = []; + // lookUpTable for coefficients needed to be calculated for quadraticVertex + this._lookUpTableQuadratic = []; + + // current curveDetail in the Bezier lookUpTable + this._lutBezierDetail = 0; + // current curveDetail in the Quadratic lookUpTable + this._lutQuadraticDetail = 0; + + // Used to distinguish between user calls to vertex() and internal calls + this.isProcessingVertices = false; + this._tessy = this._initTessy(); + + this.fontInfos = {}; + + this._curShader = undefined; + } + + /** + * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added + * to the geometry and then returned when + * endGeometry() is called. One can also use + * buildGeometry() to pass a function that + * draws shapes. + * + * If you need to draw complex shapes every frame which don't change over time, + * combining them upfront with `beginGeometry()` and `endGeometry()` and then + * drawing that will run faster than repeatedly drawing the individual pieces. + */ + beginGeometry() { + if (this.geometryBuilder) { + throw new Error('It looks like `beginGeometry()` is being called while another p5.Geometry is already being build.'); + } + this.geometryBuilder = new GeometryBuilder(this); + this.geometryBuilder.prevFillColor = [...this.states.curFillColor]; + this.states.curFillColor = [-1, -1, -1, -1]; + } - /** - * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added - * to the geometry and then returned when - * endGeometry() is called. One can also use - * buildGeometry() to pass a function that + /** + * Finishes creating a new p5.Geometry that was + * started using beginGeometry(). One can also + * use buildGeometry() to pass a function that * draws shapes. * - * If you need to draw complex shapes every frame which don't change over time, - * combining them upfront with `beginGeometry()` and `endGeometry()` and then - * drawing that will run faster than repeatedly drawing the individual pieces. - */ - beginGeometry() { - if (this.geometryBuilder) { - throw new Error('It looks like `beginGeometry()` is being called while another p5.Geometry is already being build.'); + * @returns {p5.Geometry} The model that was built. + */ + endGeometry() { + if (!this.geometryBuilder) { + throw new Error('Make sure you call beginGeometry() before endGeometry()!'); + } + const geometry = this.geometryBuilder.finish(); + this.states.curFillColor = this.geometryBuilder.prevFillColor; + this.geometryBuilder = undefined; + return geometry; } - this.geometryBuilder = new GeometryBuilder(this); - this.geometryBuilder.prevFillColor = [...this.states.curFillColor]; - this.states.curFillColor = [-1, -1, -1, -1]; - } - - /** - * Finishes creating a new p5.Geometry that was - * started using beginGeometry(). One can also - * use buildGeometry() to pass a function that - * draws shapes. - * - * @returns {p5.Geometry} The model that was built. - */ - endGeometry() { - if (!this.geometryBuilder) { - throw new Error('Make sure you call beginGeometry() before endGeometry()!'); - } - const geometry = this.geometryBuilder.finish(); - this.states.curFillColor = this.geometryBuilder.prevFillColor; - this.geometryBuilder = undefined; - return geometry; - } - /** - * Creates a new p5.Geometry that contains all - * the shapes drawn in a provided callback function. The returned combined shape - * can then be drawn all at once using model(). - * - * If you need to draw complex shapes every frame which don't change over time, - * combining them with `buildGeometry()` once and then drawing that will run - * faster than repeatedly drawing the individual pieces. - * - * One can also draw shapes directly between - * beginGeometry() and - * endGeometry() instead of using a callback - * function. - * @param {Function} callback A function that draws shapes. - * @returns {p5.Geometry} The model that was built from the callback function. - */ - buildGeometry(callback) { - this.beginGeometry(); - callback(); - return this.endGeometry(); - } + /** + * Creates a new p5.Geometry that contains all + * the shapes drawn in a provided callback function. The returned combined shape + * can then be drawn all at once using model(). + * + * If you need to draw complex shapes every frame which don't change over time, + * combining them with `buildGeometry()` once and then drawing that will run + * faster than repeatedly drawing the individual pieces. + * + * One can also draw shapes directly between + * beginGeometry() and + * endGeometry() instead of using a callback + * function. + * @param {Function} callback A function that draws shapes. + * @returns {p5.Geometry} The model that was built from the callback function. + */ + buildGeometry(callback) { + this.beginGeometry(); + callback(); + return this.endGeometry(); + } + + ////////////////////////////////////////////// + // Setting + ////////////////////////////////////////////// + + _setAttributeDefaults(pInst) { + // See issue #3850, safer to enable AA in Safari + const applyAA = navigator.userAgent.toLowerCase().includes('safari'); + const defaults = { + alpha: true, + depth: true, + stencil: true, + antialias: applyAA, + premultipliedAlpha: true, + preserveDrawingBuffer: true, + perPixelLighting: true, + version: 2 + }; + if (pInst._glAttributes === null) { + pInst._glAttributes = defaults; + } else { + pInst._glAttributes = Object.assign(defaults, pInst._glAttributes); + } + return; + } - ////////////////////////////////////////////// - // Setting - ////////////////////////////////////////////// - - _setAttributeDefaults(pInst) { - // See issue #3850, safer to enable AA in Safari - const applyAA = navigator.userAgent.toLowerCase().includes('safari'); - const defaults = { - alpha: true, - depth: true, - stencil: true, - antialias: applyAA, - premultipliedAlpha: true, - preserveDrawingBuffer: true, - perPixelLighting: true, - version: 2 - }; - if (pInst._glAttributes === null) { - pInst._glAttributes = defaults; - } else { - pInst._glAttributes = Object.assign(defaults, pInst._glAttributes); - } - return; - } + _initContext() { + if (this._pInst._glAttributes?.version !== 1) { + // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context + this.drawingContext = + this.canvas.getContext('webgl2', this._pInst._glAttributes); + } + this.webglVersion = + this.drawingContext ? constants.WEBGL2 : constants.WEBGL; + // If this is the main canvas, make sure the global `webglVersion` is set + this._pInst.webglVersion = this.webglVersion; + if (!this.drawingContext) { + // If we were unable to create a WebGL2 context (either because it was + // disabled via `setAttributes({ version: 1 })` or because the device + // doesn't support it), fall back to a WebGL1 context + this.drawingContext = + this.canvas.getContext('webgl', this._pInst._glAttributes) || + this.canvas.getContext('experimental-webgl', this._pInst._glAttributes); + } + if (this.drawingContext === null) { + throw new Error('Error creating webgl context'); + } else { + const gl = this.drawingContext; + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + // Make sure all images are loaded into the canvas premultiplied so that + // they match the way we render colors. This will make framebuffer textures + // be encoded the same way as textures from everything else. + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + this._viewport = this.drawingContext.getParameter( + this.drawingContext.VIEWPORT + ); + } + } - _initContext() { - if (this._pInst._glAttributes?.version !== 1) { - // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context - this.drawingContext = - this.canvas.getContext('webgl2', this._pInst._glAttributes); - } - this.webglVersion = - this.drawingContext ? constants.WEBGL2 : constants.WEBGL; - // If this is the main canvas, make sure the global `webglVersion` is set - this._pInst.webglVersion = this.webglVersion; - if (!this.drawingContext) { - // If we were unable to create a WebGL2 context (either because it was - // disabled via `setAttributes({ version: 1 })` or because the device - // doesn't support it), fall back to a WebGL1 context - this.drawingContext = - this.canvas.getContext('webgl', this._pInst._glAttributes) || - this.canvas.getContext('experimental-webgl', this._pInst._glAttributes); - } - if (this.drawingContext === null) { - throw new Error('Error creating webgl context'); - } else { + _getMaxTextureSize() { const gl = this.drawingContext; - gl.enable(gl.DEPTH_TEST); - gl.depthFunc(gl.LEQUAL); - gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - // Make sure all images are loaded into the canvas premultiplied so that - // they match the way we render colors. This will make framebuffer textures - // be encoded the same way as textures from everything else. - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - this._viewport = this.drawingContext.getParameter( - this.drawingContext.VIEWPORT - ); + return gl.getParameter(gl.MAX_TEXTURE_SIZE); } - } - _getMaxTextureSize() { - const gl = this.drawingContext; - return gl.getParameter(gl.MAX_TEXTURE_SIZE); - } + _adjustDimensions(width, height) { + if (!this._maxTextureSize) { + this._maxTextureSize = this._getMaxTextureSize(); + } + let maxTextureSize = this._maxTextureSize; - _adjustDimensions(width, height) { - if (!this._maxTextureSize) { - this._maxTextureSize = this._getMaxTextureSize(); - } - let maxTextureSize = this._maxTextureSize; - - let maxAllowedPixelDimensions = Math.floor( - maxTextureSize / this._pixelDensity - ); - let adjustedWidth = Math.min( - width, maxAllowedPixelDimensions - ); - let adjustedHeight = Math.min( - height, maxAllowedPixelDimensions - ); - - if (adjustedWidth !== width || adjustedHeight !== height) { - console.warn( - 'Warning: The requested width/height exceeds hardware limits. ' + - `Adjusting dimensions to width: ${adjustedWidth}, height: ${adjustedHeight}.` + let maxAllowedPixelDimensions = Math.floor( + maxTextureSize / this._pixelDensity + ); + let adjustedWidth = Math.min( + width, maxAllowedPixelDimensions + ); + let adjustedHeight = Math.min( + height, maxAllowedPixelDimensions ); - } - - return { adjustedWidth, adjustedHeight }; - } - //This is helper function to reset the context anytime the attributes - //are changed with setAttributes() - - _resetContext(options, callback) { - const w = this.width; - const h = this.height; - const defaultId = this.canvas.id; - const isPGraphics = this._pInst instanceof p5.Graphics; - - if (isPGraphics) { - const pg = this._pInst; - pg.canvas.parentNode.removeChild(pg.canvas); - pg.canvas = document.createElement('canvas'); - const node = pg._pInst._userNode || document.body; - node.appendChild(pg.canvas); - p5.Element.call(pg, pg.canvas, pg._pInst); - pg.width = w; - pg.height = h; - } else { - let c = this.canvas; - if (c) { - c.parentNode.removeChild(c); - } - c = document.createElement('canvas'); - c.id = defaultId; - if (this._pInst._userNode) { - this._pInst._userNode.appendChild(c); - } else { - document.body.appendChild(c); + if (adjustedWidth !== width || adjustedHeight !== height) { + console.warn( + 'Warning: The requested width/height exceeds hardware limits. ' + + `Adjusting dimensions to width: ${adjustedWidth}, height: ${adjustedHeight}.` + ); } - this._pInst.canvas = c; - this.canvas = c; - } - - const renderer = new p5.RendererGL( - this._pInst, - w, - h, - !isPGraphics, - this._pInst.canvas, - ); - this._pInst._renderer = renderer; - - renderer._applyDefaults(); - if (typeof callback === 'function') { - //setTimeout with 0 forces the task to the back of the queue, this ensures that - //we finish switching out the renderer - setTimeout(() => { - callback.apply(window._renderer, options); - }, 0); + return { adjustedWidth, adjustedHeight }; } - } + //This is helper function to reset the context anytime the attributes + //are changed with setAttributes() - _update() { - // reset model view and apply initial camera transform - // (containing only look at info; no projection). - this.states.uModelMatrix.reset(); - this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + _resetContext(options, callback) { + const w = this.width; + const h = this.height; + const defaultId = this.canvas.id; + const isPGraphics = this._pInst instanceof p5.Graphics; - // reset light data for new frame. + if (isPGraphics) { + const pg = this._pInst; + pg.canvas.parentNode.removeChild(pg.canvas); + pg.canvas = document.createElement('canvas'); + const node = pg._pInst._userNode || document.body; + node.appendChild(pg.canvas); + p5.Element.call(pg, pg.canvas, pg._pInst); + pg.width = w; + pg.height = h; + } else { + let c = this.canvas; + if (c) { + c.parentNode.removeChild(c); + } + c = document.createElement('canvas'); + c.id = defaultId; + if (this._pInst._userNode) { + this._pInst._userNode.appendChild(c); + } else { + document.body.appendChild(c); + } + this._pInst.canvas = c; + this.canvas = c; + } - this.states.ambientLightColors.length = 0; - this.states.specularColors = [1, 1, 1]; + const renderer = new p5.RendererGL( + this._pInst, + w, + h, + !isPGraphics, + this._pInst.canvas, + ); + this._pInst._renderer = renderer; - this.states.directionalLightDirections.length = 0; - this.states.directionalLightDiffuseColors.length = 0; - this.states.directionalLightSpecularColors.length = 0; + renderer._applyDefaults(); - this.states.pointLightPositions.length = 0; - this.states.pointLightDiffuseColors.length = 0; - this.states.pointLightSpecularColors.length = 0; + if (typeof callback === 'function') { + //setTimeout with 0 forces the task to the back of the queue, this ensures that + //we finish switching out the renderer + setTimeout(() => { + callback.apply(window._renderer, options); + }, 0); + } + } - this.states.spotLightPositions.length = 0; - this.states.spotLightDirections.length = 0; - this.states.spotLightDiffuseColors.length = 0; - this.states.spotLightSpecularColors.length = 0; - this.states.spotLightAngle.length = 0; - this.states.spotLightConc.length = 0; - this.states._enableLighting = false; + _update() { + // reset model view and apply initial camera transform + // (containing only look at info; no projection). + this.states.uModelMatrix.reset(); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); - //reset tint value for new frame - this.states.tint = [255, 255, 255, 255]; + // reset light data for new frame. - //Clear depth every frame - this.GL.clearStencil(0); - this.GL.clear(this.GL.DEPTH_BUFFER_BIT | this.GL.STENCIL_BUFFER_BIT); - this.GL.disable(this.GL.STENCIL_TEST); - } + this.states.ambientLightColors.length = 0; + this.states.specularColors = [1, 1, 1]; - /** - * [background description] - */ - background(...args) { - const _col = this._pInst.color(...args); - const _r = _col.levels[0] / 255; - const _g = _col.levels[1] / 255; - const _b = _col.levels[2] / 255; - const _a = _col.levels[3] / 255; - this.clear(_r, _g, _b, _a); - } + this.states.directionalLightDirections.length = 0; + this.states.directionalLightDiffuseColors.length = 0; + this.states.directionalLightSpecularColors.length = 0; - ////////////////////////////////////////////// - // COLOR - ////////////////////////////////////////////// - /** - * Basic fill material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } - * - * function draw() { - * background(0); - * noStroke(); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
- * - * @alt - * black canvas with purple cube spinning - */ - fill(v1, v2, v3, a) { - //see material.js for more info on color blending in webgl - const color = p5.prototype.color.apply(this._pInst, arguments); - this.states.curFillColor = color._array; - this.states.drawMode = constants.FILL; - this.states._useNormalMaterial = false; - this.states._tex = null; - } + this.states.pointLightPositions.length = 0; + this.states.pointLightDiffuseColors.length = 0; + this.states.pointLightSpecularColors.length = 0; - /** - * Basic stroke material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } - * - * function draw() { - * background(0); - * stroke(240, 150, 150); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
- * - * @alt - * black canvas with purple cube with pink outline spinning - */ - stroke(r, g, b, a) { - const color = p5.prototype.color.apply(this._pInst, arguments); - this.states.curStrokeColor = color._array; - } + this.states.spotLightPositions.length = 0; + this.states.spotLightDirections.length = 0; + this.states.spotLightDiffuseColors.length = 0; + this.states.spotLightSpecularColors.length = 0; + this.states.spotLightAngle.length = 0; + this.states.spotLightConc.length = 0; - strokeCap(cap) { - this.curStrokeCap = cap; - } + this.states._enableLighting = false; - strokeJoin(join) { - this.curStrokeJoin = join; - } - getFilterLayer() { - if (!this.filterLayer) { - this.filterLayer = this._pInst.createFramebuffer(); + //reset tint value for new frame + this.states.tint = [255, 255, 255, 255]; + + //Clear depth every frame + this.GL.clearStencil(0); + this.GL.clear(this.GL.DEPTH_BUFFER_BIT | this.GL.STENCIL_BUFFER_BIT); + this.GL.disable(this.GL.STENCIL_TEST); } - return this.filterLayer; - } - getFilterLayerTemp() { - if (!this.filterLayerTemp) { - this.filterLayerTemp = this._pInst.createFramebuffer(); + + /** + * [background description] + */ + background(...args) { + const _col = this._pInst.color(...args); + const _r = _col.levels[0] / 255; + const _g = _col.levels[1] / 255; + const _b = _col.levels[2] / 255; + const _a = _col.levels[3] / 255; + this.clear(_r, _g, _b, _a); + } + + ////////////////////////////////////////////// + // COLOR + ////////////////////////////////////////////// + /** + * Basic fill material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @chainable + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * noStroke(); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube spinning + */ + fill(v1, v2, v3, a) { + //see material.js for more info on color blending in webgl + const color = fn.color.apply(this._pInst, arguments); + this.states.curFillColor = color._array; + this.states.drawMode = constants.FILL; + this.states._useNormalMaterial = false; + this.states._tex = null; + } + + /** + * Basic stroke material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * stroke(240, 150, 150); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube with pink outline spinning + */ + stroke(r, g, b, a) { + const color = fn.color.apply(this._pInst, arguments); + this.states.curStrokeColor = color._array; } - return this.filterLayerTemp; - } - matchSize(fboToMatch, target) { - if ( - fboToMatch.width !== target.width || - fboToMatch.height !== target.height - ) { - fboToMatch.resize(target.width, target.height); + + strokeCap(cap) { + this.curStrokeCap = cap; } - if (fboToMatch.pixelDensity() !== target.pixelDensity()) { - fboToMatch.pixelDensity(target.pixelDensity()); + strokeJoin(join) { + this.curStrokeJoin = join; } - } - filter(...args) { - - let fbo = this.getFilterLayer(); - - // use internal shader for filter constants BLUR, INVERT, etc - let filterParameter = undefined; - let operation = undefined; - if (typeof args[0] === 'string') { - operation = args[0]; - let defaults = { - [constants.BLUR]: 3, - [constants.POSTERIZE]: 4, - [constants.THRESHOLD]: 0.5 - }; - let useDefaultParam = operation in defaults && args[1] === undefined; - filterParameter = useDefaultParam ? defaults[operation] : args[1]; - - // Create and store shader for constants once on initial filter call. - // Need to store multiple in case user calls different filters, - // eg. filter(BLUR) then filter(GRAY) - if (!(operation in this.defaultFilterShaders)) { - this.defaultFilterShaders[operation] = new p5.Shader( - fbo._renderer, - filterShaderVert, - filterShaderFrags[operation] - ); + getFilterLayer() { + if (!this.filterLayer) { + this.filterLayer = this._pInst.createFramebuffer(); } - this.states.filterShader = this.defaultFilterShaders[operation]; - + return this.filterLayer; } - // use custom user-supplied shader - else { - this.states.filterShader = args[0]; + getFilterLayerTemp() { + if (!this.filterLayerTemp) { + this.filterLayerTemp = this._pInst.createFramebuffer(); + } + return this.filterLayerTemp; } + matchSize(fboToMatch, target) { + if ( + fboToMatch.width !== target.width || + fboToMatch.height !== target.height + ) { + fboToMatch.resize(target.width, target.height); + } - // Setting the target to the framebuffer when applying a filter to a framebuffer. + if (fboToMatch.pixelDensity() !== target.pixelDensity()) { + fboToMatch.pixelDensity(target.pixelDensity()); + } + } + filter(...args) { + + let fbo = this.getFilterLayer(); + + // use internal shader for filter constants BLUR, INVERT, etc + let filterParameter = undefined; + let operation = undefined; + if (typeof args[0] === 'string') { + operation = args[0]; + let defaults = { + [constants.BLUR]: 3, + [constants.POSTERIZE]: 4, + [constants.THRESHOLD]: 0.5 + }; + let useDefaultParam = operation in defaults && args[1] === undefined; + filterParameter = useDefaultParam ? defaults[operation] : args[1]; + + // Create and store shader for constants once on initial filter call. + // Need to store multiple in case user calls different filters, + // eg. filter(BLUR) then filter(GRAY) + if (!(operation in this.defaultFilterShaders)) { + this.defaultFilterShaders[operation] = new p5.Shader( + fbo._renderer, + filterShaderVert, + filterShaderFrags[operation] + ); + } + this.states.filterShader = this.defaultFilterShaders[operation]; - const target = this.activeFramebuffer() || this; + } + // use custom user-supplied shader + else { + this.states.filterShader = args[0]; + } - // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. - this.matchSize(fbo, target); + // Setting the target to the framebuffer when applying a filter to a framebuffer. - fbo.draw(() => this._pInst.clear()); // prevent undesirable feedback effects accumulating secretly. + const target = this.activeFramebuffer() || this; - let texelSize = [ - 1 / (target.width * target.pixelDensity()), - 1 / (target.height * target.pixelDensity()) - ]; + // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. + this.matchSize(fbo, target); - // apply blur shader with multiple passes. - if (operation === constants.BLUR) { - // Treating 'tmp' as a framebuffer. - const tmp = this.getFilterLayerTemp(); - // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. - this.matchSize(tmp, target); - // setup - this._pInst.push(); - this._pInst.noStroke(); - this._pInst.blendMode(constants.BLEND); + fbo.draw(() => this._pInst.clear()); // prevent undesirable feedback effects accumulating secretly. - // draw main to temp buffer - this._pInst.shader(this.states.filterShader); - this.states.filterShader.setUniform('texelSize', texelSize); - this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); - this.states.filterShader.setUniform('radius', Math.max(1, filterParameter)); - - // Horiz pass: draw `target` to `tmp` - tmp.draw(() => { - this.states.filterShader.setUniform('direction', [1, 0]); - this.states.filterShader.setUniform('tex0', target); - this._pInst.clear(); - this._pInst.shader(this.states.filterShader); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); - }); + let texelSize = [ + 1 / (target.width * target.pixelDensity()), + 1 / (target.height * target.pixelDensity()) + ]; - // Vert pass: draw `tmp` to `fbo` - fbo.draw(() => { - this.states.filterShader.setUniform('direction', [0, 1]); - this.states.filterShader.setUniform('tex0', tmp); - this._pInst.clear(); - this._pInst.shader(this.states.filterShader); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); - }); - - this._pInst.pop(); - } - // every other non-blur shader uses single pass - else { - fbo.draw(() => { + // apply blur shader with multiple passes. + if (operation === constants.BLUR) { + // Treating 'tmp' as a framebuffer. + const tmp = this.getFilterLayerTemp(); + // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. + this.matchSize(tmp, target); + // setup + this._pInst.push(); this._pInst.noStroke(); this._pInst.blendMode(constants.BLEND); + + // draw main to temp buffer this._pInst.shader(this.states.filterShader); - this.states.filterShader.setUniform('tex0', target); this.states.filterShader.setUniform('texelSize', texelSize); this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); - // filterParameter uniform only used for POSTERIZE, and THRESHOLD - // but shouldn't hurt to always set - this.states.filterShader.setUniform('filterParameter', filterParameter); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); - }); + this.states.filterShader.setUniform('radius', Math.max(1, filterParameter)); + + // Horiz pass: draw `target` to `tmp` + tmp.draw(() => { + this.states.filterShader.setUniform('direction', [1, 0]); + this.states.filterShader.setUniform('tex0', target); + this._pInst.clear(); + this._pInst.shader(this.states.filterShader); + this._pInst.noLights(); + this._pInst.plane(target.width, target.height); + }); + + // Vert pass: draw `tmp` to `fbo` + fbo.draw(() => { + this.states.filterShader.setUniform('direction', [0, 1]); + this.states.filterShader.setUniform('tex0', tmp); + this._pInst.clear(); + this._pInst.shader(this.states.filterShader); + this._pInst.noLights(); + this._pInst.plane(target.width, target.height); + }); + + this._pInst.pop(); + } + // every other non-blur shader uses single pass + else { + fbo.draw(() => { + this._pInst.noStroke(); + this._pInst.blendMode(constants.BLEND); + this._pInst.shader(this.states.filterShader); + this.states.filterShader.setUniform('tex0', target); + this.states.filterShader.setUniform('texelSize', texelSize); + this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); + // filterParameter uniform only used for POSTERIZE, and THRESHOLD + // but shouldn't hurt to always set + this.states.filterShader.setUniform('filterParameter', filterParameter); + this._pInst.noLights(); + this._pInst.plane(target.width, target.height); + }); + } + // draw fbo contents onto main renderer. + this._pInst.push(); + this._pInst.noStroke(); + this.clear(); + this._pInst.push(); + this._pInst.imageMode(constants.CORNER); + this._pInst.blendMode(constants.BLEND); + target.filterCamera._resize(); + this._pInst.setCamera(target.filterCamera); + this._pInst.resetMatrix(); + this._pInst.image(fbo, -target.width / 2, -target.height / 2, + target.width, target.height); + this._pInst.clearDepth(); + this._pInst.pop(); + this._pInst.pop(); } - // draw fbo contents onto main renderer. - this._pInst.push(); - this._pInst.noStroke(); - this.clear(); - this._pInst.push(); - this._pInst.imageMode(constants.CORNER); - this._pInst.blendMode(constants.BLEND); - target.filterCamera._resize(); - this._pInst.setCamera(target.filterCamera); - this._pInst.resetMatrix(); - this._pInst.image(fbo, -target.width / 2, -target.height / 2, - target.width, target.height); - this._pInst.clearDepth(); - this._pInst.pop(); - this._pInst.pop(); - } - // Pass this off to the host instance so that we can treat a renderer and a - // framebuffer the same in filter() + // Pass this off to the host instance so that we can treat a renderer and a + // framebuffer the same in filter() - pixelDensity(newDensity) { - if (newDensity) { - return this._pInst.pixelDensity(newDensity); + pixelDensity(newDensity) { + if (newDensity) { + return this._pInst.pixelDensity(newDensity); + } + return this._pInst.pixelDensity(); + } + + blendMode(mode) { + if ( + mode === constants.DARKEST || + mode === constants.LIGHTEST || + mode === constants.ADD || + mode === constants.BLEND || + mode === constants.SUBTRACT || + mode === constants.SCREEN || + mode === constants.EXCLUSION || + mode === constants.REPLACE || + mode === constants.MULTIPLY || + mode === constants.REMOVE + ) + this.states.curBlendMode = mode; + else if ( + mode === constants.BURN || + mode === constants.OVERLAY || + mode === constants.HARD_LIGHT || + mode === constants.SOFT_LIGHT || + mode === constants.DODGE + ) { + console.warn( + 'BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.' + ); + } } - return this._pInst.pixelDensity(); - } - blendMode(mode) { - if ( - mode === constants.DARKEST || - mode === constants.LIGHTEST || - mode === constants.ADD || - mode === constants.BLEND || - mode === constants.SUBTRACT || - mode === constants.SCREEN || - mode === constants.EXCLUSION || - mode === constants.REPLACE || - mode === constants.MULTIPLY || - mode === constants.REMOVE - ) - this.states.curBlendMode = mode; - else if ( - mode === constants.BURN || - mode === constants.OVERLAY || - mode === constants.HARD_LIGHT || - mode === constants.SOFT_LIGHT || - mode === constants.DODGE - ) { - console.warn( - 'BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.' - ); + erase(opacityFill, opacityStroke) { + if (!this._isErasing) { + this.preEraseBlend = this.states.curBlendMode; + this._isErasing = true; + this.blendMode(constants.REMOVE); + this._cachedFillStyle = this.states.curFillColor.slice(); + this.states.curFillColor = [1, 1, 1, opacityFill / 255]; + this._cachedStrokeStyle = this.states.curStrokeColor.slice(); + this.states.curStrokeColor = [1, 1, 1, opacityStroke / 255]; + } } - } - erase(opacityFill, opacityStroke) { - if (!this._isErasing) { - this.preEraseBlend = this.states.curBlendMode; - this._isErasing = true; - this.blendMode(constants.REMOVE); - this._cachedFillStyle = this.states.curFillColor.slice(); - this.states.curFillColor = [1, 1, 1, opacityFill / 255]; - this._cachedStrokeStyle = this.states.curStrokeColor.slice(); - this.states.curStrokeColor = [1, 1, 1, opacityStroke / 255]; + noErase() { + if (this._isErasing) { + // Restore colors + this.states.curFillColor = this._cachedFillStyle.slice(); + this.states.curStrokeColor = this._cachedStrokeStyle.slice(); + // Restore blend mode + this.states.curBlendMode = this.preEraseBlend; + this.blendMode(this.preEraseBlend); + // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode + this._isErasing = false; + this._applyBlendMode(); + } } - } - noErase() { - if (this._isErasing) { - // Restore colors - this.states.curFillColor = this._cachedFillStyle.slice(); - this.states.curStrokeColor = this._cachedStrokeStyle.slice(); - // Restore blend mode - this.states.curBlendMode = this.preEraseBlend; - this.blendMode(this.preEraseBlend); - // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode - this._isErasing = false; - this._applyBlendMode(); + drawTarget() { + return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this; } - } - - drawTarget() { - return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this; - } - beginClip(options = {}) { - super.beginClip(options); - - this.drawTarget()._isClipApplied = true; - - const gl = this.GL; - gl.clearStencil(0); - gl.clear(gl.STENCIL_BUFFER_BIT); - gl.enable(gl.STENCIL_TEST); - this._stencilTestOn = true; - gl.stencilFunc( - gl.ALWAYS, // the test - 1, // reference value - 0xff // mask - ); - gl.stencilOp( - gl.KEEP, // what to do if the stencil test fails - gl.KEEP, // what to do if the depth test fails - gl.REPLACE // what to do if both tests pass - ); - gl.disable(gl.DEPTH_TEST); - - this._pInst.push(); - this._pInst.resetShader(); - if (this.states.doFill) this._pInst.fill(0, 0); - if (this.states.doStroke) this._pInst.stroke(0, 0); - } + beginClip(options = {}) { + super.beginClip(options); - endClip() { - this._pInst.pop(); - - const gl = this.GL; - gl.stencilOp( - gl.KEEP, // what to do if the stencil test fails - gl.KEEP, // what to do if the depth test fails - gl.KEEP // what to do if both tests pass - ); - gl.stencilFunc( - this._clipInvert ? gl.EQUAL : gl.NOTEQUAL, // the test - 0, // reference value - 0xff // mask - ); - gl.enable(gl.DEPTH_TEST); - - // Mark the depth at which the clip has been applied so that we can clear it - // when we pop past this depth - this._clipDepths.push(this._pushPopDepth); - - super.endClip(); - } + this.drawTarget()._isClipApplied = true; - _clearClip() { - this.GL.clearStencil(1); - this.GL.clear(this.GL.STENCIL_BUFFER_BIT); - if (this._clipDepths.length > 0) { - this._clipDepths.pop(); - } - this.drawTarget()._isClipApplied = false; - } + const gl = this.GL; + gl.clearStencil(0); + gl.clear(gl.STENCIL_BUFFER_BIT); + gl.enable(gl.STENCIL_TEST); + this._stencilTestOn = true; + gl.stencilFunc( + gl.ALWAYS, // the test + 1, // reference value + 0xff // mask + ); + gl.stencilOp( + gl.KEEP, // what to do if the stencil test fails + gl.KEEP, // what to do if the depth test fails + gl.REPLACE // what to do if both tests pass + ); + gl.disable(gl.DEPTH_TEST); - /** - * Change weight of stroke - * @param {Number} stroke weight to be used for drawing - * @example - *
- * - * function setup() { - * createCanvas(200, 400, WEBGL); - * setAttributes('antialias', true); - * } - * - * function draw() { - * background(0); - * noStroke(); - * translate(0, -100, 0); - * stroke(240, 150, 150); - * fill(100, 100, 240); - * push(); - * strokeWeight(8); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * sphere(75); - * pop(); - * push(); - * translate(0, 200, 0); - * strokeWeight(1); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * sphere(75); - * pop(); - * } - * - *
- * - * @alt - * black canvas with two purple rotating spheres with pink - * outlines the sphere on top has much heavier outlines, - */ - strokeWeight(w) { - if (this.curStrokeWeight !== w) { - this.pointSize = w; - this.curStrokeWeight = w; + this._pInst.push(); + this._pInst.resetShader(); + if (this.states.doFill) this._pInst.fill(0, 0); + if (this.states.doStroke) this._pInst.stroke(0, 0); } - } - // x,y are canvas-relative (pre-scaled by _pixelDensity) - _getPixel(x, y) { - const gl = this.GL; - return readPixelWebGL( - gl, - null, - x, - y, - gl.RGBA, - gl.UNSIGNED_BYTE, - this._pInst.height * this._pInst.pixelDensity() - ); - } + endClip() { + this._pInst.pop(); - /** - * Loads the pixels data for this canvas into the pixels[] attribute. - * Note that updatePixels() and set() do not work. - * Any pixel manipulation must be done directly to the pixels[] array. - * - * @private - */ + const gl = this.GL; + gl.stencilOp( + gl.KEEP, // what to do if the stencil test fails + gl.KEEP, // what to do if the depth test fails + gl.KEEP // what to do if both tests pass + ); + gl.stencilFunc( + this._clipInvert ? gl.EQUAL : gl.NOTEQUAL, // the test + 0, // reference value + 0xff // mask + ); + gl.enable(gl.DEPTH_TEST); - loadPixels() { - const pixelsState = this._pixelsState; + // Mark the depth at which the clip has been applied so that we can clear it + // when we pop past this depth + this._clipDepths.push(this._pushPopDepth); - //@todo_FES - if (this._pInst._glAttributes.preserveDrawingBuffer !== true) { - console.log( - 'loadPixels only works in WebGL when preserveDrawingBuffer ' + 'is true.' - ); - return; + super.endClip(); } - const pd = this._pixelDensity; - const gl = this.GL; + _clearClip() { + this.GL.clearStencil(1); + this.GL.clear(this.GL.STENCIL_BUFFER_BIT); + if (this._clipDepths.length > 0) { + this._clipDepths.pop(); + } + this.drawTarget()._isClipApplied = false; + } + + /** + * Change weight of stroke + * @param {Number} stroke weight to be used for drawing + * @example + *
+ * + * function setup() { + * createCanvas(200, 400, WEBGL); + * setAttributes('antialias', true); + * } + * + * function draw() { + * background(0); + * noStroke(); + * translate(0, -100, 0); + * stroke(240, 150, 150); + * fill(100, 100, 240); + * push(); + * strokeWeight(8); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * sphere(75); + * pop(); + * push(); + * translate(0, 200, 0); + * strokeWeight(1); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * sphere(75); + * pop(); + * } + * + *
+ * + * @alt + * black canvas with two purple rotating spheres with pink + * outlines the sphere on top has much heavier outlines, + */ + strokeWeight(w) { + if (this.curStrokeWeight !== w) { + this.pointSize = w; + this.curStrokeWeight = w; + } + } - pixelsState.pixels = - readPixelsWebGL( - pixelsState.pixels, + // x,y are canvas-relative (pre-scaled by _pixelDensity) + _getPixel(x, y) { + const gl = this.GL; + return readPixelWebGL( gl, null, - 0, - 0, - this.width * pd, - this.height * pd, + x, + y, gl.RGBA, gl.UNSIGNED_BYTE, - this.height * pd + this._pInst.height * this._pInst.pixelDensity() ); - } - - updatePixels() { - const fbo = this._getTempFramebuffer(); - fbo.pixels = this._pixelsState.pixels; - fbo.updatePixels(); - this._pInst.push(); - this._pInst.resetMatrix(); - this._pInst.clear(); - this._pInst.imageMode(constants.CENTER); - this._pInst.image(fbo, 0, 0); - this._pInst.pop(); - this.GL.clearDepth(1); - this.GL.clear(this.GL.DEPTH_BUFFER_BIT); - } - - /** - * @private - * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings - * of the renderer's canvas. It will be created if it does not yet exist, and - * reused if it does. - */ - _getTempFramebuffer() { - if (!this._tempFramebuffer) { - this._tempFramebuffer = this._pInst.createFramebuffer({ - format: constants.UNSIGNED_BYTE, - useDepth: this._pInst._glAttributes.depth, - depthFormat: constants.UNSIGNED_INT, - antialias: this._pInst._glAttributes.antialias - }); } - return this._tempFramebuffer; - } - + /** + * Loads the pixels data for this canvas into the pixels[] attribute. + * Note that updatePixels() and set() do not work. + * Any pixel manipulation must be done directly to the pixels[] array. + * + * @private + */ - ////////////////////////////////////////////// - // HASH | for geometry - ////////////////////////////////////////////// + loadPixels() { + const pixelsState = this._pixelsState; - geometryInHash(gId) { - return this.retainedMode.geometry[gId] !== undefined; - } + //@todo_FES + if (this._pInst._glAttributes.preserveDrawingBuffer !== true) { + console.log( + 'loadPixels only works in WebGL when preserveDrawingBuffer ' + 'is true.' + ); + return; + } - viewport(w, h) { - this._viewport = [0, 0, w, h]; - this.GL.viewport(0, 0, w, h); - } + const pd = this._pixelDensity; + const gl = this.GL; - /** - * [resize description] - * @private - * @param {Number} w [description] - * @param {Number} h [description] - */ - resize(w, h) { - super.resize(w, h); - - // save canvas properties - const props = {}; - for (const key in this.drawingContext) { - const val = this.drawingContext[key]; - if (typeof val !== 'object' && typeof val !== 'function') { - props[key] = val; - } - } - - const dimensions = this._adjustDimensions(w, h); - w = dimensions.adjustedWidth; - h = dimensions.adjustedHeight; - - this.width = w; - this.height = h; - - this.canvas.width = w * this._pixelDensity; - this.canvas.height = h * this._pixelDensity; - this.canvas.style.width = `${w}px`; - this.canvas.style.height = `${h}px`; - this._origViewport = { - width: this.GL.drawingBufferWidth, - height: this.GL.drawingBufferHeight - }; - this.viewport( - this._origViewport.width, - this._origViewport.height - ); - - this.states.curCamera._resize(); - - //resize pixels buffer - const pixelsState = this._pixelsState; - if (typeof pixelsState.pixels !== 'undefined') { pixelsState.pixels = - new Uint8Array( - this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 + readPixelsWebGL( + pixelsState.pixels, + gl, + null, + 0, + 0, + this.width * pd, + this.height * pd, + gl.RGBA, + gl.UNSIGNED_BYTE, + this.height * pd ); } - for (const framebuffer of this.framebuffers) { - // Notify framebuffers of the resize so that any auto-sized framebuffers - // can also update their size - framebuffer._canvasSizeChanged(); + updatePixels() { + const fbo = this._getTempFramebuffer(); + fbo.pixels = this._pixelsState.pixels; + fbo.updatePixels(); + this._pInst.push(); + this._pInst.resetMatrix(); + this._pInst.clear(); + this._pInst.imageMode(constants.CENTER); + this._pInst.image(fbo, 0, 0); + this._pInst.pop(); + this.GL.clearDepth(1); + this.GL.clear(this.GL.DEPTH_BUFFER_BIT); } - // reset canvas properties - for (const savedKey in props) { - try { - this.drawingContext[savedKey] = props[savedKey]; - } catch (err) { - // ignore read-only property errors + /** + * @private + * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings + * of the renderer's canvas. It will be created if it does not yet exist, and + * reused if it does. + */ + _getTempFramebuffer() { + if (!this._tempFramebuffer) { + this._tempFramebuffer = this._pInst.createFramebuffer({ + format: constants.UNSIGNED_BYTE, + useDepth: this._pInst._glAttributes.depth, + depthFormat: constants.UNSIGNED_INT, + antialias: this._pInst._glAttributes.antialias + }); } + return this._tempFramebuffer; } - } - /** - * clears color and depth buffers - * with r,g,b,a - * @private - * @param {Number} r normalized red val. - * @param {Number} g normalized green val. - * @param {Number} b normalized blue val. - * @param {Number} a normalized alpha val. - */ - clear(...args) { - const _r = args[0] || 0; - const _g = args[1] || 0; - const _b = args[2] || 0; - let _a = args[3] || 0; - - const activeFramebuffer = this.activeFramebuffer(); - if ( - activeFramebuffer && - activeFramebuffer.format === constants.UNSIGNED_BYTE && - !activeFramebuffer.antialias && - _a === 0 - ) { - // Drivers on Intel Macs check for 0,0,0,0 exactly when drawing to a - // framebuffer and ignore the command if it's the only drawing command to - // the framebuffer. To work around it, we can set the alpha to a value so - // low that it still rounds down to 0, but that circumvents the buggy - // check in the driver. - _a = 1e-10; + + + ////////////////////////////////////////////// + // HASH | for geometry + ////////////////////////////////////////////// + + geometryInHash(gId) { + return this.retainedMode.geometry[gId] !== undefined; } - this.GL.clearColor(_r * _a, _g * _a, _b * _a, _a); - this.GL.clearDepth(1); - this.GL.clear(this.GL.COLOR_BUFFER_BIT | this.GL.DEPTH_BUFFER_BIT); - } + viewport(w, h) { + this._viewport = [0, 0, w, h]; + this.GL.viewport(0, 0, w, h); + } - /** - * Resets all depth information so that nothing previously drawn will - * occlude anything subsequently drawn. + /** + * [resize description] + * @private + * @param {Number} w [description] + * @param {Number} h [description] */ - clearDepth(depth = 1) { - this.GL.clearDepth(depth); - this.GL.clear(this.GL.DEPTH_BUFFER_BIT); - } + resize(w, h) { + super.resize(w, h); + + // save canvas properties + const props = {}; + for (const key in this.drawingContext) { + const val = this.drawingContext[key]; + if (typeof val !== 'object' && typeof val !== 'function') { + props[key] = val; + } + } - applyMatrix(a, b, c, d, e, f) { - if (arguments.length === 16) { - p5.Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); - } else { - this.states.uModelMatrix.apply([ - a, b, 0, 0, - c, d, 0, 0, - 0, 0, 1, 0, - e, f, 0, 1 - ]); - } - } + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; - /** - * [translate description] - * @private - * @param {Number} x [description] - * @param {Number} y [description] - * @param {Number} z [description] - * @chainable - * @todo implement handle for components or vector as args - */ - translate(x, y, z) { - if (x instanceof p5.Vector) { - z = x.z; - y = x.y; - x = x.x; - } - this.states.uModelMatrix.translate([x, y, z]); - return this; - } + this.width = w; + this.height = h; - /** - * Scales the Model View Matrix by a vector - * @private - * @param {Number | p5.Vector | Array} x [description] - * @param {Number} [y] y-axis scalar - * @param {Number} [z] z-axis scalar - * @chainable - */ - scale(x, y, z) { - this.states.uModelMatrix.scale(x, y, z); - return this; - } + this.canvas.width = w * this._pixelDensity; + this.canvas.height = h * this._pixelDensity; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; + this._origViewport = { + width: this.GL.drawingBufferWidth, + height: this.GL.drawingBufferHeight + }; + this.viewport( + this._origViewport.width, + this._origViewport.height + ); - rotate(rad, axis) { - if (typeof axis === 'undefined') { - return this.rotateZ(rad); - } - p5.Matrix.prototype.rotate.apply(this.states.uModelMatrix, arguments); - return this; - } + this.states.curCamera._resize(); - rotateX(rad) { - this.rotate(rad, 1, 0, 0); - return this; - } + //resize pixels buffer + const pixelsState = this._pixelsState; + if (typeof pixelsState.pixels !== 'undefined') { + pixelsState.pixels = + new Uint8Array( + this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 + ); + } - rotateY(rad) { - this.rotate(rad, 0, 1, 0); - return this; - } + for (const framebuffer of this.framebuffers) { + // Notify framebuffers of the resize so that any auto-sized framebuffers + // can also update their size + framebuffer._canvasSizeChanged(); + } - rotateZ(rad) { - this.rotate(rad, 0, 0, 1); - return this; - } + // reset canvas properties + for (const savedKey in props) { + try { + this.drawingContext[savedKey] = props[savedKey]; + } catch (err) { + // ignore read-only property errors + } + } + } - pop(...args) { - if ( - this._clipDepths.length > 0 && - this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] - ) { - this._clearClip(); + /** + * clears color and depth buffers + * with r,g,b,a + * @private + * @param {Number} r normalized red val. + * @param {Number} g normalized green val. + * @param {Number} b normalized blue val. + * @param {Number} a normalized alpha val. + */ + clear(...args) { + const _r = args[0] || 0; + const _g = args[1] || 0; + const _b = args[2] || 0; + let _a = args[3] || 0; + + const activeFramebuffer = this.activeFramebuffer(); + if ( + activeFramebuffer && + activeFramebuffer.format === constants.UNSIGNED_BYTE && + !activeFramebuffer.antialias && + _a === 0 + ) { + // Drivers on Intel Macs check for 0,0,0,0 exactly when drawing to a + // framebuffer and ignore the command if it's the only drawing command to + // the framebuffer. To work around it, we can set the alpha to a value so + // low that it still rounds down to 0, but that circumvents the buggy + // check in the driver. + _a = 1e-10; + } + + this.GL.clearColor(_r * _a, _g * _a, _b * _a, _a); + this.GL.clearDepth(1); + this.GL.clear(this.GL.COLOR_BUFFER_BIT | this.GL.DEPTH_BUFFER_BIT); } - super.pop(...args); - this._applyStencilTestIfClipping(); - } - _applyStencilTestIfClipping() { - const drawTarget = this.drawTarget(); - if (drawTarget._isClipApplied !== this._stencilTestOn) { - if (drawTarget._isClipApplied) { - this.GL.enable(this.GL.STENCIL_TEST); - this._stencilTestOn = true; + + /** + * Resets all depth information so that nothing previously drawn will + * occlude anything subsequently drawn. + */ + clearDepth(depth = 1) { + this.GL.clearDepth(depth); + this.GL.clear(this.GL.DEPTH_BUFFER_BIT); + } + + applyMatrix(a, b, c, d, e, f) { + if (arguments.length === 16) { + p5.Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); } else { - this.GL.disable(this.GL.STENCIL_TEST); - this._stencilTestOn = false; + this.states.uModelMatrix.apply([ + a, b, 0, 0, + c, d, 0, 0, + 0, 0, 1, 0, + e, f, 0, 1 + ]); } } - } - resetMatrix() { - this.states.uModelMatrix.reset(); - this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); - return this; - } - ////////////////////////////////////////////// - // SHADER - ////////////////////////////////////////////// + /** + * [translate description] + * @private + * @param {Number} x [description] + * @param {Number} y [description] + * @param {Number} z [description] + * @chainable + * @todo implement handle for components or vector as args + */ + translate(x, y, z) { + if (x instanceof p5.Vector) { + z = x.z; + y = x.y; + x = x.x; + } + this.states.uModelMatrix.translate([x, y, z]); + return this; + } - /* - * shaders are created and cached on a per-renderer basis, - * on the grounds that each renderer will have its own gl context - * and the shader must be valid in that context. - */ + /** + * Scales the Model View Matrix by a vector + * @private + * @param {Number | p5.Vector | Array} x [description] + * @param {Number} [y] y-axis scalar + * @param {Number} [z] z-axis scalar + * @chainable + */ + scale(x, y, z) { + this.states.uModelMatrix.scale(x, y, z); + return this; + } - _getImmediateStrokeShader() { - // select the stroke shader to use - const stroke = this.states.userStrokeShader; - if (!stroke || !stroke.isStrokeShader()) { - return this._getLineShader(); + rotate(rad, axis) { + if (typeof axis === 'undefined') { + return this.rotateZ(rad); + } + p5.Matrix.prototype.rotate.apply(this.states.uModelMatrix, arguments); + return this; } - return stroke; - } + rotateX(rad) { + this.rotate(rad, 1, 0, 0); + return this; + } - _getRetainedStrokeShader() { - return this._getImmediateStrokeShader(); - } + rotateY(rad) { + this.rotate(rad, 0, 1, 0); + return this; + } - _getSphereMapping(img) { - if (!this.sphereMapping) { - this.sphereMapping = this._pInst.createFilterShader( - sphereMapping - ); + rotateZ(rad) { + this.rotate(rad, 0, 0, 1); + return this; + } + + pop(...args) { + if ( + this._clipDepths.length > 0 && + this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] + ) { + this._clearClip(); + } + super.pop(...args); + this._applyStencilTestIfClipping(); + } + _applyStencilTestIfClipping() { + const drawTarget = this.drawTarget(); + if (drawTarget._isClipApplied !== this._stencilTestOn) { + if (drawTarget._isClipApplied) { + this.GL.enable(this.GL.STENCIL_TEST); + this._stencilTestOn = true; + } else { + this.GL.disable(this.GL.STENCIL_TEST); + this._stencilTestOn = false; + } + } + } + resetMatrix() { + this.states.uModelMatrix.reset(); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + return this; } - this.states.uNMatrix.inverseTranspose(this.states.uViewMatrix); - this.states.uNMatrix.invert3x3(this.states.uNMatrix); - this.sphereMapping.setUniform('uFovY', this.states.curCamera.cameraFOV); - this.sphereMapping.setUniform('uAspect', this.states.curCamera.aspectRatio); - this.sphereMapping.setUniform('uNewNormalMatrix', this.states.uNMatrix.mat3); - this.sphereMapping.setUniform('uSampler', img); - return this.sphereMapping; - } - /* - * selects which fill shader should be used based on renderer state, - * for use with begin/endShape and immediate vertex mode. + ////////////////////////////////////////////// + // SHADER + ////////////////////////////////////////////// + + /* + * shaders are created and cached on a per-renderer basis, + * on the grounds that each renderer will have its own gl context + * and the shader must be valid in that context. */ - _getImmediateFillShader() { - const fill = this.states.userFillShader; - if (this.states._useNormalMaterial) { - if (!fill || !fill.isNormalShader()) { - return this._getNormalShader(); + + _getImmediateStrokeShader() { + // select the stroke shader to use + const stroke = this.states.userStrokeShader; + if (!stroke || !stroke.isStrokeShader()) { + return this._getLineShader(); } + return stroke; + } + + + _getRetainedStrokeShader() { + return this._getImmediateStrokeShader(); } - if (this.states._enableLighting) { - if (!fill || !fill.isLightShader()) { - return this._getLightShader(); + + _getSphereMapping(img) { + if (!this.sphereMapping) { + this.sphereMapping = this._pInst.createFilterShader( + sphereMapping + ); } - } else if (this.states._tex) { - if (!fill || !fill.isTextureShader()) { - return this._getLightShader(); + this.states.uNMatrix.inverseTranspose(this.states.uViewMatrix); + this.states.uNMatrix.invert3x3(this.states.uNMatrix); + this.sphereMapping.setUniform('uFovY', this.states.curCamera.cameraFOV); + this.sphereMapping.setUniform('uAspect', this.states.curCamera.aspectRatio); + this.sphereMapping.setUniform('uNewNormalMatrix', this.states.uNMatrix.mat3); + this.sphereMapping.setUniform('uSampler', img); + return this.sphereMapping; + } + + /* + * selects which fill shader should be used based on renderer state, + * for use with begin/endShape and immediate vertex mode. + */ + _getImmediateFillShader() { + const fill = this.states.userFillShader; + if (this.states._useNormalMaterial) { + if (!fill || !fill.isNormalShader()) { + return this._getNormalShader(); + } + } + if (this.states._enableLighting) { + if (!fill || !fill.isLightShader()) { + return this._getLightShader(); + } + } else if (this.states._tex) { + if (!fill || !fill.isTextureShader()) { + return this._getLightShader(); + } + } else if (!fill /*|| !fill.isColorShader()*/) { + return this._getImmediateModeShader(); } - } else if (!fill /*|| !fill.isColorShader()*/) { - return this._getImmediateModeShader(); + return fill; } - return fill; - } - /* - * selects which fill shader should be used based on renderer state - * for retained mode. - */ - _getRetainedFillShader() { - if (this.states._useNormalMaterial) { - return this._getNormalShader(); + /* + * selects which fill shader should be used based on renderer state + * for retained mode. + */ + _getRetainedFillShader() { + if (this.states._useNormalMaterial) { + return this._getNormalShader(); + } + + const fill = this.states.userFillShader; + if (this.states._enableLighting) { + if (!fill || !fill.isLightShader()) { + return this._getLightShader(); + } + } else if (this.states._tex) { + if (!fill || !fill.isTextureShader()) { + return this._getLightShader(); + } + } else if (!fill /* || !fill.isColorShader()*/) { + return this._getColorShader(); + } + return fill; } - const fill = this.states.userFillShader; - if (this.states._enableLighting) { - if (!fill || !fill.isLightShader()) { - return this._getLightShader(); + _getImmediatePointShader() { + // select the point shader to use + const point = this.states.userPointShader; + if (!point || !point.isPointShader()) { + return this._getPointShader(); } - } else if (this.states._tex) { - if (!fill || !fill.isTextureShader()) { - return this._getLightShader(); + return point; + } + + _getRetainedLineShader() { + return this._getImmediateLineShader(); + } + + baseMaterialShader() { + if (!this._pInst._glAttributes.perPixelLighting) { + throw new Error( + 'The material shader does not support hooks without perPixelLighting. Try turning it back on.' + ); } - } else if (!fill /* || !fill.isColorShader()*/) { - return this._getColorShader(); + return this._getLightShader(); + } + + _getLightShader() { + if (!this._defaultLightShader) { + if (this._pInst._glAttributes.perPixelLighting) { + this._defaultLightShader = new p5.Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'highp') + + defaultShaders.phongVert, + this._webGL2CompatibilityPrefix('frag', 'highp') + + defaultShaders.phongFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', + 'vec4 combineColors': `(ColorComponents components) { + vec4 color = vec4(0.); + color.rgb += components.diffuse * components.baseColor; + color.rgb += components.ambient * components.ambientColor; + color.rgb += components.specular * components.specularColor; + color.rgb += components.emissive; + color.a = components.opacity; + return color; + }`, + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } + ); + } else { + this._defaultLightShader = new p5.Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'highp') + + defaultShaders.lightVert, + this._webGL2CompatibilityPrefix('frag', 'highp') + + defaultShaders.lightTextureFrag + ); + } + } + + return this._defaultLightShader; } - return fill; - } - _getImmediatePointShader() { - // select the point shader to use - const point = this.states.userPointShader; - if (!point || !point.isPointShader()) { - return this._getPointShader(); + _getImmediateModeShader() { + if (!this._defaultImmediateModeShader) { + this._defaultImmediateModeShader = new p5.Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.immediateVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.vertexColorFrag + ); + } + + return this._defaultImmediateModeShader; } - return point; - } - _getRetainedLineShader() { - return this._getImmediateLineShader(); - } + baseNormalShader() { + return this._getNormalShader(); + } - baseMaterialShader() { - if (!this._pInst._glAttributes.perPixelLighting) { - throw new Error( - 'The material shader does not support hooks without perPixelLighting. Try turning it back on.' - ); + _getNormalShader() { + if (!this._defaultNormalShader) { + this._defaultNormalShader = new p5.Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.normalVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.normalFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } + ); + } + + return this._defaultNormalShader; + } + + baseColorShader() { + return this._getColorShader(); } - return this._getLightShader(); - } - _getLightShader() { - if (!this._defaultLightShader) { - if (this._pInst._glAttributes.perPixelLighting) { - this._defaultLightShader = new p5.Shader( + _getColorShader() { + if (!this._defaultColorShader) { + this._defaultColorShader = new p5.Shader( this, - this._webGL2CompatibilityPrefix('vert', 'highp') + - defaultShaders.phongVert, - this._webGL2CompatibilityPrefix('frag', 'highp') + - defaultShaders.phongFrag, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.normalVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.basicFrag, { vertex: { 'void beforeVertex': '() {}', @@ -1802,716 +1794,733 @@ p5.RendererGL = class RendererGL extends Renderer { 'vec4 getVertexColor': '(vec4 color) { return color; }', 'void afterVertex': '() {}' }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } + ); + } + + return this._defaultColorShader; + } + + /** + * TODO(dave): un-private this when there is a way to actually override the + * shader used for points + * + * Get the shader used when drawing points with `point()`. + * + * You can call `pointShader().modify()` + * and change any of the following hooks: + * - `void beforeVertex`: Called at the start of the vertex shader. + * - `vec3 getLocalPosition`: Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * - `vec3 getWorldPosition`: Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * - `float getPointSize`: Update the size of the point. It takes in `float size` and must return a modified version. + * - `void afterVertex`: Called at the end of the vertex shader. + * - `void beforeFragment`: Called at the start of the fragment shader. + * - `bool shouldDiscard`: Points are drawn inside a square, with the corners discarded in the fragment shader to create a circle. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. + * - `vec4 getFinalColor`: Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * - `void afterFragment`: Called at the end of the fragment shader. + * + * Call `pointShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @returns {p5.Shader} The `point()` shader + * @private() + */ + pointShader() { + return this._getPointShader(); + } + + _getPointShader() { + if (!this._defaultPointShader) { + this._defaultPointShader = new p5.Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.pointVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.pointFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'float getPointSize': '(float size) { return size; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'bool shouldDiscard': '(bool outside) { return outside; }', + 'void afterFragment': '() {}' + } + } + ); + } + return this._defaultPointShader; + } + + baseStrokeShader() { + return this._getLineShader(); + } + + _getLineShader() { + if (!this._defaultLineShader) { + this._defaultLineShader = new p5.Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.lineVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.lineFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'float getStrokeWeight': '(float weight) { return weight; }', + 'vec2 getLineCenter': '(vec2 center) { return center; }', + 'vec2 getLinePosition': '(vec2 position) { return position; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, fragment: { 'void beforeFragment': '() {}', 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', - 'vec4 combineColors': `(ColorComponents components) { - vec4 color = vec4(0.); - color.rgb += components.diffuse * components.baseColor; - color.rgb += components.ambient * components.ambientColor; - color.rgb += components.specular * components.specularColor; - color.rgb += components.emissive; - color.a = components.opacity; - return color; - }`, 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'bool shouldDiscard': '(bool outside) { return outside; }', 'void afterFragment': '() {}' } } ); - } else { - this._defaultLightShader = new p5.Shader( + } + + return this._defaultLineShader; + } + + _getFontShader() { + if (!this._defaultFontShader) { + if (this.webglVersion === constants.WEBGL) { + this.GL.getExtension('OES_standard_derivatives'); + } + this._defaultFontShader = new p5.Shader( this, - this._webGL2CompatibilityPrefix('vert', 'highp') + - defaultShaders.lightVert, - this._webGL2CompatibilityPrefix('frag', 'highp') + - defaultShaders.lightTextureFrag + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.fontVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.fontFrag ); } + return this._defaultFontShader; } - return this._defaultLightShader; - } + _webGL2CompatibilityPrefix( + shaderType, + floatPrecision + ) { + let code = ''; + if (this.webglVersion === constants.WEBGL2) { + code += '#version 300 es\n#define WEBGL2\n'; + } + if (shaderType === 'vert') { + code += '#define VERTEX_SHADER\n'; + } else if (shaderType === 'frag') { + code += '#define FRAGMENT_SHADER\n'; + } + if (floatPrecision) { + code += `precision ${floatPrecision} float;\n`; + } + return code; + } - _getImmediateModeShader() { - if (!this._defaultImmediateModeShader) { - this._defaultImmediateModeShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.immediateVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.vertexColorFrag - ); + _getEmptyTexture() { + if (!this._emptyTexture) { + // a plain white texture RGBA, full alpha, single pixel. + const im = new p5.Image(1, 1); + im.set(0, 0, 255); + this._emptyTexture = new p5.Texture(this, im); + } + return this._emptyTexture; } - return this._defaultImmediateModeShader; - } + getTexture(input) { + let src = input; + if (src instanceof p5.Framebuffer) { + src = src.color; + } - baseNormalShader() { - return this._getNormalShader(); - } + const texture = this.textures.get(src); + if (texture) { + return texture; + } - _getNormalShader() { - if (!this._defaultNormalShader) { - this._defaultNormalShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.normalVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.normalFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' - } - } - ); + const tex = new p5.Texture(this, src); + this.textures.set(src, tex); + return tex; + } + /* + * used in imageLight, + * To create a blurry image from the input non blurry img, if it doesn't already exist + * Add it to the diffusedTexture map, + * Returns the blurry image + * maps a p5.Image used by imageLight() to a p5.Framebuffer + */ + getDiffusedTexture(input) { + // if one already exists for a given input image + if (this.diffusedTextures.get(input) != null) { + return this.diffusedTextures.get(input); + } + // if not, only then create one + let newFramebuffer; + // hardcoded to 200px, because it's going to be blurry and smooth + let smallWidth = 200; + let width = smallWidth; + let height = Math.floor(smallWidth * (input.height / input.width)); + newFramebuffer = this._pInst.createFramebuffer({ + width, height, density: 1 + }); + // create framebuffer is like making a new sketch, all functions on main + // sketch it would be available on framebuffer + if (!this.states.diffusedShader) { + this.states.diffusedShader = this._pInst.createShader( + defaultShaders.imageLightVert, + defaultShaders.imageLightDiffusedFrag + ); + } + newFramebuffer.draw(() => { + this._pInst.shader(this.states.diffusedShader); + this.states.diffusedShader.setUniform('environmentMap', input); + this._pInst.noStroke(); + this._pInst.rectMode(constants.CENTER); + this._pInst.noLights(); + this._pInst.rect(0, 0, width, height); + }); + this.diffusedTextures.set(input, newFramebuffer); + return newFramebuffer; + } + + /* + * used in imageLight, + * To create a texture from the input non blurry image, if it doesn't already exist + * Creating 8 different levels of textures according to different + * sizes and atoring them in `levels` array + * Creating a new Mipmap texture with that `levels` array + * Storing the texture for input image in map called `specularTextures` + * maps the input p5.Image to a p5.MipmapTexture + */ + getSpecularTexture(input) { + // check if already exits (there are tex of diff resolution so which one to check) + // currently doing the whole array + if (this.specularTextures.get(input) != null) { + return this.specularTextures.get(input); + } + // Hardcoded size + const size = 512; + let tex; + const levels = []; + const framebuffer = this._pInst.createFramebuffer({ + width: size, height: size, density: 1 + }); + let count = Math.log(size) / Math.log(2); + if (!this.states.specularShader) { + this.states.specularShader = this._pInst.createShader( + defaultShaders.imageLightVert, + defaultShaders.imageLightSpecularFrag + ); + } + // currently only 8 levels + // This loop calculates 8 framebuffers of varying size of canvas + // and corresponding different roughness levels. + // Roughness increases with the decrease in canvas size, + // because rougher surfaces have less detailed/more blurry reflections. + for (let w = size; w >= 1; w /= 2) { + framebuffer.resize(w, w); + let currCount = Math.log(w) / Math.log(2); + let roughness = 1 - currCount / count; + framebuffer.draw(() => { + this._pInst.shader(this.states.specularShader); + this._pInst.clear(); + this.states.specularShader.setUniform('environmentMap', input); + this.states.specularShader.setUniform('roughness', roughness); + this._pInst.noStroke(); + this._pInst.noLights(); + this._pInst.plane(w, w); + }); + levels.push(framebuffer.get().drawingContext.getImageData(0, 0, w, w)); + } + // Free the Framebuffer + framebuffer.remove(); + tex = new p5.MipmapTexture(this, levels, {}); + this.specularTextures.set(input, tex); + return tex; } - return this._defaultNormalShader; - } + /** + * @private + * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if + * the main canvas is the current draw target. + */ + activeFramebuffer() { + return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null; + } - baseColorShader() { - return this._getColorShader(); - } + createFramebuffer(options) { + return new p5.Framebuffer(this, options); + } - _getColorShader() { - if (!this._defaultColorShader) { - this._defaultColorShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.normalVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.basicFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' - } - } - ); + _setStrokeUniforms(baseStrokeShader) { + baseStrokeShader.bindShader(); + + // set the uniform values + baseStrokeShader.setUniform('uUseLineColor', this._useLineColor); + baseStrokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); + baseStrokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); + baseStrokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); + baseStrokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); } - return this._defaultColorShader; - } + _setFillUniforms(fillShader) { + fillShader.bindShader(); - /** - * TODO(dave): un-private this when there is a way to actually override the - * shader used for points - * - * Get the shader used when drawing points with `point()`. - * - * You can call `pointShader().modify()` - * and change any of the following hooks: - * - `void beforeVertex`: Called at the start of the vertex shader. - * - `vec3 getLocalPosition`: Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. - * - `vec3 getWorldPosition`: Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. - * - `float getPointSize`: Update the size of the point. It takes in `float size` and must return a modified version. - * - `void afterVertex`: Called at the end of the vertex shader. - * - `void beforeFragment`: Called at the start of the fragment shader. - * - `bool shouldDiscard`: Points are drawn inside a square, with the corners discarded in the fragment shader to create a circle. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. - * - `vec4 getFinalColor`: Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. - * - `void afterFragment`: Called at the end of the fragment shader. - * - * Call `pointShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @returns {p5.Shader} The `point()` shader - * @private() - */ - pointShader() { - return this._getPointShader(); - } + this.mixedSpecularColor = [...this.states.curSpecularColor]; - _getPointShader() { - if (!this._defaultPointShader) { - this._defaultPointShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.pointVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.pointFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'float getPointSize': '(float size) { return size; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'bool shouldDiscard': '(bool outside) { return outside; }', - 'void afterFragment': '() {}' - } - } - ); - } - return this._defaultPointShader; - } + if (this.states._useMetalness > 0) { + this.mixedSpecularColor = this.mixedSpecularColor.map( + (mixedSpecularColor, index) => + this.states.curFillColor[index] * this.states._useMetalness + + mixedSpecularColor * (1 - this.states._useMetalness) + ); + } - baseStrokeShader() { - return this._getLineShader(); - } + // TODO: optimize + fillShader.setUniform('uUseVertexColor', this._useVertexColor); + fillShader.setUniform('uMaterialColor', this.states.curFillColor); + fillShader.setUniform('isTexture', !!this.states._tex); + if (this.states._tex) { + fillShader.setUniform('uSampler', this.states._tex); + } + fillShader.setUniform('uTint', this.states.tint); + + fillShader.setUniform('uHasSetAmbient', this.states._hasSetAmbient); + fillShader.setUniform('uAmbientMatColor', this.states.curAmbientColor); + fillShader.setUniform('uSpecularMatColor', this.mixedSpecularColor); + fillShader.setUniform('uEmissiveMatColor', this.states.curEmissiveColor); + fillShader.setUniform('uSpecular', this.states._useSpecularMaterial); + fillShader.setUniform('uEmissive', this.states._useEmissiveMaterial); + fillShader.setUniform('uShininess', this.states._useShininess); + fillShader.setUniform('uMetallic', this.states._useMetalness); + + this._setImageLightUniforms(fillShader); + + fillShader.setUniform('uUseLighting', this.states._enableLighting); + + const pointLightCount = this.states.pointLightDiffuseColors.length / 3; + fillShader.setUniform('uPointLightCount', pointLightCount); + fillShader.setUniform('uPointLightLocation', this.states.pointLightPositions); + fillShader.setUniform( + 'uPointLightDiffuseColors', + this.states.pointLightDiffuseColors + ); + fillShader.setUniform( + 'uPointLightSpecularColors', + this.states.pointLightSpecularColors + ); - _getLineShader() { - if (!this._defaultLineShader) { - this._defaultLineShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.lineVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.lineFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'float getStrokeWeight': '(float weight) { return weight; }', - 'vec2 getLineCenter': '(vec2 center) { return center; }', - 'vec2 getLinePosition': '(vec2 position) { return position; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'bool shouldDiscard': '(bool outside) { return outside; }', - 'void afterFragment': '() {}' - } - } + const directionalLightCount = this.states.directionalLightDiffuseColors.length / 3; + fillShader.setUniform('uDirectionalLightCount', directionalLightCount); + fillShader.setUniform('uLightingDirection', this.states.directionalLightDirections); + fillShader.setUniform( + 'uDirectionalDiffuseColors', + this.states.directionalLightDiffuseColors + ); + fillShader.setUniform( + 'uDirectionalSpecularColors', + this.states.directionalLightSpecularColors ); - } - return this._defaultLineShader; - } + // TODO: sum these here... + const ambientLightCount = this.states.ambientLightColors.length / 3; + this.mixedAmbientLight = [...this.states.ambientLightColors]; - _getFontShader() { - if (!this._defaultFontShader) { - if (this.webglVersion === constants.WEBGL) { - this.GL.getExtension('OES_standard_derivatives'); - } - this._defaultFontShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.fontVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.fontFrag + if (this.states._useMetalness > 0) { + this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors => { + let mixing = ambientColors - this.states._useMetalness; + return Math.max(0, mixing); + })); + } + fillShader.setUniform('uAmbientLightCount', ambientLightCount); + fillShader.setUniform('uAmbientColor', this.mixedAmbientLight); + + const spotLightCount = this.states.spotLightDiffuseColors.length / 3; + fillShader.setUniform('uSpotLightCount', spotLightCount); + fillShader.setUniform('uSpotLightAngle', this.states.spotLightAngle); + fillShader.setUniform('uSpotLightConc', this.states.spotLightConc); + fillShader.setUniform('uSpotLightDiffuseColors', this.states.spotLightDiffuseColors); + fillShader.setUniform( + 'uSpotLightSpecularColors', + this.states.spotLightSpecularColors ); - } - return this._defaultFontShader; - } + fillShader.setUniform('uSpotLightLocation', this.states.spotLightPositions); + fillShader.setUniform('uSpotLightDirection', this.states.spotLightDirections); - _webGL2CompatibilityPrefix( - shaderType, - floatPrecision - ) { - let code = ''; - if (this.webglVersion === constants.WEBGL2) { - code += '#version 300 es\n#define WEBGL2\n'; - } - if (shaderType === 'vert') { - code += '#define VERTEX_SHADER\n'; - } else if (shaderType === 'frag') { - code += '#define FRAGMENT_SHADER\n'; - } - if (floatPrecision) { - code += `precision ${floatPrecision} float;\n`; - } - return code; - } + fillShader.setUniform('uConstantAttenuation', this.states.constantAttenuation); + fillShader.setUniform('uLinearAttenuation', this.states.linearAttenuation); + fillShader.setUniform('uQuadraticAttenuation', this.states.quadraticAttenuation); - _getEmptyTexture() { - if (!this._emptyTexture) { - // a plain white texture RGBA, full alpha, single pixel. - const im = new p5.Image(1, 1); - im.set(0, 0, 255); - this._emptyTexture = new p5.Texture(this, im); + fillShader.bindTextures(); } - return this._emptyTexture; - } - getTexture(input) { - let src = input; - if (src instanceof p5.Framebuffer) { - src = src.color; - } + // getting called from _setFillUniforms + _setImageLightUniforms(shader) { + //set uniform values + shader.setUniform('uUseImageLight', this.states.activeImageLight != null); + // true + if (this.states.activeImageLight) { + // this.states.activeImageLight has image as a key + // look up the texture from the diffusedTexture map + let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); + shader.setUniform('environmentMapDiffused', diffusedLight); + let specularLight = this.getSpecularTexture(this.states.activeImageLight); - const texture = this.textures.get(src); - if (texture) { - return texture; + shader.setUniform('environmentMapSpecular', specularLight); + } } - const tex = new p5.Texture(this, src); - this.textures.set(src, tex); - return tex; - } - /* - * used in imageLight, - * To create a blurry image from the input non blurry img, if it doesn't already exist - * Add it to the diffusedTexture map, - * Returns the blurry image - * maps a p5.Image used by imageLight() to a p5.Framebuffer - */ - getDiffusedTexture(input) { - // if one already exists for a given input image - if (this.diffusedTextures.get(input) != null) { - return this.diffusedTextures.get(input); - } - // if not, only then create one - let newFramebuffer; - // hardcoded to 200px, because it's going to be blurry and smooth - let smallWidth = 200; - let width = smallWidth; - let height = Math.floor(smallWidth * (input.height / input.width)); - newFramebuffer = this._pInst.createFramebuffer({ - width, height, density: 1 - }); - // create framebuffer is like making a new sketch, all functions on main - // sketch it would be available on framebuffer - if (!this.states.diffusedShader) { - this.states.diffusedShader = this._pInst.createShader( - defaultShaders.imageLightVert, - defaultShaders.imageLightDiffusedFrag - ); - } - newFramebuffer.draw(() => { - this._pInst.shader(this.states.diffusedShader); - this.states.diffusedShader.setUniform('environmentMap', input); - this._pInst.noStroke(); - this._pInst.rectMode(constants.CENTER); - this._pInst.noLights(); - this._pInst.rect(0, 0, width, height); - }); - this.diffusedTextures.set(input, newFramebuffer); - return newFramebuffer; - } + _setPointUniforms(pointShader) { + pointShader.bindShader(); - /* - * used in imageLight, - * To create a texture from the input non blurry image, if it doesn't already exist - * Creating 8 different levels of textures according to different - * sizes and atoring them in `levels` array - * Creating a new Mipmap texture with that `levels` array - * Storing the texture for input image in map called `specularTextures` - * maps the input p5.Image to a p5.MipmapTexture - */ - getSpecularTexture(input) { - // check if already exits (there are tex of diff resolution so which one to check) - // currently doing the whole array - if (this.specularTextures.get(input) != null) { - return this.specularTextures.get(input); - } - // Hardcoded size - const size = 512; - let tex; - const levels = []; - const framebuffer = this._pInst.createFramebuffer({ - width: size, height: size, density: 1 - }); - let count = Math.log(size) / Math.log(2); - if (!this.states.specularShader) { - this.states.specularShader = this._pInst.createShader( - defaultShaders.imageLightVert, - defaultShaders.imageLightSpecularFrag + // set the uniform values + pointShader.setUniform('uMaterialColor', this.states.curStrokeColor); + // @todo is there an instance where this isn't stroke weight? + // should be they be same var? + pointShader.setUniform( + 'uPointSize', + this.pointSize * this._pixelDensity ); } - // currently only 8 levels - // This loop calculates 8 framebuffers of varying size of canvas - // and corresponding different roughness levels. - // Roughness increases with the decrease in canvas size, - // because rougher surfaces have less detailed/more blurry reflections. - for (let w = size; w >= 1; w /= 2) { - framebuffer.resize(w, w); - let currCount = Math.log(w) / Math.log(2); - let roughness = 1 - currCount / count; - framebuffer.draw(() => { - this._pInst.shader(this.states.specularShader); - this._pInst.clear(); - this.states.specularShader.setUniform('environmentMap', input); - this.states.specularShader.setUniform('roughness', roughness); - this._pInst.noStroke(); - this._pInst.noLights(); - this._pInst.plane(w, w); - }); - levels.push(framebuffer.get().drawingContext.getImageData(0, 0, w, w)); - } - // Free the Framebuffer - framebuffer.remove(); - tex = new p5.MipmapTexture(this, levels, {}); - this.specularTextures.set(input, tex); - return tex; - } - - /** - * @private - * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if - * the main canvas is the current draw target. - */ - activeFramebuffer() { - return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null; - } - createFramebuffer(options) { - return new p5.Framebuffer(this, options); - } + /* Binds a buffer to the drawing context + * when passed more than two arguments it also updates or initializes + * the data associated with the buffer + */ + _bindBuffer( + buffer, + target, + values, + type, + usage + ) { + if (!target) target = this.GL.ARRAY_BUFFER; + this.GL.bindBuffer(target, buffer); + if (values !== undefined) { + let data = values; + if (values instanceof p5.DataArray) { + data = values.dataArray(); + } else if (!(data instanceof (type || Float32Array))) { + data = new (type || Float32Array)(data); + } + this.GL.bufferData(target, data, usage || this.GL.STATIC_DRAW); + } + } - _setStrokeUniforms(baseStrokeShader) { - baseStrokeShader.bindShader(); + /////////////////////////////// + //// UTILITY FUNCTIONS + ////////////////////////////// + _arraysEqual(a, b) { + const aLength = a.length; + if (aLength !== b.length) return false; + return a.every((ai, i) => ai === b[i]); + } + + _isTypedArray(arr) { + return [ + Float32Array, + Float64Array, + Int16Array, + Uint16Array, + Uint32Array + ].some(x => arr instanceof x); + } + /** + * turn a two dimensional array into one dimensional array + * @private + * @param {Array} arr 2-dimensional array + * @return {Array} 1-dimensional array + * [[1, 2, 3],[4, 5, 6]] -> [1, 2, 3, 4, 5, 6] + */ + _flatten(arr) { + return arr.flat(); + } + + /** + * turn a p5.Vector Array into a one dimensional number array + * @private + * @param {p5.Vector[]} arr an array of p5.Vector + * @return {Number[]} a one dimensional array of numbers + * [p5.Vector(1, 2, 3), p5.Vector(4, 5, 6)] -> + * [1, 2, 3, 4, 5, 6] + */ + _vToNArray(arr) { + return arr.flatMap(item => [item.x, item.y, item.z]); + } + + // function to calculate BezierVertex Coefficients + _bezierCoefficients(t) { + const t2 = t * t; + const t3 = t2 * t; + const mt = 1 - t; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + return [mt3, 3 * mt2 * t, 3 * mt * t2, t3]; + } + + // function to calculate QuadraticVertex Coefficients + _quadraticCoefficients(t) { + const t2 = t * t; + const mt = 1 - t; + const mt2 = mt * mt; + return [mt2, 2 * mt * t, t2]; + } + + // function to convert Bezier coordinates to Catmull Rom Splines + _bezierToCatmull(w) { + const p1 = w[1]; + const p2 = w[1] + (w[2] - w[0]) / this._curveTightness; + const p3 = w[2] - (w[3] - w[1]) / this._curveTightness; + const p4 = w[2]; + const p = [p1, p2, p3, p4]; + return p; + } + _initTessy() { + this.tessyVertexSize = 12; + // function called for each vertex of tesselator output + function vertexCallback(data, polyVertArray) { + for (const element of data) { + polyVertArray.push(element); + } + } - // set the uniform values - baseStrokeShader.setUniform('uUseLineColor', this._useLineColor); - baseStrokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); - baseStrokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); - baseStrokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); - baseStrokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); - } + function begincallback(type) { + if (type !== libtess.primitiveType.GL_TRIANGLES) { + console.log(`expected TRIANGLES but got type: ${type}`); + } + } - _setFillUniforms(fillShader) { - fillShader.bindShader(); + function errorcallback(errno) { + console.log('error callback'); + console.log(`error number: ${errno}`); + } + // callback for when segments intersect and must be split + const combinecallback = (coords, data, weight) => { + const result = new Array(this.tessyVertexSize).fill(0); + for (let i = 0; i < weight.length; i++) { + for (let j = 0; j < result.length; j++) { + if (weight[i] === 0 || !data[i]) continue; + result[j] += data[i][j] * weight[i]; + } + } + return result; + }; - this.mixedSpecularColor = [...this.states.curSpecularColor]; + function edgeCallback(flag) { + // don't really care about the flag, but need no-strip/no-fan behavior + } - if (this.states._useMetalness > 0) { - this.mixedSpecularColor = this.mixedSpecularColor.map( - (mixedSpecularColor, index) => - this.states.curFillColor[index] * this.states._useMetalness + - mixedSpecularColor * (1 - this.states._useMetalness) + const tessy = new libtess.GluTesselator(); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); + tessy.gluTessProperty( + libtess.gluEnum.GLU_TESS_WINDING_RULE, + libtess.windingRule.GLU_TESS_WINDING_NONZERO ); - } - // TODO: optimize - fillShader.setUniform('uUseVertexColor', this._useVertexColor); - fillShader.setUniform('uMaterialColor', this.states.curFillColor); - fillShader.setUniform('isTexture', !!this.states._tex); - if (this.states._tex) { - fillShader.setUniform('uSampler', this.states._tex); - } - fillShader.setUniform('uTint', this.states.tint); - - fillShader.setUniform('uHasSetAmbient', this.states._hasSetAmbient); - fillShader.setUniform('uAmbientMatColor', this.states.curAmbientColor); - fillShader.setUniform('uSpecularMatColor', this.mixedSpecularColor); - fillShader.setUniform('uEmissiveMatColor', this.states.curEmissiveColor); - fillShader.setUniform('uSpecular', this.states._useSpecularMaterial); - fillShader.setUniform('uEmissive', this.states._useEmissiveMaterial); - fillShader.setUniform('uShininess', this.states._useShininess); - fillShader.setUniform('uMetallic', this.states._useMetalness); - - this._setImageLightUniforms(fillShader); - - fillShader.setUniform('uUseLighting', this.states._enableLighting); - - const pointLightCount = this.states.pointLightDiffuseColors.length / 3; - fillShader.setUniform('uPointLightCount', pointLightCount); - fillShader.setUniform('uPointLightLocation', this.states.pointLightPositions); - fillShader.setUniform( - 'uPointLightDiffuseColors', - this.states.pointLightDiffuseColors - ); - fillShader.setUniform( - 'uPointLightSpecularColors', - this.states.pointLightSpecularColors - ); - - const directionalLightCount = this.states.directionalLightDiffuseColors.length / 3; - fillShader.setUniform('uDirectionalLightCount', directionalLightCount); - fillShader.setUniform('uLightingDirection', this.states.directionalLightDirections); - fillShader.setUniform( - 'uDirectionalDiffuseColors', - this.states.directionalLightDiffuseColors - ); - fillShader.setUniform( - 'uDirectionalSpecularColors', - this.states.directionalLightSpecularColors - ); - - // TODO: sum these here... - const ambientLightCount = this.states.ambientLightColors.length / 3; - this.mixedAmbientLight = [...this.states.ambientLightColors]; - - if (this.states._useMetalness > 0) { - this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors => { - let mixing = ambientColors - this.states._useMetalness; - return Math.max(0, mixing); - })); - } - fillShader.setUniform('uAmbientLightCount', ambientLightCount); - fillShader.setUniform('uAmbientColor', this.mixedAmbientLight); - - const spotLightCount = this.states.spotLightDiffuseColors.length / 3; - fillShader.setUniform('uSpotLightCount', spotLightCount); - fillShader.setUniform('uSpotLightAngle', this.states.spotLightAngle); - fillShader.setUniform('uSpotLightConc', this.states.spotLightConc); - fillShader.setUniform('uSpotLightDiffuseColors', this.states.spotLightDiffuseColors); - fillShader.setUniform( - 'uSpotLightSpecularColors', - this.states.spotLightSpecularColors - ); - fillShader.setUniform('uSpotLightLocation', this.states.spotLightPositions); - fillShader.setUniform('uSpotLightDirection', this.states.spotLightDirections); - - fillShader.setUniform('uConstantAttenuation', this.states.constantAttenuation); - fillShader.setUniform('uLinearAttenuation', this.states.linearAttenuation); - fillShader.setUniform('uQuadraticAttenuation', this.states.quadraticAttenuation); - - fillShader.bindTextures(); - } - - // getting called from _setFillUniforms - _setImageLightUniforms(shader) { - //set uniform values - shader.setUniform('uUseImageLight', this.states.activeImageLight != null); - // true - if (this.states.activeImageLight) { - // this.states.activeImageLight has image as a key - // look up the texture from the diffusedTexture map - let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); - shader.setUniform('environmentMapDiffused', diffusedLight); - let specularLight = this.getSpecularTexture(this.states.activeImageLight); + return tessy; + } + + _triangulate(contours) { + // libtess will take 3d verts and flatten to a plane for tesselation. + // libtess is capable of calculating a plane to tesselate on, but + // if all of the vertices have the same z values, we'll just + // assume the face is facing the camera, letting us skip any performance + // issues or bugs in libtess's automatic calculation. + const z = contours[0] ? contours[0][2] : undefined; + let allSameZ = true; + for (const contour of contours) { + for ( + let j = 0; + j < contour.length; + j += this.tessyVertexSize + ) { + if (contour[j + 2] !== z) { + allSameZ = false; + break; + } + } + } + if (allSameZ) { + this._tessy.gluTessNormal(0, 0, 1); + } else { + // Let libtess pick a plane for us + this._tessy.gluTessNormal(0, 0, 0); + } - shader.setUniform('environmentMapSpecular', specularLight); - } - } + const triangleVerts = []; + this._tessy.gluTessBeginPolygon(triangleVerts); + + for (const contour of contours) { + this._tessy.gluTessBeginContour(); + for ( + let j = 0; + j < contour.length; + j += this.tessyVertexSize + ) { + const coords = contour.slice( + j, + j + this.tessyVertexSize + ); + this._tessy.gluTessVertex(coords, coords); + } + this._tessy.gluTessEndContour(); + } - _setPointUniforms(pointShader) { - pointShader.bindShader(); - - // set the uniform values - pointShader.setUniform('uMaterialColor', this.states.curStrokeColor); - // @todo is there an instance where this isn't stroke weight? - // should be they be same var? - pointShader.setUniform( - 'uPointSize', - this.pointSize * this._pixelDensity - ); - } + // finish polygon + this._tessy.gluTessEndPolygon(); - /* Binds a buffer to the drawing context - * when passed more than two arguments it also updates or initializes - * the data associated with the buffer - */ - _bindBuffer( - buffer, - target, - values, - type, - usage - ) { - if (!target) target = this.GL.ARRAY_BUFFER; - this.GL.bindBuffer(target, buffer); - if (values !== undefined) { - let data = values; - if (values instanceof p5.DataArray) { - data = values.dataArray(); - } else if (!(data instanceof (type || Float32Array))) { - data = new (type || Float32Array)(data); - } - this.GL.bufferData(target, data, usage || this.GL.STATIC_DRAW); + return triangleVerts; } - } - - /////////////////////////////// - //// UTILITY FUNCTIONS - ////////////////////////////// - _arraysEqual(a, b) { - const aLength = a.length; - if (aLength !== b.length) return false; - return a.every((ai, i) => ai === b[i]); - } - - _isTypedArray(arr) { - return [ - Float32Array, - Float64Array, - Int16Array, - Uint16Array, - Uint32Array - ].some(x => arr instanceof x); - } + }; /** - * turn a two dimensional array into one dimensional array - * @private - * @param {Array} arr 2-dimensional array - * @return {Array} 1-dimensional array - * [[1, 2, 3],[4, 5, 6]] -> [1, 2, 3, 4, 5, 6] + * ensures that p5 is using a 3d renderer. throws an error if not. */ - _flatten(arr) { - return arr.flat(); - } + fn._assert3d = function (name) { + if (!this._renderer.isP3D) + throw new Error( + `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` + ); + }; - /** - * turn a p5.Vector Array into a one dimensional number array - * @private - * @param {p5.Vector[]} arr an array of p5.Vector - * @return {Number[]} a one dimensional array of numbers - * [p5.Vector(1, 2, 3), p5.Vector(4, 5, 6)] -> - * [1, 2, 3, 4, 5, 6] - */ - _vToNArray(arr) { - return arr.flatMap(item => [item.x, item.y, item.z]); - } + p5.renderers[constants.WEBGL] = p5.RendererGL; + p5.renderers[constants.WEBGL2] = p5.RendererGL; +} - // function to calculate BezierVertex Coefficients - _bezierCoefficients(t) { - const t2 = t * t; - const t3 = t2 * t; - const mt = 1 - t; - const mt2 = mt * mt; - const mt3 = mt2 * mt; - return [mt3, 3 * mt2 * t, 3 * mt * t2, t3]; - } +/** + * @private + * @param {Uint8Array|Float32Array|undefined} pixels An existing pixels array to reuse if the size is the same + * @param {WebGLRenderingContext} gl The WebGL context + * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read + * @param {Number} x The x coordiante to read, premultiplied by pixel density + * @param {Number} y The y coordiante to read, premultiplied by pixel density + * @param {Number} width The width in pixels to be read (factoring in pixel density) + * @param {Number} height The height in pixels to be read (factoring in pixel density) + * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read + * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT + * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about + * @returns {Uint8Array|Float32Array} pixels A pixels array with the current state of the + * WebGL context read into it + */ +export function readPixelsWebGL( + pixels, + gl, + framebuffer, + x, + y, + width, + height, + format, + type, + flipY +) { + // Record the currently bound framebuffer so we can go back to it after, and + // bind the framebuffer we want to read from + const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - // function to calculate QuadraticVertex Coefficients - _quadraticCoefficients(t) { - const t2 = t * t; - const mt = 1 - t; - const mt2 = mt * mt; - return [mt2, 2 * mt * t, t2]; - } + const channels = format === gl.RGBA ? 4 : 3; - // function to convert Bezier coordinates to Catmull Rom Splines - _bezierToCatmull(w) { - const p1 = w[1]; - const p2 = w[1] + (w[2] - w[0]) / this._curveTightness; - const p3 = w[2] - (w[3] - w[1]) / this._curveTightness; - const p4 = w[2]; - const p = [p1, p2, p3, p4]; - return p; + // Make a pixels buffer if it doesn't already exist + const len = width * height * channels; + const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; + if (!(pixels instanceof TypedArrayClass) || pixels.length !== len) { + pixels = new TypedArrayClass(len); } - _initTessy() { - this.tessyVertexSize = 12; - // function called for each vertex of tesselator output - function vertexCallback(data, polyVertArray) { - for (const element of data) { - polyVertArray.push(element); - } - } - function begincallback(type) { - if (type !== libtess.primitiveType.GL_TRIANGLES) { - console.log(`expected TRIANGLES but got type: ${type}`); - } - } + gl.readPixels( + x, + flipY ? (flipY - y - height) : y, + width, + height, + format, + type, + pixels + ); - function errorcallback(errno) { - console.log('error callback'); - console.log(`error number: ${errno}`); - } - // callback for when segments intersect and must be split - const combinecallback = (coords, data, weight) => { - const result = new Array(this.tessyVertexSize).fill(0); - for (let i = 0; i < weight.length; i++) { - for (let j = 0; j < result.length; j++) { - if (weight[i] === 0 || !data[i]) continue; - result[j] += data[i][j] * weight[i]; - } - } - return result; - }; + // Re-bind whatever was previously bound + gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); - function edgeCallback(flag) { - // don't really care about the flag, but need no-strip/no-fan behavior + if (flipY) { + // WebGL pixels are inverted compared to 2D pixels, so we have to flip + // the resulting rows. Adapted from https://stackoverflow.com/a/41973289 + const halfHeight = Math.floor(height / 2); + const tmpRow = new TypedArrayClass(width * channels); + for (let y = 0; y < halfHeight; y++) { + const topOffset = y * width * 4; + const bottomOffset = (height - y - 1) * width * 4; + tmpRow.set(pixels.subarray(topOffset, topOffset + width * 4)); + pixels.copyWithin(topOffset, bottomOffset, bottomOffset + width * 4); + pixels.set(tmpRow, bottomOffset); } + } - const tessy = new libtess.GluTesselator(); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); - tessy.gluTessProperty( - libtess.gluEnum.GLU_TESS_WINDING_RULE, - libtess.windingRule.GLU_TESS_WINDING_NONZERO - ); + return pixels; +} - return tessy; - } +/** + * @private + * @param {WebGLRenderingContext} gl The WebGL context + * @param {WebGLFramebuffer|null} framebuffer The Framebuffer to read + * @param {Number} x The x coordinate to read, premultiplied by pixel density + * @param {Number} y The y coordinate to read, premultiplied by pixel density + * @param {GLEnum} format Either RGB or RGBA depending on how many channels to read + * @param {GLEnum} type The datatype of each channel, e.g. UNSIGNED_BYTE or FLOAT + * @param {Number|undefined} flipY If provided, the total height with which to flip the y axis about + * @returns {Number[]} pixels The channel data for the pixel at that location + */ +export function readPixelWebGL( + gl, + framebuffer, + x, + y, + format, + type, + flipY +) { + // Record the currently bound framebuffer so we can go back to it after, and + // bind the framebuffer we want to read from + const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - _triangulate(contours) { - // libtess will take 3d verts and flatten to a plane for tesselation. - // libtess is capable of calculating a plane to tesselate on, but - // if all of the vertices have the same z values, we'll just - // assume the face is facing the camera, letting us skip any performance - // issues or bugs in libtess's automatic calculation. - const z = contours[0] ? contours[0][2] : undefined; - let allSameZ = true; - for (const contour of contours) { - for ( - let j = 0; - j < contour.length; - j += this.tessyVertexSize - ) { - if (contour[j + 2] !== z) { - allSameZ = false; - break; - } - } - } - if (allSameZ) { - this._tessy.gluTessNormal(0, 0, 1); - } else { - // Let libtess pick a plane for us - this._tessy.gluTessNormal(0, 0, 0); - } + const channels = format === gl.RGBA ? 4 : 3; + const TypedArrayClass = type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; + const pixels = new TypedArrayClass(channels); + + gl.readPixels( + x, flipY ? (flipY - y - 1) : y, 1, 1, + format, type, + pixels + ); - const triangleVerts = []; - this._tessy.gluTessBeginPolygon(triangleVerts); + // Re-bind whatever was previously bound + gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer); - for (const contour of contours) { - this._tessy.gluTessBeginContour(); - for ( - let j = 0; - j < contour.length; - j += this.tessyVertexSize - ) { - const coords = contour.slice( - j, - j + this.tessyVertexSize - ); - this._tessy.gluTessVertex(coords, coords); - } - this._tessy.gluTessEndContour(); - } + return Array.from(pixels); +} - // finish polygon - this._tessy.gluTessEndPolygon(); +export default rendererGL; - return triangleVerts; - } -}; -/** - * ensures that p5 is using a 3d renderer. throws an error if not. - */ -p5.prototype._assert3d = function (name) { - if (!this._renderer.isP3D) - throw new Error( - `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` - ); -}; - -export default p5.RendererGL; +if(typeof p5 !== 'undefined'){ + rendererGL(p5, p5.prototype); +} \ No newline at end of file diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index f2a14725d3..f311dcc185 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -8,7 +8,6 @@ // import p5 from '../core/main'; import * as constants from '../core/constants'; -import Renderer from '../core/p5.Renderer'; function texture(p5, fn){ /** @@ -98,10 +97,10 @@ function texture(p5, fn){ typeof p5.Element !== 'undefined' && obj instanceof p5.Element && !(obj instanceof p5.Graphics) && - !(obj instanceof Renderer); + !(obj instanceof p5.Renderer); this.isSrcP5Image = obj instanceof p5.Image; this.isSrcP5Graphics = obj instanceof p5.Graphics; - this.isSrcP5Renderer = obj instanceof Renderer; + this.isSrcP5Renderer = obj instanceof p5.Renderer; this.isImageData = typeof ImageData !== 'undefined' && obj instanceof ImageData; this.isFramebufferTexture = obj instanceof p5.FramebufferTexture; @@ -524,3 +523,6 @@ export function checkWebGLCapabilities({ GL, webglVersion }) { export default texture; +if(typeof p5 !== 'undefined'){ + texture(p5, p5.prototype); +} \ No newline at end of file From 0c71618b3ae166676594b9d38f48d97750ecb579 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 13 Oct 2024 22:15:09 +0100 Subject: [PATCH 22/55] Change how all classes are defined to be more exportable --- src/app.js | 7 +- src/core/main.js | 4 + src/core/p5.Element.js | 189 +- src/core/p5.Graphics.js | 228 +- src/core/p5.Renderer.js | 841 +-- src/core/p5.Renderer2D.js | 2489 ++++---- src/core/rendering.js | 6 +- src/dom/dom.js | 10443 +++++++++++++++++---------------- src/image/p5.Image.js | 3418 +++++------ src/image/pixels.js | 3 +- src/math/p5.Vector.js | 7267 +++++++++++------------ src/shape/2d_primitives.js | 3 - src/shape/curves.js | 4 - src/webgl/3d_primitives.js | 27 +- src/webgl/material.js | 5 +- src/webgl/p5.Camera.js | 6071 +++++++++---------- src/webgl/p5.DataArray.js | 165 +- src/webgl/p5.Framebuffer.js | 2968 +++++----- src/webgl/p5.Geometry.js | 3636 ++++++------ src/webgl/p5.Matrix.js | 1859 +++--- src/webgl/p5.Quat.js | 151 +- src/webgl/p5.RenderBuffer.js | 133 +- src/webgl/p5.RendererGL.js | 5470 ++++++++++------- src/webgl/p5.Shader.js | 2571 ++++---- src/webgl/p5.Texture.js | 819 +-- 25 files changed, 25032 insertions(+), 23745 deletions(-) diff --git a/src/app.js b/src/app.js index d9fe8da11b..3eb657ea57 100644 --- a/src/app.js +++ b/src/app.js @@ -6,8 +6,8 @@ import './core/friendly_errors/file_errors'; import './core/friendly_errors/fes_core'; import './core/friendly_errors/sketch_reader'; import './core/p5.Element'; -import './core/p5.Graphics'; -import './core/rendering'; +// import './core/p5.Graphics'; +// import './core/rendering'; import shape from './shape'; shape(p5); @@ -29,7 +29,8 @@ import data from './data'; data(p5); // DOM -import './dom/dom'; +import dom from './dom/dom'; +dom(p5, p5.prototype); // events import events from './events'; diff --git a/src/core/main.js b/src/core/main.js index 279df9a421..3b58026346 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -664,6 +664,8 @@ import environment from './environment'; import rendering from './rendering'; import renderer from './p5.Renderer'; import renderer2D from './p5.Renderer2D'; +import graphics from './p5.Graphics'; +import element from './p5.Element'; p5.registerAddon(transform); p5.registerAddon(structure); @@ -671,5 +673,7 @@ p5.registerAddon(environment); p5.registerAddon(rendering); p5.registerAddon(renderer); p5.registerAddon(renderer2D); +p5.registerAddon(graphics); +p5.registerAddon(element); export default p5; diff --git a/src/core/p5.Element.js b/src/core/p5.Element.js index b81d211276..6397cf67c0 100644 --- a/src/core/p5.Element.js +++ b/src/core/p5.Element.js @@ -4,53 +4,7 @@ * @for p5.Element */ -import p5 from './main'; - -/** - * A class to describe an - * HTML element. - * - * Sketches can use many elements. Common elements include the drawing canvas, - * buttons, sliders, webcam feeds, and so on. - * - * All elements share the methods of the `p5.Element` class. They're created - * with functions such as createCanvas() and - * createButton(). - * - * @class p5.Element - * @param {HTMLElement} elt wrapped DOM element. - * @param {p5} [pInst] pointer to p5 instance. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a button element and - * // place it beneath the canvas. - * let btn = createButton('change'); - * btn.position(0, 100); - * - * // Call randomColor() when - * // the button is pressed. - * btn.mousePressed(randomColor); - * - * describe('A gray square with a button that says "change" beneath it. The square changes color when the user presses the button.'); - * } - * - * // Paint the background either - * // red, yellow, blue, or green. - * function randomColor() { - * let c = random(['red', 'yellow', 'blue', 'green']); - * background(c); - * } - * - *
- */ -p5.Element = class { +class Element { constructor(elt, pInst) { this.elt = elt; this._pInst = this._pixelsState = pInst; @@ -944,52 +898,101 @@ p5.Element = class { } }; -/** - * The element's underlying `HTMLElement` object. - * - * The - * HTMLElement - * object's properties and methods can be used directly. - * - * @example - *
- * - * function setup() { - * // Create a canvas element and - * // assign it to cnv. - * let cnv = createCanvas(100, 100); - * - * background(200); - * - * // Set the border style for the - * // canvas. - * cnv.elt.style.border = '5px dashed deeppink'; - * - * describe('A gray square with a pink border drawn with dashed lines.'); - * } - * - *
- * - * @property elt - * @for p5.Element - * @name elt - * @readOnly - */ +function element(p5, fn){ + /** + * A class to describe an + * HTML element. + * + * Sketches can use many elements. Common elements include the drawing canvas, + * buttons, sliders, webcam feeds, and so on. + * + * All elements share the methods of the `p5.Element` class. They're created + * with functions such as createCanvas() and + * createButton(). + * + * @class p5.Element + * @param {HTMLElement} elt wrapped DOM element. + * @param {p5} [pInst] pointer to p5 instance. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a button element and + * // place it beneath the canvas. + * let btn = createButton('change'); + * btn.position(0, 100); + * + * // Call randomColor() when + * // the button is pressed. + * btn.mousePressed(randomColor); + * + * describe('A gray square with a button that says "change" beneath it. The square changes color when the user presses the button.'); + * } + * + * // Paint the background either + * // red, yellow, blue, or green. + * function randomColor() { + * let c = random(['red', 'yellow', 'blue', 'green']); + * background(c); + * } + * + *
+ */ + p5.Element = Element; -/** - * A `Number` property that stores the element's width. - * - * @type {Number} - * @property width - * @for p5.Element - */ + /** + * The element's underlying `HTMLElement` object. + * + * The + * HTMLElement + * object's properties and methods can be used directly. + * + * @example + *
+ * + * function setup() { + * // Create a canvas element and + * // assign it to cnv. + * let cnv = createCanvas(100, 100); + * + * background(200); + * + * // Set the border style for the + * // canvas. + * cnv.elt.style.border = '5px dashed deeppink'; + * + * describe('A gray square with a pink border drawn with dashed lines.'); + * } + * + *
+ * + * @property elt + * @for p5.Element + * @name elt + * @readOnly + */ -/** - * A `Number` property that stores the element's height. - * - * @type {Number} - * @property height - * @for p5.Element - */ + /** + * A `Number` property that stores the element's width. + * + * @type {Number} + * @property width + * @for p5.Element + */ + + /** + * A `Number` property that stores the element's height. + * + * @type {Number} + * @property height + * @for p5.Element + */ +} -export default p5.Element; +export default element; +export { Element }; diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 4416b4f0fd..fc20604ef2 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -16,97 +16,19 @@ import loadingDisplaying from '../image/loading_displaying'; import pixels from '../image/pixels'; import transform from './transform'; -/** - * A class to describe a drawing surface that's separate from the main canvas. - * - * Each `p5.Graphics` object provides a dedicated drawing surface called a - * *graphics buffer*. Graphics buffers are helpful when drawing should happen - * offscreen. For example, separate scenes can be drawn offscreen and - * displayed only when needed. - * - * `p5.Graphics` objects have nearly all the drawing features of the main - * canvas. For example, calling the method `myGraphics.circle(50, 50, 20)` - * draws to the graphics buffer. The resulting image can be displayed on the - * main canvas by passing the `p5.Graphics` object to the - * image() function, as in `image(myGraphics, 0, 0)`. - * - * Note: createGraphics() is the recommended - * way to create an instance of this class. - * - * @class p5.Graphics - * @extends p5.Element - * @param {Number} w width width of the graphics buffer in pixels. - * @param {Number} h height height of the graphics buffer in pixels. - * @param {(P2D|WEBGL)} renderer the renderer to use, either P2D or WEBGL. - * @param {p5} [pInst] sketch instance. - * @param {HTMLCanvasElement} [canvas] existing `<canvas>` element to use. - * - * @example - *
- * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Graphics object. - * pg = createGraphics(50, 50); - * - * // Draw to the p5.Graphics object. - * pg.background(100); - * pg.circle(25, 25, 20); - * - * describe('A dark gray square with a white circle at its center drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Display the p5.Graphics object. - * image(pg, 25, 25); - * } - * - *
- * - *
- * - * // Click the canvas to display the graphics buffer. - * - * let pg; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Graphics object. - * pg = createGraphics(50, 50); - * - * describe('A square appears on a gray background when the user presses the mouse. The square cycles between white and black.'); - * } - * - * function draw() { - * background(200); - * - * // Calculate the background color. - * let bg = frameCount % 255; - * - * // Draw to the p5.Graphics object. - * pg.background(bg); - * - * // Display the p5.Graphics object while - * // the user presses the mouse. - * if (mouseIsPressed === true) { - * image(pg, 25, 25); - * } - * } - * - *
- */ -p5.Graphics = class Graphics { +import primitives3D from '../webgl/3d_primitives'; +import light from '../webgl/light'; +import material from '../webgl/material'; +import creatingReading from '../color/creating_reading'; +import trigonometry from '../math/trigonometry'; +import { renderers } from './rendering'; + +class Graphics { constructor(w, h, renderer, pInst, canvas) { const r = renderer || constants.P2D; this._pInst = pInst; - this._renderer = new p5.renderers[r](this._pInst, w, h, false, canvas); + this._renderer = new renderers[r](this._pInst, w, h, false, canvas); // Attach renderer methods // for(const p of Object.getOwnPropertyNames(p5.renderers[r].prototype)) { @@ -147,11 +69,6 @@ p5.Graphics = class Graphics { return this; } - // NOTE: Temporary no op placeholder - static _validateParameters(){ - - } - get deltaTime(){ return this._pInst.deltaTime; } @@ -664,21 +581,124 @@ p5.Graphics = class Graphics { * */ createFramebuffer(options) { - return new p5.Framebuffer(this._pInst, options); + return new p5.Framebuffer(this, options); } + + _assert3d(name) { + if (!this._renderer.isP3D) + throw new Error( + `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` + ); + }; }; -// Shapes -primitives2D(p5.Graphics, p5.Graphics.prototype); -attributes(p5.Graphics, p5.Graphics.prototype); -curves(p5.Graphics, p5.Graphics.prototype); -vertex(p5.Graphics, p5.Graphics.prototype); +function graphics(p5, fn){ + /** + * A class to describe a drawing surface that's separate from the main canvas. + * + * Each `p5.Graphics` object provides a dedicated drawing surface called a + * *graphics buffer*. Graphics buffers are helpful when drawing should happen + * offscreen. For example, separate scenes can be drawn offscreen and + * displayed only when needed. + * + * `p5.Graphics` objects have nearly all the drawing features of the main + * canvas. For example, calling the method `myGraphics.circle(50, 50, 20)` + * draws to the graphics buffer. The resulting image can be displayed on the + * main canvas by passing the `p5.Graphics` object to the + * image() function, as in `image(myGraphics, 0, 0)`. + * + * Note: createGraphics() is the recommended + * way to create an instance of this class. + * + * @class p5.Graphics + * @extends p5.Element + * @param {Number} w width width of the graphics buffer in pixels. + * @param {Number} h height height of the graphics buffer in pixels. + * @param {(P2D|WEBGL)} renderer the renderer to use, either P2D or WEBGL. + * @param {p5} [pInst] sketch instance. + * @param {HTMLCanvasElement} [canvas] existing `<canvas>` element to use. + * + * @example + *
+ * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Graphics object. + * pg = createGraphics(50, 50); + * + * // Draw to the p5.Graphics object. + * pg.background(100); + * pg.circle(25, 25, 20); + * + * describe('A dark gray square with a white circle at its center drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Display the p5.Graphics object. + * image(pg, 25, 25); + * } + * + *
+ * + *
+ * + * // Click the canvas to display the graphics buffer. + * + * let pg; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Graphics object. + * pg = createGraphics(50, 50); + * + * describe('A square appears on a gray background when the user presses the mouse. The square cycles between white and black.'); + * } + * + * function draw() { + * background(200); + * + * // Calculate the background color. + * let bg = frameCount % 255; + * + * // Draw to the p5.Graphics object. + * pg.background(bg); + * + * // Display the p5.Graphics object while + * // the user presses the mouse. + * if (mouseIsPressed === true) { + * image(pg, 25, 25); + * } + * } + * + *
+ */ + p5.Graphics = Graphics; + + // Shapes + primitives2D(p5, p5.Graphics.prototype); + attributes(p5, p5.Graphics.prototype); + curves(p5, p5.Graphics.prototype); + vertex(p5, p5.Graphics.prototype); + + setting(p5, p5.Graphics.prototype); + loadingDisplaying(p5, p5.Graphics.prototype); + image(p5, p5.Graphics.prototype); + pixels(p5, p5.Graphics.prototype); -setting(p5.Graphics, p5.Graphics.prototype); -loadingDisplaying(p5.Graphics, p5.Graphics.prototype); -image(p5.Graphics, p5.Graphics.prototype); -pixels(p5.Graphics, p5.Graphics.prototype); + transform(p5, p5.Graphics.prototype); -transform(p5.Graphics, p5.Graphics.prototype); + primitives3D(p5, p5.Graphics.prototype); + light(p5, p5.Graphics.prototype); + material(p5, p5.Graphics.prototype); + creatingReading(p5, p5.Graphics.prototype); + trigonometry(p5, p5.Graphics.prototype); +} -export default p5.Graphics; +export default graphics; +export { Graphics }; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 0ac35bb2b4..4c6f470943 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -6,432 +6,362 @@ import * as constants from '../core/constants'; -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 - */ - p5.Renderer = class Renderer { - constructor(pInst, w, h, isMainCanvas) { - this._pInst = this._pixelsState = pInst; - this._isMainCanvas = isMainCanvas; - this.pixels = []; - this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; +class Renderer { + constructor(pInst, w, h, isMainCanvas) { + this._pInst = this._pixelsState = pInst; + this._isMainCanvas = isMainCanvas; + this.pixels = []; + this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; - this.width = w; - this.height = h; + this.width = w; + this.height = h; - this._events = {}; + this._events = {}; - if (isMainCanvas) { - this._isMainCanvas = true; - } - - // Renderer state machine - this.states = { - doStroke: true, - strokeSet: false, - doFill: true, - fillSet: false, - tint: null, - imageMode: constants.CORNER, - rectMode: constants.CORNER, - ellipseMode: constants.CENTER, - textFont: 'sans-serif', - textLeading: 15, - leadingSet: false, - textSize: 12, - textAlign: constants.LEFT, - textBaseline: constants.BASELINE, - textStyle: constants.NORMAL, - textWrap: constants.WORD - }; - this._pushPopStack = []; - // NOTE: can use the length of the push pop stack instead - this._pushPopDepth = 0; - - this._clipping = false; - this._clipInvert = false; - this._curveTightness = 0; + if (isMainCanvas) { + this._isMainCanvas = true; } - remove() { + // Renderer state machine + this.states = { + doStroke: true, + strokeSet: false, + doFill: true, + fillSet: false, + tint: null, + imageMode: constants.CORNER, + rectMode: constants.CORNER, + ellipseMode: constants.CENTER, + textFont: 'sans-serif', + textLeading: 15, + leadingSet: false, + textSize: 12, + textAlign: constants.LEFT, + textBaseline: constants.BASELINE, + textStyle: constants.NORMAL, + textWrap: constants.WORD + }; + this._pushPopStack = []; + // NOTE: can use the length of the push pop stack instead + this._pushPopDepth = 0; + + this._clipping = false; + this._clipInvert = false; + this._curveTightness = 0; + } - } + 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++; - const currentStates = Object.assign({}, this.states); - // Clone properties that support it - for (const key in currentStates) { - if (currentStates[key] instanceof Array) { - currentStates[key] = currentStates[key].slice(); - } else if (currentStates[key] && currentStates[key].clone instanceof Function) { - currentStates[key] = currentStates[key].clone(); - } + pixelDensity(val){ + let returnValue; + if (typeof val === 'number') { + if (val !== this._pixelDensity) { + this._pixelDensity = val; } - this._pushPopStack.push(currentStates); - return currentStates; - } - - // Pop the previous states out of the push pop stack and - // assign it back to the current state - pop() { - this._pushPopDepth--; - Object.assign(this.states, this._pushPopStack.pop()); + returnValue = this; + this.resize(this.width, this.height); + } else { + returnValue = this._pixelDensity; } + return returnValue; + } - 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()?"); + // Makes a shallow copy of the current states + // and push it into the push pop stack + push() { + this._pushPopDepth++; + const currentStates = Object.assign({}, this.states); + // Clone properties that support it + for (const key in currentStates) { + if (currentStates[key] instanceof Array) { + currentStates[key] = currentStates[key].slice(); + } else if (currentStates[key] && currentStates[key].clone instanceof Function) { + currentStates[key] = currentStates[key].clone(); } - this._clipping = true; - this._clipInvert = options.invert; } + this._pushPopStack.push(currentStates); + return currentStates; + } - 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; + // Pop the previous states out of the push pop stack and + // assign it back to the current state + pop() { + this._pushPopDepth--; + Object.assign(this.states, this._pushPopStack.pop()); + } + + 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; + } - /** - * Resize our canvas element. - */ - resize(w, h) { - this.width = w; - this.height = h; + 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; + } - get(x, y, w, h) { - const pixelsState = this._pixelsState; - const pd = this._pixelDensity; - const canvas = this.canvas; + /** + * Resize our canvas element. + */ + resize(w, h) { + this.width = w; + this.height = h; + } - if (typeof x === 'undefined' && typeof y === 'undefined') { - // get() - x = y = 0; - w = pixelsState.width; - h = pixelsState.height; - } else { - x *= pd; - y *= pd; + get(x, y, w, h) { + const pixelsState = this._pixelsState; + const pd = this._pixelDensity; + const canvas = this.canvas; - 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]; - } + if (typeof x === 'undefined' && typeof y === 'undefined') { + // get() + x = y = 0; + w = pixelsState.width; + h = pixelsState.height; + } else { + x *= pd; + y *= pd; - return this._getPixel(x, y); + 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]; } - // get(x,y,w,h) + + return this._getPixel(x, y); } + // get(x,y,w,h) + } - const region = new p5.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); + const region = new p5.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; - } + return region; + } - scale(x, y){ + scale(x, y){ - } + } - textSize(s) { - if (typeof s === 'number') { - this.states.textSize = s; - if (!this.states.leadingSet) { - // only use a default value if not previously set (#5181) - this.states.textLeading = s * constants._DEFAULT_LEADMULT; - } - return this._applyTextProperties(); + textSize(s) { + if (typeof s === 'number') { + this.states.textSize = s; + if (!this.states.leadingSet) { + // only use a default value if not previously set (#5181) + this.states.textLeading = s * constants._DEFAULT_LEADMULT; } - - return this.states.textSize; + return this._applyTextProperties(); } - textLeading (l) { - if (typeof l === 'number') { - this.states.leadingSet = true; - this.states.textLeading = l; - return this._pInst; - } + return this.states.textSize; + } - return this.states.textLeading; + textLeading (l) { + if (typeof l === 'number') { + this.states.leadingSet = true; + this.states.textLeading = l; + return this._pInst; } - textStyle (s) { - if (s) { - if ( - s === constants.NORMAL || - s === constants.ITALIC || - s === constants.BOLD || - s === constants.BOLDITALIC - ) { - this.states.textStyle = s; - } + return this.states.textLeading; + } - return this._applyTextProperties(); + textStyle (s) { + if (s) { + if ( + s === constants.NORMAL || + s === constants.ITALIC || + s === constants.BOLD || + s === constants.BOLDITALIC + ) { + this.states.textStyle = s; } - return this.states.textStyle; + return this._applyTextProperties(); } - textAscent () { - if (this.states.textAscent === null) { - this._updateTextMetrics(); - } - return this.states.textAscent; - } + return this.states.textStyle; + } - textDescent () { - if (this.states.textDescent === null) { - this._updateTextMetrics(); - } - return this.states.textDescent; + textAscent () { + if (this.states.textAscent === null) { + this._updateTextMetrics(); } + return this.states.textAscent; + } - textAlign (h, v) { - if (typeof h !== 'undefined') { - this.states.textAlign = h; + textDescent () { + if (this.states.textDescent === null) { + this._updateTextMetrics(); + } + return this.states.textDescent; + } - if (typeof v !== 'undefined') { - this.states.textBaseline = v; - } + textAlign (h, v) { + if (typeof h !== 'undefined') { + this.states.textAlign = h; - return this._applyTextProperties(); - } else { - return { - horizontal: this.states.textAlign, - vertical: this.states.textBaseline - }; + if (typeof v !== 'undefined') { + this.states.textBaseline = v; } + + return this._applyTextProperties(); + } else { + return { + horizontal: this.states.textAlign, + vertical: this.states.textBaseline + }; + } + } + + textWrap (wrapStyle) { + this.states.textWrap = wrapStyle; + return this.states.textWrap; + } + + text(str, x, y, maxWidth, maxHeight) { + const p = this._pInst; + const textWrapStyle = this.states.textWrap; + + let lines; + let line; + let testLine; + let testWidth; + let words; + let chars; + let shiftedY; + let finalMaxHeight = Number.MAX_VALUE; + // fix for #5785 (top of bounding box) + let finalMinHeight = y; + + if (!(this.states.doFill || this.states.doStroke)) { + return; } - textWrap (wrapStyle) { - this.states.textWrap = wrapStyle; - return this.states.textWrap; + if (typeof str === 'undefined') { + return; + } else if (typeof str !== 'string') { + str = str.toString(); } - text(str, x, y, maxWidth, maxHeight) { - const p = this._pInst; - const textWrapStyle = this.states.textWrap; - - let lines; - let line; - let testLine; - let testWidth; - let words; - let chars; - let shiftedY; - let finalMaxHeight = Number.MAX_VALUE; - // fix for #5785 (top of bounding box) - let finalMinHeight = y; - - if (!(this.states.doFill || this.states.doStroke)) { - return; - } + // Replaces tabs with double-spaces and splits string on any line + // breaks present in the original string + str = str.replace(/(\t)/g, ' '); + lines = str.split('\n'); - if (typeof str === 'undefined') { - return; - } else if (typeof str !== 'string') { - str = str.toString(); + if (typeof maxWidth !== 'undefined') { + if (this.states.rectMode === constants.CENTER) { + x -= maxWidth / 2; } - // Replaces tabs with double-spaces and splits string on any line - // breaks present in the original string - str = str.replace(/(\t)/g, ' '); - lines = str.split('\n'); + switch (this.states.textAlign) { + case constants.CENTER: + x += maxWidth / 2; + break; + case constants.RIGHT: + x += maxWidth; + break; + } - if (typeof maxWidth !== 'undefined') { + if (typeof maxHeight !== 'undefined') { if (this.states.rectMode === constants.CENTER) { - x -= maxWidth / 2; + y -= maxHeight / 2; + finalMinHeight -= maxHeight / 2; } - switch (this.states.textAlign) { - case constants.CENTER: - x += maxWidth / 2; + let originalY = y; + let ascent = p.textAscent(); + + switch (this.states.textBaseline) { + case constants.BOTTOM: + shiftedY = y + maxHeight; + y = Math.max(shiftedY, y); + // fix for #5785 (top of bounding box) + finalMinHeight += ascent; break; - case constants.RIGHT: - x += maxWidth; + case constants.CENTER: + shiftedY = y + maxHeight / 2; + y = Math.max(shiftedY, y); + // fix for #5785 (top of bounding box) + finalMinHeight += ascent / 2; break; } - if (typeof maxHeight !== 'undefined') { - if (this.states.rectMode === constants.CENTER) { - y -= maxHeight / 2; - finalMinHeight -= maxHeight / 2; - } + // remember the max-allowed y-position for any line (fix to #928) + finalMaxHeight = y + maxHeight - ascent; - let originalY = y; - let ascent = p.textAscent(); - - switch (this.states.textBaseline) { - case constants.BOTTOM: - shiftedY = y + maxHeight; - y = Math.max(shiftedY, y); - // fix for #5785 (top of bounding box) - finalMinHeight += ascent; - break; - case constants.CENTER: - shiftedY = y + maxHeight / 2; - y = Math.max(shiftedY, y); - // fix for #5785 (top of bounding box) - finalMinHeight += ascent / 2; - break; - } - - // remember the max-allowed y-position for any line (fix to #928) - finalMaxHeight = y + maxHeight - ascent; - - // fix for #5785 (bottom of bounding box) - if (this.states.textBaseline === constants.CENTER) { - finalMaxHeight = originalY + maxHeight - ascent / 2; - } - } else { - // no text-height specified, show warning for BOTTOM / CENTER - if (this.states.textBaseline === constants.BOTTOM || - this.states.textBaseline === constants.CENTER) { - // use rectHeight as an approximation for text height - let rectHeight = p.textSize() * this.states.textLeading; - finalMinHeight = y - rectHeight / 2; - finalMaxHeight = y + rectHeight / 2; - } + // fix for #5785 (bottom of bounding box) + if (this.states.textBaseline === constants.CENTER) { + finalMaxHeight = originalY + maxHeight - ascent / 2; } + } else { + // no text-height specified, show warning for BOTTOM / CENTER + if (this.states.textBaseline === constants.BOTTOM || + this.states.textBaseline === constants.CENTER) { + // use rectHeight as an approximation for text height + let rectHeight = p.textSize() * this.states.textLeading; + finalMinHeight = y - rectHeight / 2; + finalMaxHeight = y + rectHeight / 2; + } + } - // Render lines of text according to settings of textWrap - // Splits lines at spaces, for loop adds one word + space - // at a time and tests length with next word added - if (textWrapStyle === constants.WORD) { - let nlines = []; - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - words = lines[lineIndex].split(' '); - for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { - testLine = `${line + words[wordIndex]}` + ' '; - testWidth = this.textWidth(testLine); - if (testWidth > maxWidth && line.length > 0) { - nlines.push(line); - line = `${words[wordIndex]}` + ' '; - } else { - line = testLine; - } - } - nlines.push(line); - } - - let offset = 0; - if (this.states.textBaseline === constants.CENTER) { - offset = (nlines.length - 1) * p.textLeading() / 2; - } else if (this.states.textBaseline === constants.BOTTOM) { - offset = (nlines.length - 1) * p.textLeading(); - } - - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - words = lines[lineIndex].split(' '); - for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { - testLine = `${line + words[wordIndex]}` + ' '; - testWidth = this.textWidth(testLine); - if (testWidth > maxWidth && line.length > 0) { - this._renderText( - p, - line.trim(), - x, - y - offset, - finalMaxHeight, - finalMinHeight - ); - line = `${words[wordIndex]}` + ' '; - y += p.textLeading(); - } else { - line = testLine; - } + // Render lines of text according to settings of textWrap + // Splits lines at spaces, for loop adds one word + space + // at a time and tests length with next word added + if (textWrapStyle === constants.WORD) { + let nlines = []; + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + words = lines[lineIndex].split(' '); + for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { + testLine = `${line + words[wordIndex]}` + ' '; + testWidth = this.textWidth(testLine); + if (testWidth > maxWidth && line.length > 0) { + nlines.push(line); + line = `${words[wordIndex]}` + ' '; + } else { + line = testLine; } - this._renderText( - p, - line.trim(), - x, - y - offset, - finalMaxHeight, - finalMinHeight - ); - y += p.textLeading(); } - } else { - let nlines = []; - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - chars = lines[lineIndex].split(''); - for (let charIndex = 0; charIndex < chars.length; charIndex++) { - testLine = `${line + chars[charIndex]}`; - testWidth = this.textWidth(testLine); - if (testWidth <= maxWidth) { - line += chars[charIndex]; - } else if (testWidth > maxWidth && line.length > 0) { - nlines.push(line); - line = `${chars[charIndex]}`; - } - } - } - nlines.push(line); - let offset = 0; - if (this.states.textBaseline === constants.CENTER) { - offset = (nlines.length - 1) * p.textLeading() / 2; - } else if (this.states.textBaseline === constants.BOTTOM) { - offset = (nlines.length - 1) * p.textLeading(); - } + } - // Splits lines at characters, for loop adds one char at a time - // and tests length with next char added - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - line = ''; - chars = lines[lineIndex].split(''); - for (let charIndex = 0; charIndex < chars.length; charIndex++) { - testLine = `${line + chars[charIndex]}`; - testWidth = this.textWidth(testLine); - if (testWidth <= maxWidth) { - line += chars[charIndex]; - } else if (testWidth > maxWidth && line.length > 0) { - this._renderText( - p, - line.trim(), - x, - y - offset, - finalMaxHeight, - finalMinHeight - ); - y += p.textLeading(); - line = `${chars[charIndex]}`; - } + let offset = 0; + if (this.states.textBaseline === constants.CENTER) { + offset = (nlines.length - 1) * p.textLeading() / 2; + } else if (this.states.textBaseline === constants.BOTTOM) { + offset = (nlines.length - 1) * p.textLeading(); + } + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + words = lines[lineIndex].split(' '); + for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { + testLine = `${line + words[wordIndex]}` + ' '; + testWidth = this.textWidth(testLine); + if (testWidth > maxWidth && line.length > 0) { + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); + line = `${words[wordIndex]}` + ' '; + y += p.textLeading(); + } else { + line = testLine; } } this._renderText( @@ -445,88 +375,160 @@ function renderer(p5, fn){ y += p.textLeading(); } } else { - // Offset to account for vertically centering multiple lines of text - no - // need to adjust anything for vertical align top or baseline + let nlines = []; + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + chars = lines[lineIndex].split(''); + for (let charIndex = 0; charIndex < chars.length; charIndex++) { + testLine = `${line + chars[charIndex]}`; + testWidth = this.textWidth(testLine); + if (testWidth <= maxWidth) { + line += chars[charIndex]; + } else if (testWidth > maxWidth && line.length > 0) { + nlines.push(line); + line = `${chars[charIndex]}`; + } + } + } + + nlines.push(line); let offset = 0; if (this.states.textBaseline === constants.CENTER) { - offset = (lines.length - 1) * p.textLeading() / 2; + offset = (nlines.length - 1) * p.textLeading() / 2; } else if (this.states.textBaseline === constants.BOTTOM) { - offset = (lines.length - 1) * p.textLeading(); + offset = (nlines.length - 1) * p.textLeading(); } - // Renders lines of text at any line breaks present in the original string - for (let i = 0; i < lines.length; i++) { - this._renderText( - p, - lines[i], - x, - y - offset, - finalMaxHeight, - finalMinHeight - offset - ); - y += p.textLeading(); + // Splits lines at characters, for loop adds one char at a time + // and tests length with next char added + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + line = ''; + chars = lines[lineIndex].split(''); + for (let charIndex = 0; charIndex < chars.length; charIndex++) { + testLine = `${line + chars[charIndex]}`; + testWidth = this.textWidth(testLine); + if (testWidth <= maxWidth) { + line += chars[charIndex]; + } else if (testWidth > maxWidth && line.length > 0) { + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); + y += p.textLeading(); + line = `${chars[charIndex]}`; + } + } } + this._renderText( + p, + line.trim(), + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); + y += p.textLeading(); + } + } else { + // Offset to account for vertically centering multiple lines of text - no + // need to adjust anything for vertical align top or baseline + let offset = 0; + if (this.states.textBaseline === constants.CENTER) { + offset = (lines.length - 1) * p.textLeading() / 2; + } else if (this.states.textBaseline === constants.BOTTOM) { + offset = (lines.length - 1) * p.textLeading(); } - return p; + // Renders lines of text at any line breaks present in the original string + for (let i = 0; i < lines.length; i++) { + this._renderText( + p, + lines[i], + x, + y - offset, + finalMaxHeight, + finalMinHeight - offset + ); + y += p.textLeading(); + } } - _applyDefaults() { - return this; - } + return p; + } - /** - * Helper function to check font type (system or otf) - */ - _isOpenType(f = this.states.textFont) { - return typeof f === 'object' && f.font && f.font.supported; + _applyDefaults() { + return this; + } + + /** + * Helper function to check font type (system or otf) + */ + _isOpenType(f = this.states.textFont) { + return typeof f === 'object' && f.font && f.font.supported; + } + + _updateTextMetrics() { + if (this._isOpenType()) { + this.states.textAscent = this.states.textFont._textAscent(); + this.states.textDescent = this.states.textFont._textDescent(); + return this; } - _updateTextMetrics() { - if (this._isOpenType()) { - this.states.textAscent = this.states.textFont._textAscent(); - this.states.textDescent = this.states.textFont._textDescent(); - return this; - } + // Adapted from http://stackoverflow.com/a/25355178 + const text = document.createElement('span'); + text.style.fontFamily = this.states.textFont; + text.style.fontSize = `${this.states.textSize}px`; + text.innerHTML = 'ABCjgq|'; - // Adapted from http://stackoverflow.com/a/25355178 - const text = document.createElement('span'); - text.style.fontFamily = this.states.textFont; - text.style.fontSize = `${this.states.textSize}px`; - text.innerHTML = 'ABCjgq|'; + const block = document.createElement('div'); + block.style.display = 'inline-block'; + block.style.width = '1px'; + block.style.height = '0px'; - const block = document.createElement('div'); - block.style.display = 'inline-block'; - block.style.width = '1px'; - block.style.height = '0px'; + const container = document.createElement('div'); + container.appendChild(text); + container.appendChild(block); - const container = document.createElement('div'); - container.appendChild(text); - container.appendChild(block); + container.style.height = '0px'; + container.style.overflow = 'hidden'; + document.body.appendChild(container); - container.style.height = '0px'; - container.style.overflow = 'hidden'; - document.body.appendChild(container); + block.style.verticalAlign = 'baseline'; + let blockOffset = calculateOffset(block); + let textOffset = calculateOffset(text); + const ascent = blockOffset[1] - textOffset[1]; - block.style.verticalAlign = 'baseline'; - let blockOffset = calculateOffset(block); - let textOffset = calculateOffset(text); - const ascent = blockOffset[1] - textOffset[1]; + block.style.verticalAlign = 'bottom'; + blockOffset = calculateOffset(block); + textOffset = calculateOffset(text); + const height = blockOffset[1] - textOffset[1]; + const descent = height - ascent; - block.style.verticalAlign = 'bottom'; - blockOffset = calculateOffset(block); - textOffset = calculateOffset(text); - const height = blockOffset[1] - textOffset[1]; - const descent = height - ascent; + document.body.removeChild(container); - document.body.removeChild(container); + this.states.textAscent = ascent; + this.states.textDescent = descent; - this.states.textAscent = ascent; - this.states.textDescent = descent; + return this; + } +}; - return this; - } - }; +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 + */ + p5.Renderer = Renderer; /** * Helper fxn to measure ascent and descent. @@ -549,3 +551,4 @@ function renderer(p5, fn){ } export default renderer; +export { Renderer }; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 967f2501f4..60cdacf0a2 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1,1477 +1,1484 @@ import * as constants from './constants'; +import p5 from './main'; +import { Renderer } from './p5.Renderer'; +import { Element } from './p5.Element'; +import { Graphics } from './p5.Graphics'; +import { Image } from '../image/p5.Image'; +import { MediaElement } from '../dom/dom'; + +const styleEmpty = 'rgba(0,0,0,0)'; +// const alphaThreshold = 0.00125; // minimum visible + +class Renderer2D extends Renderer { + constructor(pInst, w, h, isMainCanvas, elt) { + super(pInst, w, h, isMainCanvas); + + this.canvas = this.elt = elt || document.createElement('canvas'); + + if (isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } -function renderer2D(p5, fn){ - const styleEmpty = 'rgba(0,0,0,0)'; - // const alphaThreshold = 0.00125; // minimum visible - - /** - * p5.Renderer2D - * The 2D graphics canvas renderer class. - * extends p5.Renderer - * @private - */ - class Renderer2D extends p5.Renderer { - constructor(pInst, w, h, isMainCanvas, elt) { - super(pInst, w, h, isMainCanvas); - - this.canvas = this.elt = elt || document.createElement('canvas'); - - if (isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } - - this.elt.id = 'defaultCanvas0'; - this.elt.classList.add('p5Canvas'); + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); - // Extend renderer with methods of p5.Element with getters - // this.wrappedElt = new p5.Element(elt, pInst); - for (const p of Object.getOwnPropertyNames(p5.Element.prototype)) { - if (p !== 'constructor' && p[0] !== '_') { - Object.defineProperty(this, p, { - get() { - return this.wrappedElt[p]; - } - }) - } + // Extend renderer with methods of p5.Element with getters + // this.wrappedElt = new p5.Element(elt, pInst); + for (const p of Object.getOwnPropertyNames(Element.prototype)) { + if (p !== 'constructor' && p[0] !== '_') { + Object.defineProperty(this, p, { + get() { + return this.wrappedElt[p]; + } + }) } + } - // Set canvas size - this.elt.width = w * this._pixelDensity; - this.elt.height = h * this._pixelDensity; - this.elt.style.width = `${w}px`; - this.elt.style.height = `${h}px`; + // Set canvas size + this.elt.width = w * this._pixelDensity; + this.elt.height = h * this._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; + + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName('main').length === 0) { + let m = document.createElement('main'); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName('main')[0].appendChild(this.elt); + } - // Attach canvas element to DOM - if (this._pInst._userNode) { - // user input node case - this._pInst._userNode.appendChild(this.elt); - } else { - //create main element - if (document.getElementsByTagName('main').length === 0) { - let m = document.createElement('main'); - document.body.appendChild(m); - } - //append canvas to main - document.getElementsByTagName('main')[0].appendChild(this.elt); - } + // Get and store drawing context + this.drawingContext = this.canvas.getContext('2d'); + this._pInst.drawingContext = this.drawingContext; + this.scale(this._pixelDensity, this._pixelDensity); - // Get and store drawing context - this.drawingContext = this.canvas.getContext('2d'); - this._pInst.drawingContext = this.drawingContext; - this.scale(this._pixelDensity, this._pixelDensity); + // Set and return p5.Element + this.wrappedElt = new Element(this.elt, this._pInst); + } - // Set and return p5.Element - this.wrappedElt = new p5.Element(this.elt, this._pInst); - } + remove(){ + this.wrappedElt.remove(); + this.wrappedElt = null; + this.canvas = null; + this.elt = null; + } - remove(){ - this.wrappedElt.remove(); - this.wrappedElt = null; - this.canvas = null; - this.elt = null; + getFilterGraphicsLayer() { + // create hidden webgl renderer if it doesn't exist + if (!this.filterGraphicsLayer) { + const pInst = this._pInst; + + // create secondary layer + this.filterGraphicsLayer = + new Graphics( + this.width, + this.height, + constants.WEBGL, + pInst + ); } - - getFilterGraphicsLayer() { - // create hidden webgl renderer if it doesn't exist - if (!this.filterGraphicsLayer) { - const pInst = this._pInst; - - // create secondary layer - this.filterGraphicsLayer = - new p5.Graphics( - this.width, - this.height, - constants.WEBGL, - pInst - ); - } - if ( - this.filterGraphicsLayer.width !== this.width || - this.filterGraphicsLayer.height !== this.height - ) { - // Resize the graphics layer - this.filterGraphicsLayer.resizeCanvas(this.width, this.height); - } - if ( - this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() - ) { - this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); - } - return this.filterGraphicsLayer; + if ( + this.filterGraphicsLayer.width !== this.width || + this.filterGraphicsLayer.height !== this.height + ) { + // Resize the graphics layer + this.filterGraphicsLayer.resizeCanvas(this.width, this.height); } - - _applyDefaults() { - this._cachedFillStyle = this._cachedStrokeStyle = undefined; - this._cachedBlendMode = constants.BLEND; - this._setFill(constants._DEFAULT_FILL); - this._setStroke(constants._DEFAULT_STROKE); - this.drawingContext.lineCap = constants.ROUND; - this.drawingContext.font = 'normal 12px sans-serif'; + if ( + this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() + ) { + this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); } + return this.filterGraphicsLayer; + } - resize(w, h) { - super.resize(w, h); - - // save canvas properties - const props = {}; - for (const key in this.drawingContext) { - const val = this.drawingContext[key]; - if (typeof val !== 'object' && typeof val !== 'function') { - props[key] = val; - } - } + _applyDefaults() { + this._cachedFillStyle = this._cachedStrokeStyle = undefined; + this._cachedBlendMode = constants.BLEND; + this._setFill(constants._DEFAULT_FILL); + this._setStroke(constants._DEFAULT_STROKE); + this.drawingContext.lineCap = constants.ROUND; + this.drawingContext.font = 'normal 12px sans-serif'; + } - this.canvas.width = w * this._pixelDensity; - this.canvas.height = h * this._pixelDensity; - this.canvas.style.width = `${w}px`; - this.canvas.style.height = `${h}px`; - this.drawingContext.scale( - this._pixelDensity, - this._pixelDensity - ); + resize(w, h) { + super.resize(w, h); - // reset canvas properties - for (const savedKey in props) { - try { - this.drawingContext[savedKey] = props[savedKey]; - } catch (err) { - // ignore read-only property errors - } + // save canvas properties + const props = {}; + for (const key in this.drawingContext) { + const val = this.drawingContext[key]; + if (typeof val !== 'object' && typeof val !== 'function') { + props[key] = val; } } - ////////////////////////////////////////////// - // COLOR | Setting - ////////////////////////////////////////////// - - background(...args) { - this.drawingContext.save(); - this.resetMatrix(); - - if (args[0] instanceof p5.Image) { - if (args[1] >= 0) { - // set transparency of background - const img = args[0]; - this.drawingContext.globalAlpha = args[1] / 255; - this._pInst.image(img, 0, 0, this.width, this.height); - } else { - this._pInst.image(args[0], 0, 0, this.width, this.height); - } - } else { - const curFill = this._getFill(); - // create background rect - const color = this._pInst.color(...args); - - //accessible Outputs - if (this._pInst._addAccsOutput()) { - this._pInst._accsBackground(color.levels); - } - - const newFill = color.toString(); - this._setFill(newFill); - - if (this._isErasing) { - this.blendMode(this._cachedBlendMode); - } - - this.drawingContext.fillRect(0, 0, this.width, this.height); - // reset fill - this._setFill(curFill); - - if (this._isErasing) { - this._pInst.erase(); - } + this.canvas.width = w * this._pixelDensity; + this.canvas.height = h * this._pixelDensity; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; + this.drawingContext.scale( + this._pixelDensity, + this._pixelDensity + ); + + // reset canvas properties + for (const savedKey in props) { + try { + this.drawingContext[savedKey] = props[savedKey]; + } catch (err) { + // ignore read-only property errors } - this.drawingContext.restore(); } + } - clear() { - this.drawingContext.save(); - this.resetMatrix(); - this.drawingContext.clearRect(0, 0, this.width, this.height); - this.drawingContext.restore(); - } + ////////////////////////////////////////////// + // COLOR | Setting + ////////////////////////////////////////////// - fill(...args) { - const color = this._pInst.color(...args); - this._setFill(color.toString()); + background(...args) { + this.drawingContext.save(); + this.resetMatrix(); - //accessible Outputs - if (this._pInst._addAccsOutput()) { - this._pInst._accsCanvasColors('fill', color.levels); + if (args[0] instanceof Image) { + if (args[1] >= 0) { + // set transparency of background + const img = args[0]; + this.drawingContext.globalAlpha = args[1] / 255; + this._pInst.image(img, 0, 0, this.width, this.height); + } else { + this._pInst.image(args[0], 0, 0, this.width, this.height); } - } - - stroke(...args) { + } else { + const curFill = this._getFill(); + // create background rect const color = this._pInst.color(...args); - this._setStroke(color.toString()); //accessible Outputs if (this._pInst._addAccsOutput()) { - this._pInst._accsCanvasColors('stroke', color.levels); + this._pInst._accsBackground(color.levels); } - } - erase(opacityFill, opacityStroke) { - if (!this._isErasing) { - // cache the fill style - this._cachedFillStyle = this.drawingContext.fillStyle; - const newFill = this._pInst.color(255, opacityFill).toString(); - this.drawingContext.fillStyle = newFill; + const newFill = color.toString(); + this._setFill(newFill); - // cache the stroke style - this._cachedStrokeStyle = this.drawingContext.strokeStyle; - const newStroke = this._pInst.color(255, opacityStroke).toString(); - this.drawingContext.strokeStyle = newStroke; + if (this._isErasing) { + this.blendMode(this._cachedBlendMode); + } - // cache blendMode - const tempBlendMode = this._cachedBlendMode; - this.blendMode(constants.REMOVE); - this._cachedBlendMode = tempBlendMode; + this.drawingContext.fillRect(0, 0, this.width, this.height); + // reset fill + this._setFill(curFill); - this._isErasing = true; + if (this._isErasing) { + this._pInst.erase(); } } + this.drawingContext.restore(); + } - noErase() { - if (this._isErasing) { - this.drawingContext.fillStyle = this._cachedFillStyle; - this.drawingContext.strokeStyle = this._cachedStrokeStyle; + clear() { + this.drawingContext.save(); + this.resetMatrix(); + this.drawingContext.clearRect(0, 0, this.width, this.height); + this.drawingContext.restore(); + } - this.blendMode(this._cachedBlendMode); - this._isErasing = false; - } + fill(...args) { + const color = this._pInst.color(...args); + this._setFill(color.toString()); + + //accessible Outputs + if (this._pInst._addAccsOutput()) { + this._pInst._accsCanvasColors('fill', color.levels); } + } - beginClip(options = {}) { - super.beginClip(options); + stroke(...args) { + const color = this._pInst.color(...args); + this._setStroke(color.toString()); + //accessible Outputs + if (this._pInst._addAccsOutput()) { + this._pInst._accsCanvasColors('stroke', color.levels); + } + } + + erase(opacityFill, opacityStroke) { + if (!this._isErasing) { // cache the fill style this._cachedFillStyle = this.drawingContext.fillStyle; - const newFill = this._pInst.color(255, 0).toString(); + const newFill = this._pInst.color(255, opacityFill).toString(); this.drawingContext.fillStyle = newFill; // cache the stroke style this._cachedStrokeStyle = this.drawingContext.strokeStyle; - const newStroke = this._pInst.color(255, 0).toString(); + const newStroke = this._pInst.color(255, opacityStroke).toString(); this.drawingContext.strokeStyle = newStroke; // cache blendMode const tempBlendMode = this._cachedBlendMode; - this.blendMode(constants.BLEND); + this.blendMode(constants.REMOVE); this._cachedBlendMode = tempBlendMode; - // Start a new path. Everything from here on out should become part of this - // one path so that we can clip to the whole thing. - this.drawingContext.beginPath(); - - if (this._clipInvert) { - // Slight hack: draw a big rectangle over everything with reverse winding - // order. This is hopefully large enough to cover most things. - this.drawingContext.moveTo( - -2 * this.width, - -2 * this.height - ); - this.drawingContext.lineTo( - -2 * this.width, - 2 * this.height - ); - this.drawingContext.lineTo( - 2 * this.width, - 2 * this.height - ); - this.drawingContext.lineTo( - 2 * this.width, - -2 * this.height - ); - this.drawingContext.closePath(); - } + this._isErasing = true; } + } - endClip() { - this._doFillStrokeClose(); - this.drawingContext.clip(); - - super.endClip(); - + noErase() { + if (this._isErasing) { this.drawingContext.fillStyle = this._cachedFillStyle; this.drawingContext.strokeStyle = this._cachedStrokeStyle; this.blendMode(this._cachedBlendMode); + this._isErasing = false; } + } - ////////////////////////////////////////////// - // IMAGE | Loading & Displaying - ////////////////////////////////////////////// - - image( - img, - sx, - sy, - sWidth, - sHeight, - dx, - dy, - dWidth, - dHeight - ) { - let cnv; - if (img.gifProperties) { - img._animateGif(this._pInst); - } + beginClip(options = {}) { + super.beginClip(options); + + // cache the fill style + this._cachedFillStyle = this.drawingContext.fillStyle; + const newFill = this._pInst.color(255, 0).toString(); + this.drawingContext.fillStyle = newFill; + + // cache the stroke style + this._cachedStrokeStyle = this.drawingContext.strokeStyle; + const newStroke = this._pInst.color(255, 0).toString(); + this.drawingContext.strokeStyle = newStroke; + + // cache blendMode + const tempBlendMode = this._cachedBlendMode; + this.blendMode(constants.BLEND); + this._cachedBlendMode = tempBlendMode; + + // Start a new path. Everything from here on out should become part of this + // one path so that we can clip to the whole thing. + this.drawingContext.beginPath(); + + if (this._clipInvert) { + // Slight hack: draw a big rectangle over everything with reverse winding + // order. This is hopefully large enough to cover most things. + this.drawingContext.moveTo( + -2 * this.width, + -2 * this.height + ); + this.drawingContext.lineTo( + -2 * this.width, + 2 * this.height + ); + this.drawingContext.lineTo( + 2 * this.width, + 2 * this.height + ); + this.drawingContext.lineTo( + 2 * this.width, + -2 * this.height + ); + this.drawingContext.closePath(); + } + } - try { - if (p5.MediaElement && img instanceof p5.MediaElement) { - img._ensureCanvas(); - } - if (this.states.tint && img.canvas) { - cnv = this._getTintedImageCanvas(img); - } - if (!cnv) { - cnv = img.canvas || img.elt; - } - let s = 1; - if (img.width && img.width > 0) { - s = cnv.width / img.width; - } - if (this._isErasing) { - this.blendMode(this._cachedBlendMode); - } + endClip() { + this._doFillStrokeClose(); + this.drawingContext.clip(); - this.drawingContext.drawImage( - cnv, - s * sx, - s * sy, - s * sWidth, - s * sHeight, - dx, - dy, - dWidth, - dHeight - ); - if (this._isErasing) { - this._pInst.erase(); - } - } catch (e) { - if (e.name !== 'NS_ERROR_NOT_AVAILABLE') { - throw e; - } - } + super.endClip(); + + this.drawingContext.fillStyle = this._cachedFillStyle; + this.drawingContext.strokeStyle = this._cachedStrokeStyle; + + this.blendMode(this._cachedBlendMode); + } + + ////////////////////////////////////////////// + // IMAGE | Loading & Displaying + ////////////////////////////////////////////// + + image( + img, + sx, + sy, + sWidth, + sHeight, + dx, + dy, + dWidth, + dHeight + ) { + let cnv; + if (img.gifProperties) { + img._animateGif(this._pInst); } - _getTintedImageCanvas(img) { - if (!img.canvas) { - return img; + try { + if (img instanceof MediaElement) { + img._ensureCanvas(); } - - if (!img.tintCanvas) { - // Once an image has been tinted, keep its tint canvas - // around so we don't need to re-incur the cost of - // creating a new one for each tint - img.tintCanvas = document.createElement('canvas'); + if (this.states.tint && img.canvas) { + cnv = this._getTintedImageCanvas(img); } - - // Keep the size of the tint canvas up-to-date - if (img.tintCanvas.width !== img.canvas.width) { - img.tintCanvas.width = img.canvas.width; + if (!cnv) { + cnv = img.canvas || img.elt; } - if (img.tintCanvas.height !== img.canvas.height) { - img.tintCanvas.height = img.canvas.height; + let s = 1; + if (img.width && img.width > 0) { + s = cnv.width / img.width; } - - // Goal: multiply the r,g,b,a values of the source by - // the r,g,b,a values of the tint color - const ctx = img.tintCanvas.getContext('2d'); - - ctx.save(); - ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); - - if (this.states.tint[0] < 255 || this.states.tint[1] < 255 || this.states.tint[2] < 255) { - // Color tint: we need to use the multiply blend mode to change the colors. - // However, the canvas implementation of this destroys the alpha channel of - // the image. To accommodate, we first get a version of the image with full - // opacity everywhere, tint using multiply, and then use the destination-in - // blend mode to restore the alpha channel again. - - // Start with the original image - ctx.drawImage(img.canvas, 0, 0); - - // This blend mode makes everything opaque but forces the luma to match - // the original image again - ctx.globalCompositeOperation = 'luminosity'; - ctx.drawImage(img.canvas, 0, 0); - - // This blend mode forces the hue and chroma to match the original image. - // After this we should have the original again, but with full opacity. - ctx.globalCompositeOperation = 'color'; - ctx.drawImage(img.canvas, 0, 0); - - // Apply color tint - ctx.globalCompositeOperation = 'multiply'; - ctx.fillStyle = `rgb(${this.states.tint.slice(0, 3).join(', ')})`; - ctx.fillRect(0, 0, img.canvas.width, img.canvas.height); - - // Replace the alpha channel with the original alpha * the alpha tint - ctx.globalCompositeOperation = 'destination-in'; - ctx.globalAlpha = this.states.tint[3] / 255; - ctx.drawImage(img.canvas, 0, 0); - } else { - // If we only need to change the alpha, we can skip all the extra work! - ctx.globalAlpha = this.states.tint[3] / 255; - ctx.drawImage(img.canvas, 0, 0); + if (this._isErasing) { + this.blendMode(this._cachedBlendMode); } - ctx.restore(); - return img.tintCanvas; - } - - ////////////////////////////////////////////// - // IMAGE | Pixels - ////////////////////////////////////////////// - - blendMode(mode) { - if (mode === constants.SUBTRACT) { - console.warn('blendMode(SUBTRACT) only works in WEBGL mode.'); - } else if ( - mode === constants.BLEND || - mode === constants.REMOVE || - mode === constants.DARKEST || - mode === constants.LIGHTEST || - mode === constants.DIFFERENCE || - mode === constants.MULTIPLY || - mode === constants.EXCLUSION || - mode === constants.SCREEN || - mode === constants.REPLACE || - mode === constants.OVERLAY || - mode === constants.HARD_LIGHT || - mode === constants.SOFT_LIGHT || - mode === constants.DODGE || - mode === constants.BURN || - mode === constants.ADD - ) { - this._cachedBlendMode = mode; - this.drawingContext.globalCompositeOperation = mode; - } else { - throw new Error(`Mode ${mode} not recognized.`); + this.drawingContext.drawImage( + cnv, + s * sx, + s * sy, + s * sWidth, + s * sHeight, + dx, + dy, + dWidth, + dHeight + ); + if (this._isErasing) { + this._pInst.erase(); + } + } catch (e) { + if (e.name !== 'NS_ERROR_NOT_AVAILABLE') { + throw e; } } + } - blend(...args) { - const currBlend = this.drawingContext.globalCompositeOperation; - const blendMode = args[args.length - 1]; - - const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1); + _getTintedImageCanvas(img) { + if (!img.canvas) { + return img; + } - this.drawingContext.globalCompositeOperation = blendMode; + if (!img.tintCanvas) { + // Once an image has been tinted, keep its tint canvas + // around so we don't need to re-incur the cost of + // creating a new one for each tint + img.tintCanvas = document.createElement('canvas'); + } - fn.copy.apply(this, copyArgs); + // Keep the size of the tint canvas up-to-date + if (img.tintCanvas.width !== img.canvas.width) { + img.tintCanvas.width = img.canvas.width; + } + if (img.tintCanvas.height !== img.canvas.height) { + img.tintCanvas.height = img.canvas.height; + } - this.drawingContext.globalCompositeOperation = currBlend; + // Goal: multiply the r,g,b,a values of the source by + // the r,g,b,a values of the tint color + const ctx = img.tintCanvas.getContext('2d'); + + ctx.save(); + ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); + + if (this.states.tint[0] < 255 || this.states.tint[1] < 255 || this.states.tint[2] < 255) { + // Color tint: we need to use the multiply blend mode to change the colors. + // However, the canvas implementation of this destroys the alpha channel of + // the image. To accommodate, we first get a version of the image with full + // opacity everywhere, tint using multiply, and then use the destination-in + // blend mode to restore the alpha channel again. + + // Start with the original image + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode makes everything opaque but forces the luma to match + // the original image again + ctx.globalCompositeOperation = 'luminosity'; + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode forces the hue and chroma to match the original image. + // After this we should have the original again, but with full opacity. + ctx.globalCompositeOperation = 'color'; + ctx.drawImage(img.canvas, 0, 0); + + // Apply color tint + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = `rgb(${this.states.tint.slice(0, 3).join(', ')})`; + ctx.fillRect(0, 0, img.canvas.width, img.canvas.height); + + // Replace the alpha channel with the original alpha * the alpha tint + ctx.globalCompositeOperation = 'destination-in'; + ctx.globalAlpha = this.states.tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); + } else { + // If we only need to change the alpha, we can skip all the extra work! + ctx.globalAlpha = this.states.tint[3] / 255; + ctx.drawImage(img.canvas, 0, 0); } - // p5.Renderer2D.prototype.get = p5.Renderer.prototype.get; - // .get() is not overridden + ctx.restore(); + return img.tintCanvas; + } - // x,y are canvas-relative (pre-scaled by _pixelDensity) - _getPixel(x, y) { - let imageData, index; - imageData = this.drawingContext.getImageData(x, y, 1, 1).data; - index = 0; - return [ - imageData[index + 0], - imageData[index + 1], - imageData[index + 2], - imageData[index + 3] - ]; + ////////////////////////////////////////////// + // IMAGE | Pixels + ////////////////////////////////////////////// + + blendMode(mode) { + if (mode === constants.SUBTRACT) { + console.warn('blendMode(SUBTRACT) only works in WEBGL mode.'); + } else if ( + mode === constants.BLEND || + mode === constants.REMOVE || + mode === constants.DARKEST || + mode === constants.LIGHTEST || + mode === constants.DIFFERENCE || + mode === constants.MULTIPLY || + mode === constants.EXCLUSION || + mode === constants.SCREEN || + mode === constants.REPLACE || + mode === constants.OVERLAY || + mode === constants.HARD_LIGHT || + mode === constants.SOFT_LIGHT || + mode === constants.DODGE || + mode === constants.BURN || + mode === constants.ADD + ) { + this._cachedBlendMode = mode; + this.drawingContext.globalCompositeOperation = mode; + } else { + throw new Error(`Mode ${mode} not recognized.`); } + } - loadPixels() { - const pixelsState = this._pixelsState; // if called by p5.Image + blend(...args) { + const currBlend = this.drawingContext.globalCompositeOperation; + const blendMode = args[args.length - 1]; - const pd = this._pixelDensity; - const w = this.width * pd; - const h = this.height * pd; - const imageData = this.drawingContext.getImageData(0, 0, w, h); - // @todo this should actually set pixels per object, so diff buffers can - // have diff pixel arrays. - pixelsState.imageData = imageData; - this.pixels = pixelsState.pixels = imageData.data; - } + const copyArgs = Array.prototype.slice.call(args, 0, args.length - 1); - set(x, y, imgOrCol) { - // round down to get integer numbers - x = Math.floor(x); - y = Math.floor(y); - const pixelsState = this._pixelsState; - if (imgOrCol instanceof p5.Image) { - this.drawingContext.save(); - this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); - this.drawingContext.scale( - this._pixelDensity, - this._pixelDensity - ); - this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); - this.drawingContext.drawImage(imgOrCol.canvas, x, y); - this.drawingContext.restore(); - } else { - let r = 0, - g = 0, - b = 0, - a = 0; - let idx = - 4 * - (y * - this._pixelDensity * - (this.width * this._pixelDensity) + - x * this._pixelDensity); - if (!pixelsState.imageData) { - pixelsState.loadPixels(); + this.drawingContext.globalCompositeOperation = blendMode; + + p5.prototype.copy.apply(this, copyArgs); + + this.drawingContext.globalCompositeOperation = currBlend; + } + + // p5.Renderer2D.prototype.get = p5.Renderer.prototype.get; + // .get() is not overridden + + // x,y are canvas-relative (pre-scaled by _pixelDensity) + _getPixel(x, y) { + let imageData, index; + imageData = this.drawingContext.getImageData(x, y, 1, 1).data; + index = 0; + return [ + imageData[index + 0], + imageData[index + 1], + imageData[index + 2], + imageData[index + 3] + ]; + } + + loadPixels() { + const pixelsState = this._pixelsState; // if called by p5.Image + + const pd = this._pixelDensity; + const w = this.width * pd; + const h = this.height * pd; + const imageData = this.drawingContext.getImageData(0, 0, w, h); + // @todo this should actually set pixels per object, so diff buffers can + // have diff pixel arrays. + pixelsState.imageData = imageData; + this.pixels = pixelsState.pixels = imageData.data; + } + + set(x, y, imgOrCol) { + // round down to get integer numbers + x = Math.floor(x); + y = Math.floor(y); + const pixelsState = this._pixelsState; + if (imgOrCol instanceof Image) { + this.drawingContext.save(); + this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); + this.drawingContext.scale( + this._pixelDensity, + this._pixelDensity + ); + this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); + this.drawingContext.drawImage(imgOrCol.canvas, x, y); + this.drawingContext.restore(); + } else { + let r = 0, + g = 0, + b = 0, + a = 0; + let idx = + 4 * + (y * + this._pixelDensity * + (this.width * this._pixelDensity) + + x * this._pixelDensity); + if (!pixelsState.imageData) { + pixelsState.loadPixels(); + } + if (typeof imgOrCol === 'number') { + if (idx < pixelsState.pixels.length) { + r = imgOrCol; + g = imgOrCol; + b = imgOrCol; + a = 255; + //this.updatePixels.call(this); } - if (typeof imgOrCol === 'number') { - if (idx < pixelsState.pixels.length) { - r = imgOrCol; - g = imgOrCol; - b = imgOrCol; - a = 255; - //this.updatePixels.call(this); - } - } else if (Array.isArray(imgOrCol)) { - if (imgOrCol.length < 4) { - throw new Error('pixel array must be of the form [R, G, B, A]'); - } - if (idx < pixelsState.pixels.length) { - r = imgOrCol[0]; - g = imgOrCol[1]; - b = imgOrCol[2]; - a = imgOrCol[3]; - //this.updatePixels.call(this); - } - } else if (imgOrCol instanceof p5.Color) { - if (idx < pixelsState.pixels.length) { - r = imgOrCol.levels[0]; - g = imgOrCol.levels[1]; - b = imgOrCol.levels[2]; - a = imgOrCol.levels[3]; - //this.updatePixels.call(this); - } + } else if (Array.isArray(imgOrCol)) { + if (imgOrCol.length < 4) { + throw new Error('pixel array must be of the form [R, G, B, A]'); } - // loop over pixelDensity * pixelDensity - for (let i = 0; i < this._pixelDensity; i++) { - for (let j = 0; j < this._pixelDensity; j++) { - // loop over - idx = - 4 * - ((y * this._pixelDensity + j) * - this.width * - this._pixelDensity + - (x * this._pixelDensity + i)); - pixelsState.pixels[idx] = r; - pixelsState.pixels[idx + 1] = g; - pixelsState.pixels[idx + 2] = b; - pixelsState.pixels[idx + 3] = a; - } + if (idx < pixelsState.pixels.length) { + r = imgOrCol[0]; + g = imgOrCol[1]; + b = imgOrCol[2]; + a = imgOrCol[3]; + //this.updatePixels.call(this); + } + } else if (imgOrCol instanceof p5.Color) { + if (idx < pixelsState.pixels.length) { + r = imgOrCol.levels[0]; + g = imgOrCol.levels[1]; + b = imgOrCol.levels[2]; + a = imgOrCol.levels[3]; + //this.updatePixels.call(this); } } - } - - updatePixels(x, y, w, h) { - const pixelsState = this._pixelsState; - const pd = this._pixelDensity; - if ( - x === undefined && - y === undefined && - w === undefined && - h === undefined - ) { - x = 0; - y = 0; - w = this.width; - h = this.height; - } - x *= pd; - y *= pd; - w *= pd; - h *= pd; - - if (this.gifProperties) { - this.gifProperties.frames[this.gifProperties.displayIndex].image = - pixelsState.imageData; + // loop over pixelDensity * pixelDensity + for (let i = 0; i < this._pixelDensity; i++) { + for (let j = 0; j < this._pixelDensity; j++) { + // loop over + idx = + 4 * + ((y * this._pixelDensity + j) * + this.width * + this._pixelDensity + + (x * this._pixelDensity + i)); + pixelsState.pixels[idx] = r; + pixelsState.pixels[idx + 1] = g; + pixelsState.pixels[idx + 2] = b; + pixelsState.pixels[idx + 3] = a; + } } + } + } - this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); + updatePixels(x, y, w, h) { + const pixelsState = this._pixelsState; + const pd = this._pixelDensity; + if ( + x === undefined && + y === undefined && + w === undefined && + h === undefined + ) { + x = 0; + y = 0; + w = this.width; + h = this.height; + } + x *= pd; + y *= pd; + w *= pd; + h *= pd; + + if (this.gifProperties) { + this.gifProperties.frames[this.gifProperties.displayIndex].image = + pixelsState.imageData; } - ////////////////////////////////////////////// - // SHAPE | 2D Primitives - ////////////////////////////////////////////// + this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); + } - /** - * Generate a cubic Bezier representing an arc on the unit circle of total - * angle `size` radians, beginning `start` radians above the x-axis. Up to - * four of these curves are combined to make a full arc. - * - * See ecridge.com/bezier.pdf for an explanation of the method. - */ - _acuteArcToBezier( - start, - size - ) { - // Evaluate constants. - const alpha = size / 2.0, - cos_alpha = Math.cos(alpha), - sin_alpha = Math.sin(alpha), - cot_alpha = 1.0 / Math.tan(alpha), - // This is how far the arc needs to be rotated. - phi = start + alpha, - cos_phi = Math.cos(phi), - sin_phi = Math.sin(phi), - lambda = (4.0 - cos_alpha) / 3.0, - mu = sin_alpha + (cos_alpha - lambda) * cot_alpha; - - // Return rotated waypoints. - return { - ax: Math.cos(start).toFixed(7), - ay: Math.sin(start).toFixed(7), - bx: (lambda * cos_phi + mu * sin_phi).toFixed(7), - by: (lambda * sin_phi - mu * cos_phi).toFixed(7), - cx: (lambda * cos_phi - mu * sin_phi).toFixed(7), - cy: (lambda * sin_phi + mu * cos_phi).toFixed(7), - dx: Math.cos(start + size).toFixed(7), - dy: Math.sin(start + size).toFixed(7) - }; - } - - /* - * This function requires that: - * - * 0 <= start < TWO_PI - * - * start <= stop < start + TWO_PI - */ - arc(x, y, w, h, start, stop, mode) { - const ctx = this.drawingContext; - const rx = w / 2.0; - const ry = h / 2.0; - const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. - let arcToDraw = 0; - const curves = []; - - x += rx; - y += ry; - - // Create curves - while (stop - start >= epsilon) { - arcToDraw = Math.min(stop - start, constants.HALF_PI); - curves.push(this._acuteArcToBezier(start, arcToDraw)); - start += arcToDraw; - } + ////////////////////////////////////////////// + // SHAPE | 2D Primitives + ////////////////////////////////////////////// - // Fill curves - if (this.states.doFill) { - if (!this._clipping) ctx.beginPath(); - curves.forEach((curve, index) => { - if (index === 0) { - ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); - } - /* eslint-disable indent */ - ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, - x + curve.cx * rx, y + curve.cy * ry, - x + curve.dx * rx, y + curve.dy * ry); - /* eslint-enable indent */ - }); - if (mode === constants.PIE || mode == null) { - ctx.lineTo(x, y); - } - ctx.closePath(); - if (!this._clipping) ctx.fill(); - } + /** + * Generate a cubic Bezier representing an arc on the unit circle of total + * angle `size` radians, beginning `start` radians above the x-axis. Up to + * four of these curves are combined to make a full arc. + * + * See ecridge.com/bezier.pdf for an explanation of the method. + */ + _acuteArcToBezier( + start, + size + ) { + // Evaluate constants. + const alpha = size / 2.0, + cos_alpha = Math.cos(alpha), + sin_alpha = Math.sin(alpha), + cot_alpha = 1.0 / Math.tan(alpha), + // This is how far the arc needs to be rotated. + phi = start + alpha, + cos_phi = Math.cos(phi), + sin_phi = Math.sin(phi), + lambda = (4.0 - cos_alpha) / 3.0, + mu = sin_alpha + (cos_alpha - lambda) * cot_alpha; + + // Return rotated waypoints. + return { + ax: Math.cos(start).toFixed(7), + ay: Math.sin(start).toFixed(7), + bx: (lambda * cos_phi + mu * sin_phi).toFixed(7), + by: (lambda * sin_phi - mu * cos_phi).toFixed(7), + cx: (lambda * cos_phi - mu * sin_phi).toFixed(7), + cy: (lambda * sin_phi + mu * cos_phi).toFixed(7), + dx: Math.cos(start + size).toFixed(7), + dy: Math.sin(start + size).toFixed(7) + }; + } - // Stroke curves - if (this.states.doStroke) { - if (!this._clipping) ctx.beginPath(); - curves.forEach((curve, index) => { - if (index === 0) { - ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); - } - /* eslint-disable indent */ - ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, - x + curve.cx * rx, y + curve.cy * ry, - x + curve.dx * rx, y + curve.dy * ry); - /* eslint-enable indent */ - }); - if (mode === constants.PIE) { - ctx.lineTo(x, y); - ctx.closePath(); - } else if (mode === constants.CHORD) { - ctx.closePath(); - } - if (!this._clipping) ctx.stroke(); - } - return this; + /* + * This function requires that: + * + * 0 <= start < TWO_PI + * + * start <= stop < start + TWO_PI + */ + arc(x, y, w, h, start, stop, mode) { + const ctx = this.drawingContext; + const rx = w / 2.0; + const ry = h / 2.0; + const epsilon = 0.00001; // Smallest visible angle on displays up to 4K. + let arcToDraw = 0; + const curves = []; + + x += rx; + y += ry; + + // Create curves + while (stop - start >= epsilon) { + arcToDraw = Math.min(stop - start, constants.HALF_PI); + curves.push(this._acuteArcToBezier(start, arcToDraw)); + start += arcToDraw; } - ellipse(args) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - const x = parseFloat(args[0]), - y = parseFloat(args[1]), - w = parseFloat(args[2]), - h = parseFloat(args[3]); - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { - return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { - return this; + // Fill curves + if (this.states.doFill) { + if (!this._clipping) ctx.beginPath(); + curves.forEach((curve, index) => { + if (index === 0) { + ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); } + /* eslint-disable indent */ + ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, + x + curve.cx * rx, y + curve.cy * ry, + x + curve.dx * rx, y + curve.dy * ry); + /* eslint-enable indent */ + }); + if (mode === constants.PIE || mode == null) { + ctx.lineTo(x, y); } - const centerX = x + w / 2, - centerY = y + h / 2, - radiusX = w / 2, - radiusY = h / 2; - if (!this._clipping) ctx.beginPath(); - - ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); + ctx.closePath(); + if (!this._clipping) ctx.fill(); + } - if (!this._clipping && doFill) { - ctx.fill(); - } - if (!this._clipping && doStroke) { - ctx.stroke(); + // Stroke curves + if (this.states.doStroke) { + if (!this._clipping) ctx.beginPath(); + curves.forEach((curve, index) => { + if (index === 0) { + ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); + } + /* eslint-disable indent */ + ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, + x + curve.cx * rx, y + curve.cy * ry, + x + curve.dx * rx, y + curve.dy * ry); + /* eslint-enable indent */ + }); + if (mode === constants.PIE) { + ctx.lineTo(x, y); + ctx.closePath(); + } else if (mode === constants.CHORD) { + ctx.closePath(); } + if (!this._clipping) ctx.stroke(); } + return this; + } - line(x1, y1, x2, y2) { - const ctx = this.drawingContext; - if (!this.states.doStroke) { + ellipse(args) { + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + const x = parseFloat(args[0]), + y = parseFloat(args[1]), + w = parseFloat(args[2]), + h = parseFloat(args[3]); + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { return this; - } else if (this._getStroke() === styleEmpty) { + } + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { return this; } - if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); + } + const centerX = x + w / 2, + centerY = y + h / 2, + radiusX = w / 2, + radiusY = h / 2; + if (!this._clipping) ctx.beginPath(); + + ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); + + if (!this._clipping && doFill) { + ctx.fill(); + } + if (!this._clipping && doStroke) { ctx.stroke(); + } + } + + line(x1, y1, x2, y2) { + const ctx = this.drawingContext; + if (!this.states.doStroke) { + return this; + } else if (this._getStroke() === styleEmpty) { return this; } + if (!this._clipping) ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + return this; + } - point(x, y) { - const ctx = this.drawingContext; - if (!this.states.doStroke) { - return this; - } else if (this._getStroke() === styleEmpty) { + point(x, y) { + const ctx = this.drawingContext; + if (!this.states.doStroke) { + return this; + } else if (this._getStroke() === styleEmpty) { + return this; + } + const s = this._getStroke(); + const f = this._getFill(); + if (!this._clipping) { + // swapping fill color to stroke and back after for correct point rendering + this._setFill(s); + } + if (!this._clipping) ctx.beginPath(); + ctx.arc(x, y, ctx.lineWidth / 2, 0, constants.TWO_PI, false); + if (!this._clipping) { + ctx.fill(); + this._setFill(f); + } + } + + quad(x1, y1, x2, y2, x3, y3, x4, y4) { + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { return this; } - const s = this._getStroke(); - const f = this._getFill(); - if (!this._clipping) { - // swapping fill color to stroke and back after for correct point rendering - this._setFill(s); - } - if (!this._clipping) ctx.beginPath(); - ctx.arc(x, y, ctx.lineWidth / 2, 0, constants.TWO_PI, false); - if (!this._clipping) { - ctx.fill(); - this._setFill(f); + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { + return this; } } + if (!this._clipping) ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineTo(x3, y3); + ctx.lineTo(x4, y4); + ctx.closePath(); + if (!this._clipping && doFill) { + ctx.fill(); + } + if (!this._clipping && doStroke) { + ctx.stroke(); + } + return this; + } - quad(x1, y1, x2, y2, x3, y3, x4, y4) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { - return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { - return this; - } - } - if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.lineTo(x3, y3); - ctx.lineTo(x4, y4); - ctx.closePath(); - if (!this._clipping && doFill) { - ctx.fill(); + rect(args) { + const x = args[0]; + const y = args[1]; + const w = args[2]; + const h = args[3]; + let tl = args[4]; + let tr = args[5]; + let br = args[6]; + let bl = args[7]; + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { + return this; } - if (!this._clipping && doStroke) { - ctx.stroke(); + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { + return this; } - return this; } + if (!this._clipping) ctx.beginPath(); - rect(args) { - const x = args[0]; - const y = args[1]; - const w = args[2]; - const h = args[3]; - let tl = args[4]; - let tr = args[5]; - let br = args[6]; - let bl = args[7]; - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { - return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { - return this; - } + if (typeof tl === 'undefined') { + // No rounded corners + ctx.rect(x, y, w, h); + } else { + // At least one rounded corner + // Set defaults when not specified + if (typeof tr === 'undefined') { + tr = tl; + } + if (typeof br === 'undefined') { + br = tr; + } + if (typeof bl === 'undefined') { + bl = br; } - if (!this._clipping) ctx.beginPath(); - - if (typeof tl === 'undefined') { - // No rounded corners - ctx.rect(x, y, w, h); - } else { - // At least one rounded corner - // Set defaults when not specified - if (typeof tr === 'undefined') { - tr = tl; - } - if (typeof br === 'undefined') { - br = tr; - } - if (typeof bl === 'undefined') { - bl = br; - } - // corner rounding must always be positive - const absW = Math.abs(w); - const absH = Math.abs(h); - const hw = absW / 2; - const hh = absH / 2; + // corner rounding must always be positive + const absW = Math.abs(w); + const absH = Math.abs(h); + const hw = absW / 2; + const hh = absH / 2; - // Clip radii - if (absW < 2 * tl) { - tl = hw; - } - if (absH < 2 * tl) { - tl = hh; - } - if (absW < 2 * tr) { - tr = hw; - } - if (absH < 2 * tr) { - tr = hh; - } - if (absW < 2 * br) { - br = hw; - } - if (absH < 2 * br) { - br = hh; - } - if (absW < 2 * bl) { - bl = hw; - } - if (absH < 2 * bl) { - bl = hh; - } - - // Draw shape - if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x + tl, y); - ctx.arcTo(x + w, y, x + w, y + h, tr); - ctx.arcTo(x + w, y + h, x, y + h, br); - ctx.arcTo(x, y + h, x, y, bl); - ctx.arcTo(x, y, x + w, y, tl); - ctx.closePath(); + // Clip radii + if (absW < 2 * tl) { + tl = hw; } - if (!this._clipping && this.states.doFill) { - ctx.fill(); + if (absH < 2 * tl) { + tl = hh; } - if (!this._clipping && this.states.doStroke) { - ctx.stroke(); + if (absW < 2 * tr) { + tr = hw; } - return this; - } - - - triangle(args) { - const ctx = this.drawingContext; - const doFill = this.states.doFill, - doStroke = this.states.doStroke; - const x1 = args[0], - y1 = args[1]; - const x2 = args[2], - y2 = args[3]; - const x3 = args[4], - y3 = args[5]; - if (doFill && !doStroke) { - if (this._getFill() === styleEmpty) { - return this; - } - } else if (!doFill && doStroke) { - if (this._getStroke() === styleEmpty) { - return this; - } + if (absH < 2 * tr) { + tr = hh; } - if (!this._clipping) ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.lineTo(x3, y3); - ctx.closePath(); - if (!this._clipping && doFill) { - ctx.fill(); + if (absW < 2 * br) { + br = hw; + } + if (absH < 2 * br) { + br = hh; + } + if (absW < 2 * bl) { + bl = hw; } - if (!this._clipping && doStroke) { - ctx.stroke(); + if (absH < 2 * bl) { + bl = hh; } + + // Draw shape + if (!this._clipping) ctx.beginPath(); + ctx.moveTo(x + tl, y); + ctx.arcTo(x + w, y, x + w, y + h, tr); + ctx.arcTo(x + w, y + h, x, y + h, br); + ctx.arcTo(x, y + h, x, y, bl); + ctx.arcTo(x, y, x + w, y, tl); + ctx.closePath(); + } + if (!this._clipping && this.states.doFill) { + ctx.fill(); } + if (!this._clipping && this.states.doStroke) { + ctx.stroke(); + } + return this; + } - endShape( - mode, - vertices, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind - ) { - if (vertices.length === 0) { + + triangle(args) { + const ctx = this.drawingContext; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; + const x1 = args[0], + y1 = args[1]; + const x2 = args[2], + y2 = args[3]; + const x3 = args[4], + y3 = args[5]; + if (doFill && !doStroke) { + if (this._getFill() === styleEmpty) { return this; } - if (!this.states.doStroke && !this.states.doFill) { + } else if (!doFill && doStroke) { + if (this._getStroke() === styleEmpty) { return this; } - const closeShape = mode === constants.CLOSE; - let v; - if (closeShape && !isContour) { - vertices.push(vertices[0]); + } + if (!this._clipping) ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineTo(x3, y3); + ctx.closePath(); + if (!this._clipping && doFill) { + ctx.fill(); + } + if (!this._clipping && doStroke) { + ctx.stroke(); + } + } + + endShape( + mode, + vertices, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind + ) { + if (vertices.length === 0) { + return this; + } + if (!this.states.doStroke && !this.states.doFill) { + return this; + } + const closeShape = mode === constants.CLOSE; + let v; + if (closeShape && !isContour) { + vertices.push(vertices[0]); + } + let i, j; + const numVerts = vertices.length; + if (isCurve && shapeKind === null) { + if (numVerts > 3) { + const b = [], + s = 1 - this._curveTightness; + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(vertices[1][0], vertices[1][1]); + for (i = 1; i + 2 < numVerts; i++) { + v = vertices[i]; + b[0] = [v[0], v[1]]; + b[1] = [ + v[0] + (s * vertices[i + 1][0] - s * vertices[i - 1][0]) / 6, + v[1] + (s * vertices[i + 1][1] - s * vertices[i - 1][1]) / 6 + ]; + b[2] = [ + vertices[i + 1][0] + + (s * vertices[i][0] - s * vertices[i + 2][0]) / 6, + vertices[i + 1][1] + + (s * vertices[i][1] - s * vertices[i + 2][1]) / 6 + ]; + b[3] = [vertices[i + 1][0], vertices[i + 1][1]]; + this.drawingContext.bezierCurveTo( + b[1][0], + b[1][1], + b[2][0], + b[2][1], + b[3][0], + b[3][1] + ); + } + if (closeShape) { + this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); + } + this._doFillStrokeClose(closeShape); } - let i, j; - const numVerts = vertices.length; - if (isCurve && shapeKind === null) { - if (numVerts > 3) { - const b = [], - s = 1 - this._curveTightness; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[1][0], vertices[1][1]); - for (i = 1; i + 2 < numVerts; i++) { - v = vertices[i]; - b[0] = [v[0], v[1]]; - b[1] = [ - v[0] + (s * vertices[i + 1][0] - s * vertices[i - 1][0]) / 6, - v[1] + (s * vertices[i + 1][1] - s * vertices[i - 1][1]) / 6 - ]; - b[2] = [ - vertices[i + 1][0] + - (s * vertices[i][0] - s * vertices[i + 2][0]) / 6, - vertices[i + 1][1] + - (s * vertices[i][1] - s * vertices[i + 2][1]) / 6 - ]; - b[3] = [vertices[i + 1][0], vertices[i + 1][1]]; - this.drawingContext.bezierCurveTo( - b[1][0], - b[1][1], - b[2][0], - b[2][1], - b[3][0], - b[3][1] - ); - } - if (closeShape) { - this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); + } else if ( + isBezier && + shapeKind === null + ) { + if (!this._clipping) this.drawingContext.beginPath(); + for (i = 0; i < numVerts; i++) { + if (vertices[i].isVert) { + if (vertices[i].moveTo) { + this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); + } else { + this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); } - this._doFillStrokeClose(closeShape); + } else { + this.drawingContext.bezierCurveTo( + vertices[i][0], + vertices[i][1], + vertices[i][2], + vertices[i][3], + vertices[i][4], + vertices[i][5] + ); } - } else if ( - isBezier && - shapeKind === null - ) { - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 0; i < numVerts; i++) { - if (vertices[i].isVert) { - if (vertices[i].moveTo) { - this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); - } else { - this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); - } + } + this._doFillStrokeClose(closeShape); + } else if ( + isQuadratic && + shapeKind === null + ) { + if (!this._clipping) this.drawingContext.beginPath(); + for (i = 0; i < numVerts; i++) { + if (vertices[i].isVert) { + if (vertices[i].moveTo) { + this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); } else { - this.drawingContext.bezierCurveTo( - vertices[i][0], - vertices[i][1], - vertices[i][2], - vertices[i][3], - vertices[i][4], - vertices[i][5] - ); + this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); } + } else { + this.drawingContext.quadraticCurveTo( + vertices[i][0], + vertices[i][1], + vertices[i][2], + vertices[i][3] + ); } - this._doFillStrokeClose(closeShape); - } else if ( - isQuadratic && - shapeKind === null - ) { - if (!this._clipping) this.drawingContext.beginPath(); + } + this._doFillStrokeClose(closeShape); + } else { + if (shapeKind === constants.POINTS) { for (i = 0; i < numVerts; i++) { - if (vertices[i].isVert) { - if (vertices[i].moveTo) { - this.drawingContext.moveTo(vertices[i][0], vertices[i][1]); - } else { - this.drawingContext.lineTo(vertices[i][0], vertices[i][1]); - } - } else { - this.drawingContext.quadraticCurveTo( - vertices[i][0], - vertices[i][1], - vertices[i][2], - vertices[i][3] - ); + v = vertices[i]; + if (this.states.doStroke) { + this._pInst.stroke(v[6]); } + this._pInst.point(v[0], v[1]); } - this._doFillStrokeClose(closeShape); - } else { - if (shapeKind === constants.POINTS) { - for (i = 0; i < numVerts; i++) { - v = vertices[i]; - if (this.states.doStroke) { - this._pInst.stroke(v[6]); - } - this._pInst.point(v[0], v[1]); + } else if (shapeKind === constants.LINES) { + for (i = 0; i + 1 < numVerts; i += 2) { + v = vertices[i]; + if (this.states.doStroke) { + this._pInst.stroke(vertices[i + 1][6]); } - } else if (shapeKind === constants.LINES) { - for (i = 0; i + 1 < numVerts; i += 2) { - v = vertices[i]; - if (this.states.doStroke) { - this._pInst.stroke(vertices[i + 1][6]); - } - this._pInst.line(v[0], v[1], vertices[i + 1][0], vertices[i + 1][1]); + this._pInst.line(v[0], v[1], vertices[i + 1][0], vertices[i + 1][1]); + } + } else if (shapeKind === constants.TRIANGLES) { + for (i = 0; i + 2 < numVerts; i += 3) { + v = vertices[i]; + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(v[0], v[1]); + this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); + this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); + this.drawingContext.closePath(); + if (!this._clipping && this.states.doFill) { + this._pInst.fill(vertices[i + 2][5]); + this.drawingContext.fill(); } - } else if (shapeKind === constants.TRIANGLES) { - for (i = 0; i + 2 < numVerts; i += 3) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(v[0], v[1]); - this.drawingContext.lineTo(vertices[i + 1][0], vertices[i + 1][1]); + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(vertices[i + 2][6]); + this.drawingContext.stroke(); + } + } + } else if (shapeKind === constants.TRIANGLE_STRIP) { + for (i = 0; i + 1 < numVerts; i++) { + v = vertices[i]; + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(vertices[i + 1][0], vertices[i + 1][1]); + this.drawingContext.lineTo(v[0], v[1]); + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(vertices[i + 1][6]); + } + if (!this._clipping && this.states.doFill) { + this._pInst.fill(vertices[i + 1][5]); + } + if (i + 2 < numVerts) { this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); - this.drawingContext.closePath(); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 2][5]); - this.drawingContext.fill(); - } if (!this._clipping && this.states.doStroke) { this._pInst.stroke(vertices[i + 2][6]); - this.drawingContext.stroke(); - } - } - } else if (shapeKind === constants.TRIANGLE_STRIP) { - for (i = 0; i + 1 < numVerts; i++) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo(v[0], v[1]); - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 1][6]); } if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 1][5]); - } - if (i + 2 < numVerts) { - this.drawingContext.lineTo(vertices[i + 2][0], vertices[i + 2][1]); - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 2][6]); - } - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 2][5]); - } - } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.TRIANGLE_FAN) { - if (numVerts > 2) { - // For performance reasons, try to batch as many of the - // fill and stroke calls as possible. - if (!this._clipping) this.drawingContext.beginPath(); - for (i = 2; i < numVerts; i++) { - v = vertices[i]; - this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); - this.drawingContext.lineTo(vertices[i - 1][0], vertices[i - 1][1]); - this.drawingContext.lineTo(v[0], v[1]); - this.drawingContext.lineTo(vertices[0][0], vertices[0][1]); - // If the next colour is going to be different, stroke / fill now - if (i < numVerts - 1) { - if ( - (this.states.doFill && v[5] !== vertices[i + 1][5]) || - (this.states.doStroke && v[6] !== vertices[i + 1][6]) - ) { - if (!this._clipping && this.states.doFill) { - this._pInst.fill(v[5]); - this.drawingContext.fill(); - this._pInst.fill(vertices[i + 1][5]); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(v[6]); - this.drawingContext.stroke(); - this._pInst.stroke(vertices[i + 1][6]); - } - this.drawingContext.closePath(); - if (!this._clipping) this.drawingContext.beginPath(); // Begin the next one - } - } + this._pInst.fill(vertices[i + 2][5]); } - this._doFillStrokeClose(closeShape); } - } else if (shapeKind === constants.QUADS) { - for (i = 0; i + 3 < numVerts; i += 4) { + this._doFillStrokeClose(closeShape); + } + } else if (shapeKind === constants.TRIANGLE_FAN) { + if (numVerts > 2) { + // For performance reasons, try to batch as many of the + // fill and stroke calls as possible. + if (!this._clipping) this.drawingContext.beginPath(); + for (i = 2; i < numVerts; i++) { v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(v[0], v[1]); - for (j = 1; j < 4; j++) { - this.drawingContext.lineTo(vertices[i + j][0], vertices[i + j][1]); - } + this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); + this.drawingContext.lineTo(vertices[i - 1][0], vertices[i - 1][1]); this.drawingContext.lineTo(v[0], v[1]); - if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 3][5]); - } - if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 3][6]); - } - this._doFillStrokeClose(closeShape); - } - } else if (shapeKind === constants.QUAD_STRIP) { - if (numVerts > 3) { - for (i = 0; i + 1 < numVerts; i += 2) { - v = vertices[i]; - if (!this._clipping) this.drawingContext.beginPath(); - if (i + 3 < numVerts) { - this.drawingContext.moveTo( - vertices[i + 2][0], vertices[i + 2][1]); - this.drawingContext.lineTo(v[0], v[1]); - this.drawingContext.lineTo( - vertices[i + 1][0], vertices[i + 1][1]); - this.drawingContext.lineTo( - vertices[i + 3][0], vertices[i + 3][1]); + this.drawingContext.lineTo(vertices[0][0], vertices[0][1]); + // If the next colour is going to be different, stroke / fill now + if (i < numVerts - 1) { + if ( + (this.states.doFill && v[5] !== vertices[i + 1][5]) || + (this.states.doStroke && v[6] !== vertices[i + 1][6]) + ) { if (!this._clipping && this.states.doFill) { - this._pInst.fill(vertices[i + 3][5]); + this._pInst.fill(v[5]); + this.drawingContext.fill(); + this._pInst.fill(vertices[i + 1][5]); } if (!this._clipping && this.states.doStroke) { - this._pInst.stroke(vertices[i + 3][6]); + this._pInst.stroke(v[6]); + this.drawingContext.stroke(); + this._pInst.stroke(vertices[i + 1][6]); } - } else { - this.drawingContext.moveTo(v[0], v[1]); - this.drawingContext.lineTo( - vertices[i + 1][0], vertices[i + 1][1]); + this.drawingContext.closePath(); + if (!this._clipping) this.drawingContext.beginPath(); // Begin the next one } - this._doFillStrokeClose(closeShape); } } - } else { + this._doFillStrokeClose(closeShape); + } + } else if (shapeKind === constants.QUADS) { + for (i = 0; i + 3 < numVerts; i += 4) { + v = vertices[i]; if (!this._clipping) this.drawingContext.beginPath(); - this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); - for (i = 1; i < numVerts; i++) { + this.drawingContext.moveTo(v[0], v[1]); + for (j = 1; j < 4; j++) { + this.drawingContext.lineTo(vertices[i + j][0], vertices[i + j][1]); + } + this.drawingContext.lineTo(v[0], v[1]); + if (!this._clipping && this.states.doFill) { + this._pInst.fill(vertices[i + 3][5]); + } + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(vertices[i + 3][6]); + } + this._doFillStrokeClose(closeShape); + } + } else if (shapeKind === constants.QUAD_STRIP) { + if (numVerts > 3) { + for (i = 0; i + 1 < numVerts; i += 2) { v = vertices[i]; - if (v.isVert) { - if (v.moveTo) { - if (closeShape) this.drawingContext.closePath(); - this.drawingContext.moveTo(v[0], v[1]); - } else { - this.drawingContext.lineTo(v[0], v[1]); + if (!this._clipping) this.drawingContext.beginPath(); + if (i + 3 < numVerts) { + this.drawingContext.moveTo( + vertices[i + 2][0], vertices[i + 2][1]); + this.drawingContext.lineTo(v[0], v[1]); + this.drawingContext.lineTo( + vertices[i + 1][0], vertices[i + 1][1]); + this.drawingContext.lineTo( + vertices[i + 3][0], vertices[i + 3][1]); + if (!this._clipping && this.states.doFill) { + this._pInst.fill(vertices[i + 3][5]); + } + if (!this._clipping && this.states.doStroke) { + this._pInst.stroke(vertices[i + 3][6]); } + } else { + this.drawingContext.moveTo(v[0], v[1]); + this.drawingContext.lineTo( + vertices[i + 1][0], vertices[i + 1][1]); } + this._doFillStrokeClose(closeShape); } - this._doFillStrokeClose(closeShape); } + } else { + if (!this._clipping) this.drawingContext.beginPath(); + this.drawingContext.moveTo(vertices[0][0], vertices[0][1]); + for (i = 1; i < numVerts; i++) { + v = vertices[i]; + if (v.isVert) { + if (v.moveTo) { + if (closeShape) this.drawingContext.closePath(); + this.drawingContext.moveTo(v[0], v[1]); + } else { + this.drawingContext.lineTo(v[0], v[1]); + } + } + } + this._doFillStrokeClose(closeShape); } - isCurve = false; - isBezier = false; - isQuadratic = false; - isContour = false; - if (closeShape) { - vertices.pop(); - } - - return this; } - ////////////////////////////////////////////// - // SHAPE | Attributes - ////////////////////////////////////////////// - - strokeCap(cap) { - if ( - cap === constants.ROUND || - cap === constants.SQUARE || - cap === constants.PROJECT - ) { - this.drawingContext.lineCap = cap; - } - return this; + isCurve = false; + isBezier = false; + isQuadratic = false; + isContour = false; + if (closeShape) { + vertices.pop(); } - strokeJoin(join) { - if ( - join === constants.ROUND || - join === constants.BEVEL || - join === constants.MITER - ) { - this.drawingContext.lineJoin = join; - } - return this; + return this; + } + ////////////////////////////////////////////// + // SHAPE | Attributes + ////////////////////////////////////////////// + + strokeCap(cap) { + if ( + cap === constants.ROUND || + cap === constants.SQUARE || + cap === constants.PROJECT + ) { + this.drawingContext.lineCap = cap; } + return this; + } - strokeWeight(w) { - if (typeof w === 'undefined' || w === 0) { - // hack because lineWidth 0 doesn't work - this.drawingContext.lineWidth = 0.0001; - } else { - this.drawingContext.lineWidth = w; - } - return this; + strokeJoin(join) { + if ( + join === constants.ROUND || + join === constants.BEVEL || + join === constants.MITER + ) { + this.drawingContext.lineJoin = join; } + return this; + } - _getFill() { - if (!this._cachedFillStyle) { - this._cachedFillStyle = this.drawingContext.fillStyle; - } - return this._cachedFillStyle; + strokeWeight(w) { + if (typeof w === 'undefined' || w === 0) { + // hack because lineWidth 0 doesn't work + this.drawingContext.lineWidth = 0.0001; + } else { + this.drawingContext.lineWidth = w; } + return this; + } - _setFill(fillStyle) { - if (fillStyle !== this._cachedFillStyle) { - this.drawingContext.fillStyle = fillStyle; - // console.log('here', this.drawingContext.fillStyle); - // console.trace(); - this._cachedFillStyle = fillStyle; - } + _getFill() { + if (!this._cachedFillStyle) { + this._cachedFillStyle = this.drawingContext.fillStyle; } + return this._cachedFillStyle; + } - _getStroke() { - if (!this._cachedStrokeStyle) { - this._cachedStrokeStyle = this.drawingContext.strokeStyle; - } - return this._cachedStrokeStyle; + _setFill(fillStyle) { + if (fillStyle !== this._cachedFillStyle) { + this.drawingContext.fillStyle = fillStyle; + // console.log('here', this.drawingContext.fillStyle); + // console.trace(); + this._cachedFillStyle = fillStyle; } + } - _setStroke(strokeStyle) { - if (strokeStyle !== this._cachedStrokeStyle) { - this.drawingContext.strokeStyle = strokeStyle; - this._cachedStrokeStyle = strokeStyle; - } + _getStroke() { + if (!this._cachedStrokeStyle) { + this._cachedStrokeStyle = this.drawingContext.strokeStyle; } + return this._cachedStrokeStyle; + } - ////////////////////////////////////////////// - // SHAPE | Curves - ////////////////////////////////////////////// - bezier(x1, y1, x2, y2, x3, y3, x4, y4) { - this._pInst.beginShape(); - this._pInst.vertex(x1, y1); - this._pInst.bezierVertex(x2, y2, x3, y3, x4, y4); - this._pInst.endShape(); - return this; + _setStroke(strokeStyle) { + if (strokeStyle !== this._cachedStrokeStyle) { + this.drawingContext.strokeStyle = strokeStyle; + this._cachedStrokeStyle = strokeStyle; } + } - curve(x1, y1, x2, y2, x3, y3, x4, y4) { - this._pInst.beginShape(); - this._pInst.curveVertex(x1, y1); - this._pInst.curveVertex(x2, y2); - this._pInst.curveVertex(x3, y3); - this._pInst.curveVertex(x4, y4); - this._pInst.endShape(); - return this; - } + ////////////////////////////////////////////// + // SHAPE | Curves + ////////////////////////////////////////////// + bezier(x1, y1, x2, y2, x3, y3, x4, y4) { + this._pInst.beginShape(); + this._pInst.vertex(x1, y1); + this._pInst.bezierVertex(x2, y2, x3, y3, x4, y4); + this._pInst.endShape(); + return this; + } - ////////////////////////////////////////////// - // SHAPE | Vertex - ////////////////////////////////////////////// + curve(x1, y1, x2, y2, x3, y3, x4, y4) { + this._pInst.beginShape(); + this._pInst.curveVertex(x1, y1); + this._pInst.curveVertex(x2, y2); + this._pInst.curveVertex(x3, y3); + this._pInst.curveVertex(x4, y4); + this._pInst.endShape(); + return this; + } - _doFillStrokeClose(closeShape) { - if (closeShape) { - this.drawingContext.closePath(); - } - if (!this._clipping && this.states.doFill) { - this.drawingContext.fill(); - } - if (!this._clipping && this.states.doStroke) { - this.drawingContext.stroke(); - } + ////////////////////////////////////////////// + // SHAPE | Vertex + ////////////////////////////////////////////// + + _doFillStrokeClose(closeShape) { + if (closeShape) { + this.drawingContext.closePath(); + } + if (!this._clipping && this.states.doFill) { + this.drawingContext.fill(); } + if (!this._clipping && this.states.doStroke) { + this.drawingContext.stroke(); + } + } - ////////////////////////////////////////////// - // TRANSFORM - ////////////////////////////////////////////// + ////////////////////////////////////////////// + // TRANSFORM + ////////////////////////////////////////////// - applyMatrix(a, b, c, d, e, f) { - this.drawingContext.transform(a, b, c, d, e, f); - } + applyMatrix(a, b, c, d, e, f) { + this.drawingContext.transform(a, b, c, d, e, f); + } - resetMatrix() { - this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); - this.drawingContext.scale( - this._pixelDensity, - this._pixelDensity - ); - return this; - } + resetMatrix() { + this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); + this.drawingContext.scale( + this._pixelDensity, + this._pixelDensity + ); + return this; + } - rotate(rad) { - this.drawingContext.rotate(rad); - } + rotate(rad) { + this.drawingContext.rotate(rad); + } - scale(x, y) { - this.drawingContext.scale(x, y); - return this; - } + scale(x, y) { + this.drawingContext.scale(x, y); + return this; + } - translate(x, y) { - // support passing a vector as the 1st parameter - if (x instanceof p5.Vector) { - y = x.y; - x = x.x; - } - this.drawingContext.translate(x, y); - return this; + translate(x, y) { + // support passing a vector as the 1st parameter + if (x instanceof p5.Vector) { + y = x.y; + x = x.x; } + this.drawingContext.translate(x, y); + return this; + } - ////////////////////////////////////////////// - // TYPOGRAPHY - // - ////////////////////////////////////////////// + ////////////////////////////////////////////// + // TYPOGRAPHY + // + ////////////////////////////////////////////// - _renderText(p, line, x, y, maxY, minY) { - if (y < minY || y >= maxY) { - return; // don't render lines beyond our minY/maxY bounds (see #5785) - } + _renderText(p, line, x, y, maxY, minY) { + if (y < minY || y >= maxY) { + return; // don't render lines beyond our minY/maxY bounds (see #5785) + } - p.push(); // fix to #803 + p.push(); // fix to #803 - if (!this._isOpenType()) { - // a system/browser font + if (!this._isOpenType()) { + // a system/browser font - // no stroke unless specified by user - if (this.states.doStroke && this.states.strokeSet) { - this.drawingContext.strokeText(line, x, y); - } - - if (!this._clipping && this.states.doFill) { - // if fill hasn't been set by user, use default text fill - if (!this.states.fillSet) { - this._setFill(constants._DEFAULT_TEXT_FILL); - } + // no stroke unless specified by user + if (this.states.doStroke && this.states.strokeSet) { + this.drawingContext.strokeText(line, x, y); + } - this.drawingContext.fillText(line, x, y); + if (!this._clipping && this.states.doFill) { + // if fill hasn't been set by user, use default text fill + if (!this.states.fillSet) { + this._setFill(constants._DEFAULT_TEXT_FILL); } - } else { - // an opentype font, let it handle the rendering - this.states.textFont._renderPath(line, x, y, { renderer: this }); + this.drawingContext.fillText(line, x, y); } + } else { + // an opentype font, let it handle the rendering - p.pop(); - return p; + this.states.textFont._renderPath(line, x, y, { renderer: this }); } - textWidth(s) { - if (this._isOpenType()) { - return this.states.textFont._textWidth(s, this.states.textSize); - } + p.pop(); + return p; + } - return this.drawingContext.measureText(s).width; + textWidth(s) { + if (this._isOpenType()) { + return this.states.textFont._textWidth(s, this.states.textSize); } - text(str, x, y, maxWidth, maxHeight) { - let baselineHacked; - - // baselineHacked: (HACK) - // A temporary fix to conform to Processing's implementation - // of BASELINE vertical alignment in a bounding box + return this.drawingContext.measureText(s).width; + } - if (typeof maxWidth !== 'undefined') { - if (this.drawingContext.textBaseline === constants.BASELINE) { - baselineHacked = true; - this.drawingContext.textBaseline = constants.TOP; - } - } + text(str, x, y, maxWidth, maxHeight) { + let baselineHacked; - const p = fn.prototype.text.apply(this, arguments); + // baselineHacked: (HACK) + // A temporary fix to conform to Processing's implementation + // of BASELINE vertical alignment in a bounding box - if (baselineHacked) { - this.drawingContext.textBaseline = constants.BASELINE; + if (typeof maxWidth !== 'undefined') { + if (this.drawingContext.textBaseline === constants.BASELINE) { + baselineHacked = true; + this.drawingContext.textBaseline = constants.TOP; } - - return p; } - _applyTextProperties() { - let font; - const p = this._pInst; + const p = p5.prototype.text.apply(this, arguments); - this.states.textAscent = null; - this.states.textDescent = null; + if (baselineHacked) { + this.drawingContext.textBaseline = constants.BASELINE; + } - font = this.states.textFont; + return p; + } - if (this._isOpenType()) { - font = this.states.textFont.font.familyName; - this.states.textStyle = this._textFont.font.styleName; - } + _applyTextProperties() { + let font; + const p = this._pInst; - let fontNameString = font || 'sans-serif'; - if (/\s/.exec(fontNameString)) { - // If the name includes spaces, surround in quotes - fontNameString = `"${fontNameString}"`; - } - this.drawingContext.font = `${this.states.textStyle || 'normal'} ${this.states.textSize || - 12}px ${fontNameString}`; + this.states.textAscent = null; + this.states.textDescent = null; - this.drawingContext.textAlign = this.states.textAlign; - if (this.states.textBaseline === constants.CENTER) { - this.drawingContext.textBaseline = constants._CTX_MIDDLE; - } else { - this.drawingContext.textBaseline = this.states.textBaseline; - } + font = this.states.textFont; - return p; + if (this._isOpenType()) { + font = this.states.textFont.font.familyName; + this.states.textStyle = this._textFont.font.styleName; } - ////////////////////////////////////////////// - // STRUCTURE - ////////////////////////////////////////////// + let fontNameString = font || 'sans-serif'; + if (/\s/.exec(fontNameString)) { + // If the name includes spaces, surround in quotes + fontNameString = `"${fontNameString}"`; + } + this.drawingContext.font = `${this.states.textStyle || 'normal'} ${this.states.textSize || + 12}px ${fontNameString}`; + + this.drawingContext.textAlign = this.states.textAlign; + if (this.states.textBaseline === constants.CENTER) { + this.drawingContext.textBaseline = constants._CTX_MIDDLE; + } else { + this.drawingContext.textBaseline = this.states.textBaseline; + } - // a push() operation is in progress. - // the renderer should return a 'style' object that it wishes to - // store on the push stack. - // derived renderers should call the base class' push() method - // to fetch the base style object. - push() { - this.drawingContext.save(); + return p; + } - // get the base renderer style - return super.push(); - } + ////////////////////////////////////////////// + // STRUCTURE + ////////////////////////////////////////////// - // a pop() operation is in progress - // the renderer is passed the 'style' object that it returned - // from its push() method. - // derived renderers should pass this object to their base - // class' pop method - pop(style) { - this.drawingContext.restore(); - // Re-cache the fill / stroke state - this._cachedFillStyle = this.drawingContext.fillStyle; - this._cachedStrokeStyle = this.drawingContext.strokeStyle; + // a push() operation is in progress. + // the renderer should return a 'style' object that it wishes to + // store on the push stack. + // derived renderers should call the base class' push() method + // to fetch the base style object. + push() { + this.drawingContext.save(); - super.pop(style); - } + // get the base renderer style + return super.push(); + } + + // a pop() operation is in progress + // the renderer is passed the 'style' object that it returned + // from its push() method. + // derived renderers should pass this object to their base + // class' pop method + pop(style) { + this.drawingContext.restore(); + // Re-cache the fill / stroke state + this._cachedFillStyle = this.drawingContext.fillStyle; + this._cachedStrokeStyle = this.drawingContext.strokeStyle; + + super.pop(style); } +} +function renderer2D(p5, fn){ + /** + * p5.Renderer2D + * The 2D graphics canvas renderer class. + * extends p5.Renderer + * @private + */ p5.Renderer2D = Renderer2D; p5.renderers[constants.P2D] = Renderer2D; } export default renderer2D; +export { Renderer2D }; diff --git a/src/core/rendering.js b/src/core/rendering.js index 6f44f90a38..51d0494d45 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -6,11 +6,12 @@ import * as constants from './constants'; +let renderers; function rendering(p5, fn){ let defaultId = 'defaultCanvas0'; // this gets set again in createCanvas const defaultClass = 'p5Canvas'; // Extend additional renderers object to p5 class, new renderer can be similarly attached - const renderers = p5.renderers = {}; + renderers = p5.renderers = {}; /** * Creates a canvas element on the web page. @@ -680,7 +681,8 @@ function rendering(p5, fn){ } export default rendering; +export { renderers }; if(typeof p5 !== 'undefined'){ rendering(p5, p5.prototype); -} \ No newline at end of file +} diff --git a/src/dom/dom.js b/src/dom/dom.js index 80cc552b9a..549e61391a 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -16,5092 +16,4846 @@ * @requires p5 */ -import p5 from '../core/main'; +import { Element } from '../core/p5.Element'; -/** - * Searches the page for the first element that matches the given - * CSS selector string. - * - * The selector string can be an ID, class, tag name, or a combination. - * `select()` returns a p5.Element object if it - * finds a match and `null` if not. - * - * The second parameter, `container`, is optional. It specifies a container to - * search within. `container` can be CSS selector string, a - * p5.Element object, or an - * HTMLElement object. - * - * @method select - * @param {String} selectors CSS selector string of element to search for. - * @param {String|p5.Element|HTMLElement} [container] CSS selector string, p5.Element, or - * HTMLElement to search within. - * @return {p5.Element|null} p5.Element containing the element. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * background(200); - * - * // Select the canvas by its tag. - * let cnv = select('canvas'); - * cnv.style('border', '5px deeppink dashed'); - * - * describe('A gray square with a dashed pink border.'); - * } - * - *
- * - *
- * - * function setup() { - * let cnv = createCanvas(100, 100); - * - * // Add a class attribute to the canvas. - * cnv.class('pinkborder'); - * - * background(200); - * - * // Select the canvas by its class. - * cnv = select('.pinkborder'); - * - * // Style its border. - * cnv.style('border', '5px deeppink dashed'); - * - * describe('A gray square with a dashed pink border.'); - * } - * - *
- * - *
- * - * function setup() { - * let cnv = createCanvas(100, 100); - * - * // Set the canvas' ID. - * cnv.id('mycanvas'); - * - * background(200); - * - * // Select the canvas by its ID. - * cnv = select('#mycanvas'); - * - * // Style its border. - * cnv.style('border', '5px deeppink dashed'); - * - * describe('A gray square with a dashed pink border.'); - * } - * - *
- */ -p5.prototype.select = function (e, p) { - p5._validateParameters('select', arguments); - const container = this._getContainer(p); - const res = container.querySelector(e); - if (res) { - return this._wrapElement(res); - } else { - return null; - } -}; +class MediaElement extends Element { + constructor(elt, pInst) { + super(elt, pInst); -/** - * Searches the page for all elements that matches the given - * CSS selector string. - * - * The selector string can be an ID, class, tag name, or a combination. - * `selectAll()` returns an array of p5.Element - * objects if it finds any matches and an empty array if none are found. - * - * The second parameter, `container`, is optional. It specifies a container to - * search within. `container` can be CSS selector string, a - * p5.Element object, or an - * HTMLElement object. - * - * @method selectAll - * @param {String} selectors CSS selector string of element to search for. - * @param {String|p5.Element|HTMLElement} [container] CSS selector string, p5.Element, or - * HTMLElement to search within. - * @return {p5.Element[]} array of p5.Elements containing any elements found. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Create three buttons. - * createButton('1'); - * createButton('2'); - * createButton('3'); - * - * // Select the buttons by their tag. - * let buttons = selectAll('button'); - * - * // Position the buttons. - * for (let i = 0; i < 3; i += 1) { - * buttons[i].position(0, i * 30); - * } - * - * describe('Three buttons stacked vertically. The buttons are labeled, "1", "2", and "3".'); - * } - * - *
- * - *
- * - * function setup() { - * // Create three buttons and position them. - * let b1 = createButton('1'); - * b1.position(0, 0); - * let b2 = createButton('2'); - * b2.position(0, 30); - * let b3 = createButton('3'); - * b3.position(0, 60); - * - * // Add a class attribute to each button. - * b1.class('btn'); - * b2.class('btn btn-pink'); - * b3.class('btn'); - * - * // Select the buttons by their class. - * let buttons = selectAll('.btn'); - * let pinkButtons = selectAll('.btn-pink'); - * - * // Style the selected buttons. - * buttons.forEach(setFont); - * pinkButtons.forEach(setColor); - * - * describe('Three buttons stacked vertically. The buttons are labeled, "1", "2", and "3". Buttons "1" and "3" are gray. Button "2" is pink.'); - * } - * - * // Set a button's font to Comic Sans MS. - * function setFont(btn) { - * btn.style('font-family', 'Comic Sans MS'); - * } - * - * // Set a button's background and font color. - * function setColor(btn) { - * btn.style('background', 'deeppink'); - * btn.style('color', 'white'); - * } - * - *
- */ -p5.prototype.selectAll = function (e, p) { - p5._validateParameters('selectAll', arguments); - const arr = []; - const container = this._getContainer(p); - const res = container.querySelectorAll(e); - if (res) { - for (let j = 0; j < res.length; j++) { - const obj = this._wrapElement(res[j]); - arr.push(obj); - } - } - return arr; -}; + const self = this; + this.elt.crossOrigin = 'anonymous'; -/** - * Helper function for select and selectAll - */ -p5.prototype._getContainer = function (p) { - let container = document; - if (typeof p === 'string') { - container = document.querySelector(p) || document; - } else if (p instanceof p5.Element) { - container = p.elt; - } else if (p instanceof HTMLElement) { - container = p; - } - return container; -}; + this._prevTime = 0; + this._cueIDCounter = 0; + this._cues = []; + this.pixels = []; + this._pixelsState = this; + this._pixelDensity = 1; + this._modified = false; -/** - * Helper function for getElement and getElements. - */ -p5.prototype._wrapElement = function (elt) { - const children = Array.prototype.slice.call(elt.children); - if (elt.tagName === 'INPUT' && elt.type === 'checkbox') { - let converted = new p5.Element(elt, this); - converted.checked = function (...args) { - if (args.length === 0) { - return this.elt.checked; - } else if (args[0]) { - this.elt.checked = true; - } else { - this.elt.checked = 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; } - return this; + }); + + // private _onended callback, set by the method: onended(callback) + self._onended = function () { }; + self.elt.onended = function () { + self._onended(self); }; - return converted; - } else if (elt.tagName === 'VIDEO' || elt.tagName === 'AUDIO') { - return new p5.MediaElement(elt, this); - } else if (elt.tagName === 'SELECT') { - return this.createSelect(new p5.Element(elt, this)); - } else if ( - children.length > 0 && - children.every(function (c) { - return c.tagName === 'INPUT' || c.tagName === 'LABEL'; - }) && - (elt.tagName === 'DIV' || elt.tagName === 'SPAN') - ) { - return this.createRadio(new p5.Element(elt, this)); - } else { - return new p5.Element(elt, this); } -}; - -/** - * Removes all elements created by p5.js, including any event handlers. - * - * There are two exceptions: - * canvas elements created by createCanvas() - * and p5.Render objects created by - * createGraphics(). - * - * @method removeElements - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element and place - * // it in the middle of the canvas. - * let p = createP('p5*js'); - * p.position(25, 25); - * - * describe('A gray square with the text "p5*js" written in its center. The text disappears when the mouse is pressed.'); - * } - * - * // Remove all elements when the mouse is pressed. - * function mousePressed() { - * removeElements(); - * } - * - *
- * - *
- * - * let slider; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a paragraph element and place - * // it at the top of the canvas. - * let p = createP('p5*js'); - * p.position(25, 25); - * - * // Create a slider element and place it - * // beneath the canvas. - * slider = createSlider(0, 255, 200); - * slider.position(0, 100); - * - * describe('A gray square with the text "p5*js" written in its center and a range slider beneath it. The square changes color when the slider is moved. The text and slider disappear when the square is double-clicked.'); - * } - * - * function draw() { - * // Use the slider value to change the background color. - * let g = slider.value(); - * background(g); - * } - * - * // Remove all elements when the mouse is double-clicked. - * function doubleClicked() { - * removeElements(); - * } - * - *
- */ -p5.prototype.removeElements = function (e) { - p5._validateParameters('removeElements', arguments); - // el.remove splices from this._elements, so don't mix iteration with it - const isNotCanvasElement = el => !(el.elt instanceof HTMLCanvasElement); - const removeableElements = this._elements.filter(isNotCanvasElement); - removeableElements.map(el => el.remove()); -}; - -/** - * Calls a function when the element changes. - * - * Calling `myElement.changed(false)` disables the function. - * - * @method changed - * @param {Function|Boolean} fxn function to call when the element changes. - * `false` disables the function. - * @chainable - * - * @example - *
- * - * let dropdown; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a dropdown menu and add a few color options. - * dropdown = createSelect(); - * dropdown.position(0, 0); - * dropdown.option('red'); - * dropdown.option('green'); - * dropdown.option('blue'); - * - * // Call paintBackground() when the color option changes. - * dropdown.changed(paintBackground); - * - * describe('A gray square with a dropdown menu at the top. The square changes color when an option is selected.'); - * } - * - * // Paint the background with the selected color. - * function paintBackground() { - * let c = dropdown.value(); - * background(c); - * } - * - *
- * - *
- * - * let checkbox; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a checkbox and place it beneath the canvas. - * checkbox = createCheckbox(' circle'); - * checkbox.position(0, 100); - * - * // Call repaint() when the checkbox changes. - * checkbox.changed(repaint); - * - * describe('A gray square with a checkbox underneath it that says "circle". A white circle appears when the box is checked and disappears otherwise.'); - * } - * - * // Paint the background gray and determine whether to draw a circle. - * function repaint() { - * background(200); - * if (checkbox.checked() === true) { - * circle(50, 50, 30); - * } - * } - * - *
- */ -p5.Element.prototype.changed = function (fxn) { - p5.Element._adjustListener('change', fxn, this); - return this; -}; - -/** - * Calls a function when the element receives input. - * - * `myElement.input()` is often used to with text inputs and sliders. Calling - * `myElement.input(false)` disables the function. - * - * @method input - * @param {Function|Boolean} fxn function to call when input is detected within - * the element. - * `false` disables the function. - * @chainable - * - * @example - *
- * - * let slider; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a slider and place it beneath the canvas. - * slider = createSlider(0, 255, 200); - * slider.position(0, 100); - * - * // Call repaint() when the slider changes. - * slider.input(repaint); - * - * describe('A gray square with a range slider underneath it. The background changes shades of gray when the slider is moved.'); - * } - * - * // Paint the background using slider's value. - * function repaint() { - * let g = slider.value(); - * background(g); - * } - * - *
- * - *
- * - * let input; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create an input and place it beneath the canvas. - * input = createInput(''); - * input.position(0, 100); - * - * // Call repaint() when input is detected. - * input.input(repaint); - * - * describe('A gray square with a text input bar beneath it. Any text written in the input appears in the middle of the square.'); - * } - * - * // Paint the background gray and display the input's value. - * function repaint() { - * background(200); - * let msg = input.value(); - * text(msg, 5, 50); - * } - * - *
- */ -p5.Element.prototype.input = function (fxn) { - p5.Element._adjustListener('input', fxn, this); - return this; -}; -/** - * Helpers for create methods. - */ -function addElement(elt, pInst, media) { - const node = pInst._userNode ? pInst._userNode : document.body; - node.appendChild(elt); - const c = media - ? new p5.MediaElement(elt, pInst) - : new p5.Element(elt, pInst); - pInst._elements.push(c); - return c; -} -/** - * Creates a `<div></div>` element. - * - * `<div></div>` elements are commonly used as containers for - * other elements. - * - * The parameter `html` is optional. It accepts a string that sets the - * inner HTML of the new `<div></div>`. - * - * @method createDiv - * @param {String} [html] inner HTML for the new `<div></div>` element. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a div element and set its position. - * let div = createDiv('p5*js'); - * div.position(25, 35); - * - * describe('A gray square with the text "p5*js" written in its center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create an h3 element within the div. - * let div = createDiv('

p5*js

'); - * div.position(20, 5); - * - * describe('A gray square with the text "p5*js" written in its center.'); - * } - *
- *
- */ -p5.prototype.createDiv = function (html = '') { - let elt = document.createElement('div'); - elt.innerHTML = html; - return addElement(elt, this); -}; - -/** - * Creates a `<p></p>` element. - * - * `<p></p>` elements are commonly used for paragraph-length text. - * - * The parameter `html` is optional. It accepts a string that sets the - * inner HTML of the new `<p></p>`. - * - * @method createP - * @param {String} [html] inner HTML for the new `<p></p>` element. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element and set its position. - * let p = createP('Tell me a story.'); - * p.position(5, 0); - * - * describe('A gray square displaying the text "Tell me a story." written in black.'); - * } - * - *
- */ -p5.prototype.createP = function (html = '') { - let elt = document.createElement('p'); - elt.innerHTML = html; - return addElement(elt, this); -}; - -/** - * Creates a `<span></span>` element. - * - * `<span></span>` elements are commonly used as containers - * for inline elements. For example, a `<span></span>` - * can hold part of a sentence that's a - * different style. - * - * The parameter `html` is optional. It accepts a string that sets the - * inner HTML of the new `<span></span>`. - * - * @method createSpan - * @param {String} [html] inner HTML for the new `<span></span>` element. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a span element and set its position. - * let span = createSpan('p5*js'); - * span.position(25, 35); - * - * describe('A gray square with the text "p5*js" written in its center.'); - * } - * - *
- * - *
- * - * function setup() { - * background(200); - * - * // Create a div element as a container. - * let div = createDiv(); - * - * // Place the div at the center. - * div.position(25, 35); - * - * // Create a span element. - * let s1 = createSpan('p5'); - * - * // Create a second span element. - * let s2 = createSpan('*'); - * - * // Set the second span's font color. - * s2.style('color', 'deeppink'); - * - * // Create a third span element. - * let s3 = createSpan('js'); - * - * // Add all the spans to the container div. - * s1.parent(div); - * s2.parent(div); - * s3.parent(div); - * - * describe('A gray square with the text "p5*js" written in black at its center. The asterisk is pink.'); - * } - * - *
- */ -p5.prototype.createSpan = function (html = '') { - let elt = document.createElement('span'); - elt.innerHTML = html; - return addElement(elt, this); -}; - -/** - * Creates an `<img>` element that can appear outside of the canvas. - * - * The first parameter, `src`, is a string with the path to the image file. - * `src` should be a relative path, as in `'assets/image.png'`, or a URL, as - * in `'https://example.com/image.png'`. - * - * The second parameter, `alt`, is a string with the - * alternate text - * for the image. An empty string `''` can be used for images that aren't displayed. - * - * The third parameter, `crossOrigin`, is optional. It's a string that sets the - * crossOrigin property - * of the image. Use `'anonymous'` or `'use-credentials'` to fetch the image - * with cross-origin access. - * - * The fourth parameter, `callback`, is also optional. It sets a function to - * call after the image loads. The new image is passed to the callback - * function as a p5.Element object. - * - * @method createImg - * @param {String} src relative path or URL for the image. - * @param {String} alt alternate text for the image. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * let img = createImg( - * 'https://p5js.org/assets/img/asterisk-01.png', - * 'The p5.js magenta asterisk.' - * ); - * img.position(0, -10); - * - * describe('A gray square with a magenta asterisk in its center.'); - * } - * - *
- */ -/** - * @method createImg - * @param {String} src - * @param {String} alt - * @param {String} [crossOrigin] crossOrigin property to use when fetching the image. - * @param {Function} [successCallback] function to call once the image loads. The new image will be passed - * to the function as a p5.Element object. - * @return {p5.Element} new p5.Element object. - */ -p5.prototype.createImg = function () { - p5._validateParameters('createImg', arguments); - const elt = document.createElement('img'); - const args = arguments; - let self; - if (args.length > 1 && typeof args[1] === 'string') { - elt.alt = args[1]; - } - if (args.length > 2 && typeof args[2] === 'string') { - elt.crossOrigin = args[2]; - } - elt.src = args[0]; - self = addElement(elt, this); - elt.addEventListener('load', function () { - self.width = elt.offsetWidth || elt.width; - self.height = elt.offsetHeight || elt.height; - const last = args[args.length - 1]; - if (typeof last === 'function') last(self); - }); - return self; -}; - -/** - * Creates an `<a></a>` element that links to another web page. - * - * The first parmeter, `href`, is a string that sets the URL of the linked - * page. - * - * The second parameter, `html`, is a string that sets the inner HTML of the - * link. It's common to use text, images, or buttons as links. - * - * The third parameter, `target`, is optional. It's a string that tells the - * web browser where to open the link. By default, links open in the current - * browser tab. Passing `'_blank'` will cause the link to open in a new - * browser tab. MDN describes a few - * other options. - * - * @method createA - * @param {String} href URL of linked page. - * @param {String} html inner HTML of link element to display. - * @param {String} [target] target where the new link should open, - * either `'_blank'`, `'_self'`, `'_parent'`, or `'_top'`. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create an anchor element that links to p5js.org. - * let a = createA('http://p5js.org/', 'p5*js'); - * a.position(25, 35); - * - * describe('The text "p5*js" written at the center of a gray square.'); - * } - * - *
- * - *
- * - * function setup() { - * background(200); - * - * // Create an anchor tag that links to p5js.org. - * // Open the link in a new tab. - * let a = createA('http://p5js.org/', 'p5*js', '_blank'); - * a.position(25, 35); - * - * describe('The text "p5*js" written at the center of a gray square.'); - * } - * - *
- */ -p5.prototype.createA = function (href, html, target) { - p5._validateParameters('createA', arguments); - const elt = document.createElement('a'); - elt.href = href; - elt.innerHTML = html; - if (target) elt.target = target; - return addElement(elt, this); -}; - -/** INPUT **/ - -/** - * Creates a slider `<input></input>` element. - * - * Range sliders are useful for quickly selecting numbers from a given range. - * - * The first two parameters, `min` and `max`, are numbers that set the - * slider's minimum and maximum. - * - * The third parameter, `value`, is optional. It's a number that sets the - * slider's default value. - * - * The fourth parameter, `step`, is also optional. It's a number that sets the - * spacing between each value in the slider's range. Setting `step` to 0 - * allows the slider to move smoothly from `min` to `max`. - * - * @method createSlider - * @param {Number} min minimum value of the slider. - * @param {Number} max maximum value of the slider. - * @param {Number} [value] default value of the slider. - * @param {Number} [step] size for each step in the slider's range. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * let slider; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a slider and place it at the top of the canvas. - * slider = createSlider(0, 255); - * slider.position(10, 10); - * slider.size(80); - * - * describe('A dark gray square with a range slider at the top. The square changes color when the slider is moved.'); - * } - * - * function draw() { - * // Use the slider as a grayscale value. - * let g = slider.value(); - * background(g); - * } - * - *
- * - *
- * - * let slider; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a slider and place it at the top of the canvas. - * // Set its default value to 0. - * slider = createSlider(0, 255, 0); - * slider.position(10, 10); - * slider.size(80); - * - * describe('A black square with a range slider at the top. The square changes color when the slider is moved.'); - * } - * - * function draw() { - * // Use the slider as a grayscale value. - * let g = slider.value(); - * background(g); - * } - * - *
- * - *
- * - * let slider; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a slider and place it at the top of the canvas. - * // Set its default value to 0. - * // Set its step size to 50. - * slider = createSlider(0, 255, 0, 50); - * slider.position(10, 10); - * slider.size(80); - * - * describe('A black square with a range slider at the top. The square changes color when the slider is moved.'); - * } - * - * function draw() { - * // Use the slider as a grayscale value. - * let g = slider.value(); - * background(g); - * } - * - *
- * - *
- * - * let slider; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a slider and place it at the top of the canvas. - * // Set its default value to 0. - * // Set its step size to 0 so that it moves smoothly. - * slider = createSlider(0, 255, 0, 0); - * slider.position(10, 10); - * slider.size(80); - * - * describe('A black square with a range slider at the top. The square changes color when the slider is moved.'); - * } - * - * function draw() { - * // Use the slider as a grayscale value. - * let g = slider.value(); - * background(g); - * } - * - *
- */ -p5.prototype.createSlider = function (min, max, value, step) { - p5._validateParameters('createSlider', arguments); - const elt = document.createElement('input'); - elt.type = 'range'; - elt.min = min; - elt.max = max; - if (step === 0) { - elt.step = 0.000000000000000001; // smallest valid step - } else if (step) { - elt.step = step; - } - if (typeof value === 'number') elt.value = value; - return addElement(elt, this); -}; - -/** - * Creates a `<button></button>` element. - * - * The first parameter, `label`, is a string that sets the label displayed on - * the button. - * - * The second parameter, `value`, is optional. It's a string that sets the - * button's value. See - * MDN - * for more details. - * - * @method createButton - * @param {String} label label displayed on the button. - * @param {String} [value] value of the button. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a button and place it beneath the canvas. - * let button = createButton('click me'); - * button.position(0, 100); - * - * // Call repaint() when the button is pressed. - * button.mousePressed(repaint); - * - * describe('A gray square with a button that says "click me" beneath it. The square changes color when the button is clicked.'); - * } - * - * // Change the background color. - * function repaint() { - * let g = random(255); - * background(g); - * } - * - *
- * - *
- * - * let button; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a button and set its value to 0. - * // Place the button beneath the canvas. - * button = createButton('click me', 'red'); - * button.position(0, 100); - * - * // Call randomColor() when the button is pressed. - * button.mousePressed(randomColor); - * - * describe('A red square with a button that says "click me" beneath it. The square changes color when the button is clicked.'); - * } - * - * function draw() { - * // Use the button's value to set the background color. - * let c = button.value(); - * background(c); - * } - * - * // Set the button's value to a random color. - * function randomColor() { - * let c = random(['red', 'green', 'blue', 'yellow']); - * button.value(c); - * } - * - *
- */ -p5.prototype.createButton = function (label, value) { - p5._validateParameters('createButton', arguments); - const elt = document.createElement('button'); - elt.innerHTML = label; - if (value) elt.value = value; - return addElement(elt, this); -}; - -/** - * Creates a checkbox `<input></input>` element. - * - * Checkboxes extend the p5.Element class with a - * `checked()` method. Calling `myBox.checked()` returns `true` if it the box - * is checked and `false` if not. - * - * The first parameter, `label`, is optional. It's a string that sets the label - * to display next to the checkbox. - * - * The second parameter, `value`, is also optional. It's a boolean that sets the - * checkbox's value. - * - * @method createCheckbox - * @param {String} [label] label displayed after the checkbox. - * @param {Boolean} [value] value of the checkbox. Checked is `true` and unchecked is `false`. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * let checkbox; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a checkbox and place it beneath the canvas. - * checkbox = createCheckbox(); - * checkbox.position(0, 100); - * - * describe('A black square with a checkbox beneath it. The square turns white when the box is checked.'); - * } - * - * function draw() { - * // Use the checkbox to set the background color. - * if (checkbox.checked()) { - * background(255); - * } else { - * background(0); - * } - * } - * - *
- * - *
- * - * let checkbox; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a checkbox and place it beneath the canvas. - * // Label the checkbox "white". - * checkbox = createCheckbox(' white'); - * checkbox.position(0, 100); - * - * describe('A black square with a checkbox labeled "white" beneath it. The square turns white when the box is checked.'); - * } - * - * function draw() { - * // Use the checkbox to set the background color. - * if (checkbox.checked()) { - * background(255); - * } else { - * background(0); - * } - * } - * - *
- * - *
- * - * let checkbox; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a checkbox and place it beneath the canvas. - * // Label the checkbox "white" and set its value to true. - * checkbox = createCheckbox(' white', true); - * checkbox.position(0, 100); - * - * describe('A white square with a checkbox labeled "white" beneath it. The square turns black when the box is unchecked.'); - * } - * - * function draw() { - * // Use the checkbox to set the background color. - * if (checkbox.checked()) { - * background(255); - * } else { - * background(0); - * } - * } - * - *
- */ -p5.prototype.createCheckbox = function (...args) { - p5._validateParameters('createCheckbox', args); - - // Create a container element - const elt = document.createElement('div'); - - // Create checkbox type input element - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - - // Create label element and wrap it around checkbox - const label = document.createElement('label'); - label.appendChild(checkbox); - - // Append label element inside the container - elt.appendChild(label); - - //checkbox must be wrapped in p5.Element before label so that label appears after - const self = addElement(elt, this); - - self.checked = function (...args) { - const cb = self.elt.firstElementChild.getElementsByTagName('input')[0]; - if (cb) { - if (args.length === 0) { - return cb.checked; - } else if (args[0]) { - cb.checked = true; - } else { - cb.checked = false; - } + /** + * 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; } - return self; - }; + 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; + } - this.value = function (val) { - self.value = val; + /** + * 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; - }; - - // Set the span element innerHTML as the label value if passed - if (args[0]) { - self.value(args[0]); - const span = document.createElement('span'); - span.innerHTML = args[0]; - label.appendChild(span); } - // Set the checked value of checkbox if passed - if (args[1]) { - checkbox.checked = true; + /** + * 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; } - return self; -}; - -/** - * Creates a dropdown menu `<select></select>` element. - * - * The parameter is optional. If `true` is passed, as in - * `let mySelect = createSelect(true)`, then the dropdown will support - * multiple selections. If an existing `<select></select>` element - * is passed, as in `let mySelect = createSelect(otherSelect)`, the existing - * element will be wrapped in a new p5.Element - * object. - * - * Dropdowns extend the p5.Element class with a few - * helpful methods for managing options: - * - `mySelect.option(name, [value])` adds an option to the menu. The first paremeter, `name`, is a string that sets the option's name and value. The second parameter, `value`, is optional. If provided, it sets the value that corresponds to the key `name`. If an option with `name` already exists, its value is changed to `value`. - * - `mySelect.value()` returns the currently-selected option's value. - * - `mySelect.selected()` returns the currently-selected option. - * - `mySelect.selected(option)` selects the given option by default. - * - `mySelect.disable()` marks the whole dropdown element as disabled. - * - `mySelect.disable(option)` marks a given option as disabled. - * - `mySelect.enable()` marks the whole dropdown element as enabled. - * - `mySelect.enable(option)` marks a given option as enabled. - * - * @method createSelect - * @param {Boolean} [multiple] support multiple selections. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * let mySelect; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a dropdown and place it beneath the canvas. - * mySelect = createSelect(); - * mySelect.position(0, 100); - * - * // Add color options. - * mySelect.option('red'); - * mySelect.option('green'); - * mySelect.option('blue'); - * mySelect.option('yellow'); - * - * // Set the selected option to "red". - * mySelect.selected('red'); - * - * describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.'); - * } - * - * function draw() { - * // Use the selected value to paint the background. - * let c = mySelect.selected(); - * background(c); - * } - * - *
- * - *
- * - * let mySelect; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a dropdown and place it beneath the canvas. - * mySelect = createSelect(); - * mySelect.position(0, 100); - * - * // Add color options. - * mySelect.option('red'); - * mySelect.option('green'); - * mySelect.option('blue'); - * mySelect.option('yellow'); - * - * // Set the selected option to "red". - * mySelect.selected('red'); - * - * // Disable the "yellow" option. - * mySelect.disable('yellow'); - * - * describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.'); - * } - * - * function draw() { - * // Use the selected value to paint the background. - * let c = mySelect.selected(); - * background(c); - * } - * - *
- * - *
- * - * let mySelect; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a dropdown and place it beneath the canvas. - * mySelect = createSelect(); - * mySelect.position(0, 100); - * - * // Add color options with names and values. - * mySelect.option('one', 'red'); - * mySelect.option('two', 'green'); - * mySelect.option('three', 'blue'); - * mySelect.option('four', 'yellow'); - * - * // Set the selected option to "one". - * mySelect.selected('one'); - * - * describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.'); - * } - * - * function draw() { - * // Use the selected value to paint the background. - * let c = mySelect.selected(); - * background(c); - * } - * - *
- * - *
- * - * // Hold CTRL to select multiple options on Windows and Linux. - * // Hold CMD to select multiple options on macOS. - * let mySelect; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a dropdown and allow multiple selections. - * // Place it beneath the canvas. - * mySelect = createSelect(true); - * mySelect.position(0, 100); - * - * // Add color options. - * mySelect.option('red'); - * mySelect.option('green'); - * mySelect.option('blue'); - * mySelect.option('yellow'); - * - * describe('A gray square with a dropdown menu beneath it. Colorful circles appear when their color is selected.'); - * } - * - * function draw() { - * background(200); - * - * // Use the selected value(s) to draw circles. - * let colors = mySelect.selected(); - * for (let i = 0; i < colors.length; i += 1) { - * // Calculate the x-coordinate. - * let x = 10 + i * 20; - * - * // Access the color. - * let c = colors[i]; - * - * // Draw the circle. - * fill(c); - * circle(x, 50, 20); - * } - * } - * - *
- */ -/** - * @method createSelect - * @param {Object} existing select element to wrap, either as a p5.Element or - * a HTMLSelectElement. - * @return {p5.Element} - */ - -p5.prototype.createSelect = function (...args) { - p5._validateParameters('createSelect', args); - let self; - let arg = args[0]; - if (arg instanceof p5.Element && arg.elt instanceof HTMLSelectElement) { - // If given argument is p5.Element of select type - self = arg; - this.elt = arg.elt; - } else if (arg instanceof HTMLSelectElement) { - self = addElement(arg, this); - this.elt = arg; - } else { - const elt = document.createElement('select'); - if (arg && typeof arg === 'boolean') { - elt.setAttribute('multiple', 'true'); - } - self = addElement(elt, this); - this.elt = elt; + /** + * 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; } - self.option = function (name, value) { - let index; - // if no name is passed, return - if (name === undefined) { - return; - } - //see if there is already an option with this name - for (let i = 0; i < this.elt.length; i += 1) { - if (this.elt[i].textContent === name) { - index = i; - break; - } - } - //if there is an option with this name we will modify it - if (index !== undefined) { - //if the user passed in false then delete that option - if (value === false) { - this.elt.remove(index); + /** + * Sets up logic to check that autoplay succeeded. + * + * @private + */ + _setupAutoplayFailDetection() { + const timeout = setTimeout(() => { + if (typeof IS_MINIFIED === 'undefined') { + p5._friendlyAutoplayError(this.src); } else { - // Update the option at index with the value - this.elt[index].value = value; + console.error(e); } - } else { - //if it doesn't exist create it - const opt = document.createElement('option'); - opt.textContent = name; - opt.value = value === undefined ? name : value; - this.elt.appendChild(opt); - this._pInst._elements.push(opt); - } - }; + }, 500); + this.elt.addEventListener('play', () => clearTimeout(timeout), { + passive: true, + once: true + }); + } - self.selected = function (value) { - // Update selected status of option - if (value !== undefined) { - for (let i = 0; i < this.elt.length; i += 1) { - if (this.elt[i].value.toString() === value.toString()) { - this.elt.selectedIndex = i; - } - } - return this; - } else { - if (this.elt.getAttribute('multiple')) { - let arr = []; - for (const selectedOption of this.elt.selectedOptions) { - arr.push(selectedOption.value); - } - return arr; + /** + * 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(); + * } + * + *
+ * + *
+ * + * 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 { - return this.elt.value; - } - } - }; - - self.disable = function (value) { - if (typeof value === 'string') { - for (let i = 0; i < this.elt.length; i++) { - if (this.elt[i].value.toString() === value) { - this.elt[i].disabled = true; - this.elt[i].selected = false; - } + // otherwise, schedule check whenever it is ready + this.elt.addEventListener('canplay', setupAutoplayFailDetection, { + passive: true, + once: true + }); } - } else { - this.elt.disabled = true; } - return this; - }; - self.enable = function (value) { - if (typeof value === 'string') { - for (let i = 0; i < this.elt.length; i++) { - if (this.elt[i].value.toString() === value) { - this.elt[i].disabled = false; - this.elt[i].selected = false; - } - } - } else { - this.elt.disabled = false; - for (let i = 0; i < this.elt.length; i++) { - this.elt[i].disabled = false; - this.elt[i].selected = false; - } - } return this; - }; - - return self; -}; - -/** - * Creates a radio button element. - * - * The parameter is optional. If a string is passed, as in - * `let myRadio = createSelect('food')`, then each radio option will - * have `"food"` as its `name` parameter: `<input name="food"></input>`. - * If an existing `<div></div>` or `<span></span>` - * element is passed, as in `let myRadio = createSelect(container)`, it will - * become the radio button's parent element. - * - * Radio buttons extend the p5.Element class with a few - * helpful methods for managing options: - * - `myRadio.option(value, [label])` adds an option to the menu. The first paremeter, `value`, is a string that sets the option's value and label. The second parameter, `label`, is optional. If provided, it sets the label displayed for the `value`. If an option with `value` already exists, its label is changed and its value is returned. - * - `myRadio.value()` returns the currently-selected option's value. - * - `myRadio.selected()` returns the currently-selected option. - * - `myRadio.selected(value)` selects the given option and returns it as an `HTMLInputElement`. - * - `myRadio.disable(shouldDisable)` enables the entire radio button if `true` is passed and disables it if `false` is passed. - * - * @method createRadio - * @param {Object} [containerElement] container HTML Element, either a `<div></div>` - * or `<span></span>`. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * let myRadio; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a radio button element and place it - * // in the top-left corner. - * myRadio = createRadio(); - * myRadio.position(0, 0); - * myRadio.size(60); - * - * // Add a few color options. - * myRadio.option('red'); - * myRadio.option('yellow'); - * myRadio.option('blue'); - * - * // Choose a default option. - * myRadio.selected('yellow'); - * - * describe('A yellow square with three color options listed, "red", "yellow", and "blue". The square changes color when the user selects a new option.'); - * } - * - * function draw() { - * // Set the background color using the radio button. - * let g = myRadio.value(); - * background(g); - * } - * - *
- * - *
- * - * let myRadio; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a radio button element and place it - * // in the top-left corner. - * myRadio = createRadio(); - * myRadio.position(0, 0); - * myRadio.size(50); - * - * // Add a few color options. - * // Color values are labeled with - * // emotions they evoke. - * myRadio.option('red', 'love'); - * myRadio.option('yellow', 'joy'); - * myRadio.option('blue', 'trust'); - * - * // Choose a default option. - * myRadio.selected('yellow'); - * - * describe('A yellow square with three options listed, "love", "joy", and "trust". The square changes color when the user selects a new option.'); - * } - * - * function draw() { - * // Set the background color using the radio button. - * let c = myRadio.value(); - * background(c); - * } - * - *
- * - *
- * - * let myRadio; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a radio button element and place it - * // in the top-left corner. - * myRadio = createRadio(); - * myRadio.position(0, 0); - * myRadio.size(50); - * - * // Add a few color options. - * myRadio.option('red'); - * myRadio.option('yellow'); - * myRadio.option('blue'); - * - * // Choose a default option. - * myRadio.selected('yellow'); - * - * // Create a button and place it beneath the canvas. - * let btn = createButton('disable'); - * btn.position(0, 100); - * - * // Call disableRadio() when btn is pressed. - * btn.mousePressed(disableRadio); - * - * describe('A yellow square with three options listed, "red", "yellow", and "blue". The square changes color when the user selects a new option. A "disable" button beneath the canvas disables the color options when pressed.'); - * } - * - * function draw() { - * // Set the background color using the radio button. - * let c = myRadio.value(); - * background(c); - * } - * - * // Disable myRadio. - * function disableRadio() { - * myRadio.disable(true); - * } - * - *
- */ -/** - * @method createRadio - * @param {String} [name] name parameter assigned to each option's `<input></input>` element. - * @return {p5.Element} new p5.Element object. - */ -/** - * @method createRadio - * @return {p5.Element} new p5.Element object. - */ -p5.prototype.createRadio = function (...args) { - // Creates a div, adds each option as an individual input inside it. - // If already given with a containerEl, will search for all input[radio] - // it, create a p5.Element out of it, add options to it and return the p5.Element. - - let self; - let radioElement; - let name; - const arg0 = args[0]; - if ( - arg0 instanceof p5.Element && - (arg0.elt instanceof HTMLDivElement || arg0.elt instanceof HTMLSpanElement) - ) { - // If given argument is p5.Element of div/span type - self = arg0; - this.elt = arg0.elt; - } else if ( - // If existing radio Element is provided as argument 0 - arg0 instanceof HTMLDivElement || - arg0 instanceof HTMLSpanElement - ) { - self = addElement(arg0, this); - this.elt = arg0; - radioElement = arg0; - if (typeof args[1] === 'string') name = args[1]; - } else { - if (typeof arg0 === 'string') name = arg0; - radioElement = document.createElement('div'); - self = addElement(radioElement, this); - this.elt = radioElement; } - self._name = name || 'radioOption'; - - // setup member functions - const isRadioInput = el => - el instanceof HTMLInputElement && el.type === 'radio'; - const isLabelElement = el => el instanceof HTMLLabelElement; - const isSpanElement = el => el instanceof HTMLSpanElement; - - self._getOptionsArray = function () { - return Array.from(this.elt.children) - .filter( - el => - isRadioInput(el) || - (isLabelElement(el) && isRadioInput(el.firstElementChild)) - ) - .map(el => (isRadioInput(el) ? el : el.firstElementChild)); - }; - - self.option = function (value, label) { - // return an option with this value, create if not exists. - let optionEl; - for (const option of self._getOptionsArray()) { - if (option.value === value) { - optionEl = option; - break; - } - } - // Create a new option, add it to radioElement and return it. - if (optionEl === undefined) { - optionEl = document.createElement('input'); - optionEl.setAttribute('type', 'radio'); - optionEl.setAttribute('value', value); - } - optionEl.setAttribute('name', self._name); - - // Check if label element exists, else create it - let labelElement; - if (!isLabelElement(optionEl.parentElement)) { - labelElement = document.createElement('label'); - labelElement.insertAdjacentElement('afterbegin', optionEl); + /** + * 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); + * } + * + *
+ */ + /** + * @method volume + * @param {Number} val volume between 0.0 and 1.0. + * @chainable + */ + volume(val) { + if (typeof val === 'undefined') { + return this.elt.volume; } else { - labelElement = optionEl.parentElement; + this.elt.volume = val; } + } - // Check if span element exists, else create it - let spanElement; - if (!isSpanElement(labelElement.lastElementChild)) { - spanElement = document.createElement('span'); - optionEl.insertAdjacentElement('afterend', spanElement); + /** + * 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 { - spanElement = labelElement.lastElementChild; + if (this.loadedmetadata) { + this.elt.playbackRate = val; + } else { + this.presetPlaybackRate = val; + } } + } - // Set the innerHTML of span element as the label text - spanElement.innerHTML = label === undefined ? value : label; - - // Append the label element, which includes option element and - // span element to the radio container element - this.elt.appendChild(labelElement); - - return optionEl; - }; - - self.remove = function (value) { - for (const optionEl of self._getOptionsArray()) { - if (optionEl.value === value) { - if (isLabelElement(optionEl.parentElement)) { - // Remove parent label which also removes children elements - optionEl.parentElement.remove(); - } else { - // Remove the option input if parent label does not exist - optionEl.remove(); - } - return; - } + /** + * 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. + * + * @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); + * } + * + *
+ * + *
+ * + * 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); + * } + * + *
+ */ + /** + * @param {Number} time time to jump to (in seconds). + * @chainable + */ + time(val) { + if (typeof val === 'undefined') { + return this.elt.currentTime; + } else { + this.elt.currentTime = val; + return this; } - }; + } - self.value = function () { - let result = ''; - for (const option of self._getOptionsArray()) { - if (option.checked) { - result = option.value; - break; - } + /** + * 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); } - return result; - }; - self.selected = function (value) { - let result = null; - if (value === undefined) { - for (const option of self._getOptionsArray()) { - if (option.checked) { - result = option; - break; - } + // 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; } - } else { - // forEach loop to uncheck all radio buttons before - // setting any one as checked. - self._getOptionsArray().forEach(option => { - option.checked = false; - option.removeAttribute('checked'); - }); - for (const option of self._getOptionsArray()) { - if (option.value === value) { - option.setAttribute('checked', true); - option.checked = true; - result = option; - } + 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); } - } - return result; - }; - self.disable = function (shouldDisable = true) { - for (const radioInput of self._getOptionsArray()) { - radioInput.setAttribute('disabled', shouldDisable); - } - }; + this.drawingContext.drawImage( + this.elt, + 0, + 0, + this.canvas.width, + this.canvas.height + ); - return self; -}; + if (this.flipped === true) { + this.drawingContext.restore(); + } -/** - * Creates a color picker element. - * - * The parameter, `value`, is optional. If a color string or - * p5.Color object is passed, it will set the default - * color. - * - * Color pickers extend the p5.Element class with a - * couple of helpful methods for managing colors: - * - `myPicker.value()` returns the current color as a hex string in the format `'#rrggbb'`. - * - `myPicker.color()` returns the current color as a p5.Color object. - * - * @method createColorPicker - * @param {String|p5.Color} [value] default color as a CSS color string. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * let myPicker; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a color picker and set its position. - * myPicker = createColorPicker('deeppink'); - * myPicker.position(0, 100); - * - * describe('A pink square with a color picker beneath it. The square changes color when the user picks a new color.'); - * } - * - * function draw() { - * // Use the color picker to paint the background. - * let c = myPicker.color(); - * background(c); - * } - * - *
- * - *
- * - * let myPicker; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a color picker and set its position. - * myPicker = createColorPicker('deeppink'); - * myPicker.position(0, 100); - * - * describe('A number with the format "#rrggbb" is displayed on a pink canvas. The background color and number change when the user picks a new color.'); - * } - * - * function draw() { - * // Use the color picker to paint the background. - * let c = myPicker.value(); - * background(c); - * - * // Display the current color as a hex string. - * text(c, 25, 55); - * } - * - *
- */ -p5.prototype.createColorPicker = function (value) { - p5._validateParameters('createColorPicker', arguments); - const elt = document.createElement('input'); - let self; - elt.type = 'color'; - if (value) { - if (value instanceof p5.Color) { - elt.value = value.toString('#rrggbb'); - } else { - p5.prototype._colorMode = 'rgb'; - p5.prototype._colorMaxes = { - rgb: [255, 255, 255, 255], - hsb: [360, 100, 100, 1], - hsl: [360, 100, 100, 1] - }; - elt.value = p5.prototype.color(value).toString('#rrggbb'); + this.setModified(true); + this._frameOnCanvas = this._pInst.frameCount; } - } else { - elt.value = '#000000'; } - self = addElement(elt, this); - // Method to return a p5.Color object for the given color. - self.color = function () { - if (value) { - if (value.mode) { - p5.prototype._colorMode = value.mode; - } - if (value.maxes) { - p5.prototype._colorMaxes = value.maxes; - } + 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); } - return p5.prototype.color(this.elt.value); - }; - return self; -}; - -/** - * Creates a text `<input></input>` element. - * - * Call `myInput.size()` to set the length of the text box. - * - * The first parameter, `value`, is optional. It's a string that sets the - * input's default value. The input is blank by default. - * - * The second parameter, `type`, is also optional. It's a string that - * specifies the type of text being input. See MDN for a full - * list of options. - * The default is `'text'`. - * - * @method createInput - * @param {String} [value] default value of the input box. Defaults to an empty string `''`. - * @param {String} [type] type of input. Defaults to `'text'`. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * let myInput; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create an input element and place it - * // beneath the canvas. - * myInput = createInput(); - * myInput.position(0, 100); - * - * describe('A gray square with a text box beneath it. The text in the square changes when the user types something new in the input bar.'); - * } - * - * function draw() { - * background(200); - * - * // Use the input to display a message. - * let msg = myInput.value(); - * text(msg, 25, 55); - * } - * - *
- * - *
- * - * let myInput; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create an input element and place it - * // beneath the canvas. Set its default - * // text to "hello!". - * myInput = createInput('hello!'); - * myInput.position(0, 100); - * - * describe('The text "hello!" written at the center of a gray square. A text box beneath the square also says "hello!". The text in the square changes when the user types something new in the input bar.'); - * } - * - * function draw() { - * background(200); - * - * // Use the input to display a message. - * let msg = myInput.value(); - * text(msg, 25, 55); - * } - * - *
- */ -/** - * @method createInput - * @param {String} [value] - * @return {p5.Element} - */ -p5.prototype.createInput = function (value = '', type = 'text') { - p5._validateParameters('createInput', arguments); - let elt = document.createElement('input'); - elt.setAttribute('value', value); - elt.setAttribute('type', type); - return addElement(elt, this); -}; - -/** - * Creates an `<input></input>` element of type `'file'`. - * - * `createFileInput()` allows users to select local files for use in a sketch. - * It returns a p5.File object. - * - * The first parameter, `callback`, is a function that's called when the file - * loads. The callback function should have one parameter, `file`, that's a - * p5.File object. - * - * The second parameter, `multiple`, is optional. It's a boolean value that - * allows loading multiple files if set to `true`. If `true`, `callback` - * will be called once per file. - * - * @method createFileInput - * @param {Function} callback function to call once the file loads. - * @param {Boolean} [multiple] allow multiple files to be selected. - * @return {p5.File} new p5.File object. - * - * @example - *
- * - * // Use the file input to select an image to - * // load and display. - * let input; - * let img; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a file input and place it beneath - * // the canvas. - * input = createFileInput(handleImage); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user selects an image file to load, it is displayed on the square.'); - * } - * - * function draw() { - * background(200); - * - * // Draw the image if loaded. - * if (img) { - * image(img, 0, 0, width, height); - * } - * } - * - * // Create an image if the file is an image. - * function handleImage(file) { - * if (file.type === 'image') { - * img = createImg(file.data, ''); - * img.hide(); - * } else { - * img = null; - * } - * } - * - *
- * - *
- * - * // Use the file input to select multiple images - * // to load and display. - * let input; - * let images = []; - * - * function setup() { - * // Create a file input and place it beneath - * // the canvas. Allow it to load multiple files. - * input = createFileInput(handleImage, true); - * input.position(0, 100); - * } - * - * function draw() { - * background(200); - * - * // Draw the images if loaded. Each image - * // is drawn 20 pixels lower than the - * // previous image. - * for (let i = 0; i < images.length; i += 1) { - * // Calculate the y-coordinate. - * let y = i * 20; - * - * // Draw the image. - * image(img, 0, y, 100, 100); - * } - * - * describe('A gray square with a file input beneath it. If the user selects multiple image files to load, they are displayed on the square.'); - * } - * - * // Create an image if the file is an image, - * // then add it to the images array. - * function handleImage(file) { - * if (file.type === 'image') { - * let img = createImg(file.data, ''); - * img.hide(); - * images.push(img); - * } - * } - * - *
- */ -p5.prototype.createFileInput = function (callback, multiple = false) { - p5._validateParameters('createFileInput', arguments); + 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); + } - const handleFileSelect = function (event) { - for (const file of event.target.files) { - p5.File._load(file, callback); + set(x, y, imgOrCol) { + if (this.loadedmetadata) { + // wait for metadata + this._ensureCanvas(); + p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol); + this.setModified(true); } - }; - - // If File API's are not supported, throw Error - if (!(window.File && window.FileReader && window.FileList && window.Blob)) { - console.log( - 'The File APIs are not fully supported in this browser. Cannot create element.' - ); - return; } - - const fileInput = document.createElement('input'); - fileInput.setAttribute('type', 'file'); - if (multiple) fileInput.setAttribute('multiple', true); - fileInput.addEventListener('change', handleFileSelect, false); - return addElement(fileInput, this); -}; - -/** 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]; + copy(...args) { + this._ensureCanvas(); + fn.copy.apply(this, args); } - for (const mediaSource of src) { - const sourceEl = document.createElement('source'); - sourceEl.setAttribute('src', mediaSource); - elt.appendChild(sourceEl); + mask(...args) { + this.loadPixels(); + this.setModified(true); + p5.Image.prototype.mask.apply(this, args); } - - // If callback is provided, attach to element - if (typeof callback === 'function') { - const callbackHandler = () => { - callback(); - elt.removeEventListener('canplaythrough', callbackHandler); - }; - elt.addEventListener('canplaythrough', callbackHandler); + /** + * 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; } - const mediaEl = addElement(elt, pInst, true); - mediaEl.loadedmetadata = false; + /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/ - // set width and height onload metadata - elt.addEventListener('loadedmetadata', () => { - mediaEl.width = elt.videoWidth; - mediaEl.height = elt.videoHeight; + /** + * 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; - // 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; + // if p5.sound exists, same audio context + if (typeof fn.getAudioContext === 'function') { + audioContext = fn.getAudioContext(); + mainOutput = p5.soundOut.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'; + } } - mediaEl.loadedmetadata = true; - }); - - 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.'); - * } - * - *
- * - *
- * - * 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.'); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- */ -p5.prototype.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 video. If a single string is - * passed, as in `'assets/video.mp4'`, a single video is loaded. An array - * of strings can be used to load the same video in different formats. For - * example, `['assets/video.mp4', 'assets/video.ogv', 'assets/video.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 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.'); - * } - * - *
- */ -p5.prototype.createAudio = function (src, callback) { - p5._validateParameters('createAudio', arguments); - return createMedia(this, 'audio', src, callback); -}; -/** CAMERA STUFF **/ - -p5.prototype.VIDEO = 'video'; - -p5.prototype.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 = {}; -} + // create a Web Audio MediaElementAudioSourceNode if none already exists + if (!this.audioSourceNode) { + this.audioSourceNode = audioContext.createMediaElementSource(this.elt); -// 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') - ); + // connect to main output when this method is first called + this.audioSourceNode.connect(mainOutput); } - // 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.'); - * } - * - *
- * - *
- * - * 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); - * } - * - *
- *
- * - * 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.'); - * } - * - * - *
- * - *
- * - * 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.'); - * } - * - *
- */ -p5.prototype.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 === p5.prototype.VIDEO) useAudio = false; - else if (arg === p5.prototype.AUDIO) useVideo = false; - else if (typeof arg === 'object') { - if (arg.flipped !== undefined) { - flipped = arg.flipped; - delete arg.flipped; + // connect to object if provided + if (obj) { + if (obj.input) { + this.audioSourceNode.connect(obj.input); + } else { + this.audioSourceNode.connect(obj); } - constraints = Object.assign({}, constraints, arg); - } - else if (typeof arg === 'function') { - callback = arg; + } else { + // otherwise connect to main output of p5.sound / AudioContext + this.audioSourceNode.connect(mainOutput); } } - 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)'; - } + /** + * 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 { - videoEl.width = videoEl.elt.width = domElement.videoWidth; - videoEl.height = videoEl.elt.height = domElement.videoHeight; + throw 'nothing to disconnect'; } - videoEl.loadedmetadata = true; - - if (callback) callback(domElement.srcObject); - }); - videoEl.flipped = flipped; - return videoEl; -}; - + } -/** - * Creates a new p5.Element object. - * - * The first parameter, `tag`, is a string an HTML tag such as `'h5'`. - * - * The second parameter, `content`, is optional. It's a string that sets the - * HTML content to insert into the new element. New elements have no content - * by default. - * - * @method createElement - * @param {String} tag tag for the new element. - * @param {String} [content] HTML content to insert into the element. - * @return {p5.Element} new p5.Element object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create an h5 element with nothing in it. - * createElement('h5'); - * - * describe('A gray square.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create an h5 element with the content "p5*js". - * let h5 = createElement('h5', 'p5*js'); - * - * // Set the element's style and position. - * h5.style('color', 'deeppink'); - * h5.position(30, 15); - * - * describe('The text "p5*js" written in pink in the middle of a gray square.'); - * } - * - *
- */ -p5.prototype.createElement = function (tag, content) { - p5._validateParameters('createElement', arguments); - const elt = document.createElement(tag); - if (typeof content !== 'undefined') { - elt.innerHTML = content; + /*** 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; } - return addElement(elt, this); -}; -// ============================================================================= -// p5.Element additions -// ============================================================================= -/** - * - * Adds a class to the element. - * - * @for p5.Element - * @method addClass - * @param {String} class name of class to add. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a div element. - * let div = createDiv('div'); - * - * // Add a class to the div. - * div.addClass('myClass'); - * - * describe('A gray square.'); - * } - * - *
- */ -p5.Element.prototype.addClass = function (c) { - if (this.elt.className) { - if (!this.hasClass(c)) { - this.elt.className = this.elt.className + ' ' + c; - } - } else { - this.elt.className = c; + /** + * 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; } - return this; -}; -/** - * Removes a class from the element. - * - * @method removeClass - * @param {String} class name of class to remove. - * @chainable - * - * @example - *
- * - * // In this example, a class is set when the div is created - * // and removed when mouse is pressed. This could link up - * // with a CSS style rule to toggle style properties. - * - * let div; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); + /** + * Schedules a function to call when the audio/video reaches a specific time + * during its playback. * - * // Create a div element. - * div = createDiv('div'); + * The first parameter, `time`, is the time, in seconds, when the function + * should run. This value is passed to `callback` as its first argument. * - * // Add a class to the div. - * div.addClass('myClass'); + * The second parameter, `callback`, is the function to call at the specified + * cue time. * - * describe('A gray square.'); - * } + * The third parameter, `value`, is optional and can be any type of value. + * `value` is passed to `callback`. * - * // Remove 'myClass' from the div when the user presses the mouse. - * function mousePressed() { - * div.removeClass('myClass'); - * } - * - *
- */ -p5.Element.prototype.removeClass = function (c) { - // Note: Removing a class that does not exist does NOT throw an error in classList.remove method - this.elt.classList.remove(c); - return this; -}; - -/** - * Checks if a class is already applied to element. + * Calling `media.addCue()` returns an ID as a string. This is useful for + * removing the cue later. * - * @method hasClass - * @returns {boolean} a boolean value if element has specified class. - * @param c {String} name of class to check. + * @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 - *
+ *
* - * let div; - * * function setup() { * createCanvas(100, 100); * - * background(200); + * // Create a p5.MediaElement using createAudio(). + * let beat = createAudio('assets/beat.mp3'); * - * // Create a div element. - * div = createDiv('div'); + * // Play the beat in a loop. + * beat.loop(); * - * // Add the class 'show' to the div. - * div.addClass('show'); + * // 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 gray square.'); + * describe('A red square with a beat playing in the background. Its color changes every 2 seconds while the audio plays.'); * } * - * // Toggle the class 'show' when the mouse is pressed. - * function mousePressed() { - * if (div.hasClass('show')) { - * div.addClass('show'); - * } else { - * div.removeClass('show'); - * } + * // Change the background color. + * function changeBackground(c) { + * background(c); * } * *
*/ -p5.Element.prototype.hasClass = function (c) { - return this.elt.classList.contains(c); -}; + addCue(time, callback, val) { + const id = this._cueIDCounter++; -/** - * Toggles whether a class is applied to the element. - * - * @method toggleClass - * @param c {String} class name to toggle. - * @chainable - * - * @example - *
- * - * let div; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a div element. - * div = createDiv('div'); - * - * // Add the 'show' class to the div. - * div.addClass('show'); - * - * describe('A gray square.'); - * } - * - * // Toggle the 'show' class when the mouse is pressed. - * function mousePressed() { - * div.toggleClass('show'); - * } - * - *
- */ -p5.Element.prototype.toggleClass = function (c) { - // classList also has a toggle() method, but we cannot use that yet as support is unclear. - // See https://github.com/processing/p5.js/issues/3631 - // this.elt.classList.toggle(c); - if (this.elt.classList.contains(c)) { - this.elt.classList.remove(c); - } else { - this.elt.classList.add(c); + 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; } - return this; -}; -/** - * Attaches the element as a child of another element. - * - * `myElement.child()` accepts either a string ID, DOM node, or - * p5.Element. For example, - * `myElement.child(otherElement)`. If no argument is provided, an array of - * children DOM nodes is returned. + /** + * Removes a callback based on its ID. * - * @method child - * @returns {Node[]} an array of child nodes. + * @param {Number} id ID of the cue, created by `media.addCue()`. * * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create the div elements. - * let div0 = createDiv('Parent'); - * let div1 = createDiv('Child'); - * - * // Make div1 the child of div0 - * // using the p5.Element. - * div0.child(div1); - * - * describe('A gray square with the words "Parent" and "Child" written beneath it.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create the div elements. - * let div0 = createDiv('Parent'); - * let div1 = createDiv('Child'); - * - * // Give div1 an ID. - * div1.id('apples'); - * - * // Make div1 the child of div0 - * // using its ID. - * div0.child('apples'); - * - * describe('A gray square with the words "Parent" and "Child" written beneath it.'); - * } - * - *
- * - *
+ *
* - * // This example assumes there is a div already on the page - * // with id "myChildDiv". + * let lavenderID; + * let isRemoved = false; * * function setup() { * createCanvas(100, 100); * - * background(200); - * - * // Create the div elements. - * let div0 = createDiv('Parent'); - * - * // Select the child element by its ID. - * let elt = document.getElementById('myChildDiv'); - * - * // Make div1 the child of div0 - * // using its HTMLElement object. - * div0.child(elt); - * - * describe('A gray square with the words "Parent" and "Child" written beneath it.'); - * } - * - *
- */ -/** - * @method child - * @param {String|p5.Element} [child] the ID, DOM node, or p5.Element - * to add to the current element - * @chainable - */ -p5.Element.prototype.child = function (childNode) { - if (typeof childNode === 'undefined') { - return this.elt.childNodes; - } - if (typeof childNode === 'string') { - if (childNode[0] === '#') { - childNode = childNode.substring(1); - } - childNode = document.getElementById(childNode); - } else if (childNode instanceof p5.Element) { - childNode = childNode.elt; - } - - if (childNode instanceof HTMLElement) { - this.elt.appendChild(childNode); - } - return this; -}; - -/** - * Centers the element either vertically, horizontally, or both. + * // Create a p5.MediaElement using createAudio(). + * let beat = createAudio('assets/beat.mp3'); * - * `center()` will center the element relative to its parent or according to - * the page's body if the element has no parent. + * // Play the beat in a loop. + * beat.loop(); * - * If no argument is passed, as in `myElement.center()` the element is aligned - * both vertically and horizontally. + * // Schedule a few events. + * beat.addCue(0, changeBackground, 'red'); + * beat.addCue(2, changeBackground, 'deeppink'); + * beat.addCue(4, changeBackground, 'orchid'); * - * @method center - * @param {String} [align] passing 'vertical', 'horizontal' aligns element accordingly - * @chainable + * // Record the ID of the "lavender" callback. + * lavenderID = beat.addCue(6, changeBackground, 'lavender'); * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); + * 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); * - * // Create the div element and style it. - * let div = createDiv(''); - * div.size(10, 10); - * div.style('background-color', 'orange'); + * // 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); + * } + * } * - * // Center the div relative to the page's body. - * div.center(); + * // Change the background color. + * function changeBackground(c) { + * background(c); + * } * - * describe('A gray square and an orange rectangle. The rectangle is at the center of the page.'); + * // Remove the lavender color-change cue when the user double-clicks. + * function doubleClicked() { + * if (isRemoved === false) { + * beat.removeCue(lavenderID); + * isRemoved = true; + * } * } * *
*/ -p5.Element.prototype.center = function (align) { - const style = this.elt.style.display; - const hidden = this.elt.style.display === 'none'; - const parentHidden = this.parent().style.display === 'none'; - const pos = { x: this.elt.offsetLeft, y: this.elt.offsetTop }; - - if (hidden) this.show(); - if (parentHidden) this.parent().show(); - this.elt.style.display = 'block'; - - this.position(0, 0); - const wOffset = Math.abs(this.parent().offsetWidth - this.elt.offsetWidth); - const hOffset = Math.abs(this.parent().offsetHeight - this.elt.offsetHeight); - - if (align === 'both' || align === undefined) { - this.position( - wOffset / 2 + this.parent().offsetLeft, - hOffset / 2 + this.parent().offsetTop - ); - } else if (align === 'horizontal') { - this.position(wOffset / 2 + this.parent().offsetLeft, pos.y); - } else if (align === 'vertical') { - this.position(pos.x, hOffset / 2 + this.parent().offsetTop); - } - - this.style('display', style); - if (hidden) this.hide(); - if (parentHidden) this.parent().hide(); + 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); + } + } - return this; -}; + if (this._cues.length === 0) { + this.elt.ontimeupdate = null; + } + } -/** - * Sets the inner HTML of the element, replacing any existing HTML. - * - * The second parameter, `append`, is optional. If `true` is passed, as in - * `myElement.html('hi', true)`, the HTML is appended instead of replacing - * existing HTML. - * - * If no arguments are passed, as in `myElement.html()`, the element's inner - * HTML is returned. - * - * @for p5.Element - * @method html - * @returns {String} the inner HTML of the element + /** + * Removes all functions scheduled with `media.addCue()`. * * @example - *
+ *
* - * function setup() { - * createCanvas(100, 100); - * - * // Create the div element and set its size. - * let div = createDiv(''); - * div.size(100, 100); - * - * // Set the inner HTML to "hi". - * div.html('hi'); - * - * describe('A gray square with the word "hi" written beneath it.'); - * } - * - *
+ * let isChanging = true; * - *
- * * function setup() { * createCanvas(100, 100); * * background(200); * - * // Create the div element and set its size. - * let div = createDiv('Hello '); - * div.size(100, 100); + * // Create a p5.MediaElement using createAudio(). + * let beat = createAudio('assets/beat.mp3'); * - * // Append "World" to the div's HTML. - * div.html('World', true); + * // Play the beat in a loop. + * beat.loop(); * - * describe('A gray square with the text "Hello World" written beneath it.'); - * } - * - *
+ * // Schedule a few events. + * beat.addCue(0, changeBackground, 'red'); + * beat.addCue(2, changeBackground, 'deeppink'); + * beat.addCue(4, changeBackground, 'orchid'); + * beat.addCue(6, changeBackground, 'lavender'); * - *
- * - * function setup() { - * createCanvas(100, 100); + * 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); * - * // Create the div element. - * let div = createDiv('Hello'); + * // 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); + * } + * } * - * // Prints "Hello" to the console. - * print(div.html()); + * // Change the background color. + * function changeBackground(c) { + * background(c); + * } * - * describe('A gray square with the word "Hello!" written beneath it.'); + * // Remove cued functions and stop changing colors when the user + * // double-clicks. + * function doubleClicked() { + * if (isChanging === true) { + * beat.clearCues(); + * isChanging = false; + * } * } * *
*/ -/** - * @method html - * @param {String} [html] the HTML to be placed inside the element - * @param {Boolean} [append] whether to append HTML to existing - * @chainable - */ -p5.Element.prototype.html = function (...args) { - if (args.length === 0) { - return this.elt.innerHTML; - } else if (args[1]) { - this.elt.insertAdjacentHTML('beforeend', args[0]); - return this; - } else { - this.elt.innerHTML = args[0]; + 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; + } +}; + +function dom(p5, fn){ + /** + * Searches the page for the first element that matches the given + * CSS selector string. + * + * The selector string can be an ID, class, tag name, or a combination. + * `select()` returns a p5.Element object if it + * finds a match and `null` if not. + * + * The second parameter, `container`, is optional. It specifies a container to + * search within. `container` can be CSS selector string, a + * p5.Element object, or an + * HTMLElement object. + * + * @method select + * @param {String} selectors CSS selector string of element to search for. + * @param {String|p5.Element|HTMLElement} [container] CSS selector string, p5.Element, or + * HTMLElement to search within. + * @return {p5.Element|null} p5.Element containing the element. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * background(200); + * + * // Select the canvas by its tag. + * let cnv = select('canvas'); + * cnv.style('border', '5px deeppink dashed'); + * + * describe('A gray square with a dashed pink border.'); + * } + * + *
+ * + *
+ * + * function setup() { + * let cnv = createCanvas(100, 100); + * + * // Add a class attribute to the canvas. + * cnv.class('pinkborder'); + * + * background(200); + * + * // Select the canvas by its class. + * cnv = select('.pinkborder'); + * + * // Style its border. + * cnv.style('border', '5px deeppink dashed'); + * + * describe('A gray square with a dashed pink border.'); + * } + * + *
+ * + *
+ * + * function setup() { + * let cnv = createCanvas(100, 100); + * + * // Set the canvas' ID. + * cnv.id('mycanvas'); + * + * background(200); + * + * // Select the canvas by its ID. + * cnv = select('#mycanvas'); + * + * // Style its border. + * cnv.style('border', '5px deeppink dashed'); + * + * describe('A gray square with a dashed pink border.'); + * } + * + *
+ */ + fn.select = function (e, p) { + p5._validateParameters('select', arguments); + const container = this._getContainer(p); + const res = container.querySelector(e); + if (res) { + return this._wrapElement(res); + } else { + return null; + } + }; + + /** + * Searches the page for all elements that matches the given + * CSS selector string. + * + * The selector string can be an ID, class, tag name, or a combination. + * `selectAll()` returns an array of p5.Element + * objects if it finds any matches and an empty array if none are found. + * + * The second parameter, `container`, is optional. It specifies a container to + * search within. `container` can be CSS selector string, a + * p5.Element object, or an + * HTMLElement object. + * + * @method selectAll + * @param {String} selectors CSS selector string of element to search for. + * @param {String|p5.Element|HTMLElement} [container] CSS selector string, p5.Element, or + * HTMLElement to search within. + * @return {p5.Element[]} array of p5.Elements containing any elements found. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Create three buttons. + * createButton('1'); + * createButton('2'); + * createButton('3'); + * + * // Select the buttons by their tag. + * let buttons = selectAll('button'); + * + * // Position the buttons. + * for (let i = 0; i < 3; i += 1) { + * buttons[i].position(0, i * 30); + * } + * + * describe('Three buttons stacked vertically. The buttons are labeled, "1", "2", and "3".'); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create three buttons and position them. + * let b1 = createButton('1'); + * b1.position(0, 0); + * let b2 = createButton('2'); + * b2.position(0, 30); + * let b3 = createButton('3'); + * b3.position(0, 60); + * + * // Add a class attribute to each button. + * b1.class('btn'); + * b2.class('btn btn-pink'); + * b3.class('btn'); + * + * // Select the buttons by their class. + * let buttons = selectAll('.btn'); + * let pinkButtons = selectAll('.btn-pink'); + * + * // Style the selected buttons. + * buttons.forEach(setFont); + * pinkButtons.forEach(setColor); + * + * describe('Three buttons stacked vertically. The buttons are labeled, "1", "2", and "3". Buttons "1" and "3" are gray. Button "2" is pink.'); + * } + * + * // Set a button's font to Comic Sans MS. + * function setFont(btn) { + * btn.style('font-family', 'Comic Sans MS'); + * } + * + * // Set a button's background and font color. + * function setColor(btn) { + * btn.style('background', 'deeppink'); + * btn.style('color', 'white'); + * } + * + *
+ */ + fn.selectAll = function (e, p) { + p5._validateParameters('selectAll', arguments); + const arr = []; + const container = this._getContainer(p); + const res = container.querySelectorAll(e); + if (res) { + for (let j = 0; j < res.length; j++) { + const obj = this._wrapElement(res[j]); + arr.push(obj); + } + } + return arr; + }; + + /** + * Helper function for select and selectAll + */ + fn._getContainer = function (p) { + let container = document; + if (typeof p === 'string') { + container = document.querySelector(p) || document; + } else if (p instanceof p5.Element) { + container = p.elt; + } else if (p instanceof HTMLElement) { + container = p; + } + return container; + }; + + /** + * Helper function for getElement and getElements. + */ + fn._wrapElement = function (elt) { + const children = Array.prototype.slice.call(elt.children); + if (elt.tagName === 'INPUT' && elt.type === 'checkbox') { + let converted = new p5.Element(elt, this); + converted.checked = function (...args) { + if (args.length === 0) { + return this.elt.checked; + } else if (args[0]) { + this.elt.checked = true; + } else { + this.elt.checked = false; + } + return this; + }; + return converted; + } else if (elt.tagName === 'VIDEO' || elt.tagName === 'AUDIO') { + return new p5.MediaElement(elt, this); + } else if (elt.tagName === 'SELECT') { + return this.createSelect(new p5.Element(elt, this)); + } else if ( + children.length > 0 && + children.every(function (c) { + return c.tagName === 'INPUT' || c.tagName === 'LABEL'; + }) && + (elt.tagName === 'DIV' || elt.tagName === 'SPAN') + ) { + return this.createRadio(new p5.Element(elt, this)); + } else { + return new p5.Element(elt, this); + } + }; + + /** + * Removes all elements created by p5.js, including any event handlers. + * + * There are two exceptions: + * canvas elements created by createCanvas() + * and p5.Render objects created by + * createGraphics(). + * + * @method removeElements + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a paragraph element and place + * // it in the middle of the canvas. + * let p = createP('p5*js'); + * p.position(25, 25); + * + * describe('A gray square with the text "p5*js" written in its center. The text disappears when the mouse is pressed.'); + * } + * + * // Remove all elements when the mouse is pressed. + * function mousePressed() { + * removeElements(); + * } + * + *
+ * + *
+ * + * let slider; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a paragraph element and place + * // it at the top of the canvas. + * let p = createP('p5*js'); + * p.position(25, 25); + * + * // Create a slider element and place it + * // beneath the canvas. + * slider = createSlider(0, 255, 200); + * slider.position(0, 100); + * + * describe('A gray square with the text "p5*js" written in its center and a range slider beneath it. The square changes color when the slider is moved. The text and slider disappear when the square is double-clicked.'); + * } + * + * function draw() { + * // Use the slider value to change the background color. + * let g = slider.value(); + * background(g); + * } + * + * // Remove all elements when the mouse is double-clicked. + * function doubleClicked() { + * removeElements(); + * } + * + *
+ */ + fn.removeElements = function (e) { + p5._validateParameters('removeElements', arguments); + // el.remove splices from this._elements, so don't mix iteration with it + const isNotCanvasElement = el => !(el.elt instanceof HTMLCanvasElement); + const removeableElements = this._elements.filter(isNotCanvasElement); + removeableElements.map(el => el.remove()); + }; + + /** + * Calls a function when the element changes. + * + * Calling `myElement.changed(false)` disables the function. + * + * @method changed + * @param {Function|Boolean} fxn function to call when the element changes. + * `false` disables the function. + * @chainable + * + * @example + *
+ * + * let dropdown; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a dropdown menu and add a few color options. + * dropdown = createSelect(); + * dropdown.position(0, 0); + * dropdown.option('red'); + * dropdown.option('green'); + * dropdown.option('blue'); + * + * // Call paintBackground() when the color option changes. + * dropdown.changed(paintBackground); + * + * describe('A gray square with a dropdown menu at the top. The square changes color when an option is selected.'); + * } + * + * // Paint the background with the selected color. + * function paintBackground() { + * let c = dropdown.value(); + * background(c); + * } + * + *
+ * + *
+ * + * let checkbox; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a checkbox and place it beneath the canvas. + * checkbox = createCheckbox(' circle'); + * checkbox.position(0, 100); + * + * // Call repaint() when the checkbox changes. + * checkbox.changed(repaint); + * + * describe('A gray square with a checkbox underneath it that says "circle". A white circle appears when the box is checked and disappears otherwise.'); + * } + * + * // Paint the background gray and determine whether to draw a circle. + * function repaint() { + * background(200); + * if (checkbox.checked() === true) { + * circle(50, 50, 30); + * } + * } + * + *
+ */ + p5.Element.prototype.changed = function (fxn) { + p5.Element._adjustListener('change', fxn, this); return this; - } -}; + }; -/** - * Sets the element's position. - * - * The first two parameters, `x` and `y`, set the element's position relative - * to the top-left corner of the web page. - * - * The third parameter, `positionType`, is optional. It sets the element's - * positioning scheme. - * `positionType` is a string that can be either `'static'`, `'fixed'`, - * `'relative'`, `'sticky'`, `'initial'`, or `'inherit'`. - * - * If no arguments passed, as in `myElement.position()`, the method returns - * the element's position in an object, as in `{ x: 0, y: 0 }`. - * - * @method position - * @returns {Object} object of form `{ x: 0, y: 0 }` containing the element's position. - * - * @example - *
- * - * function setup() { - * let cnv = createCanvas(100, 100); - * - * background(200); - * - * // Positions the canvas 50px to the right and 100px - * // below the top-left corner of the window. - * cnv.position(50, 100); - * - * describe('A gray square that is 50 pixels to the right and 100 pixels down from the top-left corner of the web page.'); - * } - * - *
- * - *
- * - * function setup() { - * let cnv = createCanvas(100, 100); - * - * background(200); - * - * // Positions the canvas at the top-left corner - * // of the window with a 'fixed' position type. - * cnv.position(0, 0, 'fixed'); - * - * describe('A gray square in the top-left corner of the web page.'); - * } - * - *
- */ -/** - * @method position - * @param {Number} [x] x-position relative to top-left of window (optional) - * @param {Number} [y] y-position relative to top-left of window (optional) - * @param {String} [positionType] it can be static, fixed, relative, sticky, initial or inherit (optional) - * @chainable - */ -p5.Element.prototype.position = function (...args) { - if (args.length === 0) { - return { x: this.elt.offsetLeft, y: this.elt.offsetTop }; - } else { - let positionType = 'absolute'; - if ( - args[2] === 'static' || - args[2] === 'fixed' || - args[2] === 'relative' || - args[2] === 'sticky' || - args[2] === 'initial' || - args[2] === 'inherit' - ) { - positionType = args[2]; - } - this.elt.style.position = positionType; - this.elt.style.left = args[0] + 'px'; - this.elt.style.top = args[1] + 'px'; - this.x = args[0]; - this.y = args[1]; + /** + * Calls a function when the element receives input. + * + * `myElement.input()` is often used to with text inputs and sliders. Calling + * `myElement.input(false)` disables the function. + * + * @method input + * @param {Function|Boolean} fxn function to call when input is detected within + * the element. + * `false` disables the function. + * @chainable + * + * @example + *
+ * + * let slider; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a slider and place it beneath the canvas. + * slider = createSlider(0, 255, 200); + * slider.position(0, 100); + * + * // Call repaint() when the slider changes. + * slider.input(repaint); + * + * describe('A gray square with a range slider underneath it. The background changes shades of gray when the slider is moved.'); + * } + * + * // Paint the background using slider's value. + * function repaint() { + * let g = slider.value(); + * background(g); + * } + * + *
+ * + *
+ * + * let input; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create an input and place it beneath the canvas. + * input = createInput(''); + * input.position(0, 100); + * + * // Call repaint() when input is detected. + * input.input(repaint); + * + * describe('A gray square with a text input bar beneath it. Any text written in the input appears in the middle of the square.'); + * } + * + * // Paint the background gray and display the input's value. + * function repaint() { + * background(200); + * let msg = input.value(); + * text(msg, 5, 50); + * } + * + *
+ */ + p5.Element.prototype.input = function (fxn) { + p5.Element._adjustListener('input', fxn, this); return this; - } -}; + }; -/* Helper method called by p5.Element.style() */ -p5.Element.prototype._translate = function (...args) { - this.elt.style.position = 'absolute'; - // save out initial non-translate transform styling - let transform = ''; - if (this.elt.style.transform) { - transform = this.elt.style.transform.replace(/translate3d\(.*\)/g, ''); - transform = transform.replace(/translate[X-Z]?\(.*\)/g, ''); - } - if (args.length === 2) { - this.elt.style.transform = - 'translate(' + args[0] + 'px, ' + args[1] + 'px)'; - } else if (args.length > 2) { - this.elt.style.transform = - 'translate3d(' + - args[0] + - 'px,' + - args[1] + - 'px,' + - args[2] + - 'px)'; - if (args.length === 3) { - this.elt.parentElement.style.perspective = '1000px'; - } else { - this.elt.parentElement.style.perspective = args[3] + 'px'; - } + /** + * Helpers for create methods. + */ + function addElement(elt, pInst, media) { + const node = pInst._userNode ? pInst._userNode : document.body; + node.appendChild(elt); + const c = media + ? new p5.MediaElement(elt, pInst) + : new p5.Element(elt, pInst); + pInst._elements.push(c); + return c; } - // add any extra transform styling back on end - this.elt.style.transform += transform; - return this; -}; -/* Helper method called by p5.Element.style() */ -p5.Element.prototype._rotate = function (...args) { - // save out initial non-rotate transform styling - let transform = ''; - if (this.elt.style.transform) { - transform = this.elt.style.transform.replace(/rotate3d\(.*\)/g, ''); - transform = transform.replace(/rotate[X-Z]?\(.*\)/g, ''); - } + /** + * Creates a `<div></div>` element. + * + * `<div></div>` elements are commonly used as containers for + * other elements. + * + * The parameter `html` is optional. It accepts a string that sets the + * inner HTML of the new `<div></div>`. + * + * @method createDiv + * @param {String} [html] inner HTML for the new `<div></div>` element. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a div element and set its position. + * let div = createDiv('p5*js'); + * div.position(25, 35); + * + * describe('A gray square with the text "p5*js" written in its center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create an h3 element within the div. + * let div = createDiv('

p5*js

'); + * div.position(20, 5); + * + * describe('A gray square with the text "p5*js" written in its center.'); + * } + *
+ *
+ */ + fn.createDiv = function (html = '') { + let elt = document.createElement('div'); + elt.innerHTML = html; + return addElement(elt, this); + }; - if (args.length === 1) { - this.elt.style.transform = 'rotate(' + args[0] + 'deg)'; - } else if (args.length === 2) { - this.elt.style.transform = - 'rotate(' + args[0] + 'deg, ' + args[1] + 'deg)'; - } else if (args.length === 3) { - this.elt.style.transform = 'rotateX(' + args[0] + 'deg)'; - this.elt.style.transform += 'rotateY(' + args[1] + 'deg)'; - this.elt.style.transform += 'rotateZ(' + args[2] + 'deg)'; - } - // add remaining transform back on - this.elt.style.transform += transform; - return this; -}; + /** + * Creates a `<p></p>` element. + * + * `<p></p>` elements are commonly used for paragraph-length text. + * + * The parameter `html` is optional. It accepts a string that sets the + * inner HTML of the new `<p></p>`. + * + * @method createP + * @param {String} [html] inner HTML for the new `<p></p>` element. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a paragraph element and set its position. + * let p = createP('Tell me a story.'); + * p.position(5, 0); + * + * describe('A gray square displaying the text "Tell me a story." written in black.'); + * } + * + *
+ */ + fn.createP = function (html = '') { + let elt = document.createElement('p'); + elt.innerHTML = html; + return addElement(elt, this); + }; -/** - * Applies a style to the element by adding a - * CSS declaration. - * - * The first parameter, `property`, is a string. If the name of a style - * property is passed, as in `myElement.style('color')`, the method returns - * the current value as a string or `null` if it hasn't been set. If a - * `property:style` string is passed, as in - * `myElement.style('color:deeppink')`, the method sets the style `property` - * to `value`. - * - * The second parameter, `value`, is optional. It sets the property's value. - * `value` can be a string, as in - * `myElement.style('color', 'deeppink')`, or a - * p5.Color object, as in - * `myElement.style('color', myColor)`. - * - * @method style - * @param {String} property style property to set. - * @returns {String} value of the property. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element and set its font color to "deeppink". - * let p = createP('p5*js'); - * p.position(25, 20); - * p.style('color', 'deeppink'); - * - * describe('The text p5*js written in pink on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Color object. - * let c = color('deeppink'); - * - * // Create a paragraph element and set its font color using a p5.Color object. - * let p = createP('p5*js'); - * p.position(25, 20); - * p.style('color', c); - * - * describe('The text p5*js written in pink on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element and set its font color to "deeppink" - * // using property:value syntax. - * let p = createP('p5*js'); - * p.position(25, 20); - * p.style('color:deeppink'); - * - * describe('The text p5*js written in pink on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create an empty paragraph element and set its font color to "deeppink". - * let p = createP(); - * p.position(5, 5); - * p.style('color', 'deeppink'); - * - * // Get the element's color as an RGB color string. - * let c = p.style('color'); - * - * // Set the element's inner HTML using the RGB color string. - * p.html(c); - * - * describe('The text "rgb(255, 20, 147)" written in pink on a gray background.'); - * } - * - *
- */ -/** - * @method style - * @param {String} property - * @param {String|p5.Color} value value to assign to the property. - * @return {String} value of the property. - * @chainable - */ -p5.Element.prototype.style = function (prop, val) { - const self = this; - - if (val instanceof p5.Color) { - val = - 'rgba(' + - val.levels[0] + - ',' + - val.levels[1] + - ',' + - val.levels[2] + - ',' + - val.levels[3] / 255 + - ')'; - } + /** + * Creates a `<span></span>` element. + * + * `<span></span>` elements are commonly used as containers + * for inline elements. For example, a `<span></span>` + * can hold part of a sentence that's a + * different style. + * + * The parameter `html` is optional. It accepts a string that sets the + * inner HTML of the new `<span></span>`. + * + * @method createSpan + * @param {String} [html] inner HTML for the new `<span></span>` element. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a span element and set its position. + * let span = createSpan('p5*js'); + * span.position(25, 35); + * + * describe('A gray square with the text "p5*js" written in its center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * background(200); + * + * // Create a div element as a container. + * let div = createDiv(); + * + * // Place the div at the center. + * div.position(25, 35); + * + * // Create a span element. + * let s1 = createSpan('p5'); + * + * // Create a second span element. + * let s2 = createSpan('*'); + * + * // Set the second span's font color. + * s2.style('color', 'deeppink'); + * + * // Create a third span element. + * let s3 = createSpan('js'); + * + * // Add all the spans to the container div. + * s1.parent(div); + * s2.parent(div); + * s3.parent(div); + * + * describe('A gray square with the text "p5*js" written in black at its center. The asterisk is pink.'); + * } + * + *
+ */ + fn.createSpan = function (html = '') { + let elt = document.createElement('span'); + elt.innerHTML = html; + return addElement(elt, this); + }; - if (typeof val === 'undefined') { - if (prop.indexOf(':') === -1) { - // no value set, so assume requesting a value - let styles = window.getComputedStyle(self.elt); - let style = styles.getPropertyValue(prop); - return style; - } else { - // value set using `:` in a single line string - const attrs = prop.split(';'); - for (let i = 0; i < attrs.length; i++) { - const parts = attrs[i].split(':'); - if (parts[0] && parts[1]) { - this.elt.style[parts[0].trim()] = parts[1].trim(); - } - } + /** + * Creates an `<img>` element that can appear outside of the canvas. + * + * The first parameter, `src`, is a string with the path to the image file. + * `src` should be a relative path, as in `'assets/image.png'`, or a URL, as + * in `'https://example.com/image.png'`. + * + * The second parameter, `alt`, is a string with the + * alternate text + * for the image. An empty string `''` can be used for images that aren't displayed. + * + * The third parameter, `crossOrigin`, is optional. It's a string that sets the + * crossOrigin property + * of the image. Use `'anonymous'` or `'use-credentials'` to fetch the image + * with cross-origin access. + * + * The fourth parameter, `callback`, is also optional. It sets a function to + * call after the image loads. The new image is passed to the callback + * function as a p5.Element object. + * + * @method createImg + * @param {String} src relative path or URL for the image. + * @param {String} alt alternate text for the image. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * let img = createImg( + * 'https://p5js.org/assets/img/asterisk-01.png', + * 'The p5.js magenta asterisk.' + * ); + * img.position(0, -10); + * + * describe('A gray square with a magenta asterisk in its center.'); + * } + * + *
+ */ + /** + * @method createImg + * @param {String} src + * @param {String} alt + * @param {String} [crossOrigin] crossOrigin property to use when fetching the image. + * @param {Function} [successCallback] function to call once the image loads. The new image will be passed + * to the function as a p5.Element object. + * @return {p5.Element} new p5.Element object. + */ + fn.createImg = function () { + p5._validateParameters('createImg', arguments); + const elt = document.createElement('img'); + const args = arguments; + let self; + if (args.length > 1 && typeof args[1] === 'string') { + elt.alt = args[1]; } - } else { - // input provided as key,val pair - this.elt.style[prop] = val; - if ( - prop === 'width' || - prop === 'height' || - prop === 'left' || - prop === 'top' - ) { - let styles = window.getComputedStyle(self.elt); - let styleVal = styles.getPropertyValue(prop); - let numVal = styleVal.replace(/[^\d.]/g, ''); - this[prop] = Math.round(parseFloat(numVal, 10)); + if (args.length > 2 && typeof args[2] === 'string') { + elt.crossOrigin = args[2]; } - } - return this; -}; + elt.src = args[0]; + self = addElement(elt, this); + elt.addEventListener('load', function () { + self.width = elt.offsetWidth || elt.width; + self.height = elt.offsetHeight || elt.height; + const last = args[args.length - 1]; + if (typeof last === 'function') last(self); + }); + return self; + }; -/** - * Adds an - * attribute - * to the element. - * - * This method is useful for advanced tasks. Most commonly-used attributes, - * such as `id`, can be set with their dedicated methods. For example, - * `nextButton.id('next')` sets an element's `id` attribute. Calling - * `nextButton.attribute('id', 'next')` has the same effect. - * - * The first parameter, `attr`, is the attribute's name as a string. Calling - * `myElement.attribute('align')` returns the attribute's current value as a - * string or `null` if it hasn't been set. - * - * The second parameter, `value`, is optional. It's a string used to set the - * attribute's value. For example, calling - * `myElement.attribute('align', 'center')` sets the element's horizontal - * alignment to `center`. - * - * @method attribute - * @return {String} value of the attribute. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Create a container div element and place it at the top-left corner. - * let container = createDiv(); - * container.position(0, 0); - * - * // Create a paragraph element and place it within the container. - * // Set its horizontal alignment to "left". - * let p1 = createP('hi'); - * p1.parent(container); - * p1.attribute('align', 'left'); - * - * // Create a paragraph element and place it within the container. - * // Set its horizontal alignment to "center". - * let p2 = createP('hi'); - * p2.parent(container); - * p2.attribute('align', 'center'); - * - * // Create a paragraph element and place it within the container. - * // Set its horizontal alignment to "right". - * let p3 = createP('hi'); - * p3.parent(container); - * p3.attribute('align', 'right'); - * - * describe('A gray square with the text "hi" written on three separate lines, each placed further to the right.'); - * } - * - *
- */ -/** - * @method attribute - * @param {String} attr attribute to set. - * @param {String} value value to assign to the attribute. - * @chainable - */ -p5.Element.prototype.attribute = function (attr, value) { - //handling for checkboxes and radios to ensure options get - //attributes not divs - if ( - this.elt.firstChild != null && - (this.elt.firstChild.type === 'checkbox' || - this.elt.firstChild.type === 'radio') - ) { - if (typeof value === 'undefined') { - return this.elt.firstChild.getAttribute(attr); - } else { - for (let i = 0; i < this.elt.childNodes.length; i++) { - this.elt.childNodes[i].setAttribute(attr, value); + /** + * Creates an `<a></a>` element that links to another web page. + * + * The first parmeter, `href`, is a string that sets the URL of the linked + * page. + * + * The second parameter, `html`, is a string that sets the inner HTML of the + * link. It's common to use text, images, or buttons as links. + * + * The third parameter, `target`, is optional. It's a string that tells the + * web browser where to open the link. By default, links open in the current + * browser tab. Passing `'_blank'` will cause the link to open in a new + * browser tab. MDN describes a few + * other options. + * + * @method createA + * @param {String} href URL of linked page. + * @param {String} html inner HTML of link element to display. + * @param {String} [target] target where the new link should open, + * either `'_blank'`, `'_self'`, `'_parent'`, or `'_top'`. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create an anchor element that links to p5js.org. + * let a = createA('http://p5js.org/', 'p5*js'); + * a.position(25, 35); + * + * describe('The text "p5*js" written at the center of a gray square.'); + * } + * + *
+ * + *
+ * + * function setup() { + * background(200); + * + * // Create an anchor tag that links to p5js.org. + * // Open the link in a new tab. + * let a = createA('http://p5js.org/', 'p5*js', '_blank'); + * a.position(25, 35); + * + * describe('The text "p5*js" written at the center of a gray square.'); + * } + * + *
+ */ + fn.createA = function (href, html, target) { + p5._validateParameters('createA', arguments); + const elt = document.createElement('a'); + elt.href = href; + elt.innerHTML = html; + if (target) elt.target = target; + return addElement(elt, this); + }; + + /** INPUT **/ + + /** + * Creates a slider `<input></input>` element. + * + * Range sliders are useful for quickly selecting numbers from a given range. + * + * The first two parameters, `min` and `max`, are numbers that set the + * slider's minimum and maximum. + * + * The third parameter, `value`, is optional. It's a number that sets the + * slider's default value. + * + * The fourth parameter, `step`, is also optional. It's a number that sets the + * spacing between each value in the slider's range. Setting `step` to 0 + * allows the slider to move smoothly from `min` to `max`. + * + * @method createSlider + * @param {Number} min minimum value of the slider. + * @param {Number} max maximum value of the slider. + * @param {Number} [value] default value of the slider. + * @param {Number} [step] size for each step in the slider's range. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * let slider; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a slider and place it at the top of the canvas. + * slider = createSlider(0, 255); + * slider.position(10, 10); + * slider.size(80); + * + * describe('A dark gray square with a range slider at the top. The square changes color when the slider is moved.'); + * } + * + * function draw() { + * // Use the slider as a grayscale value. + * let g = slider.value(); + * background(g); + * } + * + *
+ * + *
+ * + * let slider; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a slider and place it at the top of the canvas. + * // Set its default value to 0. + * slider = createSlider(0, 255, 0); + * slider.position(10, 10); + * slider.size(80); + * + * describe('A black square with a range slider at the top. The square changes color when the slider is moved.'); + * } + * + * function draw() { + * // Use the slider as a grayscale value. + * let g = slider.value(); + * background(g); + * } + * + *
+ * + *
+ * + * let slider; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a slider and place it at the top of the canvas. + * // Set its default value to 0. + * // Set its step size to 50. + * slider = createSlider(0, 255, 0, 50); + * slider.position(10, 10); + * slider.size(80); + * + * describe('A black square with a range slider at the top. The square changes color when the slider is moved.'); + * } + * + * function draw() { + * // Use the slider as a grayscale value. + * let g = slider.value(); + * background(g); + * } + * + *
+ * + *
+ * + * let slider; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a slider and place it at the top of the canvas. + * // Set its default value to 0. + * // Set its step size to 0 so that it moves smoothly. + * slider = createSlider(0, 255, 0, 0); + * slider.position(10, 10); + * slider.size(80); + * + * describe('A black square with a range slider at the top. The square changes color when the slider is moved.'); + * } + * + * function draw() { + * // Use the slider as a grayscale value. + * let g = slider.value(); + * background(g); + * } + * + *
+ */ + fn.createSlider = function (min, max, value, step) { + p5._validateParameters('createSlider', arguments); + const elt = document.createElement('input'); + elt.type = 'range'; + elt.min = min; + elt.max = max; + if (step === 0) { + elt.step = 0.000000000000000001; // smallest valid step + } else if (step) { + elt.step = step; + } + if (typeof value === 'number') elt.value = value; + return addElement(elt, this); + }; + + /** + * Creates a `<button></button>` element. + * + * The first parameter, `label`, is a string that sets the label displayed on + * the button. + * + * The second parameter, `value`, is optional. It's a string that sets the + * button's value. See + * MDN + * for more details. + * + * @method createButton + * @param {String} label label displayed on the button. + * @param {String} [value] value of the button. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a button and place it beneath the canvas. + * let button = createButton('click me'); + * button.position(0, 100); + * + * // Call repaint() when the button is pressed. + * button.mousePressed(repaint); + * + * describe('A gray square with a button that says "click me" beneath it. The square changes color when the button is clicked.'); + * } + * + * // Change the background color. + * function repaint() { + * let g = random(255); + * background(g); + * } + * + *
+ * + *
+ * + * let button; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a button and set its value to 0. + * // Place the button beneath the canvas. + * button = createButton('click me', 'red'); + * button.position(0, 100); + * + * // Call randomColor() when the button is pressed. + * button.mousePressed(randomColor); + * + * describe('A red square with a button that says "click me" beneath it. The square changes color when the button is clicked.'); + * } + * + * function draw() { + * // Use the button's value to set the background color. + * let c = button.value(); + * background(c); + * } + * + * // Set the button's value to a random color. + * function randomColor() { + * let c = random(['red', 'green', 'blue', 'yellow']); + * button.value(c); + * } + * + *
+ */ + fn.createButton = function (label, value) { + p5._validateParameters('createButton', arguments); + const elt = document.createElement('button'); + elt.innerHTML = label; + if (value) elt.value = value; + return addElement(elt, this); + }; + + /** + * Creates a checkbox `<input></input>` element. + * + * Checkboxes extend the p5.Element class with a + * `checked()` method. Calling `myBox.checked()` returns `true` if it the box + * is checked and `false` if not. + * + * The first parameter, `label`, is optional. It's a string that sets the label + * to display next to the checkbox. + * + * The second parameter, `value`, is also optional. It's a boolean that sets the + * checkbox's value. + * + * @method createCheckbox + * @param {String} [label] label displayed after the checkbox. + * @param {Boolean} [value] value of the checkbox. Checked is `true` and unchecked is `false`. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * let checkbox; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a checkbox and place it beneath the canvas. + * checkbox = createCheckbox(); + * checkbox.position(0, 100); + * + * describe('A black square with a checkbox beneath it. The square turns white when the box is checked.'); + * } + * + * function draw() { + * // Use the checkbox to set the background color. + * if (checkbox.checked()) { + * background(255); + * } else { + * background(0); + * } + * } + * + *
+ * + *
+ * + * let checkbox; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a checkbox and place it beneath the canvas. + * // Label the checkbox "white". + * checkbox = createCheckbox(' white'); + * checkbox.position(0, 100); + * + * describe('A black square with a checkbox labeled "white" beneath it. The square turns white when the box is checked.'); + * } + * + * function draw() { + * // Use the checkbox to set the background color. + * if (checkbox.checked()) { + * background(255); + * } else { + * background(0); + * } + * } + * + *
+ * + *
+ * + * let checkbox; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a checkbox and place it beneath the canvas. + * // Label the checkbox "white" and set its value to true. + * checkbox = createCheckbox(' white', true); + * checkbox.position(0, 100); + * + * describe('A white square with a checkbox labeled "white" beneath it. The square turns black when the box is unchecked.'); + * } + * + * function draw() { + * // Use the checkbox to set the background color. + * if (checkbox.checked()) { + * background(255); + * } else { + * background(0); + * } + * } + * + *
+ */ + fn.createCheckbox = function (...args) { + p5._validateParameters('createCheckbox', args); + + // Create a container element + const elt = document.createElement('div'); + + // Create checkbox type input element + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + + // Create label element and wrap it around checkbox + const label = document.createElement('label'); + label.appendChild(checkbox); + + // Append label element inside the container + elt.appendChild(label); + + //checkbox must be wrapped in p5.Element before label so that label appears after + const self = addElement(elt, this); + + self.checked = function (...args) { + const cb = self.elt.firstElementChild.getElementsByTagName('input')[0]; + if (cb) { + if (args.length === 0) { + return cb.checked; + } else if (args[0]) { + cb.checked = true; + } else { + cb.checked = false; + } } - } - } else if (typeof value === 'undefined') { - return this.elt.getAttribute(attr); - } else { - this.elt.setAttribute(attr, value); - return this; - } -}; + return self; + }; -/** - * Removes an attribute from the element. - * - * The parameter `attr` is the attribute's name as a string. For example, - * calling `myElement.removeAttribute('align')` removes its `align` - * attribute if it's been set. - * - * @method removeAttribute - * @param {String} attr attribute to remove. - * @chainable - * - * @example - *
- * - * let p; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element and place it in the center of the canvas. - * // Set its "align" attribute to "center". - * p = createP('hi'); - * p.position(0, 20); - * p.attribute('align', 'center'); - * - * describe('The text "hi" written in black at the center of a gray square. The text moves to the left edge when double-clicked.'); - * } - * - * // Remove the 'align' attribute when the user double-clicks the paragraph. - * function doubleClicked() { - * p.removeAttribute('align'); - * } - * - *
- */ -p5.Element.prototype.removeAttribute = function (attr) { - if ( - this.elt.firstChild != null && - (this.elt.firstChild.type === 'checkbox' || - this.elt.firstChild.type === 'radio') - ) { - for (let i = 0; i < this.elt.childNodes.length; i++) { - this.elt.childNodes[i].removeAttribute(attr); - } - } - this.elt.removeAttribute(attr); - return this; -}; + this.value = function (val) { + self.value = val; + return this; + }; -/** - * Returns or sets the element's value. - * - * Calling `myElement.value()` returns the element's current value. - * - * The parameter, `value`, is an optional number or string. If provided, - * as in `myElement.value(123)`, it's used to set the element's value. - * - * @method value - * @return {String|Number} value of the element. - * - * @example - *
- * - * let input; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a text input and place it beneath the canvas. - * // Set its default value to "hello". - * input = createInput('hello'); - * input.position(0, 100); - * - * describe('The text from an input box is displayed on a gray square.'); - * } - * - * function draw() { - * background(200); - * - * // Use the input's value to display a message. - * let msg = input.value(); - * text(msg, 0, 55); - * } - * - *
- * - *
- * - * let input; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a text input and place it beneath the canvas. - * // Set its default value to "hello". - * input = createInput('hello'); - * input.position(0, 100); - * - * describe('The text from an input box is displayed on a gray square. The text resets to "hello" when the user double-clicks the square.'); - * } - * - * function draw() { - * background(200); - * - * // Use the input's value to display a message. - * let msg = input.value(); - * text(msg, 0, 55); - * } - * - * // Reset the input's value. - * function doubleClicked() { - * input.value('hello'); - * } - * - *
- */ -/** - * @method value - * @param {String|Number} value - * @chainable - */ -p5.Element.prototype.value = function (...args) { - if (args.length > 0) { - this.elt.value = args[0]; - return this; - } else { - if (this.elt.type === 'range') { - return parseFloat(this.elt.value); - } else return this.elt.value; - } -}; + // Set the span element innerHTML as the label value if passed + if (args[0]) { + self.value(args[0]); + const span = document.createElement('span'); + span.innerHTML = args[0]; + label.appendChild(span); + } -/** - * Shows the current element. - * - * @method show - * @chainable - * - * @example - *
- * - * let p; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element and hide it. - * p = createP('p5*js'); - * p.position(10, 10); - * p.hide(); - * - * describe('A gray square. The text "p5*js" appears when the user double-clicks the square.'); - * } - * - * // Show the paragraph when the user double-clicks. - * function doubleClicked() { - * p.show(); - * } - * - *
- */ -p5.Element.prototype.show = function () { - this.elt.style.display = 'block'; - return this; -}; + // Set the checked value of checkbox if passed + if (args[1]) { + checkbox.checked = true; + } -/** - * Hides the current element. - * - * @method hide - * @chainable - * - * @example - * let p; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element. - * p = createP('p5*js'); - * p.position(10, 10); - * - * describe('The text "p5*js" at the center of a gray square. The text disappears when the user double-clicks the square.'); - * } - * - * // Hide the paragraph when the user double-clicks. - * function doubleClicked() { - * p.hide(); - * } - * - *
- */ -p5.Element.prototype.hide = function () { - this.elt.style.display = 'none'; - return this; -}; + return self; + }; -/** - * Sets the element's width and height. - * - * Calling `myElement.size()` without an argument returns the element's size - * as an object with the properties `width` and `height`. For example, - * `{ width: 20, height: 10 }`. - * - * The first parameter, `width`, is optional. It's a number used to set the - * element's width. Calling `myElement.size(10)` - * - * The second parameter, 'height`, is also optional. It's a - * number used to set the element's height. For example, calling - * `myElement.size(20, 10)` sets the element's width to 20 pixels and height - * to 10 pixels. - * - * The constant `AUTO` can be used to adjust one dimension at a time while - * maintaining the aspect ratio, which is `width / height`. For example, - * consider an element that's 200 pixels wide and 100 pixels tall. Calling - * `myElement.size(20, AUTO)` sets the width to 20 pixels and height to 10 - * pixels. - * - * Note: In the case of elements that need to load data, such as images, wait - * to call `myElement.size()` until after the data loads. - * - * @method size - * @return {Object} width and height of the element in an object. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a pink div element and place it at the top-left corner. - * let div = createDiv(); - * div.position(10, 10); - * div.style('background-color', 'deeppink'); - * - * // Set the div's width to 80 pixels and height to 20 pixels. - * div.size(80, 20); - * - * describe('A gray square with a pink rectangle near its top.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a pink div element and place it at the top-left corner. - * let div = createDiv(); - * div.position(10, 10); - * div.style('background-color', 'deeppink'); - * - * // Set the div's width to 80 pixels and height to 40 pixels. - * div.size(80, 40); - * - * // Get the div's size as an object. - * let s = div.size(); - * - * // Display the div's dimensions. - * div.html(`${s.width} x ${s.height}`); - * - * describe('A gray square with a pink rectangle near its top. The text "80 x 40" is written within the rectangle.'); - * } - * - *
- * - *
- * - * let img1; - * let img2; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Load an image of an astronaut on the moon - * // and place it at the top-left of the canvas. - * img1 = createImg( - * 'assets/moonwalk.jpg', - * 'An astronaut walking on the moon', - * '' - * ); - * img1.position(0, 0); - * - * // Load an image of an astronaut on the moon - * // and place it at the top-left of the canvas. - * // Resize the image once it's loaded. - * img2 = createImg( - * 'assets/moonwalk.jpg', - * 'An astronaut walking on the moon', - * '', - * resizeImage - * ); - * img2.position(0, 0); - * - * describe('A gray square two copies of a space image at the top-left. The copy in front is smaller.'); - * } - * - * // Resize img2 and keep its aspect ratio. - * function resizeImage() { - * img2.size(50, AUTO); - * } - * - *
- */ -/** - * @method size - * @param {(Number|AUTO)} [w] width of the element, either AUTO, or a number. - * @param {(Number|AUTO)} [h] height of the element, either AUTO, or a number. - * @chainable - */ -p5.Element.prototype.size = function (w, h) { - if (arguments.length === 0) { - return { width: this.elt.offsetWidth, height: this.elt.offsetHeight }; - } else { - let aW = w; - let aH = h; - const AUTO = p5.prototype.AUTO; - if (aW !== AUTO || aH !== AUTO) { - if (aW === AUTO) { - aW = h * this.width / this.height; - } else if (aH === AUTO) { - aH = w * this.height / this.width; + /** + * Creates a dropdown menu `<select></select>` element. + * + * The parameter is optional. If `true` is passed, as in + * `let mySelect = createSelect(true)`, then the dropdown will support + * multiple selections. If an existing `<select></select>` element + * is passed, as in `let mySelect = createSelect(otherSelect)`, the existing + * element will be wrapped in a new p5.Element + * object. + * + * Dropdowns extend the p5.Element class with a few + * helpful methods for managing options: + * - `mySelect.option(name, [value])` adds an option to the menu. The first paremeter, `name`, is a string that sets the option's name and value. The second parameter, `value`, is optional. If provided, it sets the value that corresponds to the key `name`. If an option with `name` already exists, its value is changed to `value`. + * - `mySelect.value()` returns the currently-selected option's value. + * - `mySelect.selected()` returns the currently-selected option. + * - `mySelect.selected(option)` selects the given option by default. + * - `mySelect.disable()` marks the whole dropdown element as disabled. + * - `mySelect.disable(option)` marks a given option as disabled. + * - `mySelect.enable()` marks the whole dropdown element as enabled. + * - `mySelect.enable(option)` marks a given option as enabled. + * + * @method createSelect + * @param {Boolean} [multiple] support multiple selections. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * let mySelect; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a dropdown and place it beneath the canvas. + * mySelect = createSelect(); + * mySelect.position(0, 100); + * + * // Add color options. + * mySelect.option('red'); + * mySelect.option('green'); + * mySelect.option('blue'); + * mySelect.option('yellow'); + * + * // Set the selected option to "red". + * mySelect.selected('red'); + * + * describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.'); + * } + * + * function draw() { + * // Use the selected value to paint the background. + * let c = mySelect.selected(); + * background(c); + * } + * + *
+ * + *
+ * + * let mySelect; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a dropdown and place it beneath the canvas. + * mySelect = createSelect(); + * mySelect.position(0, 100); + * + * // Add color options. + * mySelect.option('red'); + * mySelect.option('green'); + * mySelect.option('blue'); + * mySelect.option('yellow'); + * + * // Set the selected option to "red". + * mySelect.selected('red'); + * + * // Disable the "yellow" option. + * mySelect.disable('yellow'); + * + * describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.'); + * } + * + * function draw() { + * // Use the selected value to paint the background. + * let c = mySelect.selected(); + * background(c); + * } + * + *
+ * + *
+ * + * let mySelect; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a dropdown and place it beneath the canvas. + * mySelect = createSelect(); + * mySelect.position(0, 100); + * + * // Add color options with names and values. + * mySelect.option('one', 'red'); + * mySelect.option('two', 'green'); + * mySelect.option('three', 'blue'); + * mySelect.option('four', 'yellow'); + * + * // Set the selected option to "one". + * mySelect.selected('one'); + * + * describe('A red square with a dropdown menu beneath it. The square changes color when a new color is selected.'); + * } + * + * function draw() { + * // Use the selected value to paint the background. + * let c = mySelect.selected(); + * background(c); + * } + * + *
+ * + *
+ * + * // Hold CTRL to select multiple options on Windows and Linux. + * // Hold CMD to select multiple options on macOS. + * let mySelect; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a dropdown and allow multiple selections. + * // Place it beneath the canvas. + * mySelect = createSelect(true); + * mySelect.position(0, 100); + * + * // Add color options. + * mySelect.option('red'); + * mySelect.option('green'); + * mySelect.option('blue'); + * mySelect.option('yellow'); + * + * describe('A gray square with a dropdown menu beneath it. Colorful circles appear when their color is selected.'); + * } + * + * function draw() { + * background(200); + * + * // Use the selected value(s) to draw circles. + * let colors = mySelect.selected(); + * for (let i = 0; i < colors.length; i += 1) { + * // Calculate the x-coordinate. + * let x = 10 + i * 20; + * + * // Access the color. + * let c = colors[i]; + * + * // Draw the circle. + * fill(c); + * circle(x, 50, 20); + * } + * } + * + *
+ */ + /** + * @method createSelect + * @param {Object} existing select element to wrap, either as a p5.Element or + * a HTMLSelectElement. + * @return {p5.Element} + */ + + fn.createSelect = function (...args) { + p5._validateParameters('createSelect', args); + let self; + let arg = args[0]; + if (arg instanceof p5.Element && arg.elt instanceof HTMLSelectElement) { + // If given argument is p5.Element of select type + self = arg; + this.elt = arg.elt; + } else if (arg instanceof HTMLSelectElement) { + self = addElement(arg, this); + this.elt = arg; + } else { + const elt = document.createElement('select'); + if (arg && typeof arg === 'boolean') { + elt.setAttribute('multiple', 'true'); + } + self = addElement(elt, this); + this.elt = elt; + } + self.option = function (name, value) { + let index; + + // if no name is passed, return + if (name === undefined) { + return; } - // set diff for cnv vs normal div - if (this.elt instanceof HTMLCanvasElement) { - const j = {}; - const k = this.elt.getContext('2d'); - let prop; - for (prop in k) { - j[prop] = k[prop]; + //see if there is already an option with this name + for (let i = 0; i < this.elt.length; i += 1) { + if (this.elt[i].textContent === name) { + index = i; + break; } - this.elt.setAttribute('width', aW * this._pInst._renderer._pixelDensity); - this.elt.setAttribute('height', aH * this._pInst._renderer._pixelDensity); - this.elt.style.width = aW + 'px'; - this.elt.style.height = aH + 'px'; - this._pInst.scale(this._pInst._renderer._pixelDensity, this._pInst._renderer._pixelDensity); - for (prop in j) { - this.elt.getContext('2d')[prop] = j[prop]; + } + //if there is an option with this name we will modify it + if (index !== undefined) { + //if the user passed in false then delete that option + if (value === false) { + this.elt.remove(index); + } else { + // Update the option at index with the value + this.elt[index].value = value; } } else { - this.elt.style.width = aW + 'px'; - this.elt.style.height = aH + 'px'; - this.elt.width = aW; - this.elt.height = aH; + //if it doesn't exist create it + const opt = document.createElement('option'); + opt.textContent = name; + opt.value = value === undefined ? name : value; + this.elt.appendChild(opt); + this._pInst._elements.push(opt); } - this.width = aW; - this.height = aH; - if (this._pInst && this._pInst._curElement) { - // main canvas associated with p5 instance - if (this._pInst._curElement.elt === this.elt) { - this._pInst._renderer.width = aW; - this._pInst._renderer.height = aH; + }; + + self.selected = function (value) { + // Update selected status of option + if (value !== undefined) { + for (let i = 0; i < this.elt.length; i += 1) { + if (this.elt[i].value.toString() === value.toString()) { + this.elt.selectedIndex = i; + } + } + return this; + } else { + if (this.elt.getAttribute('multiple')) { + let arr = []; + for (const selectedOption of this.elt.selectedOptions) { + arr.push(selectedOption.value); + } + return arr; + } else { + return this.elt.value; } } - } - return this; - } -}; + }; -/** - * Removes the element, stops all audio/video streams, and removes all - * callback functions. - * - * @method remove - * - * @example - *
- * - * let p; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a paragraph element. - * p = createP('p5*js'); - * p.position(10, 10); - * - * describe('The text "p5*js" written at the center of a gray square. '); - * } - * - * // Remove the paragraph when the user double-clicks. - * function doubleClicked() { - * p.remove(); - * } - * - *
- */ -p5.Element.prototype.remove = function () { - // stop all audios/videos and detach all devices like microphone/camera etc - // used as input/output for audios/videos. - if (this instanceof p5.MediaElement) { - this.stop(); - const sources = this.elt.srcObject; - if (sources !== null) { - const tracks = sources.getTracks(); - tracks.forEach(track => { - track.stop(); - }); + self.disable = function (value) { + if (typeof value === 'string') { + for (let i = 0; i < this.elt.length; i++) { + if (this.elt[i].value.toString() === value) { + this.elt[i].disabled = true; + this.elt[i].selected = false; + } + } + } else { + this.elt.disabled = true; + } + return this; + }; + + self.enable = function (value) { + if (typeof value === 'string') { + for (let i = 0; i < this.elt.length; i++) { + if (this.elt[i].value.toString() === value) { + this.elt[i].disabled = false; + this.elt[i].selected = false; + } + } + } else { + this.elt.disabled = false; + for (let i = 0; i < this.elt.length; i++) { + this.elt[i].disabled = false; + this.elt[i].selected = false; + } + } + return this; + }; + + return self; + }; + + /** + * Creates a radio button element. + * + * The parameter is optional. If a string is passed, as in + * `let myRadio = createSelect('food')`, then each radio option will + * have `"food"` as its `name` parameter: `<input name="food"></input>`. + * If an existing `<div></div>` or `<span></span>` + * element is passed, as in `let myRadio = createSelect(container)`, it will + * become the radio button's parent element. + * + * Radio buttons extend the p5.Element class with a few + * helpful methods for managing options: + * - `myRadio.option(value, [label])` adds an option to the menu. The first paremeter, `value`, is a string that sets the option's value and label. The second parameter, `label`, is optional. If provided, it sets the label displayed for the `value`. If an option with `value` already exists, its label is changed and its value is returned. + * - `myRadio.value()` returns the currently-selected option's value. + * - `myRadio.selected()` returns the currently-selected option. + * - `myRadio.selected(value)` selects the given option and returns it as an `HTMLInputElement`. + * - `myRadio.disable(shouldDisable)` enables the entire radio button if `true` is passed and disables it if `false` is passed. + * + * @method createRadio + * @param {Object} [containerElement] container HTML Element, either a `<div></div>` + * or `<span></span>`. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * let myRadio; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a radio button element and place it + * // in the top-left corner. + * myRadio = createRadio(); + * myRadio.position(0, 0); + * myRadio.size(60); + * + * // Add a few color options. + * myRadio.option('red'); + * myRadio.option('yellow'); + * myRadio.option('blue'); + * + * // Choose a default option. + * myRadio.selected('yellow'); + * + * describe('A yellow square with three color options listed, "red", "yellow", and "blue". The square changes color when the user selects a new option.'); + * } + * + * function draw() { + * // Set the background color using the radio button. + * let g = myRadio.value(); + * background(g); + * } + * + *
+ * + *
+ * + * let myRadio; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a radio button element and place it + * // in the top-left corner. + * myRadio = createRadio(); + * myRadio.position(0, 0); + * myRadio.size(50); + * + * // Add a few color options. + * // Color values are labeled with + * // emotions they evoke. + * myRadio.option('red', 'love'); + * myRadio.option('yellow', 'joy'); + * myRadio.option('blue', 'trust'); + * + * // Choose a default option. + * myRadio.selected('yellow'); + * + * describe('A yellow square with three options listed, "love", "joy", and "trust". The square changes color when the user selects a new option.'); + * } + * + * function draw() { + * // Set the background color using the radio button. + * let c = myRadio.value(); + * background(c); + * } + * + *
+ * + *
+ * + * let myRadio; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a radio button element and place it + * // in the top-left corner. + * myRadio = createRadio(); + * myRadio.position(0, 0); + * myRadio.size(50); + * + * // Add a few color options. + * myRadio.option('red'); + * myRadio.option('yellow'); + * myRadio.option('blue'); + * + * // Choose a default option. + * myRadio.selected('yellow'); + * + * // Create a button and place it beneath the canvas. + * let btn = createButton('disable'); + * btn.position(0, 100); + * + * // Call disableRadio() when btn is pressed. + * btn.mousePressed(disableRadio); + * + * describe('A yellow square with three options listed, "red", "yellow", and "blue". The square changes color when the user selects a new option. A "disable" button beneath the canvas disables the color options when pressed.'); + * } + * + * function draw() { + * // Set the background color using the radio button. + * let c = myRadio.value(); + * background(c); + * } + * + * // Disable myRadio. + * function disableRadio() { + * myRadio.disable(true); + * } + * + *
+ */ + /** + * @method createRadio + * @param {String} [name] name parameter assigned to each option's `<input></input>` element. + * @return {p5.Element} new p5.Element object. + */ + /** + * @method createRadio + * @return {p5.Element} new p5.Element object. + */ + fn.createRadio = function (...args) { + // Creates a div, adds each option as an individual input inside it. + // If already given with a containerEl, will search for all input[radio] + // it, create a p5.Element out of it, add options to it and return the p5.Element. + + let self; + let radioElement; + let name; + const arg0 = args[0]; + if ( + arg0 instanceof p5.Element && + (arg0.elt instanceof HTMLDivElement || arg0.elt instanceof HTMLSpanElement) + ) { + // If given argument is p5.Element of div/span type + self = arg0; + this.elt = arg0.elt; + } else if ( + // If existing radio Element is provided as argument 0 + arg0 instanceof HTMLDivElement || + arg0 instanceof HTMLSpanElement + ) { + self = addElement(arg0, this); + this.elt = arg0; + radioElement = arg0; + if (typeof args[1] === 'string') name = args[1]; + } else { + if (typeof arg0 === 'string') name = arg0; + radioElement = document.createElement('div'); + self = addElement(radioElement, this); + this.elt = radioElement; } - } + self._name = name || 'radioOption'; + + // setup member functions + const isRadioInput = el => + el instanceof HTMLInputElement && el.type === 'radio'; + const isLabelElement = el => el instanceof HTMLLabelElement; + const isSpanElement = el => el instanceof HTMLSpanElement; + + self._getOptionsArray = function () { + return Array.from(this.elt.children) + .filter( + el => + isRadioInput(el) || + (isLabelElement(el) && isRadioInput(el.firstElementChild)) + ) + .map(el => (isRadioInput(el) ? el : el.firstElementChild)); + }; - // delete the reference in this._pInst._elements - const index = this._pInst._elements.indexOf(this); - if (index !== -1) { - this._pInst._elements.splice(index, 1); - } + self.option = function (value, label) { + // return an option with this value, create if not exists. + let optionEl; + for (const option of self._getOptionsArray()) { + if (option.value === value) { + optionEl = option; + break; + } + } - // deregister events - for (let ev in this._events) { - this.elt.removeEventListener(ev, this._events[ev]); - } - if (this.elt && this.elt.parentNode) { - this.elt.parentNode.removeChild(this.elt); - } -}; + // Create a new option, add it to radioElement and return it. + if (optionEl === undefined) { + optionEl = document.createElement('input'); + optionEl.setAttribute('type', 'radio'); + optionEl.setAttribute('value', value); + } + optionEl.setAttribute('name', self._name); -/** - * Calls a function when the user drops a file on the element. - * - * The first parameter, `callback`, is a function to call once the file loads. - * The callback function should have one parameter, `file`, that's a - * p5.File object. If the user drops multiple files on - * the element, `callback`, is called once for each file. - * - * The second parameter, `fxn`, is a function to call when the browser detects - * one or more dropped files. The callback function should have one - * parameter, `event`, that's a - * DragEvent. - * - * @method drop - * @param {Function} callback called when a file loads. Called once for each file dropped. - * @param {Function} [fxn] called once when any files are dropped. - * @chainable - * - * @example - *
- * - * // Drop an image on the canvas to view - * // this example. - * let img; - * - * function setup() { - * let c = createCanvas(100, 100); - * - * background(200); - * - * // Call handleFile() when a file that's dropped on the canvas has loaded. - * c.drop(handleFile); - * - * describe('A gray square. When the user drops an image on the square, it is displayed.'); - * } - * - * // Remove the existing image and display the new one. - * function handleFile(file) { - * // Remove the current image, if any. - * if (img) { - * img.remove(); - * } - * - * // Create an element with the - * // dropped file. - * img = createImg(file.data, ''); - * img.hide(); - * - * // Draw the image. - * image(img, 0, 0, width, height); - * } - * - *
- * - *
- * - * // Drop an image on the canvas to view - * // this example. - * let img; - * let msg; - * - * function setup() { - * let c = createCanvas(100, 100); - * - * background(200); - * - * // Call functions when the user drops a file on the canvas - * // and when the file loads. - * c.drop(handleFile, handleDrop); - * - * describe('A gray square. When the user drops an image on the square, it is displayed. The id attribute of canvas element is also displayed.'); - * } - * - * // Display the image when it loads. - * function handleFile(file) { - * // Remove the current image, if any. - * if (img) { - * img.remove(); - * } - * - * // Create an img element with the dropped file. - * img = createImg(file.data, ''); - * img.hide(); - * - * // Draw the image. - * image(img, 0, 0, width, height); - * } - * - * // Display the file's name when it loads. - * function handleDrop(event) { - * // Remove current paragraph, if any. - * if (msg) { - * msg.remove(); - * } - * - * // Use event to get the drop target's id. - * let id = event.target.id; - * - * // Write the canvas' id beneath it. - * msg = createP(id); - * msg.position(0, 100); - * - * // Set the font color randomly for each drop. - * let c = random(['red', 'green', 'blue']); - * msg.style('color', c); - * msg.style('font-size', '12px'); - * } - * - *
- */ -p5.Element.prototype.drop = function (callback, fxn) { - // Is the file stuff supported? - if (window.File && window.FileReader && window.FileList && window.Blob) { - if (!this._dragDisabled) { - this._dragDisabled = true; - - const preventDefault = function (evt) { - evt.preventDefault(); - }; + // Check if label element exists, else create it + let labelElement; + if (!isLabelElement(optionEl.parentElement)) { + labelElement = document.createElement('label'); + labelElement.insertAdjacentElement('afterbegin', optionEl); + } else { + labelElement = optionEl.parentElement; + } + + // Check if span element exists, else create it + let spanElement; + if (!isSpanElement(labelElement.lastElementChild)) { + spanElement = document.createElement('span'); + optionEl.insertAdjacentElement('afterend', spanElement); + } else { + spanElement = labelElement.lastElementChild; + } + + // Set the innerHTML of span element as the label text + spanElement.innerHTML = label === undefined ? value : label; + + // Append the label element, which includes option element and + // span element to the radio container element + this.elt.appendChild(labelElement); + + return optionEl; + }; + + self.remove = function (value) { + for (const optionEl of self._getOptionsArray()) { + if (optionEl.value === value) { + if (isLabelElement(optionEl.parentElement)) { + // Remove parent label which also removes children elements + optionEl.parentElement.remove(); + } else { + // Remove the option input if parent label does not exist + optionEl.remove(); + } + return; + } + } + }; + + self.value = function () { + let result = ''; + for (const option of self._getOptionsArray()) { + if (option.checked) { + result = option.value; + break; + } + } + return result; + }; + + self.selected = function (value) { + let result = null; + if (value === undefined) { + for (const option of self._getOptionsArray()) { + if (option.checked) { + result = option; + break; + } + } + } else { + // forEach loop to uncheck all radio buttons before + // setting any one as checked. + self._getOptionsArray().forEach(option => { + option.checked = false; + option.removeAttribute('checked'); + }); + + for (const option of self._getOptionsArray()) { + if (option.value === value) { + option.setAttribute('checked', true); + option.checked = true; + result = option; + } + } + } + return result; + }; + + self.disable = function (shouldDisable = true) { + for (const radioInput of self._getOptionsArray()) { + radioInput.setAttribute('disabled', shouldDisable); + } + }; - // If you want to be able to drop you've got to turn off - // a lot of default behavior. - // avoid `attachListener` here, since it overrides other handlers. - this.elt.addEventListener('dragover', preventDefault); + return self; + }; - // If this is a drag area we need to turn off the default behavior - this.elt.addEventListener('dragleave', preventDefault); + /** + * Creates a color picker element. + * + * The parameter, `value`, is optional. If a color string or + * p5.Color object is passed, it will set the default + * color. + * + * Color pickers extend the p5.Element class with a + * couple of helpful methods for managing colors: + * - `myPicker.value()` returns the current color as a hex string in the format `'#rrggbb'`. + * - `myPicker.color()` returns the current color as a p5.Color object. + * + * @method createColorPicker + * @param {String|p5.Color} [value] default color as a CSS color string. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * let myPicker; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a color picker and set its position. + * myPicker = createColorPicker('deeppink'); + * myPicker.position(0, 100); + * + * describe('A pink square with a color picker beneath it. The square changes color when the user picks a new color.'); + * } + * + * function draw() { + * // Use the color picker to paint the background. + * let c = myPicker.color(); + * background(c); + * } + * + *
+ * + *
+ * + * let myPicker; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a color picker and set its position. + * myPicker = createColorPicker('deeppink'); + * myPicker.position(0, 100); + * + * describe('A number with the format "#rrggbb" is displayed on a pink canvas. The background color and number change when the user picks a new color.'); + * } + * + * function draw() { + * // Use the color picker to paint the background. + * let c = myPicker.value(); + * background(c); + * + * // Display the current color as a hex string. + * text(c, 25, 55); + * } + * + *
+ */ + fn.createColorPicker = function (value) { + p5._validateParameters('createColorPicker', arguments); + const elt = document.createElement('input'); + let self; + elt.type = 'color'; + if (value) { + if (value instanceof p5.Color) { + elt.value = value.toString('#rrggbb'); + } else { + fn._colorMode = 'rgb'; + fn._colorMaxes = { + rgb: [255, 255, 255, 255], + hsb: [360, 100, 100, 1], + hsl: [360, 100, 100, 1] + }; + elt.value = fn.color(value).toString('#rrggbb'); + } + } else { + elt.value = '#000000'; } - - // Deal with the files - p5.Element._attachListener( - 'drop', - function (evt) { - evt.preventDefault(); - // Call the second argument as a callback that receives the raw drop event - if (typeof fxn === 'function') { - fxn.call(this, evt); + self = addElement(elt, this); + // Method to return a p5.Color object for the given color. + self.color = function () { + if (value) { + if (value.mode) { + fn._colorMode = value.mode; } - // A FileList - const files = evt.dataTransfer.files; - - // Load each one and trigger the callback - for (const f of files) { - p5.File._load(f, callback); + if (value.maxes) { + fn._colorMaxes = value.maxes; } - }, - this - ); - } else { - console.log('The File APIs are not fully supported in this browser.'); - } - - return this; -}; + } + return fn.color(this.elt.value); + }; + return self; + }; -/** - * Makes the element draggable. - * - * The parameter, `elmnt`, is optional. If another - * p5.Element object is passed, as in - * `myElement.draggable(otherElement)`, the other element will become draggable. - * - * @method draggable - * @param {p5.Element} [elmnt] another p5.Element. - * @chainable - * - * @example - *
- * - * let stickyNote; - * let textInput; - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a div element and style it. - * stickyNote = createDiv('Note'); - * stickyNote.position(5, 5); - * stickyNote.size(80, 20); - * stickyNote.style('font-size', '16px'); - * stickyNote.style('font-family', 'Comic Sans MS'); - * stickyNote.style('background', 'orchid'); - * stickyNote.style('padding', '5px'); - * - * // Make the note draggable. - * stickyNote.draggable(); - * - * // Create a panel div and style it. - * let panel = createDiv(''); - * panel.position(5, 40); - * panel.size(80, 50); - * panel.style('background', 'orchid'); - * panel.style('font-size', '16px'); - * panel.style('padding', '5px'); - * panel.style('text-align', 'center'); - * - * // Make the panel draggable. - * panel.draggable(); - * - * // Create a text input and style it. - * textInput = createInput('Note'); - * textInput.size(70); - * - * // Add the input to the panel. - * textInput.parent(panel); - * - * // Call handleInput() when text is input. - * textInput.input(handleInput); - * - * describe( - * 'A gray square with two purple rectangles that move when dragged. The top rectangle displays the text that is typed into the bottom rectangle.' - * ); - * } - * - * // Update stickyNote's HTML when text is input. - * function handleInput() { - * stickyNote.html(textInput.value()); - * } - * - *
- */ -p5.Element.prototype.draggable = function (elmMove) { - let isTouch = 'ontouchstart' in window; - - let x = 0, - y = 0, - px = 0, - py = 0, - elmDrag, - dragMouseDownEvt = isTouch ? 'touchstart' : 'mousedown', - closeDragElementEvt = isTouch ? 'touchend' : 'mouseup', - elementDragEvt = isTouch ? 'touchmove' : 'mousemove'; - - if (elmMove === undefined) { - elmMove = this.elt; - elmDrag = elmMove; - } else if (elmMove !== this.elt && elmMove.elt !== this.elt) { - elmMove = elmMove.elt; - elmDrag = this.elt; - } + /** + * Creates a text `<input></input>` element. + * + * Call `myInput.size()` to set the length of the text box. + * + * The first parameter, `value`, is optional. It's a string that sets the + * input's default value. The input is blank by default. + * + * The second parameter, `type`, is also optional. It's a string that + * specifies the type of text being input. See MDN for a full + * list of options. + * The default is `'text'`. + * + * @method createInput + * @param {String} [value] default value of the input box. Defaults to an empty string `''`. + * @param {String} [type] type of input. Defaults to `'text'`. + * @return {p5.Element} new p5.Element object. + * + * @example + *
+ * + * let myInput; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create an input element and place it + * // beneath the canvas. + * myInput = createInput(); + * myInput.position(0, 100); + * + * describe('A gray square with a text box beneath it. The text in the square changes when the user types something new in the input bar.'); + * } + * + * function draw() { + * background(200); + * + * // Use the input to display a message. + * let msg = myInput.value(); + * text(msg, 25, 55); + * } + * + *
+ * + *
+ * + * let myInput; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create an input element and place it + * // beneath the canvas. Set its default + * // text to "hello!". + * myInput = createInput('hello!'); + * myInput.position(0, 100); + * + * describe('The text "hello!" written at the center of a gray square. A text box beneath the square also says "hello!". The text in the square changes when the user types something new in the input bar.'); + * } + * + * function draw() { + * background(200); + * + * // Use the input to display a message. + * let msg = myInput.value(); + * text(msg, 25, 55); + * } + * + *
+ */ + /** + * @method createInput + * @param {String} [value] + * @return {p5.Element} + */ + fn.createInput = function (value = '', type = 'text') { + p5._validateParameters('createInput', arguments); + let elt = document.createElement('input'); + elt.setAttribute('value', value); + elt.setAttribute('type', type); + return addElement(elt, this); + }; - elmDrag.addEventListener(dragMouseDownEvt, dragMouseDown, false); - elmDrag.style.cursor = 'move'; + /** + * Creates an `<input></input>` element of type `'file'`. + * + * `createFileInput()` allows users to select local files for use in a sketch. + * It returns a p5.File object. + * + * The first parameter, `callback`, is a function that's called when the file + * loads. The callback function should have one parameter, `file`, that's a + * p5.File object. + * + * The second parameter, `multiple`, is optional. It's a boolean value that + * allows loading multiple files if set to `true`. If `true`, `callback` + * will be called once per file. + * + * @method createFileInput + * @param {Function} callback function to call once the file loads. + * @param {Boolean} [multiple] allow multiple files to be selected. + * @return {p5.File} new p5.File object. + * + * @example + *
+ * + * // Use the file input to select an image to + * // load and display. + * let input; + * let img; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a file input and place it beneath + * // the canvas. + * input = createFileInput(handleImage); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user selects an image file to load, it is displayed on the square.'); + * } + * + * function draw() { + * background(200); + * + * // Draw the image if loaded. + * if (img) { + * image(img, 0, 0, width, height); + * } + * } + * + * // Create an image if the file is an image. + * function handleImage(file) { + * if (file.type === 'image') { + * img = createImg(file.data, ''); + * img.hide(); + * } else { + * img = null; + * } + * } + * + *
+ * + *
+ * + * // Use the file input to select multiple images + * // to load and display. + * let input; + * let images = []; + * + * function setup() { + * // Create a file input and place it beneath + * // the canvas. Allow it to load multiple files. + * input = createFileInput(handleImage, true); + * input.position(0, 100); + * } + * + * function draw() { + * background(200); + * + * // Draw the images if loaded. Each image + * // is drawn 20 pixels lower than the + * // previous image. + * for (let i = 0; i < images.length; i += 1) { + * // Calculate the y-coordinate. + * let y = i * 20; + * + * // Draw the image. + * image(img, 0, y, 100, 100); + * } + * + * describe('A gray square with a file input beneath it. If the user selects multiple image files to load, they are displayed on the square.'); + * } + * + * // Create an image if the file is an image, + * // then add it to the images array. + * function handleImage(file) { + * if (file.type === 'image') { + * let img = createImg(file.data, ''); + * img.hide(); + * images.push(img); + * } + * } + * + *
+ */ + fn.createFileInput = function (callback, multiple = false) { + p5._validateParameters('createFileInput', arguments); - function dragMouseDown(e) { - e = e || window.event; + const handleFileSelect = function (event) { + for (const file of event.target.files) { + p5.File._load(file, callback); + } + }; - if (isTouch) { - const touches = e.changedTouches; - px = parseInt(touches[0].clientX); - py = parseInt(touches[0].clientY); - } else { - px = parseInt(e.clientX); - py = parseInt(e.clientY); + // If File API's are not supported, throw Error + if (!(window.File && window.FileReader && window.FileList && window.Blob)) { + console.log( + 'The File APIs are not fully supported in this browser. Cannot create element.' + ); + return; } - document.addEventListener(closeDragElementEvt, closeDragElement, false); - document.addEventListener(elementDragEvt, elementDrag, false); - return false; - } + const fileInput = document.createElement('input'); + fileInput.setAttribute('type', 'file'); + if (multiple) fileInput.setAttribute('multiple', true); + fileInput.addEventListener('change', handleFileSelect, false); + return addElement(fileInput, this); + }; - function elementDrag(e) { - e = e || window.event; + /** VIDEO STUFF **/ - if (isTouch) { - const touches = e.changedTouches; - x = px - parseInt(touches[0].clientX); - y = py - parseInt(touches[0].clientY); - px = parseInt(touches[0].clientX); - py = parseInt(touches[0].clientY); - } else { - x = px - parseInt(e.clientX); - y = py - parseInt(e.clientY); - px = parseInt(e.clientX); - py = parseInt(e.clientY); + // 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); } - elmMove.style.left = elmMove.offsetLeft - x + 'px'; - elmMove.style.top = elmMove.offsetTop - y + 'px'; - } + // If callback is provided, attach to element + if (typeof callback === 'function') { + const callbackHandler = () => { + callback(); + elt.removeEventListener('canplaythrough', callbackHandler); + }; + elt.addEventListener('canplaythrough', callbackHandler); + } - function closeDragElement() { - document.removeEventListener(closeDragElementEvt, closeDragElement, false); - document.removeEventListener(elementDragEvt, elementDrag, false); - } + const mediaEl = addElement(elt, pInst, true); + mediaEl.loadedmetadata = false; - return this; -}; + // 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; + }); -/*** SCHEDULE EVENTS ***/ - -// Cue inspired by JavaScript setTimeout, and the -// Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org -// eslint-disable-next-line no-unused-vars -class Cue { - constructor(callback, time, id, val) { - this.callback = callback; - this.time = time; - this.id = id; - this.val = val; + return mediaEl; } -} -// ============================================================================= -// p5.MediaElement additions -// ============================================================================= + /** + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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); + }; -/** - * 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 = class MediaElement extends p5.Element { - constructor(elt, pInst) { - super(elt, pInst); + /** AUDIO STUFF **/ - const self = this; - this.elt.crossOrigin = 'anonymous'; + /** + * Creates a hidden `<audio>` element for simple audio playback. + * + * `createAudio()` returns a new + * p5.MediaElement object. + * + * The first parameter, `src`, is the path the video. If a single string is + * passed, as in `'assets/video.mp4'`, a single video is loaded. An array + * of strings can be used to load the same video in different formats. For + * example, `['assets/video.mp4', 'assets/video.ogv', 'assets/video.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 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); + }; - this._prevTime = 0; - this._cueIDCounter = 0; - this._cues = []; - this.pixels = []; - this._pixelsState = this; - this._pixelDensity = 1; - this._modified = false; + /** CAMERA STUFF **/ - // 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; + fn.VIDEO = 'video'; - 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; + 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') + ); } - }); - // private _onended callback, set by the method: onended(callback) - self._onended = function () { }; - self.elt.onended = function () { - self._onended(self); + // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise + return new Promise(function (resolve, reject) { + getUserMedia.call(navigator, constraints, resolve, reject); + }); }; } - /** - * Plays audio or video from a media element. - * - * @chainable + * 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 - *
+ *
* - * let beat; + * function setup() { + * noCanvas(); + * + * // Create the video capture. + * createCapture(VIDEO); + * + * describe('A video stream from the webcam.'); + * } + * + *
+ * + *
+ * + * let capture; * * function setup() { * createCanvas(100, 100); * - * background(200); + * // Create the video capture and hide the element. + * capture = createCapture(VIDEO); + * capture.hide(); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * describe('A video stream from the webcam with inverted colors.'); + * } * - * // Display a message. - * text('Click to play', 50, 50); + * function draw() { + * // Draw the video capture within the canvas. + * image(capture, 0, 0, width, width * capture.height / capture.width); * - * // Create a p5.MediaElement using createAudio(). - * beat = createAudio('assets/beat.mp3'); + * // Invert the colors in the stream. + * filter(INVERT); + * } + * + *
+ *
+ * + * let capture; * - * describe('The text "Click to play" written in black on a gray background. A beat plays when the user clicks the square.'); + * 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.'); * } * - * // Play the beat when the user presses the mouse. - * function mousePressed() { - * beat.play(); + * + *
+ * + *
+ * + * 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.'); * } * *
*/ - play() { - if (this.elt.currentTime === this.elt.duration) { - this.elt.currentTime = 0; + 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 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(); + + 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; + } } - 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); - } + + 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 { - // any other kind of error - console.error('Media play method encountered an unexpected error', e); + domElement.src = window.URL.createObjectURL(stream); } - }); - } - return this; - } + } + 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; + }; + /** - * Stops a media element and sets its current time to 0. + * Creates a new p5.Element object. * - * Calling `media.play()` will restart playing audio/video from the beginning. + * The first parameter, `tag`, is a string an HTML tag such as `'h5'`. * - * @chainable + * The second parameter, `content`, is optional. It's a string that sets the + * HTML content to insert into the new element. New elements have no content + * by default. + * + * @method createElement + * @param {String} tag tag for the new element. + * @param {String} [content] HTML content to insert into the element. + * @return {p5.Element} new p5.Element object. * * @example *
* - * let beat; - * let isStopped = true; - * * function setup() { * createCanvas(100, 100); * - * // Create a p5.MediaElement using createAudio(). - * beat = createAudio('assets/beat.mp3'); + * background(200); * - * describe('The text "Click to start" written in black on a gray background. The beat starts or stops when the user presses the mouse.'); + * // Create an h5 element with nothing in it. + * createElement('h5'); + * + * describe('A gray square.'); * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); * - * function draw() { * background(200); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * // Create an h5 element with the content "p5*js". + * let h5 = createElement('h5', 'p5*js'); * - * // Display different instructions based on playback. - * if (isStopped === true) { - * text('Click to start', 50, 50); - * } else { - * text('Click to stop', 50, 50); - * } - * } + * // Set the element's style and position. + * h5.style('color', 'deeppink'); + * h5.position(30, 15); * - * // 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; - * } + * describe('The text "p5*js" written in pink in the middle of a gray square.'); * } * *
*/ - stop() { - this.elt.pause(); - this.elt.currentTime = 0; - return this; - } + fn.createElement = function (tag, content) { + p5._validateParameters('createElement', arguments); + const elt = document.createElement(tag); + if (typeof content !== 'undefined') { + elt.innerHTML = content; + } + return addElement(elt, this); + }; + // ============================================================================= + // p5.Element additions + // ============================================================================= /** - * Pauses a media element. * - * Calling `media.play()` will resume playing audio/video from the moment it paused. + * Adds a class to the element. * + * @for p5.Element + * @method addClass + * @param {String} class name of class to add. * @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); + * // Create a div element. + * let div = createDiv('div'); * - * // Display different instructions based on playback. - * if (isPaused === true) { - * text('Click to play', 50, 50); - * } else { - * text('Click to pause', 50, 50); - * } - * } + * // Add a class to the div. + * div.addClass('myClass'); * - * // 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; - * } + * describe('A gray square.'); * } * *
*/ - pause() { - this.elt.pause(); + p5.Element.prototype.addClass = function (c) { + if (this.elt.className) { + if (!this.hasClass(c)) { + this.elt.className = this.elt.className + ' ' + c; + } + } else { + this.elt.className = c; + } return this; - } + }; /** - * Plays the audio/video repeatedly in a loop. + * Removes a class from the element. * + * @method removeClass + * @param {String} class name of class to remove. * @chainable * * @example - *
+ *
* - * let beat; - * let isLooping = false; + * // In this example, a class is set when the div is created + * // and removed when mouse is pressed. This could link up + * // with a CSS style rule to toggle style properties. + * + * let div; * * function setup() { * createCanvas(100, 100); * * background(200); * - * // Create a p5.MediaElement using createAudio(). - * beat = createAudio('assets/beat.mp3'); + * // Create a div element. + * div = createDiv('div'); * - * 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.'); + * // Add a class to the div. + * div.addClass('myClass'); + * + * describe('A gray square.'); * } * - * function draw() { + * // Remove 'myClass' from the div when the user presses the mouse. + * function mousePressed() { + * div.removeClass('myClass'); + * } + * + *
+ */ + p5.Element.prototype.removeClass = function (c) { + // Note: Removing a class that does not exist does NOT throw an error in classList.remove method + this.elt.classList.remove(c); + return this; + }; + + /** + * Checks if a class is already applied to element. + * + * @method hasClass + * @returns {boolean} a boolean value if element has specified class. + * @param c {String} name of class to check. + * + * @example + *
+ * + * let div; + * + * function setup() { + * createCanvas(100, 100); + * * background(200); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * // Create a div element. + * div = createDiv('div'); * - * // Display different instructions based on playback. - * if (isLooping === true) { - * text('Click to stop', 50, 50); - * } else { - * text('Click to loop', 50, 50); - * } + * // Add the class 'show' to the div. + * div.addClass('show'); + * + * describe('A gray square.'); * } * - * // Adjust playback when the user presses the mouse. - * function mousePressed() { - * if (isLooping === true) { - * // If the beat is looping, stop it. - * beat.stop(); - * isLooping = false; + * // Toggle the class 'show' when the mouse is pressed. + * function mousePressed() { + * if (div.hasClass('show')) { + * div.addClass('show'); * } else { - * // If the beat is stopped, loop it. - * beat.loop(); - * isLooping = true; + * div.removeClass('show'); * } * } * *
*/ - loop() { - this.elt.setAttribute('loop', true); - this.play(); - return this; - } + p5.Element.prototype.hasClass = function (c) { + return this.elt.classList.contains(c); + }; + /** - * Stops the audio/video from playing in a loop. - * - * The media will stop when it finishes playing. + * Toggles whether a class is applied to the element. * + * @method toggleClass + * @param c {String} class name to toggle. * @chainable * * @example - *
+ *
* - * let beat; - * let isPlaying = false; + * let div; * * 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); + * // Create a div element. + * div = createDiv('div'); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * // Add the 'show' class to the div. + * div.addClass('show'); * - * // Display different instructions based on playback. - * if (isPlaying === true) { - * text('Click to stop', 50, 50); - * } else { - * text('Click to play', 50, 50); - * } + * describe('A gray square.'); * } * - * // Adjust playback when the user presses the mouse. + * // Toggle the 'show' class when the mouse is pressed. * 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; - * } + * div.toggleClass('show'); * } * *
*/ - noLoop() { - this.elt.removeAttribute('loop'); + p5.Element.prototype.toggleClass = function (c) { + // classList also has a toggle() method, but we cannot use that yet as support is unclear. + // See https://github.com/processing/p5.js/issues/3631 + // this.elt.classList.toggle(c); + if (this.elt.classList.contains(c)) { + this.elt.classList.remove(c); + } else { + this.elt.classList.add(c); + } 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. + * Attaches the element as a child of another element. * - * 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. + * `myElement.child()` accepts either a string ID, DOM node, or + * p5.Element. For example, + * `myElement.child(otherElement)`. If no argument is provided, an array of + * children DOM nodes is returned. * - * @param {Boolean} [shouldAutoplay] whether the element should autoplay. - * @chainable + * @method child + * @returns {Node[]} an array of child nodes. * * @example - *
+ *
* - * let video; - * * function setup() { - * noCanvas(); + * createCanvas(100, 100); * - * // Call handleVideo() once the video loads. - * video = createVideo('assets/fingers.mov', handleVideo); + * background(200); * - * describe('A video of fingers walking on a treadmill.'); - * } + * // Create the div elements. + * let div0 = createDiv('Parent'); + * let div1 = createDiv('Child'); * - * // Set the video's size and play it. - * function handleVideo() { - * video.size(100, 100); - * video.autoplay(); + * // Make div1 the child of div0 + * // using the p5.Element. + * div0.child(div1); + * + * describe('A gray square with the words "Parent" and "Child" written beneath it.'); * } * *
* - *
+ *
* * function setup() { - * noCanvas(); + * createCanvas(100, 100); * - * // Load a video, but don't play it automatically. - * let video = createVideo('assets/fingers.mov', handleVideo); + * background(200); * - * // Play the video when the user clicks on it. - * video.mousePressed(handlePress); + * // Create the div elements. + * let div0 = createDiv('Parent'); + * let div1 = createDiv('Child'); * - * describe('An image of fingers on a treadmill. They start walking when the user double-clicks on them.'); + * // Give div1 an ID. + * div1.id('apples'); + * + * // Make div1 the child of div0 + * // using its ID. + * div0.child('apples'); + * + * describe('A gray square with the words "Parent" and "Child" written beneath it.'); * } * *
* - * // Set the video's size and playback mode. - * function handleVideo() { - * video.size(100, 100); - * video.autoplay(false); - * } + *
+ * + * // This example assumes there is a div already on the page + * // with id "myChildDiv". * - * // Play the video. - * function handleClick() { - * video.play(); + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create the div elements. + * let div0 = createDiv('Parent'); + * + * // Select the child element by its ID. + * let elt = document.getElementById('myChildDiv'); + * + * // Make div1 the child of div0 + * // using its HTMLElement object. + * div0.child(elt); + * + * describe('A gray square with the words "Parent" and "Child" written beneath it.'); * } + * + *
*/ - 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 - }); + /** + * @method child + * @param {String|p5.Element} [child] the ID, DOM node, or p5.Element + * to add to the current element + * @chainable + */ + p5.Element.prototype.child = function (childNode) { + if (typeof childNode === 'undefined') { + return this.elt.childNodes; + } + if (typeof childNode === 'string') { + if (childNode[0] === '#') { + childNode = childNode.substring(1); } + childNode = document.getElementById(childNode); + } else if (childNode instanceof p5.Element) { + childNode = childNode.elt; } + if (childNode instanceof HTMLElement) { + this.elt.appendChild(childNode); + } return this; - } + }; /** - * Sets the audio/video volume. + * Centers the element either vertically, horizontally, or both. * - * Calling `media.volume()` without an argument returns the current volume - * as a number in the range 0 (off) to 1 (maximum). + * `center()` will center the element relative to its parent or according to + * the page's body if the element has no parent. * - * 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. + * If no argument is passed, as in `myElement.center()` the element is aligned + * both vertically and horizontally. * - * @return {Number} current volume. + * @method center + * @param {String} [align] passing 'vertical', 'horizontal' aligns element accordingly + * @chainable * * @example *
* - * let dragon; + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create the div element and style it. + * let div = createDiv(''); + * div.size(10, 10); + * div.style('background-color', 'orange'); * + * // Center the div relative to the page's body. + * div.center(); + * + * describe('A gray square and an orange rectangle. The rectangle is at the center of the page.'); + * } + * + *
+ */ + p5.Element.prototype.center = function (align) { + const style = this.elt.style.display; + const hidden = this.elt.style.display === 'none'; + const parentHidden = this.parent().style.display === 'none'; + const pos = { x: this.elt.offsetLeft, y: this.elt.offsetTop }; + + if (hidden) this.show(); + if (parentHidden) this.parent().show(); + this.elt.style.display = 'block'; + + this.position(0, 0); + const wOffset = Math.abs(this.parent().offsetWidth - this.elt.offsetWidth); + const hOffset = Math.abs(this.parent().offsetHeight - this.elt.offsetHeight); + + if (align === 'both' || align === undefined) { + this.position( + wOffset / 2 + this.parent().offsetLeft, + hOffset / 2 + this.parent().offsetTop + ); + } else if (align === 'horizontal') { + this.position(wOffset / 2 + this.parent().offsetLeft, pos.y); + } else if (align === 'vertical') { + this.position(pos.x, hOffset / 2 + this.parent().offsetTop); + } + + this.style('display', style); + if (hidden) this.hide(); + if (parentHidden) this.parent().hide(); + + return this; + }; + + /** + * Sets the inner HTML of the element, replacing any existing HTML. + * + * The second parameter, `append`, is optional. If `true` is passed, as in + * `myElement.html('hi', true)`, the HTML is appended instead of replacing + * existing HTML. + * + * If no arguments are passed, as in `myElement.html()`, the element's inner + * HTML is returned. + * + * @for p5.Element + * @method html + * @returns {String} the inner HTML of the element + * + * @example + *
+ * * function setup() { * createCanvas(100, 100); * - * // Create a p5.MediaElement using createAudio(). - * dragon = createAudio('assets/lucky_dragons.mp3'); + * // Create the div element and set its size. + * let div = createDiv(''); + * div.size(100, 100); * - * // Show the default media controls. - * dragon.showControls(); + * // Set the inner HTML to "hi". + * div.html('hi'); * - * 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.'); + * describe('A gray square with the word "hi" written beneath it.'); * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); * - * function draw() { * background(200); * - * // Produce a number between 0 and 1. - * let n = 0.5 * sin(frameCount * 0.01) + 0.5; + * // Create the div element and set its size. + * let div = createDiv('Hello '); + * div.size(100, 100); * - * // Use n to set the volume. - * dragon.volume(n); + * // Append "World" to the div's HTML. + * div.html('World', true); * - * // Get the current volume and display it. - * let v = dragon.volume(); + * describe('A gray square with the text "Hello World" written beneath it.'); + * } + * + *
* - * // Round v to 1 decimal place for display. - * v = round(v, 1); + *
+ * + * function setup() { + * createCanvas(100, 100); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * background(200); * - * // Display the volume. - * text(`Volume: ${v}`, 50, 50); + * // Create the div element. + * let div = createDiv('Hello'); + * + * // Prints "Hello" to the console. + * print(div.html()); + * + * describe('A gray square with the word "Hello!" written beneath it.'); * } * *
*/ /** - * @method volume - * @param {Number} val volume between 0.0 and 1.0. + * @method html + * @param {String} [html] the HTML to be placed inside the element + * @param {Boolean} [append] whether to append HTML to existing * @chainable */ - volume(val) { - if (typeof val === 'undefined') { - return this.elt.volume; + p5.Element.prototype.html = function (...args) { + if (args.length === 0) { + return this.elt.innerHTML; + } else if (args[1]) { + this.elt.insertAdjacentHTML('beforeend', args[0]); + return this; } else { - this.elt.volume = val; + this.elt.innerHTML = args[0]; + return this; } - } + }; /** - * Sets the audio/video playback speed. + * Sets the element's position. * - * 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. + * The first two parameters, `x` and `y`, set the element's position relative + * to the top-left corner of the web page. * - * Calling `media.speed()` returns the current speed as a number. + * The third parameter, `positionType`, is optional. It sets the element's + * positioning scheme. + * `positionType` is a string that can be either `'static'`, `'fixed'`, + * `'relative'`, `'sticky'`, `'initial'`, or `'inherit'`. * - * Note: Not all browsers support backward playback. Even if they do, - * playback might not be smooth. + * If no arguments passed, as in `myElement.position()`, the method returns + * the element's position in an object, as in `{ x: 0, y: 0 }`. * - * @return {Number} current playback speed. + * @method position + * @returns {Object} object of form `{ x: 0, y: 0 }` containing the element's position. * * @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.'); - * } + * let cnv = createCanvas(100, 100); * - * 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); + * // Positions the canvas 50px to the right and 100px + * // below the top-left corner of the window. + * cnv.position(50, 100); * - * // Get the current speed and display it. - * let s = dragon.speed(); + * describe('A gray square that is 50 pixels to the right and 100 pixels down from the top-left corner of the web page.'); + * } + * + *
* - * // Round s to 1 decimal place for display. - * s = round(s, 1); + *
+ * + * function setup() { + * let cnv = createCanvas(100, 100); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * background(200); * - * // Display the speed. - * text(`Speed: ${s}`, 50, 50); + * // Positions the canvas at the top-left corner + * // of the window with a 'fixed' position type. + * cnv.position(0, 0, 'fixed'); + * + * describe('A gray square in the top-left corner of the web page.'); * } * + *
*/ /** - * @param {Number} speed speed multiplier for playback. + * @method position + * @param {Number} [x] x-position relative to top-left of window (optional) + * @param {Number} [y] y-position relative to top-left of window (optional) + * @param {String} [positionType] it can be static, fixed, relative, sticky, initial or inherit (optional) * @chainable */ - speed(val) { - if (typeof val === 'undefined') { - return this.presetPlaybackRate || this.elt.playbackRate; + p5.Element.prototype.position = function (...args) { + if (args.length === 0) { + return { x: this.elt.offsetLeft, y: this.elt.offsetTop }; } else { - if (this.loadedmetadata) { - this.elt.playbackRate = val; + let positionType = 'absolute'; + if ( + args[2] === 'static' || + args[2] === 'fixed' || + args[2] === 'relative' || + args[2] === 'sticky' || + args[2] === 'initial' || + args[2] === 'inherit' + ) { + positionType = args[2]; + } + this.elt.style.position = positionType; + this.elt.style.left = args[0] + 'px'; + this.elt.style.top = args[1] + 'px'; + this.x = args[0]; + this.y = args[1]; + return this; + } + }; + + /* Helper method called by p5.Element.style() */ + p5.Element.prototype._translate = function (...args) { + this.elt.style.position = 'absolute'; + // save out initial non-translate transform styling + let transform = ''; + if (this.elt.style.transform) { + transform = this.elt.style.transform.replace(/translate3d\(.*\)/g, ''); + transform = transform.replace(/translate[X-Z]?\(.*\)/g, ''); + } + if (args.length === 2) { + this.elt.style.transform = + 'translate(' + args[0] + 'px, ' + args[1] + 'px)'; + } else if (args.length > 2) { + this.elt.style.transform = + 'translate3d(' + + args[0] + + 'px,' + + args[1] + + 'px,' + + args[2] + + 'px)'; + if (args.length === 3) { + this.elt.parentElement.style.perspective = '1000px'; } else { - this.presetPlaybackRate = val; + this.elt.parentElement.style.perspective = args[3] + 'px'; } } - } + // add any extra transform styling back on end + this.elt.style.transform += transform; + return this; + }; + + /* Helper method called by p5.Element.style() */ + p5.Element.prototype._rotate = function (...args) { + // save out initial non-rotate transform styling + let transform = ''; + if (this.elt.style.transform) { + transform = this.elt.style.transform.replace(/rotate3d\(.*\)/g, ''); + transform = transform.replace(/rotate[X-Z]?\(.*\)/g, ''); + } + + if (args.length === 1) { + this.elt.style.transform = 'rotate(' + args[0] + 'deg)'; + } else if (args.length === 2) { + this.elt.style.transform = + 'rotate(' + args[0] + 'deg, ' + args[1] + 'deg)'; + } else if (args.length === 3) { + this.elt.style.transform = 'rotateX(' + args[0] + 'deg)'; + this.elt.style.transform += 'rotateY(' + args[1] + 'deg)'; + this.elt.style.transform += 'rotateZ(' + args[2] + 'deg)'; + } + // add remaining transform back on + this.elt.style.transform += transform; + return this; + }; /** - * 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. - * - * @return {Number} current time (in seconds). + * Applies a style to the element by adding a + * CSS declaration. + * + * The first parameter, `property`, is a string. If the name of a style + * property is passed, as in `myElement.style('color')`, the method returns + * the current value as a string or `null` if it hasn't been set. If a + * `property:style` string is passed, as in + * `myElement.style('color:deeppink')`, the method sets the style `property` + * to `value`. + * + * The second parameter, `value`, is optional. It sets the property's value. + * `value` can be a string, as in + * `myElement.style('color', 'deeppink')`, or a + * p5.Color object, as in + * `myElement.style('color', myColor)`. + * + * @method style + * @param {String} property style property to set. + * @returns {String} value of the property. * * @example *
* - * let dragon; - * * function setup() { * createCanvas(100, 100); * - * // Create a p5.MediaElement using createAudio(). - * dragon = createAudio('assets/lucky_dragons.mp3'); + * background(200); * - * // Show the default media controls. - * dragon.showControls(); + * // Create a paragraph element and set its font color to "deeppink". + * let p = createP('p5*js'); + * p.position(25, 20); + * p.style('color', 'deeppink'); * - * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.'); + * describe('The text p5*js written in pink on a gray background.'); * } + * + *
* - * function draw() { - * background(200); + *
+ * + * function setup() { + * createCanvas(100, 100); * - * // Get the current playback time. - * let s = dragon.time(); + * background(200); * - * // Round s to 1 decimal place for display. - * s = round(s, 1); + * // Create a p5.Color object. + * let c = color('deeppink'); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * // Create a paragraph element and set its font color using a p5.Color object. + * let p = createP('p5*js'); + * p.position(25, 20); + * p.style('color', c); * - * // Display the playback time. - * text(`${s} seconds`, 50, 50); + * describe('The text p5*js written in pink on a gray background.'); * } * *
* *
* - * 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(); + * background(200); * - * // Jump to 2 seconds to start. - * dragon.time(2); + * // Create a paragraph element and set its font color to "deeppink" + * // using property:value syntax. + * let p = createP('p5*js'); + * p.position(25, 20); + * p.style('color:deeppink'); * - * describe('The text "S seconds" on a gray square with media controls beneath it. The number "S" increases as the song plays.'); + * describe('The text p5*js written in pink on a gray background.'); * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); * - * function draw() { * background(200); * - * // Get the current playback time. - * let s = dragon.time(); + * // Create an empty paragraph element and set its font color to "deeppink". + * let p = createP(); + * p.position(5, 5); + * p.style('color', 'deeppink'); * - * // Round s to 1 decimal place for display. - * s = round(s, 1); + * // Get the element's color as an RGB color string. + * let c = p.style('color'); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * // Set the element's inner HTML using the RGB color string. + * p.html(c); * - * // Display the playback time. - * text(`${s} seconds`, 50, 50); + * describe('The text "rgb(255, 20, 147)" written in pink on a gray background.'); * } * *
*/ /** - * @param {Number} time time to jump to (in seconds). + * @method style + * @param {String} property + * @param {String|p5.Color} value value to assign to the property. + * @return {String} value of the property. * @chainable */ - time(val) { + p5.Element.prototype.style = function (prop, val) { + const self = this; + + if (val instanceof p5.Color) { + val = + 'rgba(' + + val.levels[0] + + ',' + + val.levels[1] + + ',' + + val.levels[2] + + ',' + + val.levels[3] / 255 + + ')'; + } + if (typeof val === 'undefined') { - return this.elt.currentTime; + if (prop.indexOf(':') === -1) { + // no value set, so assume requesting a value + let styles = window.getComputedStyle(self.elt); + let style = styles.getPropertyValue(prop); + return style; + } else { + // value set using `:` in a single line string + const attrs = prop.split(';'); + for (let i = 0; i < attrs.length; i++) { + const parts = attrs[i].split(':'); + if (parts[0] && parts[1]) { + this.elt.style[parts[0].trim()] = parts[1].trim(); + } + } + } } else { - this.elt.currentTime = val; - return this; + // input provided as key,val pair + this.elt.style[prop] = val; + if ( + prop === 'width' || + prop === 'height' || + prop === 'left' || + prop === 'top' + ) { + let styles = window.getComputedStyle(self.elt); + let styleVal = styles.getPropertyValue(prop); + let numVal = styleVal.replace(/[^\d.]/g, ''); + this[prop] = Math.round(parseFloat(numVal, 10)); + } } - } + return this; + }; /** - * Returns the audio/video's duration in seconds. + * Adds an + * attribute + * to the element. * - * @return {Number} duration (in seconds). + * This method is useful for advanced tasks. Most commonly-used attributes, + * such as `id`, can be set with their dedicated methods. For example, + * `nextButton.id('next')` sets an element's `id` attribute. Calling + * `nextButton.attribute('id', 'next')` has the same effect. + * + * The first parameter, `attr`, is the attribute's name as a string. Calling + * `myElement.attribute('align')` returns the attribute's current value as a + * string or `null` if it hasn't been set. + * + * The second parameter, `value`, is optional. It's a string used to set the + * attribute's value. For example, calling + * `myElement.attribute('align', 'center')` sets the element's horizontal + * alignment to `center`. + * + * @method attribute + * @return {String} value of the attribute. * * @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); + * // Create a container div element and place it at the top-left corner. + * let container = createDiv(); + * container.position(0, 0); + * + * // Create a paragraph element and place it within the container. + * // Set its horizontal alignment to "left". + * let p1 = createP('hi'); + * p1.parent(container); + * p1.attribute('align', 'left'); + * + * // Create a paragraph element and place it within the container. + * // Set its horizontal alignment to "center". + * let p2 = createP('hi'); + * p2.parent(container); + * p2.attribute('align', 'center'); + * + * // Create a paragraph element and place it within the container. + * // Set its horizontal alignment to "right". + * let p3 = createP('hi'); + * p3.parent(container); + * p3.attribute('align', 'right'); + * + * describe('A gray square with the text "hi" written on three separate lines, each placed further to the right.'); * } * - *
- */ - 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. + * @method attribute + * @param {String} attr attribute to set. + * @param {String} value value to assign to the attribute. + * @chainable + */ + p5.Element.prototype.attribute = function (attr, value) { + //handling for checkboxes and radios to ensure options get + //attributes not divs + if ( + this.elt.firstChild != null && + (this.elt.firstChild.type === 'checkbox' || + this.elt.firstChild.type === 'radio') + ) { + if (typeof value === 'undefined') { + return this.elt.firstChild.getAttribute(attr); + } else { + for (let i = 0; i < this.elt.childNodes.length; i++) { + this.elt.childNodes[i].setAttribute(attr, value); + } + } + } else if (typeof value === 'undefined') { + return this.elt.getAttribute(attr); + } else { + this.elt.setAttribute(attr, value); + return this; + } + }; + + /** + * Removes an attribute from the element. * - * Note: The function won't be called if the media is looping. + * The parameter `attr` is the attribute's name as a string. For example, + * calling `myElement.removeAttribute('align')` removes its `align` + * attribute if it's been set. * - * @param {Function} callback function to call when playback ends. - * The `p5.MediaElement` is passed as - * the argument. + * @method removeAttribute + * @param {String} attr attribute to remove. * @chainable * * @example *
* - * let beat; - * let isPlaying = false; - * let isDone = false; + * let p; * * function setup() { * createCanvas(100, 100); * - * // Create a p5.MediaElement using createAudio(). - * beat = createAudio('assets/beat.mp3'); + * background(200); * - * // Call handleEnd() when the beat finishes. - * beat.onended(handleEnd); + * // Create a paragraph element and place it in the center of the canvas. + * // Set its "align" attribute to "center". + * p = createP('hi'); + * p.position(0, 20); + * p.attribute('align', 'center'); * - * 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.'); + * describe('The text "hi" written in black at the center of a gray square. The text moves to the left edge when double-clicked.'); + * } + * + * // Remove the 'align' attribute when the user double-clicks the paragraph. + * function doubleClicked() { + * p.removeAttribute('align'); + * } + * + *
+ */ + p5.Element.prototype.removeAttribute = function (attr) { + if ( + this.elt.firstChild != null && + (this.elt.firstChild.type === 'checkbox' || + this.elt.firstChild.type === 'radio') + ) { + for (let i = 0; i < this.elt.childNodes.length; i++) { + this.elt.childNodes[i].removeAttribute(attr); + } + } + this.elt.removeAttribute(attr); + return this; + }; + + /** + * Returns or sets the element's value. + * + * Calling `myElement.value()` returns the element's current value. + * + * The parameter, `value`, is an optional number or string. If provided, + * as in `myElement.value(123)`, it's used to set the element's value. + * + * @method value + * @return {String|Number} value of the element. + * + * @example + *
+ * + * let input; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a text input and place it beneath the canvas. + * // Set its default value to "hello". + * input = createInput('hello'); + * input.position(0, 100); + * + * describe('The text from an input box is displayed on a gray square.'); * } * * function draw() { * background(200); * - * // Style the text. - * textAlign(CENTER); - * textSize(16); + * // Use the input's value to display a message. + * let msg = input.value(); + * text(msg, 0, 55); + * } + * + *
* - * // 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); - * } + *
+ * + * let input; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a text input and place it beneath the canvas. + * // Set its default value to "hello". + * input = createInput('hello'); + * input.position(0, 100); + * + * describe('The text from an input box is displayed on a gray square. The text resets to "hello" when the user double-clicks the square.'); * } * - * // Play the beat when the user presses the mouse. - * function mousePressed() { - * if (isPlaying === false) { - * isPlaying = true; - * beat.play(); - * } + * function draw() { + * background(200); + * + * // Use the input's value to display a message. + * let msg = input.value(); + * text(msg, 0, 55); * } * - * // Set isDone when playback ends. - * function handleEnd() { - * isDone = false; + * // Reset the input's value. + * function doubleClicked() { + * input.value('hello'); * } * *
*/ - onended(callback) { - this._onended = callback; - return this; - } - - /*** CONNECT TO WEB AUDIO API / p5.sound.js ***/ + /** + * @method value + * @param {String|Number} value + * @chainable + */ + p5.Element.prototype.value = function (...args) { + if (args.length > 0) { + this.elt.value = args[0]; + return this; + } else { + if (this.elt.type === 'range') { + return parseFloat(this.elt.value); + } else return this.elt.value; + } + }; /** - * Sends the element's audio to an output. + * Shows the current element. * - * The parameter, `audioNode`, can be an `AudioNode` or an object from the - * `p5.sound` library. + * @method show + * @chainable * - * If no element is provided, as in `myElement.connect()`, the element - * connects to the main output. All connections are removed by the - * `.disconnect()` method. + * @example + *
+ * + * let p; * - * Note: This method is meant to be used with the p5.sound.js addon library. + * function setup() { + * createCanvas(100, 100); * - * @param {AudioNode|Object} audioNode AudioNode from the Web Audio API, - * or an object from the p5.sound library + * background(200); + * + * // Create a paragraph element and hide it. + * p = createP('p5*js'); + * p.position(10, 10); + * p.hide(); + * + * describe('A gray square. The text "p5*js" appears when the user double-clicks the square.'); + * } + * + * // Show the paragraph when the user double-clicks. + * function doubleClicked() { + * p.show(); + * } + * + *
*/ - connect(obj) { - let audioContext, mainOutput; - - // if p5.sound exists, same audio context - if (typeof p5.prototype.getAudioContext === 'function') { - audioContext = p5.prototype.getAudioContext(); - mainOutput = p5.soundOut.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); - } - } + p5.Element.prototype.show = function () { + this.elt.style.display = 'block'; + return this; + }; /** - * Disconnect all Web Audio routing, including to the main output. + * Hides the current element. * - * This is useful if you want to re-route the output through audio effects, - * for example. + * @method hide + * @chainable + * + * @example + * let p; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a paragraph element. + * p = createP('p5*js'); + * p.position(10, 10); * + * describe('The text "p5*js" at the center of a gray square. The text disappears when the user double-clicks the square.'); + * } + * + * // Hide the paragraph when the user double-clicks. + * function doubleClicked() { + * p.hide(); + * } + *
+ *
*/ - disconnect() { - if (this.audioSourceNode) { - this.audioSourceNode.disconnect(); - } else { - throw 'nothing to disconnect'; - } - } - - /*** SHOW / HIDE CONTROLS ***/ + p5.Element.prototype.hide = function () { + this.elt.style.display = 'none'; + return this; + }; /** - * Show the default - * HTMLMediaElement - * controls. + * Sets the element's width and height. * - * Note: The controls vary between web browsers. + * Calling `myElement.size()` without an argument returns the element's size + * as an object with the properties `width` and `height`. For example, + * `{ width: 20, height: 10 }`. + * + * The first parameter, `width`, is optional. It's a number used to set the + * element's width. Calling `myElement.size(10)` + * + * The second parameter, 'height`, is also optional. It's a + * number used to set the element's height. For example, calling + * `myElement.size(20, 10)` sets the element's width to 20 pixels and height + * to 10 pixels. + * + * The constant `AUTO` can be used to adjust one dimension at a time while + * maintaining the aspect ratio, which is `width / height`. For example, + * consider an element that's 200 pixels wide and 100 pixels tall. Calling + * `myElement.size(20, AUTO)` sets the width to 20 pixels and height to 10 + * pixels. + * + * Note: In the case of elements that need to load data, such as images, wait + * to call `myElement.size()` until after the data loads. + * + * @method size + * @return {Object} width and height of the element in an object. * * @example *
@@ -5109,698 +4863,949 @@ p5.MediaElement = class MediaElement extends p5.Element { * function setup() { * createCanvas(100, 100); * - * background('cornflowerblue'); + * background(200); * - * // Style the text. - * textAlign(CENTER); - * textSize(50); + * // Create a pink div element and place it at the top-left corner. + * let div = createDiv(); + * div.position(10, 10); + * div.style('background-color', 'deeppink'); * - * // Display a dragon. - * text('🐉', 50, 50); + * // Set the div's width to 80 pixels and height to 20 pixels. + * div.size(80, 20); * - * // Create a p5.MediaElement using createAudio(). - * let dragon = createAudio('assets/lucky_dragons.mp3'); + * describe('A gray square with a pink rectangle near its top.'); + * } + * + *
* - * // Show the default media controls. - * dragon.showControls(); + *
+ * + * function setup() { + * createCanvas(100, 100); * - * 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.'); + * background(200); + * + * // Create a pink div element and place it at the top-left corner. + * let div = createDiv(); + * div.position(10, 10); + * div.style('background-color', 'deeppink'); + * + * // Set the div's width to 80 pixels and height to 40 pixels. + * div.size(80, 40); + * + * // Get the div's size as an object. + * let s = div.size(); + * + * // Display the div's dimensions. + * div.html(`${s.width} x ${s.height}`); + * + * describe('A gray square with a pink rectangle near its top. The text "80 x 40" is written within the rectangle.'); + * } + * + *
+ * + *
+ * + * let img1; + * let img2; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Load an image of an astronaut on the moon + * // and place it at the top-left of the canvas. + * img1 = createImg( + * 'assets/moonwalk.jpg', + * 'An astronaut walking on the moon', + * '' + * ); + * img1.position(0, 0); + * + * // Load an image of an astronaut on the moon + * // and place it at the top-left of the canvas. + * // Resize the image once it's loaded. + * img2 = createImg( + * 'assets/moonwalk.jpg', + * 'An astronaut walking on the moon', + * '', + * resizeImage + * ); + * img2.position(0, 0); + * + * describe('A gray square two copies of a space image at the top-left. The copy in front is smaller.'); + * } + * + * // Resize img2 and keep its aspect ratio. + * function resizeImage() { + * img2.size(50, AUTO); * } * *
*/ - showControls() { - // must set style for the element to show on the page - this.elt.style['text-align'] = 'inherit'; - this.elt.controls = true; - } + /** + * @method size + * @param {(Number|AUTO)} [w] width of the element, either AUTO, or a number. + * @param {(Number|AUTO)} [h] height of the element, either AUTO, or a number. + * @chainable + */ + p5.Element.prototype.size = function (w, h) { + if (arguments.length === 0) { + return { width: this.elt.offsetWidth, height: this.elt.offsetHeight }; + } else { + let aW = w; + let aH = h; + const AUTO = fn.AUTO; + if (aW !== AUTO || aH !== AUTO) { + if (aW === AUTO) { + aW = h * this.width / this.height; + } else if (aH === AUTO) { + aH = w * this.height / this.width; + } + // set diff for cnv vs normal div + if (this.elt instanceof HTMLCanvasElement) { + const j = {}; + const k = this.elt.getContext('2d'); + let prop; + for (prop in k) { + j[prop] = k[prop]; + } + this.elt.setAttribute('width', aW * this._pInst._renderer._pixelDensity); + this.elt.setAttribute('height', aH * this._pInst._renderer._pixelDensity); + this.elt.style.width = aW + 'px'; + this.elt.style.height = aH + 'px'; + this._pInst.scale(this._pInst._renderer._pixelDensity, this._pInst._renderer._pixelDensity); + for (prop in j) { + this.elt.getContext('2d')[prop] = j[prop]; + } + } else { + this.elt.style.width = aW + 'px'; + this.elt.style.height = aH + 'px'; + this.elt.width = aW; + this.elt.height = aH; + } + this.width = aW; + this.height = aH; + if (this._pInst && this._pInst._curElement) { + // main canvas associated with p5 instance + if (this._pInst._curElement.elt === this.elt) { + this._pInst._renderer.width = aW; + this._pInst._renderer.height = aH; + } + } + } + return this; + } + }; /** - * Hide the default - * HTMLMediaElement - * controls. + * Removes the element, stops all audio/video streams, and removes all + * callback functions. + * + * @method remove * * @example *
* - * let dragon; - * let isHidden = false; + * let p; * * 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); + * // Create a paragraph element. + * p = createP('p5*js'); + * p.position(10, 10); * - * // 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); - * } + * describe('The text "p5*js" written at the center of a gray square. '); * } * - * // Show/hide controls based on a double-click. + * // Remove the paragraph when the user double-clicks. * function doubleClicked() { - * if (isHidden === true) { - * dragon.showControls(); - * isHidden = false; - * } else { - * dragon.hideControls(); - * isHidden = true; - * } + * p.remove(); * } * *
*/ - 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); + p5.Element.prototype.remove = function () { + // stop all audios/videos and detach all devices like microphone/camera etc + // used as input/output for audios/videos. + if (this instanceof p5.MediaElement) { + this.stop(); + const sources = this.elt.srcObject; + if (sources !== null) { + const tracks = sources.getTracks(); + tracks.forEach(track => { + track.stop(); + }); + } } - 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); - } + // delete the reference in this._pInst._elements + const index = this._pInst._elements.indexOf(this); + if (index !== -1) { + this._pInst._elements.splice(index, 1); } - if (this._cues.length === 0) { - this.elt.ontimeupdate = null; + // deregister events + for (let ev in this._events) { + this.elt.removeEventListener(ev, this._events[ev]); } - } + if (this.elt && this.elt.parentNode) { + this.elt.parentNode.removeChild(this.elt); + } + }; /** - * 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; - } + * Calls a function when the user drops a file on the element. + * + * The first parameter, `callback`, is a function to call once the file loads. + * The callback function should have one parameter, `file`, that's a + * p5.File object. If the user drops multiple files on + * the element, `callback`, is called once for each file. + * + * The second parameter, `fxn`, is a function to call when the browser detects + * one or more dropped files. The callback function should have one + * parameter, `event`, that's a + * DragEvent. + * + * @method drop + * @param {Function} callback called when a file loads. Called once for each file dropped. + * @param {Function} [fxn] called once when any files are dropped. + * @chainable + * + * @example + *
+ * + * // Drop an image on the canvas to view + * // this example. + * let img; + * + * function setup() { + * let c = createCanvas(100, 100); + * + * background(200); + * + * // Call handleFile() when a file that's dropped on the canvas has loaded. + * c.drop(handleFile); + * + * describe('A gray square. When the user drops an image on the square, it is displayed.'); + * } + * + * // Remove the existing image and display the new one. + * function handleFile(file) { + * // Remove the current image, if any. + * if (img) { + * img.remove(); + * } + * + * // Create an element with the + * // dropped file. + * img = createImg(file.data, ''); + * img.hide(); + * + * // Draw the image. + * image(img, 0, 0, width, height); + * } + * + *
+ * + *
+ * + * // Drop an image on the canvas to view + * // this example. + * let img; + * let msg; + * + * function setup() { + * let c = createCanvas(100, 100); + * + * background(200); + * + * // Call functions when the user drops a file on the canvas + * // and when the file loads. + * c.drop(handleFile, handleDrop); + * + * describe('A gray square. When the user drops an image on the square, it is displayed. The id attribute of canvas element is also displayed.'); + * } + * + * // Display the image when it loads. + * function handleFile(file) { + * // Remove the current image, if any. + * if (img) { + * img.remove(); + * } + * + * // Create an img element with the dropped file. + * img = createImg(file.data, ''); + * img.hide(); + * + * // Draw the image. + * image(img, 0, 0, width, height); + * } + * + * // Display the file's name when it loads. + * function handleDrop(event) { + * // Remove current paragraph, if any. + * if (msg) { + * msg.remove(); + * } + * + * // Use event to get the drop target's id. + * let id = event.target.id; + * + * // Write the canvas' id beneath it. + * msg = createP(id); + * msg.position(0, 100); + * + * // Set the font color randomly for each drop. + * let c = random(['red', 'green', 'blue']); + * msg.style('color', c); + * msg.style('font-size', '12px'); + * } + * + *
+ */ + p5.Element.prototype.drop = function (callback, fxn) { + // Is the file stuff supported? + if (window.File && window.FileReader && window.FileList && window.Blob) { + if (!this._dragDisabled) { + this._dragDisabled = true; + + const preventDefault = function (evt) { + evt.preventDefault(); + }; + + // If you want to be able to drop you've got to turn off + // a lot of default behavior. + // avoid `attachListener` here, since it overrides other handlers. + this.elt.addEventListener('dragover', preventDefault); + + // If this is a drag area we need to turn off the default behavior + this.elt.addEventListener('dragleave', preventDefault); + } - // private method that checks for cues to be fired if events - // have been scheduled using addCue(callback, time). - _onTimeUpdate() { - const playbackTime = this.time(); + // Deal with the files + p5.Element._attachListener( + 'drop', + function (evt) { + evt.preventDefault(); + // Call the second argument as a callback that receives the raw drop event + if (typeof fxn === 'function') { + fxn.call(this, evt); + } + // A FileList + const files = evt.dataTransfer.files; - for (let i = 0; i < this._cues.length; i++) { - const callbackTime = this._cues[i].time; - const val = this._cues[i].val; + // Load each one and trigger the callback + for (const f of files) { + p5.File._load(f, callback); + } + }, + this + ); + } else { + console.log('The File APIs are not fully supported in this browser.'); + } - if (this._prevTime < callbackTime && callbackTime <= playbackTime) { - // pass the scheduled callbackTime as parameter to the callback - this._cues[i].callback(val); - } + return this; + }; + + /** + * Makes the element draggable. + * + * The parameter, `elmnt`, is optional. If another + * p5.Element object is passed, as in + * `myElement.draggable(otherElement)`, the other element will become draggable. + * + * @method draggable + * @param {p5.Element} [elmnt] another p5.Element. + * @chainable + * + * @example + *
+ * + * let stickyNote; + * let textInput; + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a div element and style it. + * stickyNote = createDiv('Note'); + * stickyNote.position(5, 5); + * stickyNote.size(80, 20); + * stickyNote.style('font-size', '16px'); + * stickyNote.style('font-family', 'Comic Sans MS'); + * stickyNote.style('background', 'orchid'); + * stickyNote.style('padding', '5px'); + * + * // Make the note draggable. + * stickyNote.draggable(); + * + * // Create a panel div and style it. + * let panel = createDiv(''); + * panel.position(5, 40); + * panel.size(80, 50); + * panel.style('background', 'orchid'); + * panel.style('font-size', '16px'); + * panel.style('padding', '5px'); + * panel.style('text-align', 'center'); + * + * // Make the panel draggable. + * panel.draggable(); + * + * // Create a text input and style it. + * textInput = createInput('Note'); + * textInput.size(70); + * + * // Add the input to the panel. + * textInput.parent(panel); + * + * // Call handleInput() when text is input. + * textInput.input(handleInput); + * + * describe( + * 'A gray square with two purple rectangles that move when dragged. The top rectangle displays the text that is typed into the bottom rectangle.' + * ); + * } + * + * // Update stickyNote's HTML when text is input. + * function handleInput() { + * stickyNote.html(textInput.value()); + * } + * + *
+ */ + p5.Element.prototype.draggable = function (elmMove) { + let isTouch = 'ontouchstart' in window; + + let x = 0, + y = 0, + px = 0, + py = 0, + elmDrag, + dragMouseDownEvt = isTouch ? 'touchstart' : 'mousedown', + closeDragElementEvt = isTouch ? 'touchend' : 'mouseup', + elementDragEvt = isTouch ? 'touchmove' : 'mousemove'; + + if (elmMove === undefined) { + elmMove = this.elt; + elmDrag = elmMove; + } else if (elmMove !== this.elt && elmMove.elt !== this.elt) { + elmMove = elmMove.elt; + elmDrag = this.elt; } - this._prevTime = playbackTime; - } -}; + elmDrag.addEventListener(dragMouseDownEvt, dragMouseDown, false); + elmDrag.style.cursor = 'move'; -/** - * 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); - * } - * - *
- */ + function dragMouseDown(e) { + e = e || window.event; + if (isTouch) { + const touches = e.changedTouches; + px = parseInt(touches[0].clientX); + py = parseInt(touches[0].clientY); + } else { + px = parseInt(e.clientX); + py = parseInt(e.clientY); + } -/** - * A class to describe a file. - * - * `p5.File` objects are used by - * myElement.drop() and - * created by - * createFileInput. - * - * @class p5.File - * @param {File} file wrapped file. - * - * @example - *
- * - * // Use the file input to load a - * // file and display its info. - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a file input and place it beneath the canvas. - * // Call displayInfo() when the file loads. - * let input = createFileInput(displayInfo); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.'); - * } - * - * // Display the p5.File's info once it loads. - * function displayInfo(file) { - * background(200); - * - * // Display the p5.File's name. - * text(file.name, 10, 10, 80, 40); - * - * // Display the p5.File's type and subtype. - * text(`${file.type}/${file.subtype}`, 10, 70); - * - * // Display the p5.File's size in bytes. - * text(file.size, 10, 90); - * } - * - *
- * - *
- * - * // Use the file input to select an image to - * // load and display. - * let img; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a file input and place it beneath the canvas. - * // Call handleImage() when the file image loads. - * let input = createFileInput(handleImage); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user selects an image file to load, it is displayed on the square.'); - * } - * - * function draw() { - * background(200); - * - * // Draw the image if it's ready. - * if (img) { - * image(img, 0, 0, width, height); - * } - * } - * - * // Use the p5.File's data once it loads. - * function handleImage(file) { - * // Check the p5.File's type. - * if (file.type === 'image') { - * // Create an image using using the p5.File's data. - * img = createImg(file.data, ''); - * - * // Hide the image element so it doesn't appear twice. - * img.hide(); - * } else { - * img = null; - * } - * } - * - *
- */ -p5.File = class File { - constructor(file, pInst) { - this.file = file; - - this._pInst = pInst; - - // Splitting out the file type into two components - // This makes determining if image or text etc simpler - const typeList = file.type.split('/'); - this.type = typeList[0]; - this.subtype = typeList[1]; - this.name = file.name; - this.size = file.size; - this.data = undefined; - } + document.addEventListener(closeDragElementEvt, closeDragElement, false); + document.addEventListener(elementDragEvt, elementDrag, false); + return false; + } + function elementDrag(e) { + e = e || window.event; - static _createLoader(theFile, callback) { - const reader = new FileReader(); - reader.onload = function (e) { - const p5file = new p5.File(theFile); - if (p5file.file.type === 'application/json') { - // Parse JSON and store the result in data - p5file.data = JSON.parse(e.target.result); - } else if (p5file.file.type === 'text/xml') { - // Parse XML, wrap it in p5.XML and store the result in data - const parser = new DOMParser(); - const xml = parser.parseFromString(e.target.result, 'text/xml'); - p5file.data = new p5.XML(xml.documentElement); + if (isTouch) { + const touches = e.changedTouches; + x = px - parseInt(touches[0].clientX); + y = py - parseInt(touches[0].clientY); + px = parseInt(touches[0].clientX); + py = parseInt(touches[0].clientY); } else { - p5file.data = e.target.result; + x = px - parseInt(e.clientX); + y = py - parseInt(e.clientY); + px = parseInt(e.clientX); + py = parseInt(e.clientY); } - callback(p5file); - }; - return reader; - } - static _load(f, callback) { - // Text or data? - // This should likely be improved - if (/^text\//.test(f.type) || f.type === 'application/json') { - p5.File._createLoader(f, callback).readAsText(f); - } else if (!/^(video|audio)\//.test(f.type)) { - p5.File._createLoader(f, callback).readAsDataURL(f); - } else { - const file = new p5.File(f); - file.data = URL.createObjectURL(f); - callback(file); + elmMove.style.left = elmMove.offsetLeft - x + 'px'; + elmMove.style.top = elmMove.offsetTop - y + 'px'; + } + + function closeDragElement() { + document.removeEventListener(closeDragElementEvt, closeDragElement, false); + document.removeEventListener(elementDragEvt, elementDrag, false); + } + + return this; + }; + + /*** SCHEDULE EVENTS ***/ + + // Cue inspired by JavaScript setTimeout, and the + // Tone.js Transport Timeline Event, MIT License Yotam Mann 2015 tonejs.org + // eslint-disable-next-line no-unused-vars + class Cue { + constructor(callback, time, id, val) { + this.callback = callback; + this.time = time; + this.id = id; + this.val = val; } } -}; -/** - * Underlying - * File - * object. All `File` properties and methods are accessible. - * - * @for p5.File - * @property file - * @example - *
- * - * // Use the file input to load a - * // file and display its info. - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a file input and place it beneath the canvas. - * // Call displayInfo() when the file loads. - * let input = createFileInput(displayInfo); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.'); - * } - * - * // Use the p5.File once it loads. - * function displayInfo(file) { - * background(200); - * - * // Display the p5.File's name. - * text(file.name, 10, 10, 80, 40); - * - * // Display the p5.File's type and subtype. - * text(`${file.type}/${file.subtype}`, 10, 70); - * - * // Display the p5.File's size in bytes. - * text(file.size, 10, 90); - * } - * - *
- */ + // ============================================================================= + // 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; + + /** + * 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); + * } + * + *
+ */ + + + /** + * A class to describe a file. + * + * `p5.File` objects are used by + * myElement.drop() and + * created by + * createFileInput. + * + * @class p5.File + * @param {File} file wrapped file. + * + * @example + *
+ * + * // Use the file input to load a + * // file and display its info. + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a file input and place it beneath the canvas. + * // Call displayInfo() when the file loads. + * let input = createFileInput(displayInfo); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.'); + * } + * + * // Display the p5.File's info once it loads. + * function displayInfo(file) { + * background(200); + * + * // Display the p5.File's name. + * text(file.name, 10, 10, 80, 40); + * + * // Display the p5.File's type and subtype. + * text(`${file.type}/${file.subtype}`, 10, 70); + * + * // Display the p5.File's size in bytes. + * text(file.size, 10, 90); + * } + * + *
+ * + *
+ * + * // Use the file input to select an image to + * // load and display. + * let img; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a file input and place it beneath the canvas. + * // Call handleImage() when the file image loads. + * let input = createFileInput(handleImage); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user selects an image file to load, it is displayed on the square.'); + * } + * + * function draw() { + * background(200); + * + * // Draw the image if it's ready. + * if (img) { + * image(img, 0, 0, width, height); + * } + * } + * + * // Use the p5.File's data once it loads. + * function handleImage(file) { + * // Check the p5.File's type. + * if (file.type === 'image') { + * // Create an image using using the p5.File's data. + * img = createImg(file.data, ''); + * + * // Hide the image element so it doesn't appear twice. + * img.hide(); + * } else { + * img = null; + * } + * } + * + *
+ */ + p5.File = class File { + constructor(file, pInst) { + this.file = file; + + this._pInst = pInst; + + // Splitting out the file type into two components + // This makes determining if image or text etc simpler + const typeList = file.type.split('/'); + this.type = typeList[0]; + this.subtype = typeList[1]; + this.name = file.name; + this.size = file.size; + this.data = undefined; + } -/** - * The file - * MIME type - * as a string. - * - * For example, `'image'` and `'text'` are both MIME types. - * - * @for p5.File - * @property type - * @example - *
- * - * // Use the file input to load a file and display its info. - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a file input and place it beneath the canvas. - * // Call displayType() when the file loads. - * let input = createFileInput(displayType); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user loads a file, its type is written in black.'); - * } - * - * // Display the p5.File's type once it loads. - * function displayType(file) { - * background(200); - * - * // Display the p5.File's type. - * text(`This is file's type is: ${file.type}`, 10, 10, 80, 80); - * } - * - *
- */ -/** - * The file subtype as a string. - * - * For example, a file with an `'image'` - * MIME type - * may have a subtype such as ``png`` or ``jpeg``. - * - * @property subtype - * @for p5.File - * - * @example - *
- * - * // Use the file input to load a - * // file and display its info. - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a file input and place it beneath the canvas. - * // Call displaySubtype() when the file loads. - * let input = createFileInput(displaySubtype); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user loads a file, its subtype is written in black.'); - * } - * - * // Display the p5.File's type once it loads. - * function displaySubtype(file) { - * background(200); - * - * // Display the p5.File's subtype. - * text(`This is file's subtype is: ${file.subtype}`, 10, 10, 80, 80); - * } - * - *
- */ + static _createLoader(theFile, callback) { + const reader = new FileReader(); + reader.onload = function (e) { + const p5file = new p5.File(theFile); + if (p5file.file.type === 'application/json') { + // Parse JSON and store the result in data + p5file.data = JSON.parse(e.target.result); + } else if (p5file.file.type === 'text/xml') { + // Parse XML, wrap it in p5.XML and store the result in data + const parser = new DOMParser(); + const xml = parser.parseFromString(e.target.result, 'text/xml'); + p5file.data = new p5.XML(xml.documentElement); + } else { + p5file.data = e.target.result; + } + callback(p5file); + }; + return reader; + } -/** - * The file name as a string. - * - * @property name - * @for p5.File - * - * @example - *
- * - * // Use the file input to load a - * // file and display its info. - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a file input and place it beneath the canvas. - * // Call displayName() when the file loads. - * let input = createFileInput(displayName); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user loads a file, its name is written in black.'); - * } - * - * // Display the p5.File's name once it loads. - * function displayName(file) { - * background(200); - * - * // Display the p5.File's name. - * text(`This is file's name is: ${file.name}`, 10, 10, 80, 80); - * } - * - *
- */ + static _load(f, callback) { + // Text or data? + // This should likely be improved + if (/^text\//.test(f.type) || f.type === 'application/json') { + p5.File._createLoader(f, callback).readAsText(f); + } else if (!/^(video|audio)\//.test(f.type)) { + p5.File._createLoader(f, callback).readAsDataURL(f); + } else { + const file = new p5.File(f); + file.data = URL.createObjectURL(f); + callback(file); + } + } + }; -/** - * The number of bytes in the file. - * - * @property size - * @for p5.File - * - * @example - *
- * - * // Use the file input to load a file and display its info. - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a file input and place it beneath the canvas. - * // Call displaySize() when the file loads. - * let input = createFileInput(displaySize); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user loads a file, its size in bytes is written in black.'); - * } - * - * // Display the p5.File's size in bytes once it loads. - * function displaySize(file) { - * background(200); - * - * // Display the p5.File's size. - * text(`This is file has ${file.size} bytes.`, 10, 10, 80, 80); - * } - * - *
- */ + /** + * Underlying + * File + * object. All `File` properties and methods are accessible. + * + * @for p5.File + * @property file + * @example + *
+ * + * // Use the file input to load a + * // file and display its info. + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a file input and place it beneath the canvas. + * // Call displayInfo() when the file loads. + * let input = createFileInput(displayInfo); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user loads a file, its info is written in black.'); + * } + * + * // Use the p5.File once it loads. + * function displayInfo(file) { + * background(200); + * + * // Display the p5.File's name. + * text(file.name, 10, 10, 80, 40); + * + * // Display the p5.File's type and subtype. + * text(`${file.type}/${file.subtype}`, 10, 70); + * + * // Display the p5.File's size in bytes. + * text(file.size, 10, 90); + * } + * + *
+ */ -/** - * A string containing the file's data. - * - * Data can be either image data, text contents, or a parsed object in the - * case of JSON and p5.XML objects. - * - * @property data - * @for p5.File - * - * @example - *
- * - * // Use the file input to load a file and display its info. - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a file input and place it beneath the canvas. - * // Call displayData() when the file loads. - * let input = createFileInput(displayData); - * input.position(0, 100); - * - * describe('A gray square with a file input beneath it. If the user loads a file, its data is written in black.'); - * } - * - * // Display the p5.File's data once it loads. - * function displayData(file) { - * background(200); - * - * // Display the p5.File's data, which looks like a random string of characters. - * text(file.data, 10, 10, 80, 80); - * } - * - *
- */ + /** + * The file + * MIME type + * as a string. + * + * For example, `'image'` and `'text'` are both MIME types. + * + * @for p5.File + * @property type + * @example + *
+ * + * // Use the file input to load a file and display its info. + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a file input and place it beneath the canvas. + * // Call displayType() when the file loads. + * let input = createFileInput(displayType); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user loads a file, its type is written in black.'); + * } + * + * // Display the p5.File's type once it loads. + * function displayType(file) { + * background(200); + * + * // Display the p5.File's type. + * text(`This is file's type is: ${file.type}`, 10, 10, 80, 80); + * } + * + *
+ */ + + /** + * The file subtype as a string. + * + * For example, a file with an `'image'` + * MIME type + * may have a subtype such as ``png`` or ``jpeg``. + * + * @property subtype + * @for p5.File + * + * @example + *
+ * + * // Use the file input to load a + * // file and display its info. + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a file input and place it beneath the canvas. + * // Call displaySubtype() when the file loads. + * let input = createFileInput(displaySubtype); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user loads a file, its subtype is written in black.'); + * } + * + * // Display the p5.File's type once it loads. + * function displaySubtype(file) { + * background(200); + * + * // Display the p5.File's subtype. + * text(`This is file's subtype is: ${file.subtype}`, 10, 10, 80, 80); + * } + * + *
+ */ + + /** + * The file name as a string. + * + * @property name + * @for p5.File + * + * @example + *
+ * + * // Use the file input to load a + * // file and display its info. + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a file input and place it beneath the canvas. + * // Call displayName() when the file loads. + * let input = createFileInput(displayName); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user loads a file, its name is written in black.'); + * } + * + * // Display the p5.File's name once it loads. + * function displayName(file) { + * background(200); + * + * // Display the p5.File's name. + * text(`This is file's name is: ${file.name}`, 10, 10, 80, 80); + * } + * + *
+ */ + + /** + * The number of bytes in the file. + * + * @property size + * @for p5.File + * + * @example + *
+ * + * // Use the file input to load a file and display its info. + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a file input and place it beneath the canvas. + * // Call displaySize() when the file loads. + * let input = createFileInput(displaySize); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user loads a file, its size in bytes is written in black.'); + * } + * + * // Display the p5.File's size in bytes once it loads. + * function displaySize(file) { + * background(200); + * + * // Display the p5.File's size. + * text(`This is file has ${file.size} bytes.`, 10, 10, 80, 80); + * } + * + *
+ */ + + /** + * A string containing the file's data. + * + * Data can be either image data, text contents, or a parsed object in the + * case of JSON and p5.XML objects. + * + * @property data + * @for p5.File + * + * @example + *
+ * + * // Use the file input to load a file and display its info. + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a file input and place it beneath the canvas. + * // Call displayData() when the file loads. + * let input = createFileInput(displayData); + * input.position(0, 100); + * + * describe('A gray square with a file input beneath it. If the user loads a file, its data is written in black.'); + * } + * + * // Display the p5.File's data once it loads. + * function displayData(file) { + * background(200); + * + * // Display the p5.File's data, which looks like a random string of characters. + * text(file.data, 10, 10, 80, 80); + * } + * + *
+ */ +} -export default p5; +export default dom; +export { MediaElement }; diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index ec110e699e..3649bfe0c5 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -11,65 +11,108 @@ * drawing images to the main display canvas. */ import Filters from './filters'; +import { Renderer2D } from '../core/p5.Renderer2D'; + +class Image { + constructor(width, height) { + this.width = width; + this.height = height; + this.canvas = document.createElement('canvas'); + this.canvas.width = this.width; + this.canvas.height = this.height; + this.drawingContext = this.canvas.getContext('2d'); + this._pixelsState = this; + this._pixelDensity = 1; + //Object for working with GIFs, defaults to null + this.gifProperties = null; + //For WebGL Texturing only: used to determine whether to reupload texture to GPU + this._modified = false; + this.pixels = []; + } -function image(p5, fn){ /** - * A class to describe an image. - * - * Images are rectangular grids of pixels that can be displayed and modified. + * Gets or sets the pixel density for high pixel density displays. + * + * By default, the density will be set to 1. + * + * Call this method with no arguments to get the default density, or pass + * in a number to set the density. If a non-positive number is provided, + * it defaults to 1. + * + * @param {Number} [density] A scaling factor for the number of pixels per + * side + * @returns {Number} The current density if called without arguments, or the instance for chaining if setting density. + */ + pixelDensity(density) { + if (typeof density !== 'undefined') { + // Setter: set the density and handle resize + if (density <= 0) { + const errorObj = { + type: 'INVALID_VALUE', + format: { types: ['Number'] }, + position: 1 + }; + + p5._friendlyParamError(errorObj, 'pixelDensity'); + + // Default to 1 in case of an invalid value + density = 1; + } + + this._pixelDensity = density; + + // Adjust canvas dimensions based on pixel density + this.width /= density; + this.height /= density; + + return this; // Return the image instance for chaining if needed + } else { + // Getter: return the default density + return this._pixelDensity; + } + } + + /** + * Helper function for animating GIF-based images with time + */ + _animateGif(pInst) { + const props = this.gifProperties; + const curTime = pInst._lastRealFrameTime || window.performance.now(); + if (props.lastChangeTime === 0) { + props.lastChangeTime = curTime; + } + if (props.playing) { + props.timeDisplayed = curTime - props.lastChangeTime; + const curDelay = props.frames[props.displayIndex].delay; + if (props.timeDisplayed >= curDelay) { + //GIF is bound to 'realtime' so can skip frames + const skips = Math.floor(props.timeDisplayed / curDelay); + props.timeDisplayed = 0; + props.lastChangeTime = curTime; + props.displayIndex += skips; + props.loopCount = Math.floor(props.displayIndex / props.numFrames); + if (props.loopLimit !== null && props.loopCount >= props.loopLimit) { + props.playing = false; + } else { + const ind = props.displayIndex % props.numFrames; + this.drawingContext.putImageData(props.frames[ind].image, 0, 0); + props.displayIndex = ind; + this.setModified(true); + } + } + } + } + + /** + * Loads the current value of each pixel in the image into the `img.pixels` + * array. * - * Existing images can be loaded by calling - * loadImage(). Blank images can be created by - * calling createImage(). `p5.Image` objects - * have methods for common tasks such as applying filters and modifying - * pixel values. + * `img.loadPixels()` must be called before reading or modifying pixel + * values. * * @example *
* - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('An image of a brick wall.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the GRAY filter. - * img.filter(GRAY); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A grayscale image of a brick wall.'); - * } - * - *
- * - *
- * * function setup() { * createCanvas(100, 100); * @@ -99,236 +142,7 @@ function image(p5, fn){ * *
* - * @class p5.Image - * @param {Number} width - * @param {Number} height - */ - p5.Image = class Image { - constructor(width, height) { - this.width = width; - this.height = height; - this.canvas = document.createElement('canvas'); - this.canvas.width = this.width; - this.canvas.height = this.height; - this.drawingContext = this.canvas.getContext('2d'); - this._pixelsState = this; - this._pixelDensity = 1; - //Object for working with GIFs, defaults to null - this.gifProperties = null; - //For WebGL Texturing only: used to determine whether to reupload texture to GPU - this._modified = false; - this.pixels = []; - } - - /** - * Gets or sets the pixel density for high pixel density displays. - * - * By default, the density will be set to 1. - * - * Call this method with no arguments to get the default density, or pass - * in a number to set the density. If a non-positive number is provided, - * it defaults to 1. - * - * @param {Number} [density] A scaling factor for the number of pixels per - * side - * @returns {Number} The current density if called without arguments, or the instance for chaining if setting density. - */ - pixelDensity(density) { - if (typeof density !== 'undefined') { - // Setter: set the density and handle resize - if (density <= 0) { - const errorObj = { - type: 'INVALID_VALUE', - format: { types: ['Number'] }, - position: 1 - }; - - p5._friendlyParamError(errorObj, 'pixelDensity'); - - // Default to 1 in case of an invalid value - density = 1; - } - - this._pixelDensity = density; - - // Adjust canvas dimensions based on pixel density - this.width /= density; - this.height /= density; - - return this; // Return the image instance for chaining if needed - } else { - // Getter: return the default density - return this._pixelDensity; - } - } - - /** - * Helper function for animating GIF-based images with time - */ - _animateGif(pInst) { - const props = this.gifProperties; - const curTime = pInst._lastRealFrameTime || window.performance.now(); - if (props.lastChangeTime === 0) { - props.lastChangeTime = curTime; - } - if (props.playing) { - props.timeDisplayed = curTime - props.lastChangeTime; - const curDelay = props.frames[props.displayIndex].delay; - if (props.timeDisplayed >= curDelay) { - //GIF is bound to 'realtime' so can skip frames - const skips = Math.floor(props.timeDisplayed / curDelay); - props.timeDisplayed = 0; - props.lastChangeTime = curTime; - props.displayIndex += skips; - props.loopCount = Math.floor(props.displayIndex / props.numFrames); - if (props.loopLimit !== null && props.loopCount >= props.loopLimit) { - props.playing = false; - } else { - const ind = props.displayIndex % props.numFrames; - this.drawingContext.putImageData(props.frames[ind].image, 0, 0); - props.displayIndex = ind; - this.setModified(true); - } - } - } - } - - /** - * Loads the current value of each pixel in the image into the `img.pixels` - * array. - * - * `img.loadPixels()` must be called before reading or modifying pixel - * values. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Image object. - * let img = createImage(66, 66); - * - * // Load the image's pixels. - * img.loadPixels(); - * - * // Set the pixels to black. - * for (let x = 0; x < img.width; x += 1) { - * for (let y = 0; y < img.height; y += 1) { - * img.set(x, y, 0); - * } - * } - * - * // Update the image. - * img.updatePixels(); - * - * // Display the image. - * image(img, 17, 17); - * - * describe('A black square drawn in the middle of a gray square.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Image object. - * let img = createImage(66, 66); - * - * // Load the image's pixels. - * img.loadPixels(); - * - * for (let i = 0; i < img.pixels.length; i += 4) { - * // Red. - * img.pixels[i] = 0; - * // Green. - * img.pixels[i + 1] = 0; - * // Blue. - * img.pixels[i + 2] = 0; - * // Alpha. - * img.pixels[i + 3] = 255; - * } - * - * // Update the image. - * img.updatePixels(); - * - * // Display the image. - * image(img, 17, 17); - * - * describe('A black square drawn in the middle of a gray square.'); - * } - * - *
- */ - loadPixels() { - p5.Renderer2D.prototype.loadPixels.call(this); - this.setModified(true); - } - - /** - * Updates the canvas with the RGBA values in the - * img.pixels array. - * - * `img.updatePixels()` only needs to be called after changing values in - * the img.pixels array. Such changes can be - * made directly after calling - * img.loadPixels() or by calling - * img.set(). - * - * The optional parameters `x`, `y`, `width`, and `height` define a - * subsection of the image to update. Doing so can improve performance in - * some cases. - * - * If the image was loaded from a GIF, then calling `img.updatePixels()` - * will update the pixels in current frame. - * - * @param {Integer} x x-coordinate of the upper-left corner - * of the subsection to update. - * @param {Integer} y y-coordinate of the upper-left corner - * of the subsection to update. - * @param {Integer} w width of the subsection to update. - * @param {Integer} h height of the subsection to update. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Image object. - * let img = createImage(66, 66); - * - * // Load the image's pixels. - * img.loadPixels(); - * - * // Set the pixels to black. - * for (let x = 0; x < img.width; x += 1) { - * for (let y = 0; y < img.height; y += 1) { - * img.set(x, y, 0); - * } - * } - * - * // Update the image. - * img.updatePixels(); - * - * // Display the image. - * image(img, 17, 17); - * - * describe('A black square drawn in the middle of a gray square.'); - * } - * - *
- * - *
+ *
* * function setup() { * createCanvas(100, 100); @@ -341,7 +155,6 @@ function image(p5, fn){ * // Load the image's pixels. * img.loadPixels(); * - * // Set the pixels to black. * for (let i = 0; i < img.pixels.length; i += 4) { * // Red. * img.pixels[i] = 0; @@ -363,1467 +176,1657 @@ function image(p5, fn){ * } * *
- */ - /** - */ - updatePixels(x, y, w, h) { - p5.Renderer2D.prototype.updatePixels.call(this, x, y, w, h); - this.setModified(true); - } - - /** - * Gets a pixel or a region of pixels from the image. - * - * `img.get()` is easy to use but it's not as fast as - * img.pixels. Use - * img.pixels to read many pixel values. - * - * The version of `img.get()` with no parameters returns the entire image. - * - * The version of `img.get()` with two parameters, as in `img.get(10, 20)`, - * 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 `img.get()` with four parameters, as in - * `img,get(10, 20, 50, 90)`, interprets them as - * coordinates and dimensions. The first two parameters are the coordinates - * of the upper-left corner of the subsection. The last two parameters are - * the width and height of the subsection. It returns a subsection of the - * canvas in a new p5.Image object. - * - * Use `img.get()` instead of get() to work directly - * with images. - * - * @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; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Display the image. - * image(img, 0, 0); - * - * // Copy the image. - * let img2 = get(); - * - * // Display the copied image on the right. - * image(img2, 50, 0); - * - * describe('Two identical mountain landscapes shown side-by-side.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Display the image. - * image(img, 0, 0); - * - * // Get a pixel's color. - * let c = img.get(50, 90); - * - * // Style the square using the pixel's color. - * fill(c); - * noStroke(); - * - * // Draw the square. - * square(25, 25, 50); - * - * describe('A mountain landscape with an olive green square in its center.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Display the image. - * image(img, 0, 0); - * - * // Copy half of the image. - * let img2 = img.get(0, 0, img.width / 2, img.height / 2); - * - * // Display half of the image. - * image(img2, 50, 50); - * - * describe('A mountain landscape drawn on top of another mountain landscape.'); - * } - * - *
- */ - /** - * @return {p5.Image} whole p5.Image - */ - /** - * @param {Number} x - * @param {Number} y - * @return {Number[]} color of the pixel at (x, y) in array format `[R, G, B, A]`. - */ - get(x, y, w, h) { - p5._validateParameters('p5.Image.get', arguments); - return p5.Renderer2D.prototype.get.apply(this, arguments); - } - - _getPixel(...args) { - return p5.Renderer2D.prototype._getPixel.apply(this, args); - } + */ + loadPixels() { + Renderer2D.prototype.loadPixels.call(this); + this.setModified(true); + } - /** - * Sets the color of one or more pixels within an image. - * - * `img.set()` is easy to use but it's not as fast as - * img.pixels. Use - * img.pixels to set many pixel values. - * - * `img.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 another - * p5.Image object. - * - * img.updatePixels() must be called - * after using `img.set()` for changes to appear. - * - * @param {Number} x x-coordinate of the pixel. - * @param {Number} y y-coordinate of the pixel. - * @param {Number|Number[]|Object} a grayscale value | pixel array | - * p5.Color object | - * p5.Image to copy. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Image object. - * let img = createImage(100, 100); - * - * // Set four pixels to black. - * img.set(30, 20, 0); - * img.set(85, 20, 0); - * img.set(85, 75, 0); - * img.set(30, 75, 0); - * - * // Update the image. - * img.updatePixels(); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('Four black dots arranged in a square drawn on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Image object. - * let img = createImage(100, 100); - * - * // Create a p5.Color object. - * let black = color(0); - * - * // Set four pixels to black. - * img.set(30, 20, black); - * img.set(85, 20, black); - * img.set(85, 75, black); - * img.set(30, 75, black); - * - * // Update the image. - * img.updatePixels(); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('Four black dots arranged in a square drawn on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Image object. - * let img = createImage(66, 66); - * - * // Draw a color gradient. - * for (let x = 0; x < img.width; x += 1) { - * for (let y = 0; y < img.height; y += 1) { - * let c = map(x, 0, img.width, 0, 255); - * img.set(x, y, c); - * } - * } - * - * // Update the image. - * img.updatePixels(); - * - * // Display the image. - * image(img, 17, 17); - * - * describe('A square with a horiztonal color gradient from black to white drawn on a gray background.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Create a p5.Image object. - * let img2 = createImage(100, 100); - * - * // Set the blank image's pixels using the landscape. - * img2.set(0, 0, img); - * - * // Display the second image. - * image(img2, 0, 0); - * - * describe('An image of a mountain landscape.'); - * } - * - *
- */ - set(x, y, imgOrCol) { - p5.Renderer2D.prototype.set.call(this, x, y, imgOrCol); - this.setModified(true); - } - - /** - * Resizes the image to a given width and height. - * - * The image's original aspect ratio can be kept by passing 0 for either - * `width` or `height`. For example, calling `img.resize(50, 0)` on an image - * that was 500 × 300 pixels will resize it to 50 × 30 pixels. - * - * @param {Number} width resized image width. - * @param {Number} height resized image height. - * - * @example - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Display the image. - * image(img, 0, 0); - * - * // Resize the image. - * img.resize(50, 100); - * - * // Display the resized image. - * image(img, 0, 0); - * - * describe('Two images of a mountain landscape. One copy of the image is squeezed horizontally.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Display the image. - * image(img, 0, 0); - * - * // Resize the image, keeping the aspect ratio. - * img.resize(0, 30); - * - * // Display the resized image. - * image(img, 0, 0); - * - * describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Display the image. - * image(img, 0, 0); - * - * // Resize the image, keeping the aspect ratio. - * img.resize(60, 0); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.'); - * } - * - *
- */ - resize(width, height) { - // Copy contents to a temporary canvas, resize the original - // and then copy back. - // - // There is a faster approach that involves just one copy and swapping the - // this.canvas reference. We could switch to that approach if (as i think - // is the case) there an expectation that the user would not hold a - // reference to the backing canvas of a p5.Image. But since we do not - // enforce that at the moment, I am leaving in the slower, but safer - // implementation. - - // auto-resize - if (width === 0 && height === 0) { - width = this.canvas.width; - height = this.canvas.height; - } else if (width === 0) { - width = this.canvas.width * height / this.canvas.height; - } else if (height === 0) { - height = this.canvas.height * width / this.canvas.width; - } - - width = Math.floor(width); - height = Math.floor(height); - - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = width; - tempCanvas.height = height; - - if (this.gifProperties) { - const props = this.gifProperties; - //adapted from github.com/LinusU/resize-image-data - const nearestNeighbor = (src, dst) => { - let pos = 0; - for (let y = 0; y < dst.height; y++) { - for (let x = 0; x < dst.width; x++) { - const srcX = Math.floor(x * src.width / dst.width); - const srcY = Math.floor(y * src.height / dst.height); - let srcPos = (srcY * src.width + srcX) * 4; - dst.data[pos++] = src.data[srcPos++]; // R - dst.data[pos++] = src.data[srcPos++]; // G - dst.data[pos++] = src.data[srcPos++]; // B - dst.data[pos++] = src.data[srcPos++]; // A - } - } - }; - for (let i = 0; i < props.numFrames; i++) { - const resizedImageData = this.drawingContext.createImageData( - width, - height - ); - nearestNeighbor(props.frames[i].image, resizedImageData); - props.frames[i].image = resizedImageData; - } - } - - tempCanvas.getContext('2d').drawImage( - this.canvas, - 0, 0, this.canvas.width, this.canvas.height, - 0, 0, tempCanvas.width, tempCanvas.height - ); - - // Resize the original canvas, which will clear its contents - this.canvas.width = this.width = width; - this.canvas.height = this.height = height; - - //Copy the image back - this.drawingContext.drawImage( - tempCanvas, - 0, 0, width, height, - 0, 0, width, height - ); + /** + * Updates the canvas with the RGBA values in the + * img.pixels array. + * + * `img.updatePixels()` only needs to be called after changing values in + * the img.pixels array. Such changes can be + * made directly after calling + * img.loadPixels() or by calling + * img.set(). + * + * The optional parameters `x`, `y`, `width`, and `height` define a + * subsection of the image to update. Doing so can improve performance in + * some cases. + * + * If the image was loaded from a GIF, then calling `img.updatePixels()` + * will update the pixels in current frame. + * + * @param {Integer} x x-coordinate of the upper-left corner + * of the subsection to update. + * @param {Integer} y y-coordinate of the upper-left corner + * of the subsection to update. + * @param {Integer} w width of the subsection to update. + * @param {Integer} h height of the subsection to update. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Image object. + * let img = createImage(66, 66); + * + * // Load the image's pixels. + * img.loadPixels(); + * + * // Set the pixels to black. + * for (let x = 0; x < img.width; x += 1) { + * for (let y = 0; y < img.height; y += 1) { + * img.set(x, y, 0); + * } + * } + * + * // Update the image. + * img.updatePixels(); + * + * // Display the image. + * image(img, 17, 17); + * + * describe('A black square drawn in the middle of a gray square.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Image object. + * let img = createImage(66, 66); + * + * // Load the image's pixels. + * img.loadPixels(); + * + * // Set the pixels to black. + * for (let i = 0; i < img.pixels.length; i += 4) { + * // Red. + * img.pixels[i] = 0; + * // Green. + * img.pixels[i + 1] = 0; + * // Blue. + * img.pixels[i + 2] = 0; + * // Alpha. + * img.pixels[i + 3] = 255; + * } + * + * // Update the image. + * img.updatePixels(); + * + * // Display the image. + * image(img, 17, 17); + * + * describe('A black square drawn in the middle of a gray square.'); + * } + * + *
+ */ + /** + */ + updatePixels(x, y, w, h) { + Renderer2D.prototype.updatePixels.call(this, x, y, w, h); + this.setModified(true); + } - if (this.pixels.length > 0) { - this.loadPixels(); - } + /** + * Gets a pixel or a region of pixels from the image. + * + * `img.get()` is easy to use but it's not as fast as + * img.pixels. Use + * img.pixels to read many pixel values. + * + * The version of `img.get()` with no parameters returns the entire image. + * + * The version of `img.get()` with two parameters, as in `img.get(10, 20)`, + * 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 `img.get()` with four parameters, as in + * `img,get(10, 20, 50, 90)`, interprets them as + * coordinates and dimensions. The first two parameters are the coordinates + * of the upper-left corner of the subsection. The last two parameters are + * the width and height of the subsection. It returns a subsection of the + * canvas in a new p5.Image object. + * + * Use `img.get()` instead of get() to work directly + * with images. + * + * @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; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Display the image. + * image(img, 0, 0); + * + * // Copy the image. + * let img2 = get(); + * + * // Display the copied image on the right. + * image(img2, 50, 0); + * + * describe('Two identical mountain landscapes shown side-by-side.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Display the image. + * image(img, 0, 0); + * + * // Get a pixel's color. + * let c = img.get(50, 90); + * + * // Style the square using the pixel's color. + * fill(c); + * noStroke(); + * + * // Draw the square. + * square(25, 25, 50); + * + * describe('A mountain landscape with an olive green square in its center.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Display the image. + * image(img, 0, 0); + * + * // Copy half of the image. + * let img2 = img.get(0, 0, img.width / 2, img.height / 2); + * + * // Display half of the image. + * image(img2, 50, 50); + * + * describe('A mountain landscape drawn on top of another mountain landscape.'); + * } + * + *
+ */ + /** + * @return {p5.Image} whole p5.Image + */ + /** + * @param {Number} x + * @param {Number} y + * @return {Number[]} color of the pixel at (x, y) in array format `[R, G, B, A]`. + */ + get(x, y, w, h) { + p5._validateParameters('p5.Image.get', arguments); + return Renderer2D.prototype.get.apply(this, arguments); + } - this.setModified(true); - } + _getPixel(...args) { + return Renderer2D.prototype._getPixel.apply(this, args); + } - /** - * Copies pixels from a source image to this image. - * - * The first parameter, `srcImage`, is an optional - * p5.Image object to copy. If a source image isn't - * passed, then `img.copy()` can copy a region of this image to another - * 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 this image to copy into. `(dx, dy)` is the top-left corner of the - * region. `dw` and `dh` are the region's width and height. - * - * Calling `img.copy()` will scale pixels from the source region if it isn't - * the same size as the destination region. - * - * @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; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Copy one region of the image to another. - * img.copy(7, 22, 10, 10, 35, 25, 50, 50); - * - * // Display the image. - * image(img, 0, 0); - * - * // 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.'); - * } - * - *
- * - *
- * - * let mountains; - * let bricks; - * - * // Load the images. - * function preload() { - * mountains = loadImage('assets/rockies.jpg'); - * bricks = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Calculate the center of the bricks image. - * let x = bricks.width / 2; - * let y = bricks.height / 2; - * - * // Copy the bricks to the mountains image. - * mountains.copy(bricks, 0, 0, x, y, 0, 0, x, y); - * - * // Display the mountains image. - * image(mountains, 0, 0); - * - * describe('An image of a brick wall drawn at the top-left of an image of a mountain landscape.'); - * } - * - *
- */ - /** - * @param {Integer} sx - * @param {Integer} sy - * @param {Integer} sw - * @param {Integer} sh - * @param {Integer} dx - * @param {Integer} dy - * @param {Integer} dw - * @param {Integer} dh - */ - copy(...args) { - fn.copy.apply(this, args); - } + /** + * Sets the color of one or more pixels within an image. + * + * `img.set()` is easy to use but it's not as fast as + * img.pixels. Use + * img.pixels to set many pixel values. + * + * `img.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 another + * p5.Image object. + * + * img.updatePixels() must be called + * after using `img.set()` for changes to appear. + * + * @param {Number} x x-coordinate of the pixel. + * @param {Number} y y-coordinate of the pixel. + * @param {Number|Number[]|Object} a grayscale value | pixel array | + * p5.Color object | + * p5.Image to copy. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Image object. + * let img = createImage(100, 100); + * + * // Set four pixels to black. + * img.set(30, 20, 0); + * img.set(85, 20, 0); + * img.set(85, 75, 0); + * img.set(30, 75, 0); + * + * // Update the image. + * img.updatePixels(); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('Four black dots arranged in a square drawn on a gray background.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Image object. + * let img = createImage(100, 100); + * + * // Create a p5.Color object. + * let black = color(0); + * + * // Set four pixels to black. + * img.set(30, 20, black); + * img.set(85, 20, black); + * img.set(85, 75, black); + * img.set(30, 75, black); + * + * // Update the image. + * img.updatePixels(); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('Four black dots arranged in a square drawn on a gray background.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Image object. + * let img = createImage(66, 66); + * + * // Draw a color gradient. + * for (let x = 0; x < img.width; x += 1) { + * for (let y = 0; y < img.height; y += 1) { + * let c = map(x, 0, img.width, 0, 255); + * img.set(x, y, c); + * } + * } + * + * // Update the image. + * img.updatePixels(); + * + * // Display the image. + * image(img, 17, 17); + * + * describe('A square with a horiztonal color gradient from black to white drawn on a gray background.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Create a p5.Image object. + * let img2 = createImage(100, 100); + * + * // Set the blank image's pixels using the landscape. + * img2.set(0, 0, img); + * + * // Display the second image. + * image(img2, 0, 0); + * + * describe('An image of a mountain landscape.'); + * } + * + *
+ */ + set(x, y, imgOrCol) { + Renderer2D.prototype.set.call(this, x, y, imgOrCol); + this.setModified(true); + } - /** - * Masks part of the image with another. - * - * `img.mask()` uses another p5.Image object's - * alpha channel as the alpha channel for this image. Masks are cumulative - * and can't be removed once applied. If the mask has a different - * pixel density from this image, the mask will be scaled. - * - * @param {p5.Image} srcImage source image. - * - * @example - *
- * - * let photo; - * let maskImage; - * - * // Load the images. - * function preload() { - * photo = loadImage('assets/rockies.jpg'); - * maskImage = loadImage('assets/mask2.png'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the mask. - * photo.mask(maskImage); - * - * // Display the image. - * image(photo, 0, 0); - * - * describe('An image of a mountain landscape. The right side of the image has a faded patch of white.'); - * } - * - *
- */ - // TODO: - Accept an array of alpha values. - mask(p5Image) { - if (p5Image === undefined) { - p5Image = this; - } - const currBlend = this.drawingContext.globalCompositeOperation; + /** + * Resizes the image to a given width and height. + * + * The image's original aspect ratio can be kept by passing 0 for either + * `width` or `height`. For example, calling `img.resize(50, 0)` on an image + * that was 500 × 300 pixels will resize it to 50 × 30 pixels. + * + * @param {Number} width resized image width. + * @param {Number} height resized image height. + * + * @example + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Display the image. + * image(img, 0, 0); + * + * // Resize the image. + * img.resize(50, 100); + * + * // Display the resized image. + * image(img, 0, 0); + * + * describe('Two images of a mountain landscape. One copy of the image is squeezed horizontally.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Display the image. + * image(img, 0, 0); + * + * // Resize the image, keeping the aspect ratio. + * img.resize(0, 30); + * + * // Display the resized image. + * image(img, 0, 0); + * + * describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Display the image. + * image(img, 0, 0); + * + * // Resize the image, keeping the aspect ratio. + * img.resize(60, 0); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('Two images of a mountain landscape. The small copy of the image covers the top-left corner of the larger image.'); + * } + * + *
+ */ + resize(width, height) { + // Copy contents to a temporary canvas, resize the original + // and then copy back. + // + // There is a faster approach that involves just one copy and swapping the + // this.canvas reference. We could switch to that approach if (as i think + // is the case) there an expectation that the user would not hold a + // reference to the backing canvas of a p5.Image. But since we do not + // enforce that at the moment, I am leaving in the slower, but safer + // implementation. - let imgScaleFactor = this._pixelDensity; - let maskScaleFactor = 1; - if (p5Image instanceof p5.Renderer) { - maskScaleFactor = p5Image._pInst._renderer._pixelDensity; - } + // auto-resize + if (width === 0 && height === 0) { + width = this.canvas.width; + height = this.canvas.height; + } else if (width === 0) { + width = this.canvas.width * height / this.canvas.height; + } else if (height === 0) { + height = this.canvas.height * width / this.canvas.width; + } - const copyArgs = [ - p5Image, - 0, - 0, - maskScaleFactor * p5Image.width, - maskScaleFactor * p5Image.height, - 0, - 0, - imgScaleFactor * this.width, - imgScaleFactor * this.height - ]; + width = Math.floor(width); + height = Math.floor(height); - this.drawingContext.globalCompositeOperation = 'destination-in'; - if (this.gifProperties) { - for (let i = 0; i < this.gifProperties.frames.length; i++) { - this.drawingContext.putImageData( - this.gifProperties.frames[i].image, - 0, - 0 - ); - this.copy(...copyArgs); - this.gifProperties.frames[i].image = this.drawingContext.getImageData( - 0, - 0, - imgScaleFactor * this.width, - imgScaleFactor * this.height - ); + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + + if (this.gifProperties) { + const props = this.gifProperties; + //adapted from github.com/LinusU/resize-image-data + const nearestNeighbor = (src, dst) => { + let pos = 0; + for (let y = 0; y < dst.height; y++) { + for (let x = 0; x < dst.width; x++) { + const srcX = Math.floor(x * src.width / dst.width); + const srcY = Math.floor(y * src.height / dst.height); + let srcPos = (srcY * src.width + srcX) * 4; + dst.data[pos++] = src.data[srcPos++]; // R + dst.data[pos++] = src.data[srcPos++]; // G + dst.data[pos++] = src.data[srcPos++]; // B + dst.data[pos++] = src.data[srcPos++]; // A + } } - this.drawingContext.putImageData( - this.gifProperties.frames[this.gifProperties.displayIndex].image, - 0, - 0 + }; + for (let i = 0; i < props.numFrames; i++) { + const resizedImageData = this.drawingContext.createImageData( + width, + height ); - } else { - this.copy(...copyArgs); + nearestNeighbor(props.frames[i].image, resizedImageData); + props.frames[i].image = resizedImageData; } - this.drawingContext.globalCompositeOperation = currBlend; - this.setModified(true); } - /** - * Applies an image filter to the image. - * - * 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 be 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. - * - * @param {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|ERODE|DILATE|BLUR)} filterType either THRESHOLD, GRAY, OPAQUE, INVERT, - * POSTERIZE, ERODE, DILATE or BLUR. - * @param {Number} [filterParam] parameter unique to each filter. - * - * @example - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the INVERT filter. - * img.filter(INVERT); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A blue brick wall.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the GRAY filter. - * img.filter(GRAY); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A brick wall drawn in grayscale.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the THRESHOLD filter. - * img.filter(THRESHOLD); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A brick wall drawn in black and white.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the OPAQUE filter. - * img.filter(OPAQUE); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A red brick wall.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the POSTERIZE filter. - * img.filter(POSTERIZE, 3); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('An image of a red brick wall drawn with a limited color palette.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the BLUR filter. - * img.filter(BLUR, 3); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A blurry image of a red brick wall.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the DILATE filter. - * img.filter(DILATE); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A red brick wall with bright lines between each brick.'); - * } - * - *
- * - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Apply the ERODE filter. - * img.filter(ERODE); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('A red brick wall with faint lines between each brick.'); - * } - * - *
- */ - filter(operation, value) { - Filters.apply(this.canvas, Filters[operation], value); - this.setModified(true); + tempCanvas.getContext('2d').drawImage( + this.canvas, + 0, 0, this.canvas.width, this.canvas.height, + 0, 0, tempCanvas.width, tempCanvas.height + ); + + // Resize the original canvas, which will clear its contents + this.canvas.width = this.width = width; + this.canvas.height = this.height = height; + + //Copy the image back + this.drawingContext.drawImage( + tempCanvas, + 0, 0, width, height, + 0, 0, width, height + ); + + if (this.pixels.length > 0) { + this.loadPixels(); } - /** - * Copies a region of pixels from another image into this one. - * - * 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`. - * - * @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. - * - * Available blend modes are: normal | multiply | screen | overlay | - * darken | lighten | color-dodge | color-burn | hard-light | - * soft-light | difference | exclusion | hue | saturation | - * color | luminosity - * - * http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/ - * - * @example - *
- * - * let mountains; - * let bricks; - * - * // Load the images. - * function preload() { - * mountains = loadImage('assets/rockies.jpg'); - * bricks = loadImage('assets/bricks_third.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Blend the bricks image into the mountains. - * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, ADD); - * - * // Display the mountains image. - * image(mountains, 0, 0); - * - * // Display the bricks image. - * image(bricks, 0, 0); - * - * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears faded on the right of the image.'); - * } - * - *
- * - *
- * - * let mountains; - * let bricks; - * - * // Load the images. - * function preload() { - * mountains = loadImage('assets/rockies.jpg'); - * bricks = loadImage('assets/bricks_third.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Blend the bricks image into the mountains. - * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, DARKEST); - * - * // Display the mountains image. - * image(mountains, 0, 0); - * - * // Display the bricks image. - * image(bricks, 0, 0); - * - * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears transparent on the right of the image.'); - * } - * - *
- * - *
- * - * let mountains; - * let bricks; - * - * // Load the images. - * function preload() { - * mountains = loadImage('assets/rockies.jpg'); - * bricks = loadImage('assets/bricks_third.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Blend the bricks image into the mountains. - * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, LIGHTEST); - * - * // Display the mountains image. - * image(mountains, 0, 0); - * - * // Display the bricks image. - * image(bricks, 0, 0); - * - * 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.'); - * } - * - *
- */ - /** - * @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 - */ - blend(...args) { - p5._validateParameters('p5.Image.blend', arguments); - fn.blend.apply(this, args); - this.setModified(true); + this.setModified(true); + } + + /** + * Copies pixels from a source image to this image. + * + * The first parameter, `srcImage`, is an optional + * p5.Image object to copy. If a source image isn't + * passed, then `img.copy()` can copy a region of this image to another + * 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 this image to copy into. `(dx, dy)` is the top-left corner of the + * region. `dw` and `dh` are the region's width and height. + * + * Calling `img.copy()` will scale pixels from the source region if it isn't + * the same size as the destination region. + * + * @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; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Copy one region of the image to another. + * img.copy(7, 22, 10, 10, 35, 25, 50, 50); + * + * // Display the image. + * image(img, 0, 0); + * + * // 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.'); + * } + * + *
+ * + *
+ * + * let mountains; + * let bricks; + * + * // Load the images. + * function preload() { + * mountains = loadImage('assets/rockies.jpg'); + * bricks = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Calculate the center of the bricks image. + * let x = bricks.width / 2; + * let y = bricks.height / 2; + * + * // Copy the bricks to the mountains image. + * mountains.copy(bricks, 0, 0, x, y, 0, 0, x, y); + * + * // Display the mountains image. + * image(mountains, 0, 0); + * + * describe('An image of a brick wall drawn at the top-left of an image of a mountain landscape.'); + * } + * + *
+ */ + /** + * @param {Integer} sx + * @param {Integer} sy + * @param {Integer} sw + * @param {Integer} sh + * @param {Integer} dx + * @param {Integer} dy + * @param {Integer} dw + * @param {Integer} dh + */ + copy(...args) { + fn.copy.apply(this, args); + } + + /** + * Masks part of the image with another. + * + * `img.mask()` uses another p5.Image object's + * alpha channel as the alpha channel for this image. Masks are cumulative + * and can't be removed once applied. If the mask has a different + * pixel density from this image, the mask will be scaled. + * + * @param {p5.Image} srcImage source image. + * + * @example + *
+ * + * let photo; + * let maskImage; + * + * // Load the images. + * function preload() { + * photo = loadImage('assets/rockies.jpg'); + * maskImage = loadImage('assets/mask2.png'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the mask. + * photo.mask(maskImage); + * + * // Display the image. + * image(photo, 0, 0); + * + * describe('An image of a mountain landscape. The right side of the image has a faded patch of white.'); + * } + * + *
+ */ + // TODO: - Accept an array of alpha values. + mask(p5Image) { + if (p5Image === undefined) { + p5Image = this; } + const currBlend = this.drawingContext.globalCompositeOperation; - /** - * helper method for web GL mode to indicate that an image has been - * changed or unchanged since last upload. gl texture upload will - * set this value to false after uploading the texture. - * @param {boolean} val sets whether or not the image has been - * modified. - * @private - */ - setModified(val) { - this._modified = val; //enforce boolean? + let imgScaleFactor = this._pixelDensity; + let maskScaleFactor = 1; + if (p5Image instanceof p5.Renderer) { + maskScaleFactor = p5Image._pInst._renderer._pixelDensity; } - /** - * helper method for web GL mode to figure out if the image - * 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; - } + const copyArgs = [ + p5Image, + 0, + 0, + maskScaleFactor * p5Image.width, + maskScaleFactor * p5Image.height, + 0, + 0, + imgScaleFactor * this.width, + imgScaleFactor * this.height + ]; + + this.drawingContext.globalCompositeOperation = 'destination-in'; + if (this.gifProperties) { + for (let i = 0; i < this.gifProperties.frames.length; i++) { + this.drawingContext.putImageData( + this.gifProperties.frames[i].image, + 0, + 0 + ); + this.copy(...copyArgs); + this.gifProperties.frames[i].image = this.drawingContext.getImageData( + 0, + 0, + imgScaleFactor * this.width, + imgScaleFactor * this.height + ); + } + this.drawingContext.putImageData( + this.gifProperties.frames[this.gifProperties.displayIndex].image, + 0, + 0 + ); + } else { + this.copy(...copyArgs); + } + this.drawingContext.globalCompositeOperation = currBlend; + this.setModified(true); + } + + /** + * Applies an image filter to the image. + * + * 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 be 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. + * + * @param {(THRESHOLD|GRAY|OPAQUE|INVERT|POSTERIZE|ERODE|DILATE|BLUR)} filterType either THRESHOLD, GRAY, OPAQUE, INVERT, + * POSTERIZE, ERODE, DILATE or BLUR. + * @param {Number} [filterParam] parameter unique to each filter. + * + * @example + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the INVERT filter. + * img.filter(INVERT); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A blue brick wall.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the GRAY filter. + * img.filter(GRAY); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A brick wall drawn in grayscale.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the THRESHOLD filter. + * img.filter(THRESHOLD); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A brick wall drawn in black and white.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the OPAQUE filter. + * img.filter(OPAQUE); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A red brick wall.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the POSTERIZE filter. + * img.filter(POSTERIZE, 3); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('An image of a red brick wall drawn with a limited color palette.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the BLUR filter. + * img.filter(BLUR, 3); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A blurry image of a red brick wall.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the DILATE filter. + * img.filter(DILATE); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A red brick wall with bright lines between each brick.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the ERODE filter. + * img.filter(ERODE); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A red brick wall with faint lines between each brick.'); + * } + * + *
+ */ + filter(operation, value) { + Filters.apply(this.canvas, Filters[operation], value); + this.setModified(true); + } + + /** + * Copies a region of pixels from another image into this one. + * + * 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`. + * + * @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. + * + * Available blend modes are: normal | multiply | screen | overlay | + * darken | lighten | color-dodge | color-burn | hard-light | + * soft-light | difference | exclusion | hue | saturation | + * color | luminosity + * + * http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/ + * + * @example + *
+ * + * let mountains; + * let bricks; + * + * // Load the images. + * function preload() { + * mountains = loadImage('assets/rockies.jpg'); + * bricks = loadImage('assets/bricks_third.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Blend the bricks image into the mountains. + * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, ADD); + * + * // Display the mountains image. + * image(mountains, 0, 0); + * + * // Display the bricks image. + * image(bricks, 0, 0); + * + * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears faded on the right of the image.'); + * } + * + *
+ * + *
+ * + * let mountains; + * let bricks; + * + * // Load the images. + * function preload() { + * mountains = loadImage('assets/rockies.jpg'); + * bricks = loadImage('assets/bricks_third.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Blend the bricks image into the mountains. + * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, DARKEST); + * + * // Display the mountains image. + * image(mountains, 0, 0); + * + * // Display the bricks image. + * image(bricks, 0, 0); + * + * describe('A wall of bricks in front of a mountain landscape. The same wall of bricks appears transparent on the right of the image.'); + * } + * + *
+ * + *
+ * + * let mountains; + * let bricks; + * + * // Load the images. + * function preload() { + * mountains = loadImage('assets/rockies.jpg'); + * bricks = loadImage('assets/bricks_third.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Blend the bricks image into the mountains. + * mountains.blend(bricks, 0, 0, 33, 100, 67, 0, 33, 100, LIGHTEST); + * + * // Display the mountains image. + * image(mountains, 0, 0); + * + * // Display the bricks image. + * image(bricks, 0, 0); + * + * 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.'); + * } + * + *
+ */ + /** + * @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 + */ + blend(...args) { + p5._validateParameters('p5.Image.blend', arguments); + fn.blend.apply(this, args); + this.setModified(true); + } + + /** + * helper method for web GL mode to indicate that an image has been + * changed or unchanged since last upload. gl texture upload will + * set this value to false after uploading the texture. + * @param {boolean} val sets whether or not the image has been + * modified. + * @private + */ + setModified(val) { + this._modified = val; //enforce boolean? + } - /** - * Saves the image to a file. - * - * By default, `img.save()` saves the image as a PNG image called - * `untitled.png`. - * - * The first parameter, `filename`, is optional. It's a string that sets the - * file's name. If a file extension is included, as in - * `img.save('drawing.png')`, then the image will be saved using that - * format. - * - * The second parameter, `extension`, is also optional. It sets the files format. - * Either `'png'` or `'jpg'` can be used. For example, `img.save('drawing', 'jpg')` - * saves the canvas to a file called `drawing.jpg`. - * - * Note: The browser will either save the file immediately or prompt the user - * with a dialogue window. - * - * The image will only be downloaded as an animated GIF if it was loaded - * from a GIF file. See saveGif() to create new - * GIFs. - * - * @param {String} filename filename. Defaults to 'untitled'. - * @param {String} [extension] file extension, either 'png' or 'jpg'. - * Defaults to 'png'. - * - * @example - *
- * - * let img; - * - * // Load the image. - * function preload() { - * img = loadImage('assets/rockies.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Display the image. - * image(img, 0, 0); - * - * describe('An image of a mountain landscape. The image is downloaded when the user presses the "s", "j", or "p" key.'); - * } - * - * // Save the image with different options when the user presses a key. - * function keyPressed() { - * if (key === 's') { - * img.save(); - * } else if (key === 'j') { - * img.save('rockies.jpg'); - * } else if (key === 'p') { - * img.save('rockies', 'png'); - * } - * } - * - *
- */ - save(filename, extension) { - if (this.gifProperties) { - fn.encodeAndDownloadGif(this, filename); - } else { - fn.saveCanvas(this.canvas, filename, extension); - } - } + /** + * helper method for web GL mode to figure out if the image + * 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; + } - // GIF Section - /** - * Restarts an animated GIF at its first frame. - * - * @example - *
- * - * let gif; - * - * // Load the image. - * function preload() { - * gif = loadImage('assets/arnott-wallace-wink-loop-once.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A cartoon face winks once and then freezes. Clicking resets the face and makes it wink again.'); - * } - * - * function draw() { - * background(255); - * - * // Display the image. - * image(gif, 0, 0); - * } - * - * // Reset the GIF when the user presses the mouse. - * function mousePressed() { - * gif.reset(); - * } - * - *
- */ - reset() { - if (this.gifProperties) { - const props = this.gifProperties; - props.playing = true; - props.timeSinceStart = 0; - props.timeDisplayed = 0; - props.lastChangeTime = 0; - props.loopCount = 0; - props.displayIndex = 0; - this.drawingContext.putImageData(props.frames[0].image, 0, 0); - } + /** + * Saves the image to a file. + * + * By default, `img.save()` saves the image as a PNG image called + * `untitled.png`. + * + * The first parameter, `filename`, is optional. It's a string that sets the + * file's name. If a file extension is included, as in + * `img.save('drawing.png')`, then the image will be saved using that + * format. + * + * The second parameter, `extension`, is also optional. It sets the files format. + * Either `'png'` or `'jpg'` can be used. For example, `img.save('drawing', 'jpg')` + * saves the canvas to a file called `drawing.jpg`. + * + * Note: The browser will either save the file immediately or prompt the user + * with a dialogue window. + * + * The image will only be downloaded as an animated GIF if it was loaded + * from a GIF file. See saveGif() to create new + * GIFs. + * + * @param {String} filename filename. Defaults to 'untitled'. + * @param {String} [extension] file extension, either 'png' or 'jpg'. + * Defaults to 'png'. + * + * @example + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/rockies.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('An image of a mountain landscape. The image is downloaded when the user presses the "s", "j", or "p" key.'); + * } + * + * // Save the image with different options when the user presses a key. + * function keyPressed() { + * if (key === 's') { + * img.save(); + * } else if (key === 'j') { + * img.save('rockies.jpg'); + * } else if (key === 'p') { + * img.save('rockies', 'png'); + * } + * } + * + *
+ */ + save(filename, extension) { + if (this.gifProperties) { + fn.encodeAndDownloadGif(this, filename); + } else { + fn.saveCanvas(this.canvas, filename, extension); } + } - /** - * Gets the index of the current frame in an animated GIF. - * - * @return {Number} index of the GIF's current frame. - * - * @example - *
- * - * let gif; - * - * // Load the image. - * function preload() { - * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A cartoon eye repeatedly looks around, then outwards. A number displayed in the bottom-left corner increases from 0 to 124, then repeats.'); - * } - * - * function draw() { - * // Get the index of the current GIF frame. - * let index = gif.getCurrentFrame(); - * - * // Display the image. - * image(gif, 0, 0); - * - * // Display the current frame. - * text(index, 10, 90); - * } - * - *
- */ - getCurrentFrame() { - if (this.gifProperties) { - const props = this.gifProperties; - return props.displayIndex % props.numFrames; - } + // GIF Section + /** + * Restarts an animated GIF at its first frame. + * + * @example + *
+ * + * let gif; + * + * // Load the image. + * function preload() { + * gif = loadImage('assets/arnott-wallace-wink-loop-once.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A cartoon face winks once and then freezes. Clicking resets the face and makes it wink again.'); + * } + * + * function draw() { + * background(255); + * + * // Display the image. + * image(gif, 0, 0); + * } + * + * // Reset the GIF when the user presses the mouse. + * function mousePressed() { + * gif.reset(); + * } + * + *
+ */ + reset() { + if (this.gifProperties) { + const props = this.gifProperties; + props.playing = true; + props.timeSinceStart = 0; + props.timeDisplayed = 0; + props.lastChangeTime = 0; + props.loopCount = 0; + props.displayIndex = 0; + this.drawingContext.putImageData(props.frames[0].image, 0, 0); } + } - /** - * Sets the current frame in an animated GIF. - * - * @param {Number} index index of the frame to display. - * - * @example - *
- * - * let gif; - * let frameSlider; - * - * // Load the image. - * function preload() { - * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Get the index of the last frame. - * let maxFrame = gif.numFrames() - 1; - * - * // Create a slider to control which frame is drawn. - * frameSlider = createSlider(0, maxFrame); - * frameSlider.position(10, 80); - * frameSlider.size(80); - * - * describe('A cartoon eye looks around when a slider is moved.'); - * } - * - * function draw() { - * // Get the slider's value. - * let index = frameSlider.value(); - * - * // Set the GIF's frame. - * gif.setFrame(index); - * - * // Display the image. - * image(gif, 0, 0); - * } - * - *
- */ - setFrame(index) { - if (this.gifProperties) { - const props = this.gifProperties; - if (index < props.numFrames && index >= 0) { - props.timeDisplayed = 0; - props.lastChangeTime = 0; - props.displayIndex = index; - this.drawingContext.putImageData(props.frames[index].image, 0, 0); - } else { - console.log( - 'Cannot set GIF to a frame number that is higher than total number of frames or below zero.' - ); - } - } + /** + * Gets the index of the current frame in an animated GIF. + * + * @return {Number} index of the GIF's current frame. + * + * @example + *
+ * + * let gif; + * + * // Load the image. + * function preload() { + * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A cartoon eye repeatedly looks around, then outwards. A number displayed in the bottom-left corner increases from 0 to 124, then repeats.'); + * } + * + * function draw() { + * // Get the index of the current GIF frame. + * let index = gif.getCurrentFrame(); + * + * // Display the image. + * image(gif, 0, 0); + * + * // Display the current frame. + * text(index, 10, 90); + * } + * + *
+ */ + getCurrentFrame() { + if (this.gifProperties) { + const props = this.gifProperties; + return props.displayIndex % props.numFrames; } + } - /** - * Returns the number of frames in an animated GIF. - * - * @return {Number} number of frames in the GIF. - * - * @example - *
- * - * let gif; - * - * // Load the image. - * function preload() { - * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A cartoon eye looks around. The text "n / 125" is shown at the bottom of the canvas.'); - * } - * - * function draw() { - * // Display the image. - * image(gif, 0, 0); - * - * // Display the current state of playback. - * let total = gif.numFrames(); - * let index = gif.getCurrentFrame(); - * text(`${index} / ${total}`, 30, 90); - * } - * - *
- */ - numFrames() { - if (this.gifProperties) { - return this.gifProperties.numFrames; + /** + * Sets the current frame in an animated GIF. + * + * @param {Number} index index of the frame to display. + * + * @example + *
+ * + * let gif; + * let frameSlider; + * + * // Load the image. + * function preload() { + * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Get the index of the last frame. + * let maxFrame = gif.numFrames() - 1; + * + * // Create a slider to control which frame is drawn. + * frameSlider = createSlider(0, maxFrame); + * frameSlider.position(10, 80); + * frameSlider.size(80); + * + * describe('A cartoon eye looks around when a slider is moved.'); + * } + * + * function draw() { + * // Get the slider's value. + * let index = frameSlider.value(); + * + * // Set the GIF's frame. + * gif.setFrame(index); + * + * // Display the image. + * image(gif, 0, 0); + * } + * + *
+ */ + setFrame(index) { + if (this.gifProperties) { + const props = this.gifProperties; + if (index < props.numFrames && index >= 0) { + props.timeDisplayed = 0; + props.lastChangeTime = 0; + props.displayIndex = index; + this.drawingContext.putImageData(props.frames[index].image, 0, 0); + } else { + console.log( + 'Cannot set GIF to a frame number that is higher than total number of frames or below zero.' + ); } } + } - /** - * Plays an animated GIF that was paused with - * img.pause(). - * - * @example - *
- * - * let gif; - * - * // Load the image. - * function preload() { - * gif = loadImage('assets/nancy-liang-wind-loop-forever.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.'); - * } - * - * function draw() { - * background(255); - * image(gif, 0, 0); - * } - * - * // Pause the GIF when the user presses the mouse. - * function mousePressed() { - * gif.pause(); - * } - * - * // Play the GIF when the user releases the mouse. - * function mouseReleased() { - * gif.play(); - * } - * - *
- */ - play() { - if (this.gifProperties) { - this.gifProperties.playing = true; - } + /** + * Returns the number of frames in an animated GIF. + * + * @return {Number} number of frames in the GIF. + * + * @example + *
+ * + * let gif; + * + * // Load the image. + * function preload() { + * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A cartoon eye looks around. The text "n / 125" is shown at the bottom of the canvas.'); + * } + * + * function draw() { + * // Display the image. + * image(gif, 0, 0); + * + * // Display the current state of playback. + * let total = gif.numFrames(); + * let index = gif.getCurrentFrame(); + * text(`${index} / ${total}`, 30, 90); + * } + * + *
+ */ + numFrames() { + if (this.gifProperties) { + return this.gifProperties.numFrames; } + } - /** - * Pauses an animated GIF. - * - * The GIF can be resumed by calling - * img.play(). - * - * @example - *
- * - * let gif; - * - * // Load the image. - * function preload() { - * gif = loadImage('assets/nancy-liang-wind-loop-forever.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.'); - * } - * - * function draw() { - * background(255); - * - * // Display the image. - * image(gif, 0, 0); - * } - * - * // Pause the GIF when the user presses the mouse. - * function mousePressed() { - * gif.pause(); - * } - * - * // Play the GIF when the user presses the mouse. - * function mouseReleased() { - * gif.play(); - * } - * - *
- */ - pause() { - if (this.gifProperties) { - this.gifProperties.playing = false; - } + /** + * Plays an animated GIF that was paused with + * img.pause(). + * + * @example + *
+ * + * let gif; + * + * // Load the image. + * function preload() { + * gif = loadImage('assets/nancy-liang-wind-loop-forever.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.'); + * } + * + * function draw() { + * background(255); + * image(gif, 0, 0); + * } + * + * // Pause the GIF when the user presses the mouse. + * function mousePressed() { + * gif.pause(); + * } + * + * // Play the GIF when the user releases the mouse. + * function mouseReleased() { + * gif.play(); + * } + * + *
+ */ + play() { + if (this.gifProperties) { + this.gifProperties.playing = true; } + } - /** - * Changes the delay between frames in an animated GIF. - * - * The first parameter, `delay`, is the length of the delay in milliseconds. - * - * The second parameter, `index`, is optional. If provided, only the frame - * at `index` will have its delay modified. All other frames will keep - * their default delay. - * - * @param {Number} d delay in milliseconds between switching frames. - * @param {Number} [index] index of the frame that will have its delay modified. - * - * @example - *
- * - * let gifFast; - * let gifSlow; - * - * // Load the images. - * function preload() { - * gifFast = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); - * gifSlow = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Resize the images. - * gifFast.resize(50, 50); - * gifSlow.resize(50, 50); - * - * // Set the delay lengths. - * gifFast.delay(10); - * gifSlow.delay(100); - * - * describe('Two animated eyes looking around. The eye on the left moves faster than the eye on the right.'); - * } - * - * function draw() { - * // Display the images. - * image(gifFast, 0, 0); - * image(gifSlow, 50, 0); - * } - * - *
- * - *
- * - * let gif; - * - * // Load the image. - * function preload() { - * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); - * } - * - * function setup() { - * createCanvas(100, 100); - * - * // Set the delay of frame 67. - * gif.delay(3000, 67); - * - * describe('An animated eye looking around. It pauses for three seconds while it looks down.'); - * } - * - * function draw() { - * // Display the image. - * image(gif, 0, 0); - * } - * - *
- */ - delay(d, index) { - if (this.gifProperties) { - const props = this.gifProperties; - if (index < props.numFrames && index >= 0) { - props.frames[index].delay = d; - } else { - // change all frames - for (const frame of props.frames) { - frame.delay = d; - } + /** + * Pauses an animated GIF. + * + * The GIF can be resumed by calling + * img.play(). + * + * @example + *
+ * + * let gif; + * + * // Load the image. + * function preload() { + * gif = loadImage('assets/nancy-liang-wind-loop-forever.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * describe('A drawing of a child with hair blowing in the wind. The animation freezes when clicked and resumes when released.'); + * } + * + * function draw() { + * background(255); + * + * // Display the image. + * image(gif, 0, 0); + * } + * + * // Pause the GIF when the user presses the mouse. + * function mousePressed() { + * gif.pause(); + * } + * + * // Play the GIF when the user presses the mouse. + * function mouseReleased() { + * gif.play(); + * } + * + *
+ */ + pause() { + if (this.gifProperties) { + this.gifProperties.playing = false; + } + } + + /** + * Changes the delay between frames in an animated GIF. + * + * The first parameter, `delay`, is the length of the delay in milliseconds. + * + * The second parameter, `index`, is optional. If provided, only the frame + * at `index` will have its delay modified. All other frames will keep + * their default delay. + * + * @param {Number} d delay in milliseconds between switching frames. + * @param {Number} [index] index of the frame that will have its delay modified. + * + * @example + *
+ * + * let gifFast; + * let gifSlow; + * + * // Load the images. + * function preload() { + * gifFast = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); + * gifSlow = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Resize the images. + * gifFast.resize(50, 50); + * gifSlow.resize(50, 50); + * + * // Set the delay lengths. + * gifFast.delay(10); + * gifSlow.delay(100); + * + * describe('Two animated eyes looking around. The eye on the left moves faster than the eye on the right.'); + * } + * + * function draw() { + * // Display the images. + * image(gifFast, 0, 0); + * image(gifSlow, 50, 0); + * } + * + *
+ * + *
+ * + * let gif; + * + * // Load the image. + * function preload() { + * gif = loadImage('assets/arnott-wallace-eye-loop-forever.gif'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Set the delay of frame 67. + * gif.delay(3000, 67); + * + * describe('An animated eye looking around. It pauses for three seconds while it looks down.'); + * } + * + * function draw() { + * // Display the image. + * image(gif, 0, 0); + * } + * + *
+ */ + delay(d, index) { + if (this.gifProperties) { + const props = this.gifProperties; + if (index < props.numFrames && index >= 0) { + props.frames[index].delay = d; + } else { + // change all frames + for (const frame of props.frames) { + frame.delay = d; } } } - }; + } +}; + +function image(p5, fn){ + /** + * A class to describe an image. + * + * Images are rectangular grids of pixels that can be displayed and modified. + * + * Existing images can be loaded by calling + * loadImage(). Blank images can be created by + * calling createImage(). `p5.Image` objects + * have methods for common tasks such as applying filters and modifying + * pixel values. + * + * @example + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('An image of a brick wall.'); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load the image. + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100); + * + * // Apply the GRAY filter. + * img.filter(GRAY); + * + * // Display the image. + * image(img, 0, 0); + * + * describe('A grayscale image of a brick wall.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Image object. + * let img = createImage(66, 66); + * + * // Load the image's pixels. + * img.loadPixels(); + * + * // Set the pixels to black. + * for (let x = 0; x < img.width; x += 1) { + * for (let y = 0; y < img.height; y += 1) { + * img.set(x, y, 0); + * } + * } + * + * // Update the image. + * img.updatePixels(); + * + * // Display the image. + * image(img, 17, 17); + * + * describe('A black square drawn in the middle of a gray square.'); + * } + * + *
+ * + * @class p5.Image + * @param {Number} width + * @param {Number} height + */ + p5.Image = Image; /** * The image's width in pixels. @@ -2003,6 +2006,7 @@ function image(p5, fn){ } export default image; +export { Image }; if(typeof p5 !== 'undefined'){ image(p5, p5.prototype); diff --git a/src/image/pixels.js b/src/image/pixels.js index 3b0716c657..a85348bef5 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -410,8 +410,7 @@ function pixels(p5, fn){ dstImage.noLights(); dstImage.blendMode(dstImage.BLEND); dstImage.imageMode(dstImage.CORNER); - p5.RendererGL.prototype.image.call( - dstImage._renderer, + dstImage._renderer.image( srcImage, sx + sxMod, sy + syMod, diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index 2d663fa531..c4f14e8c2e 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -6,54 +6,69 @@ import * as constants from '../core/constants'; -function vector(p5, fn) { - /// HELPERS FOR REMAINDER METHOD - const calculateRemainder2D = function (xComponent, yComponent) { - if (xComponent !== 0) { - this.x = this.x % xComponent; - } - if (yComponent !== 0) { - this.y = this.y % yComponent; - } - return this; - }; - - const calculateRemainder3D = function (xComponent, yComponent, zComponent) { - if (xComponent !== 0) { - this.x = this.x % xComponent; - } - if (yComponent !== 0) { - this.y = this.y % yComponent; - } - if (zComponent !== 0) { - this.z = this.z % zComponent; +class Vector { + // This is how it comes in with createVector() + // This check if the first argument is a function + constructor(...args) { + let x, y, z; + if (typeof args[0] === 'function') { + this.isPInst = true; + this._fromRadians = args[0]; + this._toRadians = args[1]; + x = args[2] || 0; + y = args[3] || 0; + z = args[4] || 0; + // This is what we'll get with new Vector() + } else { + x = args[0] || 0; + y = args[1] || 0; + z = args[2] || 0; } - return this; - }; + this.x = x; + this.y = y; + this.z = z; + } /** - * A class to describe a two or three-dimensional vector. + * Returns a string representation of a vector. * - * A vector can be thought of in different ways. In one view, a vector is like - * an arrow pointing in space. Vectors have both magnitude (length) and - * direction. + * Calling `toString()` is useful for printing vectors to the console while + * debugging. * - * `p5.Vector` objects are often used to program motion because they simplify - * the math. For example, a moving ball has a position and a velocity. - * Position describes where the ball is in space. The ball's position vector - * extends from the origin to the ball's center. Velocity describes the ball's - * speed and the direction it's moving. If the ball is moving straight up, its - * velocity vector points straight up. Adding the ball's velocity vector to - * its position vector moves it, as in `pos.add(vel)`. Vector math relies on - * methods inside the `p5.Vector` class. + * @return {String} string representation of the vector. * - * Note: createVector() is the recommended way - * to make an instance of this class. + * @example + *
+ * + * function setup() { + * let v = createVector(20, 30); + * + * // Prints 'p5.Vector Object : [20, 30, 0]'. + * print(v.toString()); + * } + * + *
+ */ + toString() { + return `p5.Vector Object : [${this.x}, ${this.y}, ${this.z}]`; + } + + /** + * Sets the vector's `x`, `y`, and `z` components. + * + * `set()` can use separate numbers, as in `v.set(1, 2, 3)`, a + * p5.Vector object, as in `v.set(v2)`, or an + * array of numbers, as in `v.set([1, 2, 3])`. + * + * If a value isn't provided for a component, it will be set to 0. For + * example, `v.set(4, 5)` sets `v.x` to 4, `v.y` to 5, and `v.z` to 0. + * Calling `set()` with no arguments, as in `v.set()`, sets all the vector's + * components to 0. * - * @class p5.Vector * @param {Number} [x] x component of the vector. * @param {Number} [y] y component of the vector. * @param {Number} [z] z component of the vector. + * @chainable * @example *
* @@ -62,3744 +77,3731 @@ function vector(p5, fn) { * * background(200); * - * // Create p5.Vector objects. - * let p1 = createVector(25, 25); + * // Style the points. + * strokeWeight(5); + * + * // Top left. + * let pos = createVector(25, 25); + * point(pos); + * + * // Top right. + * // set() with numbers. + * pos.set(75, 25); + * point(pos); + * + * // Bottom right. + * // set() with a p5.Vector. * let p2 = createVector(75, 75); + * pos.set(p2); + * point(pos); + * + * // Bottom left. + * // set() with an array. + * let arr = [25, 75]; + * pos.set(arr); + * point(pos); + * + * describe('Four black dots arranged in a square on a gray background.'); + * } + * + *
+ */ + /** + * @param {p5.Vector|Number[]} value vector to set. + * @chainable + */ + set(x, y, z) { + if (x instanceof Vector) { + this.x = x.x || 0; + this.y = x.y || 0; + this.z = x.z || 0; + return this; + } + if (Array.isArray(x)) { + this.x = x[0] || 0; + this.y = x[1] || 0; + this.z = x[2] || 0; + return this; + } + this.x = x || 0; + this.y = y || 0; + this.z = z || 0; + + return this; + } + + /** + * Returns a copy of the p5.Vector object. + * + * @return {p5.Vector} copy of the p5.Vector object. + * + * @example + *
+ * + * function setup() { + * createCanvas(100 ,100); + * + * background(200); + * + * // Create a p5.Vector object. + * let pos = createVector(50, 50); + * + * // Make a copy. + * let pc = pos.copy(); + * + * // Draw the point. + * strokeWeight(5); + * point(pc); + * + * describe('A black point drawn in the middle of a gray square.'); + * } + * + *
+ */ + copy() { + if (this.isPInst) { + return new Vector( + this._fromRadians, + this._toRadians, + this.x, + this.y, + this.z + ); + } else { + return new Vector(this.x, this.y, this.z); + } + } + + /** + * Adds to a vector's `x`, `y`, and `z` components. + * + * `add()` can use separate numbers, as in `v.add(1, 2, 3)`, + * another p5.Vector object, as in `v.add(v2)`, or + * an array of numbers, as in `v.add([1, 2, 3])`. + * + * If a value isn't provided for a component, it won't change. For + * example, `v.add(4, 5)` adds 4 to `v.x`, 5 to `v.y`, and 0 to `v.z`. + * Calling `add()` with no arguments, as in `v.add()`, has no effect. + * + * The static version of `add()`, as in `p5.Vector.add(v2, v1)`, returns a new + * p5.Vector object and doesn't change the + * originals. + * + * @param {Number} x x component of the vector to be added. + * @param {Number} [y] y component of the vector to be added. + * @param {Number} [z] z component of the vector to be added. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); * * // Style the points. * strokeWeight(5); * - * // Draw the first point using a p5.Vector. - * point(p1); + * // Top left. + * let pos = createVector(25, 25); + * point(pos); * - * // Draw the second point using a p5.Vector's components. - * point(p2.x, p2.y); + * // Top right. + * // Add numbers. + * pos.add(50, 0); + * point(pos); * - * describe('Two black dots on a gray square, one at the top left and the other at the bottom right.'); + * // Bottom right. + * // Add a p5.Vector. + * let p2 = createVector(0, 50); + * pos.add(p2); + * point(pos); + * + * // Bottom left. + * // Add an array. + * let arr = [-50, 0]; + * pos.add(arr); + * point(pos); + * + * describe('Four black dots arranged in a square on a gray background.'); * } * *
* *
* - * let pos; - * let vel; - * * function setup() { * createCanvas(100, 100); * - * // Create p5.Vector objects. - * pos = createVector(50, 100); - * vel = createVector(0, -1); + * background(200); * - * describe('A black dot moves from bottom to top on a gray square. The dot reappears at the bottom when it reaches the top.'); + * // Top left. + * let p1 = createVector(25, 25); + * + * // Center. + * let p2 = createVector(50, 50); + * + * // Bottom right. + * // Add p1 and p2. + * let p3 = p5.Vector.add(p1, p2); + * + * // Draw the points. + * strokeWeight(5); + * point(p1); + * point(p2); + * point(p3); + * + * describe('Three black dots in a diagonal line from top left to bottom right.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Three arrows drawn on a gray square. A red arrow extends from the top left corner to the center. A blue arrow extends from the tip of the red arrow. A purple arrow extends from the origin to the tip of the blue arrow.'); * } * * function draw() { * background(200); * - * // Add velocity to position. - * pos.add(vel); + * let origin = createVector(0, 0); * - * // If the dot reaches the top of the canvas, - * // restart from the bottom. - * if (pos.y < 0) { - * pos.y = 100; - * } + * // Draw the red arrow. + * let v1 = createVector(50, 50); + * drawArrow(origin, v1, 'red'); * - * // Draw the dot. - * strokeWeight(5); - * point(pos); + * // Draw the blue arrow. + * let v2 = createVector(-30, 20); + * drawArrow(v1, v2, 'blue'); + * + * // Purple arrow. + * let v3 = p5.Vector.add(v1, v2); + * drawArrow(origin, v3, 'purple'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); * } * *
*/ - p5.Vector = class Vector { - // This is how it comes in with createVector() - // This check if the first argument is a function - constructor(...args) { - let x, y, z; - if (typeof args[0] === 'function') { - this.isPInst = true; - this._fromRadians = args[0]; - this._toRadians = args[1]; - x = args[2] || 0; - y = args[3] || 0; - z = args[4] || 0; - // This is what we'll get with new p5.Vector() - } else { - x = args[0] || 0; - y = args[1] || 0; - z = args[2] || 0; - } - this.x = x; - this.y = y; - this.z = z; - } - - /** - * Returns a string representation of a vector. - * - * Calling `toString()` is useful for printing vectors to the console while - * debugging. - * - * @return {String} string representation of the vector. - * - * @example - *
- * - * function setup() { - * let v = createVector(20, 30); - * - * // Prints 'p5.Vector Object : [20, 30, 0]'. - * print(v.toString()); - * } - * - *
- */ - toString() { - return `p5.Vector Object : [${this.x}, ${this.y}, ${this.z}]`; + /** + * @param {p5.Vector|Number[]} value The vector to add + * @chainable + */ + add(x, y, z) { + if (x instanceof Vector) { + this.x += x.x || 0; + this.y += x.y || 0; + this.z += x.z || 0; + return this; } - - /** - * Sets the vector's `x`, `y`, and `z` components. - * - * `set()` can use separate numbers, as in `v.set(1, 2, 3)`, a - * p5.Vector object, as in `v.set(v2)`, or an - * array of numbers, as in `v.set([1, 2, 3])`. - * - * If a value isn't provided for a component, it will be set to 0. For - * example, `v.set(4, 5)` sets `v.x` to 4, `v.y` to 5, and `v.z` to 0. - * Calling `set()` with no arguments, as in `v.set()`, sets all the vector's - * components to 0. - * - * @param {Number} [x] x component of the vector. - * @param {Number} [y] y component of the vector. - * @param {Number} [z] z component of the vector. - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Top left. - * let pos = createVector(25, 25); - * point(pos); - * - * // Top right. - * // set() with numbers. - * pos.set(75, 25); - * point(pos); - * - * // Bottom right. - * // set() with a p5.Vector. - * let p2 = createVector(75, 75); - * pos.set(p2); - * point(pos); - * - * // Bottom left. - * // set() with an array. - * let arr = [25, 75]; - * pos.set(arr); - * point(pos); - * - * describe('Four black dots arranged in a square on a gray background.'); - * } - * - *
- */ - /** - * @param {p5.Vector|Number[]} value vector to set. - * @chainable - */ - set(x, y, z) { - if (x instanceof p5.Vector) { - this.x = x.x || 0; - this.y = x.y || 0; - this.z = x.z || 0; - return this; - } - if (Array.isArray(x)) { - this.x = x[0] || 0; - this.y = x[1] || 0; - this.z = x[2] || 0; - return this; - } - this.x = x || 0; - this.y = y || 0; - this.z = z || 0; - + if (Array.isArray(x)) { + this.x += x[0] || 0; + this.y += x[1] || 0; + this.z += x[2] || 0; return this; } + this.x += x || 0; + this.y += y || 0; + this.z += z || 0; + return this; + } - /** - * Returns a copy of the p5.Vector object. - * - * @return {p5.Vector} copy of the p5.Vector object. - * - * @example - *
- * - * function setup() { - * createCanvas(100 ,100); - * - * background(200); - * - * // Create a p5.Vector object. - * let pos = createVector(50, 50); - * - * // Make a copy. - * let pc = pos.copy(); - * - * // Draw the point. - * strokeWeight(5); - * point(pc); - * - * describe('A black point drawn in the middle of a gray square.'); - * } - * - *
- */ - copy() { - if (this.isPInst) { - return new p5.Vector( - this._fromRadians, - this._toRadians, - this.x, - this.y, - this.z + /** + * Performs modulo (remainder) division with a vector's `x`, `y`, and `z` + * components. + * + * `rem()` can use separate numbers, as in `v.rem(1, 2, 3)`, + * another p5.Vector object, as in `v.rem(v2)`, or + * an array of numbers, as in `v.rem([1, 2, 3])`. + * + * If only one value is provided, as in `v.rem(2)`, then all the components + * will be set to their values modulo 2. If two values are provided, as in + * `v.rem(2, 3)`, then `v.z` won't change. Calling `rem()` with no + * arguments, as in `v.rem()`, has no effect. + * + * The static version of `rem()`, as in `p5.Vector.rem(v2, v1)`, returns a + * new p5.Vector object and doesn't change the + * originals. + * + * @param {Number} x x component of divisor vector. + * @param {Number} y y component of divisor vector. + * @param {Number} z z component of divisor vector. + * @chainable + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(3, 4, 5); + * + * // Divide numbers. + * v.rem(2); + * + * // Prints 'p5.Vector Object : [1, 0, 1]'. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(3, 4, 5); + * + * // Divide numbers. + * v.rem(2, 3); + * + * // Prints 'p5.Vector Object : [1, 1, 5]'. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(3, 4, 5); + * + * // Divide numbers. + * v.rem(2, 3, 4); + * + * // Prints 'p5.Vector Object : [1, 1, 1]'. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v1 = createVector(3, 4, 5); + * let v2 = createVector(2, 3, 4); + * + * // Divide a p5.Vector. + * v1.rem(v2); + * + * // Prints 'p5.Vector Object : [1, 1, 1]'. + * print(v1.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(3, 4, 5); + * + * // Divide an array. + * let arr = [2, 3, 4]; + * v.rem(arr); + * + * // Prints 'p5.Vector Object : [1, 1, 1]'. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v1 = createVector(3, 4, 5); + * let v2 = createVector(2, 3, 4); + * + * // Divide without modifying the original vectors. + * let v3 = p5.Vector.rem(v1, v2); + * + * // Prints 'p5.Vector Object : [1, 1, 1]'. + * print(v3.toString()); + * } + * + *
+ */ + /** + * @param {p5.Vector | Number[]} value divisor vector. + * @chainable + */ + rem(x, y, z) { + if (x instanceof Vector) { + if ([x.x, x.y, x.z].every(Number.isFinite)) { + const xComponent = parseFloat(x.x); + const yComponent = parseFloat(x.y); + const zComponent = parseFloat(x.z); + return calculateRemainder3D.call( + this, + xComponent, + yComponent, + zComponent ); - } else { - return new p5.Vector(this.x, this.y, this.z); } - } - - /** - * Adds to a vector's `x`, `y`, and `z` components. - * - * `add()` can use separate numbers, as in `v.add(1, 2, 3)`, - * another p5.Vector object, as in `v.add(v2)`, or - * an array of numbers, as in `v.add([1, 2, 3])`. - * - * If a value isn't provided for a component, it won't change. For - * example, `v.add(4, 5)` adds 4 to `v.x`, 5 to `v.y`, and 0 to `v.z`. - * Calling `add()` with no arguments, as in `v.add()`, has no effect. - * - * The static version of `add()`, as in `p5.Vector.add(v2, v1)`, returns a new - * p5.Vector object and doesn't change the - * originals. - * - * @param {Number} x x component of the vector to be added. - * @param {Number} [y] y component of the vector to be added. - * @param {Number} [z] z component of the vector to be added. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Top left. - * let pos = createVector(25, 25); - * point(pos); - * - * // Top right. - * // Add numbers. - * pos.add(50, 0); - * point(pos); - * - * // Bottom right. - * // Add a p5.Vector. - * let p2 = createVector(0, 50); - * pos.add(p2); - * point(pos); - * - * // Bottom left. - * // Add an array. - * let arr = [-50, 0]; - * pos.add(arr); - * point(pos); - * - * describe('Four black dots arranged in a square on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Top left. - * let p1 = createVector(25, 25); - * - * // Center. - * let p2 = createVector(50, 50); - * - * // Bottom right. - * // Add p1 and p2. - * let p3 = p5.Vector.add(p1, p2); - * - * // Draw the points. - * strokeWeight(5); - * point(p1); - * point(p2); - * point(p3); - * - * describe('Three black dots in a diagonal line from top left to bottom right.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Three arrows drawn on a gray square. A red arrow extends from the top left corner to the center. A blue arrow extends from the tip of the red arrow. A purple arrow extends from the origin to the tip of the blue arrow.'); - * } - * - * function draw() { - * background(200); - * - * let origin = createVector(0, 0); - * - * // Draw the red arrow. - * let v1 = createVector(50, 50); - * drawArrow(origin, v1, 'red'); - * - * // Draw the blue arrow. - * let v2 = createVector(-30, 20); - * drawArrow(v1, v2, 'blue'); - * - * // Purple arrow. - * let v3 = p5.Vector.add(v1, v2); - * drawArrow(origin, v3, 'purple'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - /** - * @param {p5.Vector|Number[]} value The vector to add - * @chainable - */ - add(x, y, z) { - if (x instanceof p5.Vector) { - this.x += x.x || 0; - this.y += x.y || 0; - this.z += x.z || 0; - return this; + } else if (Array.isArray(x)) { + if (x.every(element => Number.isFinite(element))) { + if (x.length === 2) { + return calculateRemainder2D.call(this, x[0], x[1]); + } + if (x.length === 3) { + return calculateRemainder3D.call(this, x[0], x[1], x[2]); + } } - if (Array.isArray(x)) { - this.x += x[0] || 0; - this.y += x[1] || 0; - this.z += x[2] || 0; + } else if (arguments.length === 1) { + if (Number.isFinite(arguments[0]) && arguments[0] !== 0) { + this.x = this.x % arguments[0]; + this.y = this.y % arguments[0]; + this.z = this.z % arguments[0]; return this; } - this.x += x || 0; - this.y += y || 0; - this.z += z || 0; - return this; - } - - /** - * Performs modulo (remainder) division with a vector's `x`, `y`, and `z` - * components. - * - * `rem()` can use separate numbers, as in `v.rem(1, 2, 3)`, - * another p5.Vector object, as in `v.rem(v2)`, or - * an array of numbers, as in `v.rem([1, 2, 3])`. - * - * If only one value is provided, as in `v.rem(2)`, then all the components - * will be set to their values modulo 2. If two values are provided, as in - * `v.rem(2, 3)`, then `v.z` won't change. Calling `rem()` with no - * arguments, as in `v.rem()`, has no effect. - * - * The static version of `rem()`, as in `p5.Vector.rem(v2, v1)`, returns a - * new p5.Vector object and doesn't change the - * originals. - * - * @param {Number} x x component of divisor vector. - * @param {Number} y y component of divisor vector. - * @param {Number} z z component of divisor vector. - * @chainable - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(3, 4, 5); - * - * // Divide numbers. - * v.rem(2); - * - * // Prints 'p5.Vector Object : [1, 0, 1]'. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(3, 4, 5); - * - * // Divide numbers. - * v.rem(2, 3); - * - * // Prints 'p5.Vector Object : [1, 1, 5]'. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(3, 4, 5); - * - * // Divide numbers. - * v.rem(2, 3, 4); - * - * // Prints 'p5.Vector Object : [1, 1, 1]'. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v1 = createVector(3, 4, 5); - * let v2 = createVector(2, 3, 4); - * - * // Divide a p5.Vector. - * v1.rem(v2); - * - * // Prints 'p5.Vector Object : [1, 1, 1]'. - * print(v1.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(3, 4, 5); - * - * // Divide an array. - * let arr = [2, 3, 4]; - * v.rem(arr); - * - * // Prints 'p5.Vector Object : [1, 1, 1]'. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v1 = createVector(3, 4, 5); - * let v2 = createVector(2, 3, 4); - * - * // Divide without modifying the original vectors. - * let v3 = p5.Vector.rem(v1, v2); - * - * // Prints 'p5.Vector Object : [1, 1, 1]'. - * print(v3.toString()); - * } - * - *
- */ - /** - * @param {p5.Vector | Number[]} value divisor vector. - * @chainable - */ - rem(x, y, z) { - if (x instanceof p5.Vector) { - if ([x.x, x.y, x.z].every(Number.isFinite)) { - const xComponent = parseFloat(x.x); - const yComponent = parseFloat(x.y); - const zComponent = parseFloat(x.z); - return calculateRemainder3D.call( + } else if (arguments.length === 2) { + const vectorComponents = [...arguments]; + if (vectorComponents.every(element => Number.isFinite(element))) { + if (vectorComponents.length === 2) { + return calculateRemainder2D.call( this, - xComponent, - yComponent, - zComponent + vectorComponents[0], + vectorComponents[1] ); } - } else if (Array.isArray(x)) { - if (x.every(element => Number.isFinite(element))) { - if (x.length === 2) { - return calculateRemainder2D.call(this, x[0], x[1]); - } - if (x.length === 3) { - return calculateRemainder3D.call(this, x[0], x[1], x[2]); - } - } - } else if (arguments.length === 1) { - if (Number.isFinite(arguments[0]) && arguments[0] !== 0) { - this.x = this.x % arguments[0]; - this.y = this.y % arguments[0]; - this.z = this.z % arguments[0]; - return this; - } - } else if (arguments.length === 2) { - const vectorComponents = [...arguments]; - if (vectorComponents.every(element => Number.isFinite(element))) { - if (vectorComponents.length === 2) { - return calculateRemainder2D.call( - this, - vectorComponents[0], - vectorComponents[1] - ); - } - } - } else if (arguments.length === 3) { - const vectorComponents = [...arguments]; - if (vectorComponents.every(element => Number.isFinite(element))) { - if (vectorComponents.length === 3) { - return calculateRemainder3D.call( - this, - vectorComponents[0], - vectorComponents[1], - vectorComponents[2] - ); - } - } } - } - - /** - * Subtracts from a vector's `x`, `y`, and `z` components. - * - * `sub()` can use separate numbers, as in `v.sub(1, 2, 3)`, another - * p5.Vector object, as in `v.sub(v2)`, or an array - * of numbers, as in `v.sub([1, 2, 3])`. - * - * If a value isn't provided for a component, it won't change. For - * example, `v.sub(4, 5)` subtracts 4 from `v.x`, 5 from `v.y`, and 0 from `v.z`. - * Calling `sub()` with no arguments, as in `v.sub()`, has no effect. - * - * The static version of `sub()`, as in `p5.Vector.sub(v2, v1)`, returns a new - * p5.Vector object and doesn't change the - * originals. - * - * @param {Number} x x component of the vector to subtract. - * @param {Number} [y] y component of the vector to subtract. - * @param {Number} [z] z component of the vector to subtract. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Bottom right. - * let pos = createVector(75, 75); - * point(pos); - * - * // Top right. - * // Subtract numbers. - * pos.sub(0, 50); - * point(pos); - * - * // Top left. - * // Subtract a p5.Vector. - * let p2 = createVector(50, 0); - * pos.sub(p2); - * point(pos); - * - * // Bottom left. - * // Subtract an array. - * let arr = [0, -50]; - * pos.sub(arr); - * point(pos); - * - * describe('Four black dots arranged in a square on a gray background.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create p5.Vector objects. - * let p1 = createVector(75, 75); - * let p2 = createVector(50, 50); - * - * // Subtract with modifying the original vectors. - * let p3 = p5.Vector.sub(p1, p2); - * - * // Draw the points. - * strokeWeight(5); - * point(p1); - * point(p2); - * point(p3); - * - * describe('Three black dots in a diagonal line from top left to bottom right.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow.'); - * } - * - * function draw() { - * background(200); - * - * let origin = createVector(0, 0); - * - * // Draw the red arrow. - * let v1 = createVector(50, 50); - * drawArrow(origin, v1, 'red'); - * - * // Draw the blue arrow. - * let v2 = createVector(20, 70); - * drawArrow(origin, v2, 'blue'); - * - * // Purple arrow. - * let v3 = p5.Vector.sub(v2, v1); - * drawArrow(v1, v3, 'purple'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - /** - * @param {p5.Vector|Number[]} value the vector to subtract - * @chainable - */ - sub(x, y, z) { - if (x instanceof p5.Vector) { - this.x -= x.x || 0; - this.y -= x.y || 0; - this.z -= x.z || 0; - return this; - } - if (Array.isArray(x)) { - this.x -= x[0] || 0; - this.y -= x[1] || 0; - this.z -= x[2] || 0; - return this; - } - this.x -= x || 0; - this.y -= y || 0; - this.z -= z || 0; - return this; - } - - /** - * Multiplies a vector's `x`, `y`, and `z` components. - * - * `mult()` can use separate numbers, as in `v.mult(1, 2, 3)`, another - * p5.Vector object, as in `v.mult(v2)`, or an array - * of numbers, as in `v.mult([1, 2, 3])`. - * - * If only one value is provided, as in `v.mult(2)`, then all the components - * will be multiplied by 2. If a value isn't provided for a component, it - * won't change. For example, `v.mult(4, 5)` multiplies `v.x` by, `v.y` by 5, - * and `v.z` by 1. Calling `mult()` with no arguments, as in `v.mult()`, has - * no effect. - * - * The static version of `mult()`, as in `p5.Vector.mult(v, 2)`, returns a new - * p5.Vector object and doesn't change the - * originals. - * - * @param {Number} n The number to multiply with the vector - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Top-left. - * let p = createVector(25, 25); - * point(p); - * - * // Center. - * // Multiply all components by 2. - * p.mult(2); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.'); - * } - * - *
- * - *
- * - * function setup() { - * strokeWeight(5); - * - * // Top-left. - * let p = createVector(25, 25); - * point(p); - * - * // Bottom-right. - * // Multiply p.x * 2 and p.y * 3 - * p.mult(2, 3); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Top-left. - * let p = createVector(25, 25); - * point(p); - * - * // Bottom-right. - * // Multiply p.x * 2 and p.y * 3 - * let arr = [2, 3]; - * p.mult(arr); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Top-left. - * let p = createVector(25, 25); - * point(p); - * - * // Bottom-right. - * // Multiply p.x * p2.x and p.y * p2.y - * let p2 = createVector(2, 3); - * p.mult(p2); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Top-left. - * let p = createVector(25, 25); - * point(p); - * - * // Bottom-right. - * // Create a new p5.Vector with - * // p3.x = p.x * p2.x - * // p3.y = p.y * p2.y - * let p2 = createVector(2, 3); - * let p3 = p5.Vector.mult(p, p2); - * point(p3); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Two arrows extending from the top left corner. The blue arrow is twice the length of the red arrow.'); - * } - * function draw() { - * background(200); - * - * let origin = createVector(0, 0); - * - * // Draw the red arrow. - * let v1 = createVector(25, 25); - * drawArrow(origin, v1, 'red'); - * - * // Draw the blue arrow. - * let v2 = p5.Vector.mult(v1, 2); - * drawArrow(origin, v2, 'blue'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - /** - * @param {Number} x number to multiply with the x component of the vector. - * @param {Number} y number to multiply with the y component of the vector. - * @param {Number} [z] number to multiply with the z component of the vector. - * @chainable - */ - /** - * @param {Number[]} arr array to multiply with the components of the vector. - * @chainable - */ - /** - * @param {p5.Vector} v vector to multiply with the components of the original vector. - * @chainable - */ - mult(x, y, z) { - if (x instanceof p5.Vector) { - // new p5.Vector will check that values are valid upon construction but it's possible - // that someone could change the value of a component after creation, which is why we still - // perform this check - if ( - Number.isFinite(x.x) && - Number.isFinite(x.y) && - Number.isFinite(x.z) && - typeof x.x === 'number' && - typeof x.y === 'number' && - typeof x.z === 'number' - ) { - this.x *= x.x; - this.y *= x.y; - this.z *= x.z; - } else { - console.warn( - 'p5.Vector.prototype.mult:', - 'x contains components that are either undefined or not finite numbers' - ); - } - return this; - } - if (Array.isArray(x)) { - if ( - x.every(element => Number.isFinite(element)) && - x.every(element => typeof element === 'number') - ) { - if (x.length === 1) { - this.x *= x[0]; - this.y *= x[0]; - this.z *= x[0]; - } else if (x.length === 2) { - this.x *= x[0]; - this.y *= x[1]; - } else if (x.length === 3) { - this.x *= x[0]; - this.y *= x[1]; - this.z *= x[2]; - } - } else { - console.warn( - 'p5.Vector.prototype.mult:', - 'x contains elements that are either undefined or not finite numbers' + } else if (arguments.length === 3) { + const vectorComponents = [...arguments]; + if (vectorComponents.every(element => Number.isFinite(element))) { + if (vectorComponents.length === 3) { + return calculateRemainder3D.call( + this, + vectorComponents[0], + vectorComponents[1], + vectorComponents[2] ); } - return this; } + } + } - const vectorComponents = [...arguments]; + /** + * Subtracts from a vector's `x`, `y`, and `z` components. + * + * `sub()` can use separate numbers, as in `v.sub(1, 2, 3)`, another + * p5.Vector object, as in `v.sub(v2)`, or an array + * of numbers, as in `v.sub([1, 2, 3])`. + * + * If a value isn't provided for a component, it won't change. For + * example, `v.sub(4, 5)` subtracts 4 from `v.x`, 5 from `v.y`, and 0 from `v.z`. + * Calling `sub()` with no arguments, as in `v.sub()`, has no effect. + * + * The static version of `sub()`, as in `p5.Vector.sub(v2, v1)`, returns a new + * p5.Vector object and doesn't change the + * originals. + * + * @param {Number} x x component of the vector to subtract. + * @param {Number} [y] y component of the vector to subtract. + * @param {Number} [z] z component of the vector to subtract. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Bottom right. + * let pos = createVector(75, 75); + * point(pos); + * + * // Top right. + * // Subtract numbers. + * pos.sub(0, 50); + * point(pos); + * + * // Top left. + * // Subtract a p5.Vector. + * let p2 = createVector(50, 0); + * pos.sub(p2); + * point(pos); + * + * // Bottom left. + * // Subtract an array. + * let arr = [0, -50]; + * pos.sub(arr); + * point(pos); + * + * describe('Four black dots arranged in a square on a gray background.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create p5.Vector objects. + * let p1 = createVector(75, 75); + * let p2 = createVector(50, 50); + * + * // Subtract with modifying the original vectors. + * let p3 = p5.Vector.sub(p1, p2); + * + * // Draw the points. + * strokeWeight(5); + * point(p1); + * point(p2); + * point(p3); + * + * describe('Three black dots in a diagonal line from top left to bottom right.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow.'); + * } + * + * function draw() { + * background(200); + * + * let origin = createVector(0, 0); + * + * // Draw the red arrow. + * let v1 = createVector(50, 50); + * drawArrow(origin, v1, 'red'); + * + * // Draw the blue arrow. + * let v2 = createVector(20, 70); + * drawArrow(origin, v2, 'blue'); + * + * // Purple arrow. + * let v3 = p5.Vector.sub(v2, v1); + * drawArrow(v1, v3, 'purple'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + /** + * @param {p5.Vector|Number[]} value the vector to subtract + * @chainable + */ + sub(x, y, z) { + if (x instanceof Vector) { + this.x -= x.x || 0; + this.y -= x.y || 0; + this.z -= x.z || 0; + return this; + } + if (Array.isArray(x)) { + this.x -= x[0] || 0; + this.y -= x[1] || 0; + this.z -= x[2] || 0; + return this; + } + this.x -= x || 0; + this.y -= y || 0; + this.z -= z || 0; + return this; + } + + /** + * Multiplies a vector's `x`, `y`, and `z` components. + * + * `mult()` can use separate numbers, as in `v.mult(1, 2, 3)`, another + * p5.Vector object, as in `v.mult(v2)`, or an array + * of numbers, as in `v.mult([1, 2, 3])`. + * + * If only one value is provided, as in `v.mult(2)`, then all the components + * will be multiplied by 2. If a value isn't provided for a component, it + * won't change. For example, `v.mult(4, 5)` multiplies `v.x` by, `v.y` by 5, + * and `v.z` by 1. Calling `mult()` with no arguments, as in `v.mult()`, has + * no effect. + * + * The static version of `mult()`, as in `p5.Vector.mult(v, 2)`, returns a new + * p5.Vector object and doesn't change the + * originals. + * + * @param {Number} n The number to multiply with the vector + * @chainable + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Top-left. + * let p = createVector(25, 25); + * point(p); + * + * // Center. + * // Multiply all components by 2. + * p.mult(2); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * strokeWeight(5); + * + * // Top-left. + * let p = createVector(25, 25); + * point(p); + * + * // Bottom-right. + * // Multiply p.x * 2 and p.y * 3 + * p.mult(2, 3); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Top-left. + * let p = createVector(25, 25); + * point(p); + * + * // Bottom-right. + * // Multiply p.x * 2 and p.y * 3 + * let arr = [2, 3]; + * p.mult(arr); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Top-left. + * let p = createVector(25, 25); + * point(p); + * + * // Bottom-right. + * // Multiply p.x * p2.x and p.y * p2.y + * let p2 = createVector(2, 3); + * p.mult(p2); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Top-left. + * let p = createVector(25, 25); + * point(p); + * + * // Bottom-right. + * // Create a new p5.Vector with + * // p3.x = p.x * p2.x + * // p3.y = p.y * p2.y + * let p2 = createVector(2, 3); + * let p3 = p5.Vector.mult(p, p2); + * point(p3); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Two arrows extending from the top left corner. The blue arrow is twice the length of the red arrow.'); + * } + * function draw() { + * background(200); + * + * let origin = createVector(0, 0); + * + * // Draw the red arrow. + * let v1 = createVector(25, 25); + * drawArrow(origin, v1, 'red'); + * + * // Draw the blue arrow. + * let v2 = p5.Vector.mult(v1, 2); + * drawArrow(origin, v2, 'blue'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + /** + * @param {Number} x number to multiply with the x component of the vector. + * @param {Number} y number to multiply with the y component of the vector. + * @param {Number} [z] number to multiply with the z component of the vector. + * @chainable + */ + /** + * @param {Number[]} arr array to multiply with the components of the vector. + * @chainable + */ + /** + * @param {p5.Vector} v vector to multiply with the components of the original vector. + * @chainable + */ + mult(x, y, z) { + if (x instanceof Vector) { + // new p5.Vector will check that values are valid upon construction but it's possible + // that someone could change the value of a component after creation, which is why we still + // perform this check if ( - vectorComponents.every(element => Number.isFinite(element)) && - vectorComponents.every(element => typeof element === 'number') + Number.isFinite(x.x) && + Number.isFinite(x.y) && + Number.isFinite(x.z) && + typeof x.x === 'number' && + typeof x.y === 'number' && + typeof x.z === 'number' ) { - if (arguments.length === 1) { - this.x *= x; - this.y *= x; - this.z *= x; - } - if (arguments.length === 2) { - this.x *= x; - this.y *= y; - } - if (arguments.length === 3) { - this.x *= x; - this.y *= y; - this.z *= z; - } + this.x *= x.x; + this.y *= x.y; + this.z *= x.z; } else { console.warn( 'p5.Vector.prototype.mult:', - 'x, y, or z arguments are either undefined or not a finite number' + 'x contains components that are either undefined or not finite numbers' ); } - return this; } - - /** - * Divides a vector's `x`, `y`, and `z` components. - * - * `div()` can use separate numbers, as in `v.div(1, 2, 3)`, another - * p5.Vector object, as in `v.div(v2)`, or an array - * of numbers, as in `v.div([1, 2, 3])`. - * - * If only one value is provided, as in `v.div(2)`, then all the components - * will be divided by 2. If a value isn't provided for a component, it - * won't change. For example, `v.div(4, 5)` divides `v.x` by, `v.y` by 5, - * and `v.z` by 1. Calling `div()` with no arguments, as in `v.div()`, has - * no effect. - * - * The static version of `div()`, as in `p5.Vector.div(v, 2)`, returns a new - * p5.Vector object and doesn't change the - * originals. - * - * @param {Number} n The number to divide the vector by - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Center. - * let p = createVector(50, 50); - * point(p); - * - * // Top-left. - * // Divide p.x / 2 and p.y / 2 - * p.div(2); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Bottom-right. - * let p = createVector(50, 75); - * point(p); - * - * // Top-left. - * // Divide p.x / 2 and p.y / 3 - * p.div(2, 3); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Bottom-right. - * let p = createVector(50, 75); - * point(p); - * - * // Top-left. - * // Divide p.x / 2 and p.y / 3 - * let arr = [2, 3]; - * p.div(arr); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Bottom-right. - * let p = createVector(50, 75); - * point(p); - * - * // Top-left. - * // Divide p.x / 2 and p.y / 3 - * let p2 = createVector(2, 3); - * p.div(p2); - * point(p); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Style the points. - * strokeWeight(5); - * - * // Bottom-right. - * let p = createVector(50, 75); - * point(p); - * - * // Top-left. - * // Create a new p5.Vector with - * // p3.x = p.x / p2.x - * // p3.y = p.y / p2.y - * let p2 = createVector(2, 3); - * let p3 = p5.Vector.div(p, p2); - * point(p3); - * - * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); - * } - * - *
- * - *
- * - * function draw() { - * background(200); - * - * let origin = createVector(0, 0); - * - * // Draw the red arrow. - * let v1 = createVector(50, 50); - * drawArrow(origin, v1, 'red'); - * - * // Draw the blue arrow. - * let v2 = p5.Vector.div(v1, 2); - * drawArrow(origin, v2, 'blue'); - * - * describe('Two arrows extending from the top left corner. The blue arrow is half the length of the red arrow.'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - /** - * @param {Number} x number to divide with the x component of the vector. - * @param {Number} y number to divide with the y component of the vector. - * @param {Number} [z] number to divide with the z component of the vector. - * @chainable - */ - /** - * @param {Number[]} arr array to divide the components of the vector by. - * @chainable - */ - /** - * @param {p5.Vector} v vector to divide the components of the original vector by. - * @chainable - */ - div(x, y, z) { - if (x instanceof p5.Vector) { - // new p5.Vector will check that values are valid upon construction but it's possible - // that someone could change the value of a component after creation, which is why we still - // perform this check - if ( - Number.isFinite(x.x) && - Number.isFinite(x.y) && - Number.isFinite(x.z) && - typeof x.x === 'number' && - typeof x.y === 'number' && - typeof x.z === 'number' - ) { - const isLikely2D = x.z === 0 && this.z === 0; - if (x.x === 0 || x.y === 0 || (!isLikely2D && x.z === 0)) { - console.warn('p5.Vector.prototype.div:', 'divide by 0'); - return this; - } - this.x /= x.x; - this.y /= x.y; - if (!isLikely2D) { - this.z /= x.z; - } - } else { - console.warn( - 'p5.Vector.prototype.div:', - 'x contains components that are either undefined or not finite numbers' - ); - } - return this; - } - if (Array.isArray(x)) { - if ( - x.every(element => Number.isFinite(element)) && - x.every(element => typeof element === 'number') - ) { - if (x.some(element => element === 0)) { - console.warn('p5.Vector.prototype.div:', 'divide by 0'); - return this; - } - - if (x.length === 1) { - this.x /= x[0]; - this.y /= x[0]; - this.z /= x[0]; - } else if (x.length === 2) { - this.x /= x[0]; - this.y /= x[1]; - } else if (x.length === 3) { - this.x /= x[0]; - this.y /= x[1]; - this.z /= x[2]; - } - } else { - console.warn( - 'p5.Vector.prototype.div:', - 'x contains components that are either undefined or not finite numbers' - ); - } - - return this; - } - - const vectorComponents = [...arguments]; + if (Array.isArray(x)) { if ( - vectorComponents.every(element => Number.isFinite(element)) && - vectorComponents.every(element => typeof element === 'number') + x.every(element => Number.isFinite(element)) && + x.every(element => typeof element === 'number') ) { - if (vectorComponents.some(element => element === 0)) { - console.warn('p5.Vector.prototype.div:', 'divide by 0'); - return this; - } - - if (arguments.length === 1) { - this.x /= x; - this.y /= x; - this.z /= x; - } - if (arguments.length === 2) { - this.x /= x; - this.y /= y; - } - if (arguments.length === 3) { - this.x /= x; - this.y /= y; - this.z /= z; + if (x.length === 1) { + this.x *= x[0]; + this.y *= x[0]; + this.z *= x[0]; + } else if (x.length === 2) { + this.x *= x[0]; + this.y *= x[1]; + } else if (x.length === 3) { + this.x *= x[0]; + this.y *= x[1]; + this.z *= x[2]; } } else { console.warn( - 'p5.Vector.prototype.div:', - 'x, y, or z arguments are either undefined or not a finite number' + 'p5.Vector.prototype.mult:', + 'x contains elements that are either undefined or not finite numbers' ); } - return this; } - /** - * Calculates the magnitude (length) of the vector. - * - * Use mag() to calculate the magnitude of a 2D vector - * using components as in `mag(x, y)`. - * - * @return {Number} magnitude of the vector. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Vector object. - * let p = createVector(30, 40); - * - * // Draw a line from the origin. - * line(0, 0, p.x, p.y); - * - * // Style the text. - * textAlign(CENTER); - * textSize(16); - * - * // Display the vector's magnitude. - * let m = p.mag(); - * text(m, p.x, p.y); - * - * describe('A diagonal black line extends from the top left corner of a gray square. The number 50 is written at the end of the line.'); - * } - * - *
- */ - mag() { - return Math.sqrt(this.magSq()); - } - - /** - * Calculates the magnitude (length) of the vector squared. - * - * @return {Number} squared magnitude of the vector. - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Vector object. - * let p = createVector(30, 40); - * - * // Draw a line from the origin. - * line(0, 0, p.x, p.y); - * - * // Style the text. - * textAlign(CENTER); - * textSize(16); - * - * // Display the vector's magnitude squared. - * let m = p.magSq(); - * text(m, p.x, p.y); - * - * describe('A diagonal black line extends from the top left corner of a gray square. The number 2500 is written at the end of the line.'); - * } - * - *
- */ - magSq() { - const x = this.x; - const y = this.y; - const z = this.z; - return x * x + y * y + z * z; - } - - /** - * Calculates the dot product of two vectors. - * - * The dot product is a number that describes the overlap between two vectors. - * Visually, the dot product can be thought of as the "shadow" one vector - * casts on another. The dot product's magnitude is largest when two vectors - * point in the same or opposite directions. Its magnitude is 0 when two - * vectors form a right angle. - * - * The version of `dot()` with one parameter interprets it as another - * p5.Vector object. - * - * The version of `dot()` with multiple parameters interprets them as the - * `x`, `y`, and `z` components of another vector. - * - * The static version of `dot()`, as in `p5.Vector.dot(v1, v2)`, is the same - * as calling `v1.dot(v2)`. - * - * @param {Number} x x component of the vector. - * @param {Number} [y] y component of the vector. - * @param {Number} [z] z component of the vector. - * @return {Number} dot product. - * - * @example - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v1 = createVector(3, 4); - * let v2 = createVector(3, 0); - * - * // Calculate the dot product. - * let dp = v1.dot(v2); - * - * // Prints "9" to the console. - * print(dp); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v1 = createVector(1, 0); - * let v2 = createVector(0, 1); - * - * // Calculate the dot product. - * let dp = p5.Vector.dot(v1, v2); - * - * // Prints "0" to the console. - * print(dp); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Two arrows drawn on a gray square. A black arrow points to the right and a red arrow follows the mouse. The text "v1 • v2 = something" changes as the mouse moves.'); - * } - * - * function draw() { - * background(200); - * - * // Center. - * let v0 = createVector(50, 50); - * - * // Draw the black arrow. - * let v1 = createVector(30, 0); - * drawArrow(v0, v1, 'black'); - * - * // Draw the red arrow. - * let v2 = createVector(mouseX - 50, mouseY - 50); - * drawArrow(v0, v2, 'red'); - * - * // Display the dot product. - * let dp = v2.dot(v1); - * text(`v2 • v1 = ${dp}`, 10, 20); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - /** - * @param {p5.Vector} v p5.Vector to be dotted. - * @return {Number} - */ - dot(x, y, z) { - if (x instanceof p5.Vector) { - return this.dot(x.x, x.y, x.z); + const vectorComponents = [...arguments]; + if ( + vectorComponents.every(element => Number.isFinite(element)) && + vectorComponents.every(element => typeof element === 'number') + ) { + if (arguments.length === 1) { + this.x *= x; + this.y *= x; + this.z *= x; } - return this.x * (x || 0) + this.y * (y || 0) + this.z * (z || 0); - } - - /** - * Calculates the cross product of two vectors. - * - * The cross product is a vector that points straight out of the plane created - * by two vectors. The cross product's magnitude is the area of the parallelogram - * formed by the original two vectors. - * - * The static version of `cross()`, as in `p5.Vector.cross(v1, v2)`, is the same - * as calling `v1.cross(v2)`. - * - * @param {p5.Vector} v p5.Vector to be crossed. - * @return {p5.Vector} cross product as a p5.Vector. - * - * @example - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v1 = createVector(1, 0); - * let v2 = createVector(3, 4); - * - * // Calculate the cross product. - * let cp = v1.cross(v2); - * - * // Prints "p5.Vector Object : [0, 0, 4]" to the console. - * print(cp.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v1 = createVector(1, 0); - * let v2 = createVector(3, 4); - * - * // Calculate the cross product. - * let cp = p5.Vector.cross(v1, v2); - * - * // Prints "p5.Vector Object : [0, 0, 4]" to the console. - * print(cp.toString()); - * } - * - *
- */ - cross(v) { - const x = this.y * v.z - this.z * v.y; - const y = this.z * v.x - this.x * v.z; - const z = this.x * v.y - this.y * v.x; - if (this.isPInst) { - return new p5.Vector(this._fromRadians, this._toRadians, x, y, z); - } else { - return new p5.Vector(x, y, z); + if (arguments.length === 2) { + this.x *= x; + this.y *= y; } - } - - /** - * Calculates the distance between two points represented by vectors. - * - * A point's coordinates can be represented by the components of a vector - * that extends from the origin to the point. - * - * The static version of `dist()`, as in `p5.Vector.dist(v1, v2)`, is the same - * as calling `v1.dist(v2)`. - * - * Use dist() to calculate the distance between points - * using coordinates as in `dist(x1, y1, x2, y2)`. - * - * @param {p5.Vector} v x, y, and z coordinates of a p5.Vector. - * @return {Number} distance. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create p5.Vector objects. - * let v1 = createVector(1, 0); - * let v2 = createVector(0, 1); - * - * // Calculate the distance between them. - * let d = v1.dist(v2); - * - * // Prints "1.414..." to the console. - * print(d); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create p5.Vector objects. - * let v1 = createVector(1, 0); - * let v2 = createVector(0, 1); - * - * // Calculate the distance between them. - * let d = p5.Vector.dist(v1, v2); - * - * // Prints "1.414..." to the console. - * print(d); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow. The number 36 is written in black near the purple arrow.'); - * } - * - * function draw() { - * background(200); - * - * let origin = createVector(0, 0); - * - * // Draw the red arrow. - * let v1 = createVector(50, 50); - * drawArrow(origin, v1, 'red'); - * - * // Draw the blue arrow. - * let v2 = createVector(20, 70); - * drawArrow(origin, v2, 'blue'); - * - * // Purple arrow. - * let v3 = p5.Vector.sub(v2, v1); - * drawArrow(v1, v3, 'purple'); - * - * // Style the text. - * textAlign(CENTER); - * - * // Display the magnitude. - * let m = floor(v3.mag()); - * text(m, 50, 75); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - dist(v) { - return v - .copy() - .sub(this) - .mag(); - } - - /** - * Scales the components of a p5.Vector object so - * that its magnitude is 1. - * - * The static version of `normalize()`, as in `p5.Vector.normalize(v)`, - * returns a new p5.Vector object and doesn't change - * the original. - * - * @return {p5.Vector} normalized p5.Vector. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Vector. - * let v = createVector(10, 20, 2); - * - * // Normalize. - * v.normalize(); - * - * // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Vector. - * let v0 = createVector(10, 20, 2); - * - * // Create a normalized copy. - * let v1 = p5.Vector.normalize(v0); - * - * // Prints "p5.Vector Object : [10, 20, 2]" to the console. - * print(v0.toString()); - * // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console. - * print(v1.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow's length is fixed to the circle's radius."); - * } - * - * function draw() { - * background(240); - * - * // Vector to the center. - * let v0 = createVector(50, 50); - * - * // Vector from the center to the mouse. - * let v1 = createVector(mouseX - 50, mouseY - 50); - * - * // Circle's radius. - * let r = 25; - * - * // Draw the red arrow. - * drawArrow(v0, v1, 'red'); - * - * // Draw the blue arrow. - * v1.normalize(); - * drawArrow(v0, v1.mult(r), 'blue'); - * - * // Draw the circle. - * noFill(); - * circle(50, 50, r * 2); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - normalize() { - const len = this.mag(); - // here we multiply by the reciprocal instead of calling 'div()' - // since div duplicates this zero check. - if (len !== 0) this.mult(1 / len); - return this; - } - - /** - * Limits a vector's magnitude to a maximum value. - * - * The static version of `limit()`, as in `p5.Vector.limit(v, 5)`, returns a - * new p5.Vector object and doesn't change the - * original. - * - * @param {Number} max maximum magnitude for the vector. - * @chainable - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(10, 20, 2); - * - * // Limit its magnitude. - * v.limit(5); - * - * // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v0 = createVector(10, 20, 2); - * - * // Create a copy an limit its magintude. - * let v1 = p5.Vector.limit(v0, 5); - * - * // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console. - * print(v1.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow never crosses the circle's edge."); - * } - * function draw() { - * background(240); - * - * // Vector to the center. - * let v0 = createVector(50, 50); - * - * // Vector from the center to the mouse. - * let v1 = createVector(mouseX - 50, mouseY - 50); - * - * // Circle's radius. - * let r = 25; - * - * // Draw the red arrow. - * drawArrow(v0, v1, 'red'); - * - * // Draw the blue arrow. - * drawArrow(v0, v1.limit(r), 'blue'); - * - * // Draw the circle. - * noFill(); - * circle(50, 50, r * 2); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - limit(max) { - const mSq = this.magSq(); - if (mSq > max * max) { - this.div(Math.sqrt(mSq)) //normalize it - .mult(max); + if (arguments.length === 3) { + this.x *= x; + this.y *= y; + this.z *= z; } - return this; + } else { + console.warn( + 'p5.Vector.prototype.mult:', + 'x, y, or z arguments are either undefined or not a finite number' + ); } - /** - * Sets a vector's magnitude to a given value. - * - * The static version of `setMag()`, as in `p5.Vector.setMag(v, 10)`, returns - * a new p5.Vector object and doesn't change the - * original. - * - * @param {Number} len new length for this vector. - * @chainable - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(3, 4, 0); - * - * // Prints "5" to the console. - * print(v.mag()); - * - * // Set its magnitude to 10. - * v.setMag(10); - * - * // Prints "p5.Vector Object : [6, 8, 0]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v0 = createVector(3, 4, 0); - * - * // Create a copy with a magnitude of 10. - * let v1 = p5.Vector.setMag(v0, 10); - * - * // Prints "5" to the console. - * print(v0.mag()); - * - * // Prints "p5.Vector Object : [6, 8, 0]" to the console. - * print(v1.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Two arrows extend from the top left corner of a square toward its center. The red arrow reaches the center and the blue arrow only extends part of the way.'); - * } - * - * function draw() { - * background(240); - * - * let origin = createVector(0, 0); - * let v = createVector(50, 50); - * - * // Draw the red arrow. - * drawArrow(origin, v, 'red'); - * - * // Set v's magnitude to 30. - * v.setMag(30); - * - * // Draw the blue arrow. - * drawArrow(origin, v, 'blue'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - setMag(n) { - return this.normalize().mult(n); - } - - /** - * Calculates the angle a 2D vector makes with the positive x-axis. - * - * By convention, the positive x-axis has an angle of 0. Angles increase in - * the clockwise direction. - * - * If the vector was created with - * createVector(), `heading()` returns angles - * in the units of the current angleMode(). - * - * The static version of `heading()`, as in `p5.Vector.heading(v)`, works the - * same way. - * - * @return {Number} angle of rotation. - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(1, 1); - * - * // Prints "0.785..." to the console. - * print(v.heading()); - * - * // Use degrees. - * angleMode(DEGREES); - * - * // Prints "45" to the console. - * print(v.heading()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(1, 1); - * - * // Prints "0.785..." to the console. - * print(p5.Vector.heading(v)); - * - * // Use degrees. - * angleMode(DEGREES); - * - * // Prints "45" to the console. - * print(p5.Vector.heading(v)); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A black arrow extends from the top left of a square to its center. The text "Radians: 0.79" and "Degrees: 45" is written near the tip of the arrow.'); - * } - * - * function draw() { - * background(200); - * - * let origin = createVector(0, 0); - * let v = createVector(50, 50); - * - * // Draw the black arrow. - * drawArrow(origin, v, 'black'); - * - * // Use radians. - * angleMode(RADIANS); - * - * // Display the heading in radians. - * let h = round(v.heading(), 2); - * text(`Radians: ${h}`, 20, 70); - * - * // Use degrees. - * angleMode(DEGREES); - * - * // Display the heading in degrees. - * h = v.heading(); - * text(`Degrees: ${h}`, 20, 85); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - heading() { - const h = Math.atan2(this.y, this.x); - if (this.isPInst) return this._fromRadians(h); - return h; - } - - /** - * Rotates a 2D vector to a specific angle without changing its magnitude. - * - * By convention, the positive x-axis has an angle of 0. Angles increase in - * the clockwise direction. - * - * If the vector was created with - * createVector(), `setHeading()` uses - * the units of the current angleMode(). - * - * @param {Number} angle angle of rotation. - * @chainable - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(0, 1); - * - * // Prints "1.570..." to the console. - * print(v.heading()); - * - * // Point to the left. - * v.setHeading(PI); - * - * // Prints "3.141..." to the console. - * print(v.heading()); - * } - * - *
- * - *
- * - * function setup() { - * // Use degrees. - * angleMode(DEGREES); - * - * // Create a p5.Vector object. - * let v = createVector(0, 1); - * - * // Prints "90" to the console. - * print(v.heading()); - * - * // Point to the left. - * v.setHeading(180); - * - * // Prints "180" to the console. - * print(v.heading()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Two arrows extend from the center of a gray square. The red arrow points to the right and the blue arrow points down.'); - * } - * - * function draw() { - * background(200); - * - * // Create p5.Vector objects. - * let v0 = createVector(50, 50); - * let v1 = createVector(30, 0); - * - * // Draw the red arrow. - * drawArrow(v0, v1, 'red'); - * - * // Point down. - * v1.setHeading(HALF_PI); - * - * // Draw the blue arrow. - * drawArrow(v0, v1, 'blue'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - setHeading(a) { - if (this.isPInst) a = this._toRadians(a); - let m = this.mag(); - this.x = m * Math.cos(a); - this.y = m * Math.sin(a); - return this; - } + return this; + } - /** - * Rotates a 2D vector by an angle without changing its magnitude. - * - * By convention, the positive x-axis has an angle of 0. Angles increase in - * the clockwise direction. - * - * If the vector was created with - * createVector(), `rotate()` uses - * the units of the current angleMode(). - * - * The static version of `rotate()`, as in `p5.Vector.rotate(v, PI)`, - * returns a new p5.Vector object and doesn't change - * the original. - * - * @param {Number} angle angle of rotation. - * @chainable - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(1, 0); - * - * // Prints "p5.Vector Object : [1, 0, 0]" to the console. - * print(v.toString()); - * - * // Rotate a quarter turn. - * v.rotate(HALF_PI); - * - * // Prints "p5.Vector Object : [0, 1, 0]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Use degrees. - * angleMode(DEGREES); - * - * // Create a p5.Vector object. - * let v = createVector(1, 0); - * - * // Prints "p5.Vector Object : [1, 0, 0]" to the console. - * print(v.toString()); - * - * // Rotate a quarter turn. - * v.rotate(90); - * - * // Prints "p5.Vector Object : [0, 1, 0]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v0 = createVector(1, 0); - * - * // Create a rotated copy. - * let v1 = p5.Vector.rotate(v0, HALF_PI); - * - * // Prints "p5.Vector Object : [1, 0, 0]" to the console. - * print(v0.toString()); - * // Prints "p5.Vector Object : [0, 1, 0]" to the console. - * print(v1.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Use degrees. - * angleMode(DEGREES); - * - * // Create a p5.Vector object. - * let v0 = createVector(1, 0); - * - * // Create a rotated copy. - * let v1 = p5.Vector.rotate(v0, 90); - * - * // Prints "p5.Vector Object : [1, 0, 0]" to the console. - * print(v0.toString()); - * - * // Prints "p5.Vector Object : [0, 1, 0]" to the console. - * print(v1.toString()); - * } - * - *
- * - *
- * - * let v0; - * let v1; - * - * function setup() { - * createCanvas(100, 100); - * - * // Create p5.Vector objects. - * v0 = createVector(50, 50); - * v1 = createVector(30, 0); - * - * describe('A black arrow extends from the center of a gray square. The arrow rotates clockwise.'); - * } - * - * function draw() { - * background(240); - * - * // Rotate v1. - * v1.rotate(0.01); - * - * // Draw the black arrow. - * drawArrow(v0, v1, 'black'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - rotate(a) { - let newHeading = this.heading() + a; - if (this.isPInst) newHeading = this._toRadians(newHeading); - const mag = this.mag(); - this.x = Math.cos(newHeading) * mag; - this.y = Math.sin(newHeading) * mag; + /** + * Divides a vector's `x`, `y`, and `z` components. + * + * `div()` can use separate numbers, as in `v.div(1, 2, 3)`, another + * p5.Vector object, as in `v.div(v2)`, or an array + * of numbers, as in `v.div([1, 2, 3])`. + * + * If only one value is provided, as in `v.div(2)`, then all the components + * will be divided by 2. If a value isn't provided for a component, it + * won't change. For example, `v.div(4, 5)` divides `v.x` by, `v.y` by 5, + * and `v.z` by 1. Calling `div()` with no arguments, as in `v.div()`, has + * no effect. + * + * The static version of `div()`, as in `p5.Vector.div(v, 2)`, returns a new + * p5.Vector object and doesn't change the + * originals. + * + * @param {Number} n The number to divide the vector by + * @chainable + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Center. + * let p = createVector(50, 50); + * point(p); + * + * // Top-left. + * // Divide p.x / 2 and p.y / 2 + * p.div(2); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Bottom-right. + * let p = createVector(50, 75); + * point(p); + * + * // Top-left. + * // Divide p.x / 2 and p.y / 3 + * p.div(2, 3); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Bottom-right. + * let p = createVector(50, 75); + * point(p); + * + * // Top-left. + * // Divide p.x / 2 and p.y / 3 + * let arr = [2, 3]; + * p.div(arr); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Bottom-right. + * let p = createVector(50, 75); + * point(p); + * + * // Top-left. + * // Divide p.x / 2 and p.y / 3 + * let p2 = createVector(2, 3); + * p.div(p2); + * point(p); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Style the points. + * strokeWeight(5); + * + * // Bottom-right. + * let p = createVector(50, 75); + * point(p); + * + * // Top-left. + * // Create a new p5.Vector with + * // p3.x = p.x / p2.x + * // p3.y = p.y / p2.y + * let p2 = createVector(2, 3); + * let p3 = p5.Vector.div(p, p2); + * point(p3); + * + * describe('Two black dots drawn on a gray square. One dot is in the top left corner and the other is in the bottom center.'); + * } + * + *
+ * + *
+ * + * function draw() { + * background(200); + * + * let origin = createVector(0, 0); + * + * // Draw the red arrow. + * let v1 = createVector(50, 50); + * drawArrow(origin, v1, 'red'); + * + * // Draw the blue arrow. + * let v2 = p5.Vector.div(v1, 2); + * drawArrow(origin, v2, 'blue'); + * + * describe('Two arrows extending from the top left corner. The blue arrow is half the length of the red arrow.'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + /** + * @param {Number} x number to divide with the x component of the vector. + * @param {Number} y number to divide with the y component of the vector. + * @param {Number} [z] number to divide with the z component of the vector. + * @chainable + */ + /** + * @param {Number[]} arr array to divide the components of the vector by. + * @chainable + */ + /** + * @param {p5.Vector} v vector to divide the components of the original vector by. + * @chainable + */ + div(x, y, z) { + if (x instanceof Vector) { + // new p5.Vector will check that values are valid upon construction but it's possible + // that someone could change the value of a component after creation, which is why we still + // perform this check + if ( + Number.isFinite(x.x) && + Number.isFinite(x.y) && + Number.isFinite(x.z) && + typeof x.x === 'number' && + typeof x.y === 'number' && + typeof x.z === 'number' + ) { + const isLikely2D = x.z === 0 && this.z === 0; + if (x.x === 0 || x.y === 0 || (!isLikely2D && x.z === 0)) { + console.warn('p5.Vector.prototype.div:', 'divide by 0'); + return this; + } + this.x /= x.x; + this.y /= x.y; + if (!isLikely2D) { + this.z /= x.z; + } + } else { + console.warn( + 'p5.Vector.prototype.div:', + 'x contains components that are either undefined or not finite numbers' + ); + } return this; } + if (Array.isArray(x)) { + if ( + x.every(element => Number.isFinite(element)) && + x.every(element => typeof element === 'number') + ) { + if (x.some(element => element === 0)) { + console.warn('p5.Vector.prototype.div:', 'divide by 0'); + return this; + } - /** - * Calculates the angle between two vectors. - * - * The angles returned are signed, which means that - * `v1.angleBetween(v2) === -v2.angleBetween(v1)`. - * - * If the vector was created with - * createVector(), `angleBetween()` returns - * angles in the units of the current - * angleMode(). - * - * @param {p5.Vector} value x, y, and z components of a p5.Vector. - * @return {Number} angle between the vectors. - * @example - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v0 = createVector(1, 0); - * let v1 = createVector(0, 1); - * - * // Prints "1.570..." to the console. - * print(v0.angleBetween(v1)); - * - * // Prints "-1.570..." to the console. - * print(v1.angleBetween(v0)); - * } - * - *
- * - *
- * - * function setup() { - * // Use degrees. - * angleMode(DEGREES); - * // Create p5.Vector objects. - * let v0 = createVector(1, 0); - * let v1 = createVector(0, 1); - * - * // Prints "90" to the console. - * print(v0.angleBetween(v1)); - * - * // Prints "-90" to the console. - * print(v1.angleBetween(v0)); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v0 = createVector(1, 0); - * let v1 = createVector(0, 1); - * - * // Prints "1.570..." to the console. - * print(p5.Vector.angleBetween(v0, v1)); - * - * // Prints "-1.570..." to the console. - * print(p5.Vector.angleBetween(v1, v0)); - * } - * - *
- * - *
- * - * function setup() { - * // Use degrees. - * angleMode(DEGREES); - * - * // Create p5.Vector objects. - * let v0 = createVector(1, 0); - * let v1 = createVector(0, 1); - * - * // Prints "90" to the console. - * print(p5.Vector.angleBetween(v0, v1)); - * - * // Prints "-90" to the console. - * print(p5.Vector.angleBetween(v1, v0)); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Two arrows extend from the center of a gray square. A red arrow points to the right and a blue arrow points down. The text "Radians: 1.57" and "Degrees: 90" is written above the arrows.'); - * } - * function draw() { - * background(200); - * - * // Create p5.Vector objects. - * let v0 = createVector(50, 50); - * let v1 = createVector(30, 0); - * let v2 = createVector(0, 30); - * - * // Draw the red arrow. - * drawArrow(v0, v1, 'red'); - * - * // Draw the blue arrow. - * drawArrow(v0, v2, 'blue'); - * - * // Use radians. - * angleMode(RADIANS); - * - * // Display the angle in radians. - * let angle = round(v1.angleBetween(v2), 2); - * text(`Radians: ${angle}`, 20, 20); - * - * // Use degrees. - * angleMode(DEGREES); - * - * // Display the angle in degrees. - * angle = round(v1.angleBetween(v2), 2); - * text(`Degrees: ${angle}`, 20, 35); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - angleBetween(v) { - const magSqMult = this.magSq() * v.magSq(); - // Returns NaN if either vector is the zero vector. - if (magSqMult === 0) { - return NaN; - } - const u = this.cross(v); - // The dot product computes the cos value, and the cross product computes - // the sin value. Find the angle based on them. In addition, in the case of - // 2D vectors, a sign is added according to the direction of the vector. - let angle = Math.atan2(u.mag(), this.dot(v)) * Math.sign(u.z || 1); - if (this.isPInst) { - angle = this._fromRadians(angle); + if (x.length === 1) { + this.x /= x[0]; + this.y /= x[0]; + this.z /= x[0]; + } else if (x.length === 2) { + this.x /= x[0]; + this.y /= x[1]; + } else if (x.length === 3) { + this.x /= x[0]; + this.y /= x[1]; + this.z /= x[2]; + } + } else { + console.warn( + 'p5.Vector.prototype.div:', + 'x contains components that are either undefined or not finite numbers' + ); } - return angle; - } - /** - * Calculates new `x`, `y`, and `z` components that are proportionally the - * same distance between two vectors. - * - * The `amt` parameter is the amount to interpolate between the old vector and - * the new vector. 0.0 keeps all components equal to the old vector's, 0.5 is - * halfway between, and 1.0 sets all components equal to the new vector's. - * - * The static version of `lerp()`, as in `p5.Vector.lerp(v0, v1, 0.5)`, - * returns a new p5.Vector object and doesn't change - * the original. - * - * @param {Number} x x component. - * @param {Number} y y component. - * @param {Number} z z component. - * @param {Number} amt amount of interpolation between 0.0 (old vector) - * and 1.0 (new vector). 0.5 is halfway between. - * @chainable - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v0 = createVector(1, 1, 1); - * let v1 = createVector(3, 3, 3); - * - * // Interpolate. - * v0.lerp(v1, 0.5); - * - * // Prints "p5.Vector Object : [2, 2, 2]" to the console. - * print(v0.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(1, 1, 1); - * - * // Interpolate. - * v.lerp(3, 3, 3, 0.5); - * - * // Prints "p5.Vector Object : [2, 2, 2]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v0 = createVector(1, 1, 1); - * let v1 = createVector(3, 3, 3); - * - * // Interpolate. - * let v2 = p5.Vector.lerp(v0, v1, 0.5); - * - * // Prints "p5.Vector Object : [2, 2, 2]" to the console. - * print(v2.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points down, and a purple arrow points to the bottom right.'); - * } - * function draw() { - * background(200); - * - * // Create p5.Vector objects. - * let v0 = createVector(50, 50); - * let v1 = createVector(30, 0); - * let v2 = createVector(0, 30); - * - * // Interpolate. - * let v3 = p5.Vector.lerp(v1, v2, 0.5); - * - * // Draw the red arrow. - * drawArrow(v0, v1, 'red'); - * - * // Draw the blue arrow. - * drawArrow(v0, v2, 'blue'); - * - * // Draw the purple arrow. - * drawArrow(v0, v3, 'purple'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - /** - * @param {p5.Vector} v p5.Vector to lerp toward. - * @param {Number} amt - * @chainable - */ - lerp(x, y, z, amt) { - if (x instanceof p5.Vector) { - return this.lerp(x.x, x.y, x.z, y); - } - this.x += (x - this.x) * amt || 0; - this.y += (y - this.y) * amt || 0; - this.z += (z - this.z) * amt || 0; return this; } - /** - * Calculates a new heading and magnitude that are between two vectors. - * - * The `amt` parameter is the amount to interpolate between the old vector and - * the new vector. 0.0 keeps the heading and magnitude equal to the old - * vector's, 0.5 sets them halfway between, and 1.0 sets the heading and - * magnitude equal to the new vector's. - * - * `slerp()` differs from lerp() because - * it interpolates magnitude. Calling `v0.slerp(v1, 0.5)` sets `v0`'s - * magnitude to a value halfway between its original magnitude and `v1`'s. - * Calling `v0.lerp(v1, 0.5)` makes no such guarantee. - * - * The static version of `slerp()`, as in `p5.Vector.slerp(v0, v1, 0.5)`, - * returns a new p5.Vector object and doesn't change - * the original. - * - * @param {p5.Vector} v p5.Vector to slerp toward. - * @param {Number} amt amount of interpolation between 0.0 (old vector) - * and 1.0 (new vector). 0.5 is halfway between. - * @return {p5.Vector} - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v0 = createVector(3, 0); - * - * // Prints "3" to the console. - * print(v0.mag()); - * - * // Prints "0" to the console. - * print(v0.heading()); - * - * // Create a p5.Vector object. - * let v1 = createVector(0, 1); - * - * // Prints "1" to the console. - * print(v1.mag()); - * - * // Prints "1.570..." to the console. - * print(v1.heading()); - * - * // Interpolate halfway between v0 and v1. - * v0.slerp(v1, 0.5); - * - * // Prints "2" to the console. - * print(v0.mag()); - * - * // Prints "0.785..." to the console. - * print(v0.heading()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v0 = createVector(3, 0); - * - * // Prints "3" to the console. - * print(v0.mag()); - * - * // Prints "0" to the console. - * print(v0.heading()); - * - * // Create a p5.Vector object. - * let v1 = createVector(0, 1); - * - * // Prints "1" to the console. - * print(v1.mag()); - * - * // Prints "1.570..." to the console. - * print(v1.heading()); - * - * // Create a p5.Vector that's halfway between v0 and v1. - * let v3 = p5.Vector.slerp(v0, v1, 0.5); - * - * // Prints "2" to the console. - * print(v3.mag()); - * - * // Prints "0.785..." to the console. - * print(v3.heading()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points to the left, and a purple arrow points down.'); - * } - * - * function draw() { - * background(200); - * - * // Create p5.Vector objects. - * let v0 = createVector(50, 50); - * let v1 = createVector(20, 0); - * let v2 = createVector(-40, 0); - * - * // Create a p5.Vector that's halfway between v1 and v2. - * let v3 = p5.Vector.slerp(v1, v2, 0.5); - * - * // Draw the red arrow. - * drawArrow(v0, v1, 'red'); - * - * // Draw the blue arrow. - * drawArrow(v0, v2, 'blue'); - * - * // Draw the purple arrow. - * drawArrow(v0, v3, 'purple'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - slerp(v, amt) { - // edge cases. - if (amt === 0) { return this; } - if (amt === 1) { return this.set(v); } - - // calculate magnitudes - const selfMag = this.mag(); - const vMag = v.mag(); - const magmag = selfMag * vMag; - // if either is a zero vector, linearly interpolate by these vectors - if (magmag === 0) { - this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); + const vectorComponents = [...arguments]; + if ( + vectorComponents.every(element => Number.isFinite(element)) && + vectorComponents.every(element => typeof element === 'number') + ) { + if (vectorComponents.some(element => element === 0)) { + console.warn('p5.Vector.prototype.div:', 'divide by 0'); return this; } - // the cross product of 'this' and 'v' is the axis of rotation - const axis = this.cross(v); - const axisMag = axis.mag(); - // Calculates the angle between 'this' and 'v' - const theta = Math.atan2(axisMag, this.dot(v)); - - // However, if the norm of axis is 0, normalization cannot be performed, - // so we will divide the cases - if (axisMag > 0) { - axis.x /= axisMag; - axis.y /= axisMag; - axis.z /= axisMag; - } else if (theta < Math.PI * 0.5) { - // if the norm is 0 and the angle is less than PI/2, - // the angle is very close to 0, so do linear interpolation. - this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); - return this; - } else { - // If the norm is 0 and the angle is more than PI/2, the angle is - // very close to PI. - // In this case v can be regarded as '-this', so take any vector - // that is orthogonal to 'this' and use that as the axis. - if (this.z === 0 && v.z === 0) { - // if both this and v are 2D vectors, use (0,0,1) - // this makes the result also a 2D vector. - axis.set(0, 0, 1); - } else if (this.x !== 0) { - // if the x components is not 0, use (y, -x, 0) - axis.set(this.y, -this.x, 0).normalize(); - } else { - // if the x components is 0, use (1,0,0) - axis.set(1, 0, 0); - } + + if (arguments.length === 1) { + this.x /= x; + this.y /= x; + this.z /= x; } + if (arguments.length === 2) { + this.x /= x; + this.y /= y; + } + if (arguments.length === 3) { + this.x /= x; + this.y /= y; + this.z /= z; + } + } else { + console.warn( + 'p5.Vector.prototype.div:', + 'x, y, or z arguments are either undefined or not a finite number' + ); + } - // Since 'axis' is a unit vector, ey is a vector of the same length as 'this'. - const ey = axis.cross(this); - // interpolate the length with 'this' and 'v'. - const lerpedMagFactor = (1 - amt) + amt * vMag / selfMag; - // imagine a situation where 'axis', 'this', and 'ey' are pointing - // along the z, x, and y axes, respectively. - // rotates 'this' around 'axis' by amt * theta towards 'ey'. - const cosMultiplier = lerpedMagFactor * Math.cos(amt * theta); - const sinMultiplier = lerpedMagFactor * Math.sin(amt * theta); - // then, calculate 'result'. - this.x = this.x * cosMultiplier + ey.x * sinMultiplier; - this.y = this.y * cosMultiplier + ey.y * sinMultiplier; - this.z = this.z * cosMultiplier + ey.z * sinMultiplier; + return this; + } - return this; + /** + * Calculates the magnitude (length) of the vector. + * + * Use mag() to calculate the magnitude of a 2D vector + * using components as in `mag(x, y)`. + * + * @return {Number} magnitude of the vector. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Vector object. + * let p = createVector(30, 40); + * + * // Draw a line from the origin. + * line(0, 0, p.x, p.y); + * + * // Style the text. + * textAlign(CENTER); + * textSize(16); + * + * // Display the vector's magnitude. + * let m = p.mag(); + * text(m, p.x, p.y); + * + * describe('A diagonal black line extends from the top left corner of a gray square. The number 50 is written at the end of the line.'); + * } + * + *
+ */ + mag() { + return Math.sqrt(this.magSq()); + } + + /** + * Calculates the magnitude (length) of the vector squared. + * + * @return {Number} squared magnitude of the vector. + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Vector object. + * let p = createVector(30, 40); + * + * // Draw a line from the origin. + * line(0, 0, p.x, p.y); + * + * // Style the text. + * textAlign(CENTER); + * textSize(16); + * + * // Display the vector's magnitude squared. + * let m = p.magSq(); + * text(m, p.x, p.y); + * + * describe('A diagonal black line extends from the top left corner of a gray square. The number 2500 is written at the end of the line.'); + * } + * + *
+ */ + magSq() { + const x = this.x; + const y = this.y; + const z = this.z; + return x * x + y * y + z * z; + } + + /** + * Calculates the dot product of two vectors. + * + * The dot product is a number that describes the overlap between two vectors. + * Visually, the dot product can be thought of as the "shadow" one vector + * casts on another. The dot product's magnitude is largest when two vectors + * point in the same or opposite directions. Its magnitude is 0 when two + * vectors form a right angle. + * + * The version of `dot()` with one parameter interprets it as another + * p5.Vector object. + * + * The version of `dot()` with multiple parameters interprets them as the + * `x`, `y`, and `z` components of another vector. + * + * The static version of `dot()`, as in `p5.Vector.dot(v1, v2)`, is the same + * as calling `v1.dot(v2)`. + * + * @param {Number} x x component of the vector. + * @param {Number} [y] y component of the vector. + * @param {Number} [z] z component of the vector. + * @return {Number} dot product. + * + * @example + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v1 = createVector(3, 4); + * let v2 = createVector(3, 0); + * + * // Calculate the dot product. + * let dp = v1.dot(v2); + * + * // Prints "9" to the console. + * print(dp); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v1 = createVector(1, 0); + * let v2 = createVector(0, 1); + * + * // Calculate the dot product. + * let dp = p5.Vector.dot(v1, v2); + * + * // Prints "0" to the console. + * print(dp); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Two arrows drawn on a gray square. A black arrow points to the right and a red arrow follows the mouse. The text "v1 • v2 = something" changes as the mouse moves.'); + * } + * + * function draw() { + * background(200); + * + * // Center. + * let v0 = createVector(50, 50); + * + * // Draw the black arrow. + * let v1 = createVector(30, 0); + * drawArrow(v0, v1, 'black'); + * + * // Draw the red arrow. + * let v2 = createVector(mouseX - 50, mouseY - 50); + * drawArrow(v0, v2, 'red'); + * + * // Display the dot product. + * let dp = v2.dot(v1); + * text(`v2 • v1 = ${dp}`, 10, 20); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + /** + * @param {p5.Vector} v p5.Vector to be dotted. + * @return {Number} + */ + dot(x, y, z) { + if (x instanceof Vector) { + return this.dot(x.x, x.y, x.z); + } + return this.x * (x || 0) + this.y * (y || 0) + this.z * (z || 0); + } + + /** + * Calculates the cross product of two vectors. + * + * The cross product is a vector that points straight out of the plane created + * by two vectors. The cross product's magnitude is the area of the parallelogram + * formed by the original two vectors. + * + * The static version of `cross()`, as in `p5.Vector.cross(v1, v2)`, is the same + * as calling `v1.cross(v2)`. + * + * @param {p5.Vector} v p5.Vector to be crossed. + * @return {p5.Vector} cross product as a p5.Vector. + * + * @example + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v1 = createVector(1, 0); + * let v2 = createVector(3, 4); + * + * // Calculate the cross product. + * let cp = v1.cross(v2); + * + * // Prints "p5.Vector Object : [0, 0, 4]" to the console. + * print(cp.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v1 = createVector(1, 0); + * let v2 = createVector(3, 4); + * + * // Calculate the cross product. + * let cp = p5.Vector.cross(v1, v2); + * + * // Prints "p5.Vector Object : [0, 0, 4]" to the console. + * print(cp.toString()); + * } + * + *
+ */ + cross(v) { + const x = this.y * v.z - this.z * v.y; + const y = this.z * v.x - this.x * v.z; + const z = this.x * v.y - this.y * v.x; + if (this.isPInst) { + return new Vector(this._fromRadians, this._toRadians, x, y, z); + } else { + return new Vector(x, y, z); + } + } + + /** + * Calculates the distance between two points represented by vectors. + * + * A point's coordinates can be represented by the components of a vector + * that extends from the origin to the point. + * + * The static version of `dist()`, as in `p5.Vector.dist(v1, v2)`, is the same + * as calling `v1.dist(v2)`. + * + * Use dist() to calculate the distance between points + * using coordinates as in `dist(x1, y1, x2, y2)`. + * + * @param {p5.Vector} v x, y, and z coordinates of a p5.Vector. + * @return {Number} distance. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create p5.Vector objects. + * let v1 = createVector(1, 0); + * let v2 = createVector(0, 1); + * + * // Calculate the distance between them. + * let d = v1.dist(v2); + * + * // Prints "1.414..." to the console. + * print(d); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create p5.Vector objects. + * let v1 = createVector(1, 0); + * let v2 = createVector(0, 1); + * + * // Calculate the distance between them. + * let d = p5.Vector.dist(v1, v2); + * + * // Prints "1.414..." to the console. + * print(d); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Three arrows drawn on a gray square. A red and a blue arrow extend from the top left. A purple arrow extends from the tip of the red arrow to the tip of the blue arrow. The number 36 is written in black near the purple arrow.'); + * } + * + * function draw() { + * background(200); + * + * let origin = createVector(0, 0); + * + * // Draw the red arrow. + * let v1 = createVector(50, 50); + * drawArrow(origin, v1, 'red'); + * + * // Draw the blue arrow. + * let v2 = createVector(20, 70); + * drawArrow(origin, v2, 'blue'); + * + * // Purple arrow. + * let v3 = p5.Vector.sub(v2, v1); + * drawArrow(v1, v3, 'purple'); + * + * // Style the text. + * textAlign(CENTER); + * + * // Display the magnitude. + * let m = floor(v3.mag()); + * text(m, 50, 75); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + dist(v) { + return v + .copy() + .sub(this) + .mag(); + } + + /** + * Scales the components of a p5.Vector object so + * that its magnitude is 1. + * + * The static version of `normalize()`, as in `p5.Vector.normalize(v)`, + * returns a new p5.Vector object and doesn't change + * the original. + * + * @return {p5.Vector} normalized p5.Vector. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Vector. + * let v = createVector(10, 20, 2); + * + * // Normalize. + * v.normalize(); + * + * // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Vector. + * let v0 = createVector(10, 20, 2); + * + * // Create a normalized copy. + * let v1 = p5.Vector.normalize(v0); + * + * // Prints "p5.Vector Object : [10, 20, 2]" to the console. + * print(v0.toString()); + * // Prints "p5.Vector Object : [0.445..., 0.890..., 0.089...]" to the console. + * print(v1.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow's length is fixed to the circle's radius."); + * } + * + * function draw() { + * background(240); + * + * // Vector to the center. + * let v0 = createVector(50, 50); + * + * // Vector from the center to the mouse. + * let v1 = createVector(mouseX - 50, mouseY - 50); + * + * // Circle's radius. + * let r = 25; + * + * // Draw the red arrow. + * drawArrow(v0, v1, 'red'); + * + * // Draw the blue arrow. + * v1.normalize(); + * drawArrow(v0, v1.mult(r), 'blue'); + * + * // Draw the circle. + * noFill(); + * circle(50, 50, r * 2); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + normalize() { + const len = this.mag(); + // here we multiply by the reciprocal instead of calling 'div()' + // since div duplicates this zero check. + if (len !== 0) this.mult(1 / len); + return this; + } + + /** + * Limits a vector's magnitude to a maximum value. + * + * The static version of `limit()`, as in `p5.Vector.limit(v, 5)`, returns a + * new p5.Vector object and doesn't change the + * original. + * + * @param {Number} max maximum magnitude for the vector. + * @chainable + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(10, 20, 2); + * + * // Limit its magnitude. + * v.limit(5); + * + * // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v0 = createVector(10, 20, 2); + * + * // Create a copy an limit its magintude. + * let v1 = p5.Vector.limit(v0, 5); + * + * // Prints "p5.Vector Object : [2.227..., 4.454..., 0.445...]" to the console. + * print(v1.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe("A red and blue arrow extend from the center of a circle. Both arrows follow the mouse, but the blue arrow never crosses the circle's edge."); + * } + * function draw() { + * background(240); + * + * // Vector to the center. + * let v0 = createVector(50, 50); + * + * // Vector from the center to the mouse. + * let v1 = createVector(mouseX - 50, mouseY - 50); + * + * // Circle's radius. + * let r = 25; + * + * // Draw the red arrow. + * drawArrow(v0, v1, 'red'); + * + * // Draw the blue arrow. + * drawArrow(v0, v1.limit(r), 'blue'); + * + * // Draw the circle. + * noFill(); + * circle(50, 50, r * 2); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + limit(max) { + const mSq = this.magSq(); + if (mSq > max * max) { + this.div(Math.sqrt(mSq)) //normalize it + .mult(max); + } + return this; + } + + /** + * Sets a vector's magnitude to a given value. + * + * The static version of `setMag()`, as in `p5.Vector.setMag(v, 10)`, returns + * a new p5.Vector object and doesn't change the + * original. + * + * @param {Number} len new length for this vector. + * @chainable + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(3, 4, 0); + * + * // Prints "5" to the console. + * print(v.mag()); + * + * // Set its magnitude to 10. + * v.setMag(10); + * + * // Prints "p5.Vector Object : [6, 8, 0]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v0 = createVector(3, 4, 0); + * + * // Create a copy with a magnitude of 10. + * let v1 = p5.Vector.setMag(v0, 10); + * + * // Prints "5" to the console. + * print(v0.mag()); + * + * // Prints "p5.Vector Object : [6, 8, 0]" to the console. + * print(v1.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Two arrows extend from the top left corner of a square toward its center. The red arrow reaches the center and the blue arrow only extends part of the way.'); + * } + * + * function draw() { + * background(240); + * + * let origin = createVector(0, 0); + * let v = createVector(50, 50); + * + * // Draw the red arrow. + * drawArrow(origin, v, 'red'); + * + * // Set v's magnitude to 30. + * v.setMag(30); + * + * // Draw the blue arrow. + * drawArrow(origin, v, 'blue'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + setMag(n) { + return this.normalize().mult(n); + } + + /** + * Calculates the angle a 2D vector makes with the positive x-axis. + * + * By convention, the positive x-axis has an angle of 0. Angles increase in + * the clockwise direction. + * + * If the vector was created with + * createVector(), `heading()` returns angles + * in the units of the current angleMode(). + * + * The static version of `heading()`, as in `p5.Vector.heading(v)`, works the + * same way. + * + * @return {Number} angle of rotation. + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(1, 1); + * + * // Prints "0.785..." to the console. + * print(v.heading()); + * + * // Use degrees. + * angleMode(DEGREES); + * + * // Prints "45" to the console. + * print(v.heading()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(1, 1); + * + * // Prints "0.785..." to the console. + * print(p5.Vector.heading(v)); + * + * // Use degrees. + * angleMode(DEGREES); + * + * // Prints "45" to the console. + * print(p5.Vector.heading(v)); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A black arrow extends from the top left of a square to its center. The text "Radians: 0.79" and "Degrees: 45" is written near the tip of the arrow.'); + * } + * + * function draw() { + * background(200); + * + * let origin = createVector(0, 0); + * let v = createVector(50, 50); + * + * // Draw the black arrow. + * drawArrow(origin, v, 'black'); + * + * // Use radians. + * angleMode(RADIANS); + * + * // Display the heading in radians. + * let h = round(v.heading(), 2); + * text(`Radians: ${h}`, 20, 70); + * + * // Use degrees. + * angleMode(DEGREES); + * + * // Display the heading in degrees. + * h = v.heading(); + * text(`Degrees: ${h}`, 20, 85); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + heading() { + const h = Math.atan2(this.y, this.x); + if (this.isPInst) return this._fromRadians(h); + return h; + } + + /** + * Rotates a 2D vector to a specific angle without changing its magnitude. + * + * By convention, the positive x-axis has an angle of 0. Angles increase in + * the clockwise direction. + * + * If the vector was created with + * createVector(), `setHeading()` uses + * the units of the current angleMode(). + * + * @param {Number} angle angle of rotation. + * @chainable + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(0, 1); + * + * // Prints "1.570..." to the console. + * print(v.heading()); + * + * // Point to the left. + * v.setHeading(PI); + * + * // Prints "3.141..." to the console. + * print(v.heading()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Use degrees. + * angleMode(DEGREES); + * + * // Create a p5.Vector object. + * let v = createVector(0, 1); + * + * // Prints "90" to the console. + * print(v.heading()); + * + * // Point to the left. + * v.setHeading(180); + * + * // Prints "180" to the console. + * print(v.heading()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Two arrows extend from the center of a gray square. The red arrow points to the right and the blue arrow points down.'); + * } + * + * function draw() { + * background(200); + * + * // Create p5.Vector objects. + * let v0 = createVector(50, 50); + * let v1 = createVector(30, 0); + * + * // Draw the red arrow. + * drawArrow(v0, v1, 'red'); + * + * // Point down. + * v1.setHeading(HALF_PI); + * + * // Draw the blue arrow. + * drawArrow(v0, v1, 'blue'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + setHeading(a) { + if (this.isPInst) a = this._toRadians(a); + let m = this.mag(); + this.x = m * Math.cos(a); + this.y = m * Math.sin(a); + return this; + } + + /** + * Rotates a 2D vector by an angle without changing its magnitude. + * + * By convention, the positive x-axis has an angle of 0. Angles increase in + * the clockwise direction. + * + * If the vector was created with + * createVector(), `rotate()` uses + * the units of the current angleMode(). + * + * The static version of `rotate()`, as in `p5.Vector.rotate(v, PI)`, + * returns a new p5.Vector object and doesn't change + * the original. + * + * @param {Number} angle angle of rotation. + * @chainable + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(1, 0); + * + * // Prints "p5.Vector Object : [1, 0, 0]" to the console. + * print(v.toString()); + * + * // Rotate a quarter turn. + * v.rotate(HALF_PI); + * + * // Prints "p5.Vector Object : [0, 1, 0]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Use degrees. + * angleMode(DEGREES); + * + * // Create a p5.Vector object. + * let v = createVector(1, 0); + * + * // Prints "p5.Vector Object : [1, 0, 0]" to the console. + * print(v.toString()); + * + * // Rotate a quarter turn. + * v.rotate(90); + * + * // Prints "p5.Vector Object : [0, 1, 0]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v0 = createVector(1, 0); + * + * // Create a rotated copy. + * let v1 = p5.Vector.rotate(v0, HALF_PI); + * + * // Prints "p5.Vector Object : [1, 0, 0]" to the console. + * print(v0.toString()); + * // Prints "p5.Vector Object : [0, 1, 0]" to the console. + * print(v1.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Use degrees. + * angleMode(DEGREES); + * + * // Create a p5.Vector object. + * let v0 = createVector(1, 0); + * + * // Create a rotated copy. + * let v1 = p5.Vector.rotate(v0, 90); + * + * // Prints "p5.Vector Object : [1, 0, 0]" to the console. + * print(v0.toString()); + * + * // Prints "p5.Vector Object : [0, 1, 0]" to the console. + * print(v1.toString()); + * } + * + *
+ * + *
+ * + * let v0; + * let v1; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create p5.Vector objects. + * v0 = createVector(50, 50); + * v1 = createVector(30, 0); + * + * describe('A black arrow extends from the center of a gray square. The arrow rotates clockwise.'); + * } + * + * function draw() { + * background(240); + * + * // Rotate v1. + * v1.rotate(0.01); + * + * // Draw the black arrow. + * drawArrow(v0, v1, 'black'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + rotate(a) { + let newHeading = this.heading() + a; + if (this.isPInst) newHeading = this._toRadians(newHeading); + const mag = this.mag(); + this.x = Math.cos(newHeading) * mag; + this.y = Math.sin(newHeading) * mag; + return this; + } + + /** + * Calculates the angle between two vectors. + * + * The angles returned are signed, which means that + * `v1.angleBetween(v2) === -v2.angleBetween(v1)`. + * + * If the vector was created with + * createVector(), `angleBetween()` returns + * angles in the units of the current + * angleMode(). + * + * @param {p5.Vector} value x, y, and z components of a p5.Vector. + * @return {Number} angle between the vectors. + * @example + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v0 = createVector(1, 0); + * let v1 = createVector(0, 1); + * + * // Prints "1.570..." to the console. + * print(v0.angleBetween(v1)); + * + * // Prints "-1.570..." to the console. + * print(v1.angleBetween(v0)); + * } + * + *
+ * + *
+ * + * function setup() { + * // Use degrees. + * angleMode(DEGREES); + * // Create p5.Vector objects. + * let v0 = createVector(1, 0); + * let v1 = createVector(0, 1); + * + * // Prints "90" to the console. + * print(v0.angleBetween(v1)); + * + * // Prints "-90" to the console. + * print(v1.angleBetween(v0)); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v0 = createVector(1, 0); + * let v1 = createVector(0, 1); + * + * // Prints "1.570..." to the console. + * print(p5.Vector.angleBetween(v0, v1)); + * + * // Prints "-1.570..." to the console. + * print(p5.Vector.angleBetween(v1, v0)); + * } + * + *
+ * + *
+ * + * function setup() { + * // Use degrees. + * angleMode(DEGREES); + * + * // Create p5.Vector objects. + * let v0 = createVector(1, 0); + * let v1 = createVector(0, 1); + * + * // Prints "90" to the console. + * print(p5.Vector.angleBetween(v0, v1)); + * + * // Prints "-90" to the console. + * print(p5.Vector.angleBetween(v1, v0)); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Two arrows extend from the center of a gray square. A red arrow points to the right and a blue arrow points down. The text "Radians: 1.57" and "Degrees: 90" is written above the arrows.'); + * } + * function draw() { + * background(200); + * + * // Create p5.Vector objects. + * let v0 = createVector(50, 50); + * let v1 = createVector(30, 0); + * let v2 = createVector(0, 30); + * + * // Draw the red arrow. + * drawArrow(v0, v1, 'red'); + * + * // Draw the blue arrow. + * drawArrow(v0, v2, 'blue'); + * + * // Use radians. + * angleMode(RADIANS); + * + * // Display the angle in radians. + * let angle = round(v1.angleBetween(v2), 2); + * text(`Radians: ${angle}`, 20, 20); + * + * // Use degrees. + * angleMode(DEGREES); + * + * // Display the angle in degrees. + * angle = round(v1.angleBetween(v2), 2); + * text(`Degrees: ${angle}`, 20, 35); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + angleBetween(v) { + const magSqMult = this.magSq() * v.magSq(); + // Returns NaN if either vector is the zero vector. + if (magSqMult === 0) { + return NaN; + } + const u = this.cross(v); + // The dot product computes the cos value, and the cross product computes + // the sin value. Find the angle based on them. In addition, in the case of + // 2D vectors, a sign is added according to the direction of the vector. + let angle = Math.atan2(u.mag(), this.dot(v)) * Math.sign(u.z || 1); + if (this.isPInst) { + angle = this._fromRadians(angle); + } + return angle; + } + + /** + * Calculates new `x`, `y`, and `z` components that are proportionally the + * same distance between two vectors. + * + * The `amt` parameter is the amount to interpolate between the old vector and + * the new vector. 0.0 keeps all components equal to the old vector's, 0.5 is + * halfway between, and 1.0 sets all components equal to the new vector's. + * + * The static version of `lerp()`, as in `p5.Vector.lerp(v0, v1, 0.5)`, + * returns a new p5.Vector object and doesn't change + * the original. + * + * @param {Number} x x component. + * @param {Number} y y component. + * @param {Number} z z component. + * @param {Number} amt amount of interpolation between 0.0 (old vector) + * and 1.0 (new vector). 0.5 is halfway between. + * @chainable + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v0 = createVector(1, 1, 1); + * let v1 = createVector(3, 3, 3); + * + * // Interpolate. + * v0.lerp(v1, 0.5); + * + * // Prints "p5.Vector Object : [2, 2, 2]" to the console. + * print(v0.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(1, 1, 1); + * + * // Interpolate. + * v.lerp(3, 3, 3, 0.5); + * + * // Prints "p5.Vector Object : [2, 2, 2]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v0 = createVector(1, 1, 1); + * let v1 = createVector(3, 3, 3); + * + * // Interpolate. + * let v2 = p5.Vector.lerp(v0, v1, 0.5); + * + * // Prints "p5.Vector Object : [2, 2, 2]" to the console. + * print(v2.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points down, and a purple arrow points to the bottom right.'); + * } + * function draw() { + * background(200); + * + * // Create p5.Vector objects. + * let v0 = createVector(50, 50); + * let v1 = createVector(30, 0); + * let v2 = createVector(0, 30); + * + * // Interpolate. + * let v3 = p5.Vector.lerp(v1, v2, 0.5); + * + * // Draw the red arrow. + * drawArrow(v0, v1, 'red'); + * + * // Draw the blue arrow. + * drawArrow(v0, v2, 'blue'); + * + * // Draw the purple arrow. + * drawArrow(v0, v3, 'purple'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + /** + * @param {p5.Vector} v p5.Vector to lerp toward. + * @param {Number} amt + * @chainable + */ + lerp(x, y, z, amt) { + if (x instanceof Vector) { + return this.lerp(x.x, x.y, x.z, y); + } + this.x += (x - this.x) * amt || 0; + this.y += (y - this.y) * amt || 0; + this.z += (z - this.z) * amt || 0; + return this; + } + + /** + * Calculates a new heading and magnitude that are between two vectors. + * + * The `amt` parameter is the amount to interpolate between the old vector and + * the new vector. 0.0 keeps the heading and magnitude equal to the old + * vector's, 0.5 sets them halfway between, and 1.0 sets the heading and + * magnitude equal to the new vector's. + * + * `slerp()` differs from lerp() because + * it interpolates magnitude. Calling `v0.slerp(v1, 0.5)` sets `v0`'s + * magnitude to a value halfway between its original magnitude and `v1`'s. + * Calling `v0.lerp(v1, 0.5)` makes no such guarantee. + * + * The static version of `slerp()`, as in `p5.Vector.slerp(v0, v1, 0.5)`, + * returns a new p5.Vector object and doesn't change + * the original. + * + * @param {p5.Vector} v p5.Vector to slerp toward. + * @param {Number} amt amount of interpolation between 0.0 (old vector) + * and 1.0 (new vector). 0.5 is halfway between. + * @return {p5.Vector} + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v0 = createVector(3, 0); + * + * // Prints "3" to the console. + * print(v0.mag()); + * + * // Prints "0" to the console. + * print(v0.heading()); + * + * // Create a p5.Vector object. + * let v1 = createVector(0, 1); + * + * // Prints "1" to the console. + * print(v1.mag()); + * + * // Prints "1.570..." to the console. + * print(v1.heading()); + * + * // Interpolate halfway between v0 and v1. + * v0.slerp(v1, 0.5); + * + * // Prints "2" to the console. + * print(v0.mag()); + * + * // Prints "0.785..." to the console. + * print(v0.heading()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v0 = createVector(3, 0); + * + * // Prints "3" to the console. + * print(v0.mag()); + * + * // Prints "0" to the console. + * print(v0.heading()); + * + * // Create a p5.Vector object. + * let v1 = createVector(0, 1); + * + * // Prints "1" to the console. + * print(v1.mag()); + * + * // Prints "1.570..." to the console. + * print(v1.heading()); + * + * // Create a p5.Vector that's halfway between v0 and v1. + * let v3 = p5.Vector.slerp(v0, v1, 0.5); + * + * // Prints "2" to the console. + * print(v3.mag()); + * + * // Prints "0.785..." to the console. + * print(v3.heading()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Three arrows extend from the center of a gray square. A red arrow points to the right, a blue arrow points to the left, and a purple arrow points down.'); + * } + * + * function draw() { + * background(200); + * + * // Create p5.Vector objects. + * let v0 = createVector(50, 50); + * let v1 = createVector(20, 0); + * let v2 = createVector(-40, 0); + * + * // Create a p5.Vector that's halfway between v1 and v2. + * let v3 = p5.Vector.slerp(v1, v2, 0.5); + * + * // Draw the red arrow. + * drawArrow(v0, v1, 'red'); + * + * // Draw the blue arrow. + * drawArrow(v0, v2, 'blue'); + * + * // Draw the purple arrow. + * drawArrow(v0, v3, 'purple'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + slerp(v, amt) { + // edge cases. + if (amt === 0) { return this; } + if (amt === 1) { return this.set(v); } + + // calculate magnitudes + const selfMag = this.mag(); + const vMag = v.mag(); + const magmag = selfMag * vMag; + // if either is a zero vector, linearly interpolate by these vectors + if (magmag === 0) { + this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); + return this; + } + // the cross product of 'this' and 'v' is the axis of rotation + const axis = this.cross(v); + const axisMag = axis.mag(); + // Calculates the angle between 'this' and 'v' + const theta = Math.atan2(axisMag, this.dot(v)); + + // However, if the norm of axis is 0, normalization cannot be performed, + // so we will divide the cases + if (axisMag > 0) { + axis.x /= axisMag; + axis.y /= axisMag; + axis.z /= axisMag; + } else if (theta < Math.PI * 0.5) { + // if the norm is 0 and the angle is less than PI/2, + // the angle is very close to 0, so do linear interpolation. + this.mult(1 - amt).add(v.x * amt, v.y * amt, v.z * amt); + return this; + } else { + // If the norm is 0 and the angle is more than PI/2, the angle is + // very close to PI. + // In this case v can be regarded as '-this', so take any vector + // that is orthogonal to 'this' and use that as the axis. + if (this.z === 0 && v.z === 0) { + // if both this and v are 2D vectors, use (0,0,1) + // this makes the result also a 2D vector. + axis.set(0, 0, 1); + } else if (this.x !== 0) { + // if the x components is not 0, use (y, -x, 0) + axis.set(this.y, -this.x, 0).normalize(); + } else { + // if the x components is 0, use (1,0,0) + axis.set(1, 0, 0); + } + } + + // Since 'axis' is a unit vector, ey is a vector of the same length as 'this'. + const ey = axis.cross(this); + // interpolate the length with 'this' and 'v'. + const lerpedMagFactor = (1 - amt) + amt * vMag / selfMag; + // imagine a situation where 'axis', 'this', and 'ey' are pointing + // along the z, x, and y axes, respectively. + // rotates 'this' around 'axis' by amt * theta towards 'ey'. + const cosMultiplier = lerpedMagFactor * Math.cos(amt * theta); + const sinMultiplier = lerpedMagFactor * Math.sin(amt * theta); + // then, calculate 'result'. + this.x = this.x * cosMultiplier + ey.x * sinMultiplier; + this.y = this.y * cosMultiplier + ey.y * sinMultiplier; + this.z = this.z * cosMultiplier + ey.z * sinMultiplier; + + return this; + } + + /** + * Reflects a vector about a line in 2D or a plane in 3D. + * + * The orientation of the line or plane is described by a normal vector that + * points away from the shape. + * + * The static version of `reflect()`, as in `p5.Vector.reflect(v, n)`, + * returns a new p5.Vector object and doesn't change + * the original. + * + * @param {p5.Vector} surfaceNormal p5.Vector + * to reflect about. + * @chainable + * @example + *
+ * + * function setup() { + * // Create a normal vector. + * let n = createVector(0, 1); + * // Create a vector to reflect. + * let v = createVector(4, 6); + * + * // Reflect v about n. + * v.reflect(n); + * + * // Prints "p5.Vector Object : [4, -6, 0]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a normal vector. + * let n = createVector(0, 1); + * + * // Create a vector to reflect. + * let v0 = createVector(4, 6); + * + * // Create a reflected vector. + * let v1 = p5.Vector.reflect(v0, n); + * + * // Prints "p5.Vector Object : [4, -6, 0]" to the console. + * print(v1.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('Three arrows extend from the center of a gray square with a vertical line down its middle. A black arrow points to the right, a blue arrow points to the bottom left, and a red arrow points to the bottom right.'); + * } + * function draw() { + * background(200); + * + * // Draw a vertical line. + * line(50, 0, 50, 100); + * + * // Create a normal vector. + * let n = createVector(1, 0); + * + * // Center. + * let v0 = createVector(50, 50); + * + * // Create a vector to reflect. + * let v1 = createVector(30, 40); + * + * // Create a reflected vector. + * let v2 = p5.Vector.reflect(v1, n); + * + * // Scale the normal vector for drawing. + * n.setMag(30); + * + * // Draw the black arrow. + * drawArrow(v0, n, 'black'); + * + * // Draw the red arrow. + * drawArrow(v0, v1, 'red'); + * + * // Draw the blue arrow. + * drawArrow(v0, v2, 'blue'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + reflect(surfaceNormal) { + const surfaceNormalCopy = Vector.normalize(surfaceNormal); + return this.sub(surfaceNormalCopy.mult(2 * this.dot(surfaceNormalCopy))); + } + + /** + * Returns the vector's components as an array of numbers. + * + * @return {Number[]} array with the vector's components. + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = createVector(20, 30); + * + * // Prints "[20, 30, 0]" to the console. + * print(v.array()); + * } + * + *
+ */ + array() { + return [this.x || 0, this.y || 0, this.z || 0]; + } + + /** + * Checks whether all the vector's components are equal to another vector's. + * + * `equals()` returns `true` if the vector's components are all the same as another + * vector's and `false` if not. + * + * The version of `equals()` with one parameter interprets it as another + * p5.Vector object. + * + * The version of `equals()` with multiple parameters interprets them as the + * components of another vector. Any missing parameters are assigned the value + * 0. + * + * The static version of `equals()`, as in `p5.Vector.equals(v0, v1)`, + * interprets both parameters as p5.Vector objects. + * + * @param {Number} [x] x component of the vector. + * @param {Number} [y] y component of the vector. + * @param {Number} [z] z component of the vector. + * @return {Boolean} whether the vectors are equal. + * @example + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v0 = createVector(10, 20, 30); + * let v1 = createVector(10, 20, 30); + * let v2 = createVector(0, 0, 0); + * + * // Prints "true" to the console. + * print(v0.equals(v1)); + * + * // Prints "false" to the console. + * print(v0.equals(v2)); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v0 = createVector(5, 10, 20); + * let v1 = createVector(5, 10, 20); + * let v2 = createVector(13, 10, 19); + * + * // Prints "true" to the console. + * print(v0.equals(v1.x, v1.y, v1.z)); + * + * // Prints "false" to the console. + * print(v0.equals(v2.x, v2.y, v2.z)); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create p5.Vector objects. + * let v0 = createVector(10, 20, 30); + * let v1 = createVector(10, 20, 30); + * let v2 = createVector(0, 0, 0); + * + * // Prints "true" to the console. + * print(p5.Vector.equals(v0, v1)); + * + * // Prints "false" to the console. + * print(p5.Vector.equals(v0, v2)); + * } + * + *
+ */ + /** + * @param {p5.Vector|Array} value vector to compare. + * @return {Boolean} + */ + equals(x, y, z) { + let a, b, c; + if (x instanceof Vector) { + a = x.x || 0; + b = x.y || 0; + c = x.z || 0; + } else if (Array.isArray(x)) { + a = x[0] || 0; + b = x[1] || 0; + c = x[2] || 0; + } else { + a = x || 0; + b = y || 0; + c = z || 0; } + return this.x === a && this.y === b && this.z === c; + } - /** - * Reflects a vector about a line in 2D or a plane in 3D. - * - * The orientation of the line or plane is described by a normal vector that - * points away from the shape. - * - * The static version of `reflect()`, as in `p5.Vector.reflect(v, n)`, - * returns a new p5.Vector object and doesn't change - * the original. - * - * @param {p5.Vector} surfaceNormal p5.Vector - * to reflect about. - * @chainable - * @example - *
- * - * function setup() { - * // Create a normal vector. - * let n = createVector(0, 1); - * // Create a vector to reflect. - * let v = createVector(4, 6); - * - * // Reflect v about n. - * v.reflect(n); - * - * // Prints "p5.Vector Object : [4, -6, 0]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a normal vector. - * let n = createVector(0, 1); - * - * // Create a vector to reflect. - * let v0 = createVector(4, 6); - * - * // Create a reflected vector. - * let v1 = p5.Vector.reflect(v0, n); - * - * // Prints "p5.Vector Object : [4, -6, 0]" to the console. - * print(v1.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('Three arrows extend from the center of a gray square with a vertical line down its middle. A black arrow points to the right, a blue arrow points to the bottom left, and a red arrow points to the bottom right.'); - * } - * function draw() { - * background(200); - * - * // Draw a vertical line. - * line(50, 0, 50, 100); - * - * // Create a normal vector. - * let n = createVector(1, 0); - * - * // Center. - * let v0 = createVector(50, 50); - * - * // Create a vector to reflect. - * let v1 = createVector(30, 40); - * - * // Create a reflected vector. - * let v2 = p5.Vector.reflect(v1, n); - * - * // Scale the normal vector for drawing. - * n.setMag(30); - * - * // Draw the black arrow. - * drawArrow(v0, n, 'black'); - * - * // Draw the red arrow. - * drawArrow(v0, v1, 'red'); - * - * // Draw the blue arrow. - * drawArrow(v0, v2, 'blue'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - reflect(surfaceNormal) { - const surfaceNormalCopy = p5.Vector.normalize(surfaceNormal); - return this.sub(surfaceNormalCopy.mult(2 * this.dot(surfaceNormalCopy))); - } + /** + * Replaces the components of a p5.Vector that are very close to zero with zero. + * + * In computers, handling numbers with decimals can give slightly imprecise answers due to the way those numbers are represented. + * This can make it hard to check if a number is zero, as it may be close but not exactly zero. + * This method rounds very close numbers to zero to make those checks easier + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON + * + * @method clampToZero + * @return {p5.Vector} with components very close to zero replaced with zero. + * @chainable + */ + clampToZero() { + this.x = this._clampToZero(this.x); + this.y = this._clampToZero(this.y); + this.z = this._clampToZero(this.z); + return this; + } - /** - * Returns the vector's components as an array of numbers. - * - * @return {Number[]} array with the vector's components. - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = createVector(20, 30); - * - * // Prints "[20, 30, 0]" to the console. - * print(v.array()); - * } - * - *
- */ - array() { - return [this.x || 0, this.y || 0, this.z || 0]; - } + /** + * Helper function for clampToZero + * @private + */ + _clampToZero(val) { + return Math.abs((val || 0) - 0) <= Number.EPSILON ? 0 : val; + } - /** - * Checks whether all the vector's components are equal to another vector's. - * - * `equals()` returns `true` if the vector's components are all the same as another - * vector's and `false` if not. - * - * The version of `equals()` with one parameter interprets it as another - * p5.Vector object. - * - * The version of `equals()` with multiple parameters interprets them as the - * components of another vector. Any missing parameters are assigned the value - * 0. - * - * The static version of `equals()`, as in `p5.Vector.equals(v0, v1)`, - * interprets both parameters as p5.Vector objects. - * - * @param {Number} [x] x component of the vector. - * @param {Number} [y] y component of the vector. - * @param {Number} [z] z component of the vector. - * @return {Boolean} whether the vectors are equal. - * @example - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v0 = createVector(10, 20, 30); - * let v1 = createVector(10, 20, 30); - * let v2 = createVector(0, 0, 0); - * - * // Prints "true" to the console. - * print(v0.equals(v1)); - * - * // Prints "false" to the console. - * print(v0.equals(v2)); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v0 = createVector(5, 10, 20); - * let v1 = createVector(5, 10, 20); - * let v2 = createVector(13, 10, 19); - * - * // Prints "true" to the console. - * print(v0.equals(v1.x, v1.y, v1.z)); - * - * // Prints "false" to the console. - * print(v0.equals(v2.x, v2.y, v2.z)); - * } - * - *
- * - *
- * - * function setup() { - * // Create p5.Vector objects. - * let v0 = createVector(10, 20, 30); - * let v1 = createVector(10, 20, 30); - * let v2 = createVector(0, 0, 0); - * - * // Prints "true" to the console. - * print(p5.Vector.equals(v0, v1)); - * - * // Prints "false" to the console. - * print(p5.Vector.equals(v0, v2)); - * } - * - *
- */ - /** - * @param {p5.Vector|Array} value vector to compare. - * @return {Boolean} - */ - equals(x, y, z) { - let a, b, c; - if (x instanceof p5.Vector) { - a = x.x || 0; - b = x.y || 0; - c = x.z || 0; - } else if (Array.isArray(x)) { - a = x[0] || 0; - b = x[1] || 0; - c = x[2] || 0; - } else { - a = x || 0; - b = y || 0; - c = z || 0; - } - return this.x === a && this.y === b && this.z === c; - } + // Static Methods - /** - * Replaces the components of a p5.Vector that are very close to zero with zero. - * - * In computers, handling numbers with decimals can give slightly imprecise answers due to the way those numbers are represented. - * This can make it hard to check if a number is zero, as it may be close but not exactly zero. - * This method rounds very close numbers to zero to make those checks easier - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON - * - * @method clampToZero - * @return {p5.Vector} with components very close to zero replaced with zero. - * @chainable - */ - clampToZero() { - this.x = this._clampToZero(this.x); - this.y = this._clampToZero(this.y); - this.z = this._clampToZero(this.z); - return this; + /** + * Creates a new 2D vector from an angle. + * + * @static + * @param {Number} angle desired angle, in radians. Unaffected by angleMode(). + * @param {Number} [length] length of the new vector (defaults to 1). + * @return {p5.Vector} new p5.Vector object. + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = p5.Vector.fromAngle(0); + * + * // Prints "p5.Vector Object : [1, 0, 0]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = p5.Vector.fromAngle(0, 30); + * + * // Prints "p5.Vector Object : [30, 0, 0]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * describe('A black arrow extends from the center of a gray square. It points to the right.'); + * } + * function draw() { + * background(200); + * + * // Create a p5.Vector to the center. + * let v0 = createVector(50, 50); + * + * // Create a p5.Vector with an angle 0 and magnitude 30. + * let v1 = p5.Vector.fromAngle(0, 30); + * + * // Draw the black arrow. + * drawArrow(v0, v1, 'black'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + static fromAngle(angle, length) { + if (typeof length === 'undefined') { + length = 1; } + return new Vector( + length * Math.cos(angle), + length * Math.sin(angle), + 0 + ); + } - /** - * Helper function for clampToZero - * @private - */ - _clampToZero(val) { - return Math.abs((val || 0) - 0) <= Number.EPSILON ? 0 : val; + /** + * Creates a new 3D vector from a pair of ISO spherical angles. + * + * @static + * @param {Number} theta polar angle in radians (zero is up). + * @param {Number} phi azimuthal angle in radians + * (zero is out of the screen). + * @param {Number} [length] length of the new vector (defaults to 1). + * @return {p5.Vector} new p5.Vector object. + * + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = p5.Vector.fromAngles(0, 0); + * + * // Prints "p5.Vector Object : [0, -1, 0]" to the console. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A light shines on a pink sphere as it orbits.'); + * } + * + * function draw() { + * background(0); + * + * // Calculate the ISO angles. + * let theta = frameCount * 0.05; + * let phi = 0; + * + * // Create a p5.Vector object. + * let v = p5.Vector.fromAngles(theta, phi, 100); + * + * // Create a point light using the p5.Vector. + * let c = color('deeppink'); + * pointLight(c, v); + * + * // Style the sphere. + * fill(255); + * noStroke(); + * + * // Draw the sphere. + * sphere(35); + * } + * + *
+ */ + static fromAngles(theta, phi, length) { + if (typeof length === 'undefined') { + length = 1; } + const cosPhi = Math.cos(phi); + const sinPhi = Math.sin(phi); + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); + + return new Vector( + length * sinTheta * sinPhi, + -length * cosTheta, + length * sinTheta * cosPhi + ); + } - // Static Methods + /** + * Creates a new 2D unit vector with a random heading. + * + * @static + * @return {p5.Vector} new p5.Vector object. + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = p5.Vector.random2D(); + * + * // Prints "p5.Vector Object : [x, y, 0]" to the console + * // where x and y are small random numbers. + * print(v.toString()); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * // Slow the frame rate. + * frameRate(1); + * + * describe('A black arrow in extends from the center of a gray square. It changes direction once per second.'); + * } + * + * function draw() { + * background(200); + * + * // Create a p5.Vector to the center. + * let v0 = createVector(50, 50); + * + * // Create a random p5.Vector. + * let v1 = p5.Vector.random2D(); + * + * // Scale v1 for drawing. + * v1.mult(30); + * + * // Draw the black arrow. + * drawArrow(v0, v1, 'black'); + * } + * + * // Draws an arrow between two vectors. + * function drawArrow(base, vec, myColor) { + * push(); + * stroke(myColor); + * strokeWeight(3); + * fill(myColor); + * translate(base.x, base.y); + * line(0, 0, vec.x, vec.y); + * rotate(vec.heading()); + * let arrowSize = 7; + * translate(vec.mag() - arrowSize, 0); + * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); + * pop(); + * } + * + *
+ */ + static random2D() { + return this.fromAngle(Math.random() * constants.TWO_PI); + } - /** - * Creates a new 2D vector from an angle. - * - * @static - * @param {Number} angle desired angle, in radians. Unaffected by angleMode(). - * @param {Number} [length] length of the new vector (defaults to 1). - * @return {p5.Vector} new p5.Vector object. - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = p5.Vector.fromAngle(0); - * - * // Prints "p5.Vector Object : [1, 0, 0]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = p5.Vector.fromAngle(0, 30); - * - * // Prints "p5.Vector Object : [30, 0, 0]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * describe('A black arrow extends from the center of a gray square. It points to the right.'); - * } - * function draw() { - * background(200); - * - * // Create a p5.Vector to the center. - * let v0 = createVector(50, 50); - * - * // Create a p5.Vector with an angle 0 and magnitude 30. - * let v1 = p5.Vector.fromAngle(0, 30); - * - * // Draw the black arrow. - * drawArrow(v0, v1, 'black'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - static fromAngle(angle, length) { - if (typeof length === 'undefined') { - length = 1; - } - return new p5.Vector( - length * Math.cos(angle), - length * Math.sin(angle), - 0 - ); - } + /** + * Creates a new 3D unit vector with a random heading. + * + * @static + * @return {p5.Vector} new p5.Vector object. + * @example + *
+ * + * function setup() { + * // Create a p5.Vector object. + * let v = p5.Vector.random3D(); + * + * // Prints "p5.Vector Object : [x, y, z]" to the console + * // where x, y, and z are small random numbers. + * print(v.toString()); + * } + * + *
+ */ + static random3D() { + const angle = Math.random() * constants.TWO_PI; + const vz = Math.random() * 2 - 1; + const vzBase = Math.sqrt(1 - vz * vz); + const vx = vzBase * Math.cos(angle); + const vy = vzBase * Math.sin(angle); + return new Vector(vx, vy, vz); + } + + // Returns a copy of a vector. + /** + * @static + * @param {p5.Vector} v the p5.Vector to create a copy of + * @return {p5.Vector} the copy of the p5.Vector object + */ + static copy(v) { + return v.copy(v); + } - /** - * Creates a new 3D vector from a pair of ISO spherical angles. - * - * @static - * @param {Number} theta polar angle in radians (zero is up). - * @param {Number} phi azimuthal angle in radians - * (zero is out of the screen). - * @param {Number} [length] length of the new vector (defaults to 1). - * @return {p5.Vector} new p5.Vector object. - * - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = p5.Vector.fromAngles(0, 0); - * - * // Prints "p5.Vector Object : [0, -1, 0]" to the console. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A light shines on a pink sphere as it orbits.'); - * } - * - * function draw() { - * background(0); - * - * // Calculate the ISO angles. - * let theta = frameCount * 0.05; - * let phi = 0; - * - * // Create a p5.Vector object. - * let v = p5.Vector.fromAngles(theta, phi, 100); - * - * // Create a point light using the p5.Vector. - * let c = color('deeppink'); - * pointLight(c, v); - * - * // Style the sphere. - * fill(255); - * noStroke(); - * - * // Draw the sphere. - * sphere(35); - * } - * - *
- */ - static fromAngles(theta, phi, length) { - if (typeof length === 'undefined') { - length = 1; + // Adds two vectors together and returns a new one. + /** + * @static + * @param {p5.Vector} v1 A p5.Vector to add + * @param {p5.Vector} v2 A p5.Vector to add + * @param {p5.Vector} [target] vector to receive the result. + * @return {p5.Vector} resulting p5.Vector. + */ + static add(v1, v2, target) { + if (!target) { + target = v1.copy(); + if (arguments.length === 3) { + p5._friendlyError( + 'The target parameter is undefined, it should be of type p5.Vector', + 'p5.Vector.add' + ); } - const cosPhi = Math.cos(phi); - const sinPhi = Math.sin(phi); - const cosTheta = Math.cos(theta); - const sinTheta = Math.sin(theta); - - return new p5.Vector( - length * sinTheta * sinPhi, - -length * cosTheta, - length * sinTheta * cosPhi - ); - } - - /** - * Creates a new 2D unit vector with a random heading. - * - * @static - * @return {p5.Vector} new p5.Vector object. - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = p5.Vector.random2D(); - * - * // Prints "p5.Vector Object : [x, y, 0]" to the console - * // where x and y are small random numbers. - * print(v.toString()); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * // Slow the frame rate. - * frameRate(1); - * - * describe('A black arrow in extends from the center of a gray square. It changes direction once per second.'); - * } - * - * function draw() { - * background(200); - * - * // Create a p5.Vector to the center. - * let v0 = createVector(50, 50); - * - * // Create a random p5.Vector. - * let v1 = p5.Vector.random2D(); - * - * // Scale v1 for drawing. - * v1.mult(30); - * - * // Draw the black arrow. - * drawArrow(v0, v1, 'black'); - * } - * - * // Draws an arrow between two vectors. - * function drawArrow(base, vec, myColor) { - * push(); - * stroke(myColor); - * strokeWeight(3); - * fill(myColor); - * translate(base.x, base.y); - * line(0, 0, vec.x, vec.y); - * rotate(vec.heading()); - * let arrowSize = 7; - * translate(vec.mag() - arrowSize, 0); - * triangle(0, arrowSize / 2, 0, -arrowSize / 2, arrowSize, 0); - * pop(); - * } - * - *
- */ - static random2D() { - return this.fromAngle(Math.random() * constants.TWO_PI); + } else { + target.set(v1); } + target.add(v2); + return target; + } - /** - * Creates a new 3D unit vector with a random heading. - * - * @static - * @return {p5.Vector} new p5.Vector object. - * @example - *
- * - * function setup() { - * // Create a p5.Vector object. - * let v = p5.Vector.random3D(); - * - * // Prints "p5.Vector Object : [x, y, z]" to the console - * // where x, y, and z are small random numbers. - * print(v.toString()); - * } - * - *
- */ - static random3D() { - const angle = Math.random() * constants.TWO_PI; - const vz = Math.random() * 2 - 1; - const vzBase = Math.sqrt(1 - vz * vz); - const vx = vzBase * Math.cos(angle); - const vy = vzBase * Math.sin(angle); - return new p5.Vector(vx, vy, vz); - } - - // Returns a copy of a vector. - /** - * @static - * @param {p5.Vector} v the p5.Vector to create a copy of - * @return {p5.Vector} the copy of the p5.Vector object - */ - static copy(v) { - return v.copy(v); - } - - // Adds two vectors together and returns a new one. - /** - * @static - * @param {p5.Vector} v1 A p5.Vector to add - * @param {p5.Vector} v2 A p5.Vector to add - * @param {p5.Vector} [target] vector to receive the result. - * @return {p5.Vector} resulting p5.Vector. - */ - static add(v1, v2, target) { - if (!target) { - target = v1.copy(); - if (arguments.length === 3) { - p5._friendlyError( - 'The target parameter is undefined, it should be of type p5.Vector', - 'p5.Vector.add' - ); - } - } else { - target.set(v1); - } - target.add(v2); + // Returns a vector remainder when it is divided by another vector + /** + * @static + * @param {p5.Vector} v1 The dividend p5.Vector + * @param {p5.Vector} v2 The divisor p5.Vector + */ + /** + * @static + * @param {p5.Vector} v1 + * @param {p5.Vector} v2 + * @return {p5.Vector} The resulting p5.Vector + */ + static rem(v1, v2) { + if (v1 instanceof Vector && v2 instanceof Vector) { + let target = v1.copy(); + target.rem(v2); return target; } + } - // Returns a vector remainder when it is divided by another vector - /** - * @static - * @param {p5.Vector} v1 The dividend p5.Vector - * @param {p5.Vector} v2 The divisor p5.Vector - */ - /** - * @static - * @param {p5.Vector} v1 - * @param {p5.Vector} v2 - * @return {p5.Vector} The resulting p5.Vector - */ - static rem(v1, v2) { - if (v1 instanceof p5.Vector && v2 instanceof p5.Vector) { - let target = v1.copy(); - target.rem(v2); - return target; - } - } - - /* - * Subtracts one p5.Vector from another and returns a new one. The second - * vector (`v2`) is subtracted from the first (`v1`), resulting in `v1-v2`. - */ - /** - * @static - * @param {p5.Vector} v1 A p5.Vector to subtract from - * @param {p5.Vector} v2 A p5.Vector to subtract - * @param {p5.Vector} [target] vector to receive the result. - * @return {p5.Vector} The resulting p5.Vector - */ - static sub(v1, v2, target) { - if (!target) { - target = v1.copy(); - if (arguments.length === 3) { - p5._friendlyError( - 'The target parameter is undefined, it should be of type p5.Vector', - 'p5.Vector.sub' - ); - } - } else { - target.set(v1); + /* + * Subtracts one p5.Vector from another and returns a new one. The second + * vector (`v2`) is subtracted from the first (`v1`), resulting in `v1-v2`. + */ + /** + * @static + * @param {p5.Vector} v1 A p5.Vector to subtract from + * @param {p5.Vector} v2 A p5.Vector to subtract + * @param {p5.Vector} [target] vector to receive the result. + * @return {p5.Vector} The resulting p5.Vector + */ + static sub(v1, v2, target) { + if (!target) { + target = v1.copy(); + if (arguments.length === 3) { + p5._friendlyError( + 'The target parameter is undefined, it should be of type p5.Vector', + 'p5.Vector.sub' + ); } - target.sub(v2); - return target; + } else { + target.set(v1); } + target.sub(v2); + return target; + } - /** - * Multiplies a vector by a scalar and returns a new vector. - */ - /** - * @static - * @param {Number} x - * @param {Number} y - * @param {Number} [z] - * @return {p5.Vector} resulting new p5.Vector. - */ - /** - * @static - * @param {p5.Vector} v - * @param {Number} n - * @param {p5.Vector} [target] vector to receive the result. - */ - /** - * @static - * @param {p5.Vector} v0 - * @param {p5.Vector} v1 - * @param {p5.Vector} [target] - */ - /** - * @static - * @param {p5.Vector} v0 - * @param {Number[]} arr - * @param {p5.Vector} [target] - */ - static mult(v, n, target) { - if (!target) { - target = v.copy(); - if (arguments.length === 3) { - p5._friendlyError( - 'The target parameter is undefined, it should be of type p5.Vector', - 'p5.Vector.mult' - ); - } - } else { - target.set(v); + /** + * Multiplies a vector by a scalar and returns a new vector. + */ + /** + * @static + * @param {Number} x + * @param {Number} y + * @param {Number} [z] + * @return {p5.Vector} resulting new p5.Vector. + */ + /** + * @static + * @param {p5.Vector} v + * @param {Number} n + * @param {p5.Vector} [target] vector to receive the result. + */ + /** + * @static + * @param {p5.Vector} v0 + * @param {p5.Vector} v1 + * @param {p5.Vector} [target] + */ + /** + * @static + * @param {p5.Vector} v0 + * @param {Number[]} arr + * @param {p5.Vector} [target] + */ + static mult(v, n, target) { + if (!target) { + target = v.copy(); + if (arguments.length === 3) { + p5._friendlyError( + 'The target parameter is undefined, it should be of type p5.Vector', + 'p5.Vector.mult' + ); } - target.mult(n); - return target; + } else { + target.set(v); } + target.mult(n); + return target; + } - /** - * Rotates the vector (only 2D vectors) by the given angle; magnitude remains the same. Returns a new vector. - */ - /** - * @static - * @param {p5.Vector} v - * @param {Number} angle - * @param {p5.Vector} [target] The vector to receive the result - */ - static rotate(v, a, target) { - if (arguments.length === 2) { - target = v.copy(); - } else { - if (!(target instanceof p5.Vector)) { - p5._friendlyError( - 'The target parameter should be of type p5.Vector', - 'p5.Vector.rotate' - ); - } - target.set(v); + /** + * Rotates the vector (only 2D vectors) by the given angle; magnitude remains the same. Returns a new vector. + */ + /** + * @static + * @param {p5.Vector} v + * @param {Number} angle + * @param {p5.Vector} [target] The vector to receive the result + */ + static rotate(v, a, target) { + if (arguments.length === 2) { + target = v.copy(); + } else { + if (!(target instanceof Vector)) { + p5._friendlyError( + 'The target parameter should be of type p5.Vector', + 'p5.Vector.rotate' + ); } - target.rotate(a); - return target; + target.set(v); } + target.rotate(a); + return target; + } - /** - * Divides a vector by a scalar and returns a new vector. - */ - /** - * @static - * @param {Number} x - * @param {Number} y - * @param {Number} [z] - * @return {p5.Vector} The resulting new p5.Vector - */ - /** - * @static - * @param {p5.Vector} v - * @param {Number} n - * @param {p5.Vector} [target] The vector to receive the result - */ - /** - * @static - * @param {p5.Vector} v0 - * @param {p5.Vector} v1 - * @param {p5.Vector} [target] - */ - /** - * @static - * @param {p5.Vector} v0 - * @param {Number[]} arr - * @param {p5.Vector} [target] - */ - static div(v, n, target) { - if (!target) { - target = v.copy(); - - if (arguments.length === 3) { - p5._friendlyError( - 'The target parameter is undefined, it should be of type p5.Vector', - 'p5.Vector.div' - ); - } - } else { - target.set(v); - } - target.div(n); - return target; - } + /** + * Divides a vector by a scalar and returns a new vector. + */ + /** + * @static + * @param {Number} x + * @param {Number} y + * @param {Number} [z] + * @return {p5.Vector} The resulting new p5.Vector + */ + /** + * @static + * @param {p5.Vector} v + * @param {Number} n + * @param {p5.Vector} [target] The vector to receive the result + */ + /** + * @static + * @param {p5.Vector} v0 + * @param {p5.Vector} v1 + * @param {p5.Vector} [target] + */ + /** + * @static + * @param {p5.Vector} v0 + * @param {Number[]} arr + * @param {p5.Vector} [target] + */ + static div(v, n, target) { + if (!target) { + target = v.copy(); - /** - * Calculates the dot product of two vectors. - */ - /** - * @static - * @param {p5.Vector} v1 first p5.Vector. - * @param {p5.Vector} v2 second p5.Vector. - * @return {Number} dot product. - */ - static dot(v1, v2) { - return v1.dot(v2); + if (arguments.length === 3) { + p5._friendlyError( + 'The target parameter is undefined, it should be of type p5.Vector', + 'p5.Vector.div' + ); + } + } else { + target.set(v); } + target.div(n); + return target; + } - /** - * Calculates the cross product of two vectors. - */ - /** - * @static - * @param {p5.Vector} v1 first p5.Vector. - * @param {p5.Vector} v2 second p5.Vector. - * @return {Number} cross product. - */ - static cross(v1, v2) { - return v1.cross(v2); - } + /** + * Calculates the dot product of two vectors. + */ + /** + * @static + * @param {p5.Vector} v1 first p5.Vector. + * @param {p5.Vector} v2 second p5.Vector. + * @return {Number} dot product. + */ + static dot(v1, v2) { + return v1.dot(v2); + } - /** - * Calculates the Euclidean distance between two points (considering a - * point as a vector object). - */ - /** - * @static - * @param {p5.Vector} v1 The first p5.Vector - * @param {p5.Vector} v2 The second p5.Vector - * @return {Number} The distance - */ - static dist(v1, v2) { - return v1.dist(v2); - } + /** + * Calculates the cross product of two vectors. + */ + /** + * @static + * @param {p5.Vector} v1 first p5.Vector. + * @param {p5.Vector} v2 second p5.Vector. + * @return {Number} cross product. + */ + static cross(v1, v2) { + return v1.cross(v2); + } - /** - * Linear interpolate a vector to another vector and return the result as a - * new vector. + /** + * Calculates the Euclidean distance between two points (considering a + * point as a vector object). */ - /** + /** * @static - * @param {p5.Vector} v1 - * @param {p5.Vector} v2 - * @param {Number} amt - * @param {p5.Vector} [target] The vector to receive the result - * @return {p5.Vector} The lerped value + * @param {p5.Vector} v1 The first p5.Vector + * @param {p5.Vector} v2 The second p5.Vector + * @return {Number} The distance */ - static lerp(v1, v2, amt, target) { - if (!target) { - target = v1.copy(); - if (arguments.length === 4) { - p5._friendlyError( - 'The target parameter is undefined, it should be of type p5.Vector', - 'p5.Vector.lerp' - ); - } - } else { - target.set(v1); + static dist(v1, v2) { + return v1.dist(v2); + } + + /** + * Linear interpolate a vector to another vector and return the result as a + * new vector. + */ + /** + * @static + * @param {p5.Vector} v1 + * @param {p5.Vector} v2 + * @param {Number} amt + * @param {p5.Vector} [target] The vector to receive the result + * @return {p5.Vector} The lerped value + */ + static lerp(v1, v2, amt, target) { + if (!target) { + target = v1.copy(); + if (arguments.length === 4) { + p5._friendlyError( + 'The target parameter is undefined, it should be of type p5.Vector', + 'p5.Vector.lerp' + ); } - target.lerp(v2, amt); - return target; + } else { + target.set(v1); } + target.lerp(v2, amt); + return target; + } - /** - * Performs spherical linear interpolation with the other vector - * and returns the resulting vector. - * This works in both 3D and 2D. As for 2D, the result of slerping - * between 2D vectors is always a 2D vector. - */ - /** - * @static - * @param {p5.Vector} v1 old vector. - * @param {p5.Vector} v2 new vector. - * @param {Number} amt - * @param {p5.Vector} [target] vector to receive the result. - * @return {p5.Vector} slerped vector between v1 and v2 - */ - static slerp(v1, v2, amt, target) { - if (!target) { - target = v1.copy(); - if (arguments.length === 4) { - p5._friendlyError( - 'The target parameter is undefined, it should be of type p5.Vector', - 'p5.Vector.slerp' - ); - } - } else { - target.set(v1); + /** + * Performs spherical linear interpolation with the other vector + * and returns the resulting vector. + * This works in both 3D and 2D. As for 2D, the result of slerping + * between 2D vectors is always a 2D vector. + */ + /** + * @static + * @param {p5.Vector} v1 old vector. + * @param {p5.Vector} v2 new vector. + * @param {Number} amt + * @param {p5.Vector} [target] vector to receive the result. + * @return {p5.Vector} slerped vector between v1 and v2 + */ + static slerp(v1, v2, amt, target) { + if (!target) { + target = v1.copy(); + if (arguments.length === 4) { + p5._friendlyError( + 'The target parameter is undefined, it should be of type p5.Vector', + 'p5.Vector.slerp' + ); } - target.slerp(v2, amt); - return target; + } else { + target.set(v1); } + target.slerp(v2, amt); + return target; + } - /** - * Calculates the magnitude (length) of the vector and returns the result as - * a float (this is simply the equation `sqrt(x*x + y*y + z*z)`.) - */ - /** - * @static - * @param {p5.Vector} vecT The vector to return the magnitude of - * @return {Number} The magnitude of vecT - */ - static mag(vecT) { - return vecT.mag(); - } + /** + * Calculates the magnitude (length) of the vector and returns the result as + * a float (this is simply the equation `sqrt(x*x + y*y + z*z)`.) + */ + /** + * @static + * @param {p5.Vector} vecT The vector to return the magnitude of + * @return {Number} The magnitude of vecT + */ + static mag(vecT) { + return vecT.mag(); + } - /** - * Calculates the squared magnitude of the vector and returns the result - * as a float (this is simply the equation (x\*x + y\*y + z\*z).) - * Faster if the real length is not required in the - * case of comparing vectors, etc. - */ - /** - * @static - * @param {p5.Vector} vecT the vector to return the squared magnitude of - * @return {Number} the squared magnitude of vecT - */ - static magSq(vecT) { - return vecT.magSq(); - } + /** + * Calculates the squared magnitude of the vector and returns the result + * as a float (this is simply the equation (x\*x + y\*y + z\*z).) + * Faster if the real length is not required in the + * case of comparing vectors, etc. + */ + /** + * @static + * @param {p5.Vector} vecT the vector to return the squared magnitude of + * @return {Number} the squared magnitude of vecT + */ + static magSq(vecT) { + return vecT.magSq(); + } - /** - * Normalize the vector to length 1 (make it a unit vector). - */ - /** - * @static - * @param {p5.Vector} v The vector to normalize - * @param {p5.Vector} [target] The vector to receive the result - * @return {p5.Vector} The vector v, normalized to a length of 1 - */ - static normalize(v, target) { - if (arguments.length < 2) { - target = v.copy(); - } else { - if (!(target instanceof p5.Vector)) { - p5._friendlyError( - 'The target parameter should be of type p5.Vector', - 'p5.Vector.normalize' - ); - } - target.set(v); + /** + * Normalize the vector to length 1 (make it a unit vector). + */ + /** + * @static + * @param {p5.Vector} v The vector to normalize + * @param {p5.Vector} [target] The vector to receive the result + * @return {p5.Vector} The vector v, normalized to a length of 1 + */ + static normalize(v, target) { + if (arguments.length < 2) { + target = v.copy(); + } else { + if (!(target instanceof Vector)) { + p5._friendlyError( + 'The target parameter should be of type p5.Vector', + 'p5.Vector.normalize' + ); } - return target.normalize(); + target.set(v); } + return target.normalize(); + } - /** - * Limit the magnitude of the vector to the value used for the max - * parameter. - */ - /** - * @static - * @param {p5.Vector} v the vector to limit - * @param {Number} max - * @param {p5.Vector} [target] the vector to receive the result (Optional) - * @return {p5.Vector} v with a magnitude limited to max - */ - static limit(v, max, target) { - if (arguments.length < 3) { - target = v.copy(); - } else { - if (!(target instanceof p5.Vector)) { - p5._friendlyError( - 'The target parameter should be of type p5.Vector', - 'p5.Vector.limit' - ); - } - target.set(v); + /** + * Limit the magnitude of the vector to the value used for the max + * parameter. + */ + /** + * @static + * @param {p5.Vector} v the vector to limit + * @param {Number} max + * @param {p5.Vector} [target] the vector to receive the result (Optional) + * @return {p5.Vector} v with a magnitude limited to max + */ + static limit(v, max, target) { + if (arguments.length < 3) { + target = v.copy(); + } else { + if (!(target instanceof Vector)) { + p5._friendlyError( + 'The target parameter should be of type p5.Vector', + 'p5.Vector.limit' + ); } - return target.limit(max); + target.set(v); } + return target.limit(max); + } - /** - * Set the magnitude of the vector to the value used for the len - * parameter. - */ - /** - * @static - * @param {p5.Vector} v the vector to set the magnitude of - * @param {Number} len - * @param {p5.Vector} [target] the vector to receive the result (Optional) - * @return {p5.Vector} v with a magnitude set to len - */ - static setMag(v, len, target) { - if (arguments.length < 3) { - target = v.copy(); - } else { - if (!(target instanceof p5.Vector)) { - p5._friendlyError( - 'The target parameter should be of type p5.Vector', - 'p5.Vector.setMag' - ); - } - target.set(v); + /** + * Set the magnitude of the vector to the value used for the len + * parameter. + */ + /** + * @static + * @param {p5.Vector} v the vector to set the magnitude of + * @param {Number} len + * @param {p5.Vector} [target] the vector to receive the result (Optional) + * @return {p5.Vector} v with a magnitude set to len + */ + static setMag(v, len, target) { + if (arguments.length < 3) { + target = v.copy(); + } else { + if (!(target instanceof Vector)) { + p5._friendlyError( + 'The target parameter should be of type p5.Vector', + 'p5.Vector.setMag' + ); } - return target.setMag(len); + target.set(v); } + return target.setMag(len); + } - /** - * Calculate the angle of rotation for this vector (only 2D vectors). - * p5.Vectors created using createVector() - * will take the current angleMode into - * consideration, and give the angle in radians or degrees accordingly. - */ - /** - * @static - * @param {p5.Vector} v the vector to find the angle of - * @return {Number} the angle of rotation - */ - static heading(v) { - return v.heading(); - } + /** + * Calculate the angle of rotation for this vector (only 2D vectors). + * p5.Vectors created using createVector() + * will take the current angleMode into + * consideration, and give the angle in radians or degrees accordingly. + */ + /** + * @static + * @param {p5.Vector} v the vector to find the angle of + * @return {Number} the angle of rotation + */ + static heading(v) { + return v.heading(); + } - /** - * Calculates and returns the angle between two vectors. This function will take - * the angleMode on v1 into consideration, and - * give the angle in radians or degrees accordingly. - */ - /** - * @static - * @param {p5.Vector} v1 the first vector. - * @param {p5.Vector} v2 the second vector. - * @return {Number} angle between the two vectors. - */ - static angleBetween(v1, v2) { - return v1.angleBetween(v2); - } + /** + * Calculates and returns the angle between two vectors. This function will take + * the angleMode on v1 into consideration, and + * give the angle in radians or degrees accordingly. + */ + /** + * @static + * @param {p5.Vector} v1 the first vector. + * @param {p5.Vector} v2 the second vector. + * @return {Number} angle between the two vectors. + */ + static angleBetween(v1, v2) { + return v1.angleBetween(v2); + } - /** - * Reflect a vector about a normal to a line in 2D, or about a normal to a - * plane in 3D. - */ - /** - * @static - * @param {p5.Vector} incidentVector vector to be reflected. - * @param {p5.Vector} surfaceNormal - * @param {p5.Vector} [target] vector to receive the result. - * @return {p5.Vector} the reflected vector - */ - static reflect(incidentVector, surfaceNormal, target) { - if (arguments.length < 3) { - target = incidentVector.copy(); - } else { - if (!(target instanceof p5.Vector)) { - p5._friendlyError( - 'The target parameter should be of type p5.Vector', - 'p5.Vector.reflect' - ); - } - target.set(incidentVector); + /** + * Reflect a vector about a normal to a line in 2D, or about a normal to a + * plane in 3D. + */ + /** + * @static + * @param {p5.Vector} incidentVector vector to be reflected. + * @param {p5.Vector} surfaceNormal + * @param {p5.Vector} [target] vector to receive the result. + * @return {p5.Vector} the reflected vector + */ + static reflect(incidentVector, surfaceNormal, target) { + if (arguments.length < 3) { + target = incidentVector.copy(); + } else { + if (!(target instanceof Vector)) { + p5._friendlyError( + 'The target parameter should be of type p5.Vector', + 'p5.Vector.reflect' + ); } - return target.reflect(surfaceNormal); + target.set(incidentVector); } + return target.reflect(surfaceNormal); + } - /** - * Return a representation of this vector as a float array. This is only - * for temporary use. If used in any other fashion, the contents should be - * copied by using the p5.Vector.copy() - * method to copy into your own vector. - */ - /** - * @static - * @param {p5.Vector} v the vector to convert to an array - * @return {Number[]} an Array with the 3 values - */ - static array(v) { - return v.array(); + /** + * Return a representation of this vector as a float array. This is only + * for temporary use. If used in any other fashion, the contents should be + * copied by using the p5.Vector.copy() + * method to copy into your own vector. + */ + /** + * @static + * @param {p5.Vector} v the vector to convert to an array + * @return {Number[]} an Array with the 3 values + */ + static array(v) { + return v.array(); + } + + /** + * Equality check against a p5.Vector + */ + /** + * @static + * @param {p5.Vector|Array} v1 the first vector to compare + * @param {p5.Vector|Array} v2 the second vector to compare + * @return {Boolean} + */ + static equals(v1, v2) { + let v; + if (v1 instanceof Vector) { + v = v1; + } else if (v1 instanceof Array) { + v = new Vector().set(v1); + } else { + p5._friendlyError( + 'The v1 parameter should be of type Array or p5.Vector', + 'p5.Vector.equals' + ); } + return v.equals(v2); + } +}; - /** - * Equality check against a p5.Vector - */ - /** - * @static - * @param {p5.Vector|Array} v1 the first vector to compare - * @param {p5.Vector|Array} v2 the second vector to compare - * @return {Boolean} - */ - static equals(v1, v2) { - let v; - if (v1 instanceof p5.Vector) { - v = v1; - } else if (v1 instanceof Array) { - v = new p5.Vector().set(v1); - } else { - p5._friendlyError( - 'The v1 parameter should be of type Array or p5.Vector', - 'p5.Vector.equals' - ); - } - return v.equals(v2); +function vector(p5, fn) { + /// HELPERS FOR REMAINDER METHOD + const calculateRemainder2D = function (xComponent, yComponent) { + if (xComponent !== 0) { + this.x = this.x % xComponent; + } + if (yComponent !== 0) { + this.y = this.y % yComponent; + } + return this; + }; + + const calculateRemainder3D = function (xComponent, yComponent, zComponent) { + if (xComponent !== 0) { + this.x = this.x % xComponent; + } + if (yComponent !== 0) { + this.y = this.y % yComponent; } + if (zComponent !== 0) { + this.z = this.z % zComponent; + } + return this; }; + /** + * A class to describe a two or three-dimensional vector. + * + * A vector can be thought of in different ways. In one view, a vector is like + * an arrow pointing in space. Vectors have both magnitude (length) and + * direction. + * + * `p5.Vector` objects are often used to program motion because they simplify + * the math. For example, a moving ball has a position and a velocity. + * Position describes where the ball is in space. The ball's position vector + * extends from the origin to the ball's center. Velocity describes the ball's + * speed and the direction it's moving. If the ball is moving straight up, its + * velocity vector points straight up. Adding the ball's velocity vector to + * its position vector moves it, as in `pos.add(vel)`. Vector math relies on + * methods inside the `p5.Vector` class. + * + * Note: createVector() is the recommended way + * to make an instance of this class. + * + * @class p5.Vector + * @param {Number} [x] x component of the vector. + * @param {Number} [y] y component of the vector. + * @param {Number} [z] z component of the vector. + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create p5.Vector objects. + * let p1 = createVector(25, 25); + * let p2 = createVector(75, 75); + * + * // Style the points. + * strokeWeight(5); + * + * // Draw the first point using a p5.Vector. + * point(p1); + * + * // Draw the second point using a p5.Vector's components. + * point(p2.x, p2.y); + * + * describe('Two black dots on a gray square, one at the top left and the other at the bottom right.'); + * } + * + *
+ * + *
+ * + * let pos; + * let vel; + * + * function setup() { + * createCanvas(100, 100); + * + * // Create p5.Vector objects. + * pos = createVector(50, 100); + * vel = createVector(0, -1); + * + * describe('A black dot moves from bottom to top on a gray square. The dot reappears at the bottom when it reaches the top.'); + * } + * + * function draw() { + * background(200); + * + * // Add velocity to position. + * pos.add(vel); + * + * // If the dot reaches the top of the canvas, + * // restart from the bottom. + * if (pos.y < 0) { + * pos.y = 100; + * } + * + * // Draw the dot. + * strokeWeight(5); + * point(pos); + * } + * + *
+ */ + p5.Vector = Vector; + /** * The x component of the vector * @type {Number} @@ -3826,6 +3828,7 @@ function vector(p5, fn) { } export default vector; +export { Vector }; if (typeof p5 !== 'undefined') { vector(p5, p5.prototype); diff --git a/src/shape/2d_primitives.js b/src/shape/2d_primitives.js index 0fe9f6bb60..6e4f551322 100644 --- a/src/shape/2d_primitives.js +++ b/src/shape/2d_primitives.js @@ -8,9 +8,6 @@ import * as constants from '../core/constants'; import canvas from '../core/helpers'; -import '../core/friendly_errors/fes_core'; -import '../core/friendly_errors/file_errors'; -import '../core/friendly_errors/validate_params'; function primitives(p5, fn){ /** diff --git a/src/shape/curves.js b/src/shape/curves.js index 97ace85d22..949f60e8ec 100644 --- a/src/shape/curves.js +++ b/src/shape/curves.js @@ -5,10 +5,6 @@ * @requires core */ -import '../core/friendly_errors/fes_core'; -import '../core/friendly_errors/file_errors'; -import '../core/friendly_errors/validate_params'; - function curves(p5, fn){ /** * Draws a Bézier curve. diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index d34e7890cb..5de854256b 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -7,6 +7,7 @@ */ import * as constants from '../core/constants'; +import { RendererGL } from './p5.RendererGL'; function primitives3D(p5, fn){ /** @@ -2439,7 +2440,7 @@ function primitives3D(p5, fn){ * *
*/ - p5.RendererGL.prototype.point = function(x, y, z = 0) { + RendererGL.prototype.point = function(x, y, z = 0) { const _vertex = []; _vertex.push(new p5.Vector(x, y, z)); @@ -2448,7 +2449,7 @@ function primitives3D(p5, fn){ return this; }; - p5.RendererGL.prototype.triangle = function(args) { + RendererGL.prototype.triangle = function(args) { const x1 = args[0], y1 = args[1]; const x2 = args[2], @@ -2501,7 +2502,7 @@ function primitives3D(p5, fn){ return this; }; - p5.RendererGL.prototype.ellipse = function(args) { + RendererGL.prototype.ellipse = function(args) { this.arc( args[0], args[1], @@ -2514,7 +2515,7 @@ function primitives3D(p5, fn){ ); }; - p5.RendererGL.prototype.arc = function(...args) { + RendererGL.prototype.arc = function(...args) { const x = args[0]; const y = args[1]; const width = args[2]; @@ -2631,7 +2632,7 @@ function primitives3D(p5, fn){ return this; }; - p5.RendererGL.prototype.rect = function(args) { + RendererGL.prototype.rect = function(args) { const x = args[0]; const y = args[1]; const width = args[2]; @@ -2764,7 +2765,7 @@ function primitives3D(p5, fn){ }; /* eslint-disable max-len */ - p5.RendererGL.prototype.quad = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, detailX=2, detailY=2) { + RendererGL.prototype.quad = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, detailX=2, detailY=2) { /* eslint-enable max-len */ const gId = @@ -2827,7 +2828,7 @@ function primitives3D(p5, fn){ //this implementation of bezier curve //is based on Bernstein polynomial // pretier-ignore - p5.RendererGL.prototype.bezier = function( + RendererGL.prototype.bezier = function( x1, y1, z1, // x2 @@ -2868,7 +2869,7 @@ function primitives3D(p5, fn){ }; // pretier-ignore - p5.RendererGL.prototype.curve = function( + RendererGL.prototype.curve = function( x1, y1, z1, // x2 @@ -2948,7 +2949,7 @@ function primitives3D(p5, fn){ * *
*/ - p5.RendererGL.prototype.line = function(...args) { + RendererGL.prototype.line = function(...args) { if (args.length === 6) { this.beginShape(constants.LINES); this.vertex(args[0], args[1], args[2]); @@ -2963,7 +2964,7 @@ function primitives3D(p5, fn){ return this; }; - p5.RendererGL.prototype.bezierVertex = function(...args) { + RendererGL.prototype.bezierVertex = function(...args) { if (this.immediateMode._bezierVertex.length === 0) { throw Error('vertex() must be used once before calling bezierVertex()'); } else { @@ -3187,7 +3188,7 @@ function primitives3D(p5, fn){ } }; - p5.RendererGL.prototype.quadraticVertex = function(...args) { + RendererGL.prototype.quadraticVertex = function(...args) { if (this.immediateMode._quadraticVertex.length === 0) { throw Error('vertex() must be used once before calling quadraticVertex()'); } else { @@ -3398,7 +3399,7 @@ function primitives3D(p5, fn){ } }; - p5.RendererGL.prototype.curveVertex = function(...args) { + RendererGL.prototype.curveVertex = function(...args) { let w_x = []; let w_y = []; let w_z = []; @@ -3516,7 +3517,7 @@ function primitives3D(p5, fn){ } }; - p5.RendererGL.prototype.image = function( + RendererGL.prototype.image = function( img, sx, sy, diff --git a/src/webgl/material.js b/src/webgl/material.js index b1283d2510..f50f7a2582 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -6,6 +6,7 @@ */ import * as constants from '../core/constants'; +import { RendererGL } from './p5.RendererGL'; function material(p5, fn){ /** @@ -3190,7 +3191,7 @@ function material(p5, fn){ * transparency internally, e.g. via vertex colors * @return {Number[]} Normalized numbers array */ - p5.RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { + RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { const gl = this.GL; const isTexture = this.states.drawMode === constants.TEXTURE; @@ -3226,7 +3227,7 @@ function material(p5, fn){ * @param {Number[]} color [description] * @return {Number[]} Normalized numbers array */ - p5.RendererGL.prototype._applyBlendMode = function () { + RendererGL.prototype._applyBlendMode = function () { if (this._cachedBlendMode === this.states.curBlendMode) { return; } diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 586971a5f7..ee3971a464 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -4,6 +4,3042 @@ * @requires core */ +import { Matrix } from './p5.Matrix'; + +class Camera { + constructor(renderer) { + this._renderer = renderer; + + this.cameraType = 'default'; + this.useLinePerspective = true; + this.cameraMatrix = new Matrix(); + this.projMatrix = new Matrix(); + this.yScale = 1; + } + /** + * The camera’s y-coordinate. + * + * By default, the camera’s y-coordinate is set to 0 in "world" space. + * + * @property {Number} eyeX + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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, 55); + * } + * + *
+ */ + + /** + * The camera’s y-coordinate. + * + * By default, the camera’s y-coordinate is set to 0 in "world" space. + * + * @property {Number} eyeY + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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(`eyeX: ${round(cam.eyeY)}`, 0, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ + + /** + * The camera’s z-coordinate. + * + * By default, the camera’s z-coordinate is set to 800 in "world" space. + * + * @property {Number} eyeZ + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ + + /** + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 (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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ + + /** + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 (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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ + + /** + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 (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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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, 55); + * } + * + *
+ */ + + /** + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ */ + + /** + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ */ + + /** + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ */ + + //////////////////////////////////////////////////////////////////////////////// + // 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; + * } + * } + * + *
+ * + *
+ * + * // 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) { + 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 = Matrix.identity(); + + const f = 1.0 / Math.tan(this.cameraFOV / 2); + const nf = 1.0 / (this.cameraNear - this.cameraFar); + + /* eslint-disable indent */ + this.projMatrix.set(f / aspect, 0, 0, 0, + 0, -f * this.yScale, 0, 0, + 0, 0, (far + near) * nf, -1, + 0, 0, (2 * far * near) * nf, 0); + /* eslint-enable indent */ + + if (this._isActive()) { + 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; + * } + * } + * + *
+ * + *
+ * + * // 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.0 / w; + const y = +2.0 / h * this.yScale; + const z = -2.0 / d; + const tx = -(right + left) / w; + const ty = -(top + bottom) / h; + const tz = -(far + near) / d; + this.projMatrix = p5.Matrix.identity(); + /* eslint-disable indent */ + this.projMatrix.set(x, 0, 0, 0, + 0, -y, 0, 0, + 0, 0, z, 0, + tx, ty, tz, 1); + /* eslint-enable indent */ + if (this._isActive()) { + 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 = p5.Matrix.identity(); + + /* eslint-disable indent */ + this.projMatrix.set(x, 0, 0, 0, + 0, -y, 0, 0, + tx, ty, tz, -1, + 0, 0, z, 0); + /* eslint-enable indent */ + + if (this._isActive()) { + 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 = p5.Matrix.identity(this._renderer._pInst); + rotation.rotate(this._renderer._pInst._toRadians(a), x, y, z); + + /* eslint-disable max-len */ + 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] + ]; + /* eslint-enable max-len */ + + // 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(); + * } + * + * 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 = p5.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 p5.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(); + * + * // 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(); + * + * // 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(); + * + * // 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; + * } + * } + * + *
+ * + *
+ * + * // 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. + /* eslint-disable indent */ + 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); + /* eslint-enable indent */ + + const tx = -eyeX; + const ty = -eyeY; + const tz = -eyeZ; + + this.cameraMatrix.translate([tx, ty, tz]); + + if (this._isActive()) { + 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); + * + * 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. + * if (keyIsPressed === true) { + * + * // Move horizontally. + * if (keyCode === LEFT_ARROW) { + * cam.move(-1, 0, 0); + * } + * if (keyCode === RIGHT_ARROW) { + * cam.move(1, 0, 0); + * } + * + * // Move vertically. + * if (keyCode === UP_ARROW) { + * cam.move(0, -1, 0); + * } + * if (keyCode === DOWN_ARROW) { + * cam.move(0, 1, 0); + * } + * + * // Move in/out of the screen. + * if (key === 'i') { + * cam.move(0, 0, -1); + * } + * if (key === '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; + * } + * } + * + *
+ * + *
+ * + * // 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); + * + * 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' + ]; + 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.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.mat4[0] = + cam0.projMatrix.mat4[0] * + Math.pow(cam1.projMatrix.mat4[0] / cam0.projMatrix.mat4[0], amt); + this.projMatrix.mat4[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.uPMatrix.mat4 = this.projMatrix.mat4.slice(); + } + } + + // prepare eye vector and center vector of argument cameras. + const eye0 = new p5.Vector(cam0.eyeX, cam0.eyeY, cam0.eyeZ); + const eye1 = new p5.Vector(cam1.eyeX, cam1.eyeY, cam1.eyeZ); + const center0 = new p5.Vector(cam0.centerX, cam0.centerY, cam0.centerZ); + const center1 = new p5.Vector(cam1.centerX, cam1.centerY, cam1.centerZ); + + // Calculate the distance between eye and center for each camera. + // Logarithmically interpolate these with amt. + const dist0 = p5.Vector.dist(eye0, center0); + const dist1 = p5.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 = p5.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 = p5.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 = p5.Vector.lerp( + p5.Vector.lerp(eye0, center0, ratio), + p5.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 p5.Vector(); + const newUp = new p5.Vector(); + const newEye = new p5.Vector(); + const newCenter = new p5.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.mult3x3(rotMat0.copy().transpose3x3()); + + // 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(p5.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(p5.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; + const lerpedRotMat = new p5.Matrix('mat3', [ + 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.multiplyVec3(front0, newFront); + + newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium); + newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium); + + lerpedRotMat.multiplyVec3(up0, newUp); + + // 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 * 0.1; + this.defaultCameraFar = this.defaultEyeZ * 10; + } + + //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.cameraMatrix = this.cameraMatrix.copy(); + _cam.projMatrix = this.projMatrix.copy(); + _cam.yScale = this.yScale; + + 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 p5.Vector(diffX, diffY, diffZ).normalize(); + // up vector. normalized camera's up vector. + const up = new p5.Vector(this.upX, this.upY, this.upZ).normalize(); // y-axis + // side vector. Right when viewed from the front + const side = p5.Vector.cross(up, front).normalize(); // x-axis + // vertical vector. normalized vector of projection of front vector. + const vertical = p5.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, p5.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 p5.Vector(diffX, diffY, diffZ).normalize(); + // up vector. camera's up vector. + const up = new p5.Vector(this.upX, this.upY, this.upZ); + // side vector. Right when viewed from the front. (like x-axis) + const side = p5.Vector.cross(up, front).normalize(); + // down vector. Bottom when viewed from the front. (like y-axis) + const down = p5.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 = p5.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 @@ -832,3039 +3868,7 @@ function camera(p5, fn){ *
*
*/ - p5.Camera = class Camera { - constructor(renderer) { - this._renderer = renderer; - - this.cameraType = 'default'; - this.useLinePerspective = true; - this.cameraMatrix = new p5.Matrix(); - this.projMatrix = new p5.Matrix(); - this.yScale = 1; - } - /** - * The camera’s y-coordinate. - * - * By default, the camera’s y-coordinate is set to 0 in "world" space. - * - * @property {Number} eyeX - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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, 55); - * } - * - *
- */ - - /** - * The camera’s y-coordinate. - * - * By default, the camera’s y-coordinate is set to 0 in "world" space. - * - * @property {Number} eyeY - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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(`eyeX: ${round(cam.eyeY)}`, 0, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ - - /** - * The camera’s z-coordinate. - * - * By default, the camera’s z-coordinate is set to 800 in "world" space. - * - * @property {Number} eyeZ - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ - - /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 (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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ - - /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 (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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ - - /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 (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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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, 55); - * } - * - *
- */ - - /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- */ - - /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- */ - - /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- */ - - //////////////////////////////////////////////////////////////////////////////// - // 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; - * } - * } - * - *
- * - *
- * - * // 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) { - 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 = p5.Matrix.identity(); - - const f = 1.0 / Math.tan(this.cameraFOV / 2); - const nf = 1.0 / (this.cameraNear - this.cameraFar); - - /* eslint-disable indent */ - this.projMatrix.set(f / aspect, 0, 0, 0, - 0, -f * this.yScale, 0, 0, - 0, 0, (far + near) * nf, -1, - 0, 0, (2 * far * near) * nf, 0); - /* eslint-enable indent */ - - if (this._isActive()) { - 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; - * } - * } - * - *
- * - *
- * - * // 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.0 / w; - const y = +2.0 / h * this.yScale; - const z = -2.0 / d; - const tx = -(right + left) / w; - const ty = -(top + bottom) / h; - const tz = -(far + near) / d; - this.projMatrix = p5.Matrix.identity(); - /* eslint-disable indent */ - this.projMatrix.set(x, 0, 0, 0, - 0, -y, 0, 0, - 0, 0, z, 0, - tx, ty, tz, 1); - /* eslint-enable indent */ - if (this._isActive()) { - 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 = p5.Matrix.identity(); - - /* eslint-disable indent */ - this.projMatrix.set(x, 0, 0, 0, - 0, -y, 0, 0, - tx, ty, tz, -1, - 0, 0, z, 0); - /* eslint-enable indent */ - - if (this._isActive()) { - 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 = p5.Matrix.identity(this._renderer._pInst); - rotation.rotate(this._renderer._pInst._toRadians(a), x, y, z); - - /* eslint-disable max-len */ - 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] - ]; - /* eslint-enable max-len */ - - // 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(); - * } - * - * 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 = p5.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 p5.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(); - * - * // 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(); - * - * // 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(); - * - * // 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; - * } - * } - * - *
- * - *
- * - * // 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. - /* eslint-disable indent */ - 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); - /* eslint-enable indent */ - - const tx = -eyeX; - const ty = -eyeY; - const tz = -eyeZ; - - this.cameraMatrix.translate([tx, ty, tz]); - - if (this._isActive()) { - 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); - * - * 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. - * if (keyIsPressed === true) { - * - * // Move horizontally. - * if (keyCode === LEFT_ARROW) { - * cam.move(-1, 0, 0); - * } - * if (keyCode === RIGHT_ARROW) { - * cam.move(1, 0, 0); - * } - * - * // Move vertically. - * if (keyCode === UP_ARROW) { - * cam.move(0, -1, 0); - * } - * if (keyCode === DOWN_ARROW) { - * cam.move(0, 1, 0); - * } - * - * // Move in/out of the screen. - * if (key === 'i') { - * cam.move(0, 0, -1); - * } - * if (key === '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; - * } - * } - * - *
- * - *
- * - * // 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); - * - * 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' - ]; - 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.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.mat4[0] = - cam0.projMatrix.mat4[0] * - Math.pow(cam1.projMatrix.mat4[0] / cam0.projMatrix.mat4[0], amt); - this.projMatrix.mat4[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.uPMatrix.mat4 = this.projMatrix.mat4.slice(); - } - } - - // prepare eye vector and center vector of argument cameras. - const eye0 = new p5.Vector(cam0.eyeX, cam0.eyeY, cam0.eyeZ); - const eye1 = new p5.Vector(cam1.eyeX, cam1.eyeY, cam1.eyeZ); - const center0 = new p5.Vector(cam0.centerX, cam0.centerY, cam0.centerZ); - const center1 = new p5.Vector(cam1.centerX, cam1.centerY, cam1.centerZ); - - // Calculate the distance between eye and center for each camera. - // Logarithmically interpolate these with amt. - const dist0 = p5.Vector.dist(eye0, center0); - const dist1 = p5.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 = p5.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 = p5.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 = p5.Vector.lerp( - p5.Vector.lerp(eye0, center0, ratio), - p5.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 p5.Vector(); - const newUp = new p5.Vector(); - const newEye = new p5.Vector(); - const newCenter = new p5.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.mult3x3(rotMat0.copy().transpose3x3()); - - // 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(p5.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(p5.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; - const lerpedRotMat = new p5.Matrix('mat3', [ - 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.multiplyVec3(front0, newFront); - - newEye.set(newFront).mult(ratio * lerpedDist).add(lerpedMedium); - newCenter.set(newFront).mult((ratio - 1) * lerpedDist).add(lerpedMedium); - - lerpedRotMat.multiplyVec3(up0, newUp); - - // 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 * 0.1; - this.defaultCameraFar = this.defaultEyeZ * 10; - } - - //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 p5.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.cameraMatrix = this.cameraMatrix.copy(); - _cam.projMatrix = this.projMatrix.copy(); - _cam.yScale = this.yScale; - - 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 p5.Vector(diffX, diffY, diffZ).normalize(); - // up vector. normalized camera's up vector. - const up = new p5.Vector(this.upX, this.upY, this.upZ).normalize(); // y-axis - // side vector. Right when viewed from the front - const side = p5.Vector.cross(up, front).normalize(); // x-axis - // vertical vector. normalized vector of projection of front vector. - const vertical = p5.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, p5.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 p5.Vector(diffX, diffY, diffZ).normalize(); - // up vector. camera's up vector. - const up = new p5.Vector(this.upX, this.upY, this.upZ); - // side vector. Right when viewed from the front. (like x-axis) - const side = p5.Vector.cross(up, front).normalize(); - // down vector. Bottom when viewed from the front. (like y-axis) - const down = p5.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 = p5.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; - } - }; + p5.Camera = Camera; /** * Sets the current (active) camera of a 3D sketch. @@ -3937,6 +3941,7 @@ function camera(p5, fn){ } export default camera; +export { Camera }; if(typeof p5 !== 'undefined'){ camera(p5, p5.prototype); diff --git a/src/webgl/p5.DataArray.js b/src/webgl/p5.DataArray.js index c0aebdacec..00306105b1 100644 --- a/src/webgl/p5.DataArray.js +++ b/src/webgl/p5.DataArray.js @@ -1,3 +1,85 @@ +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. @@ -24,90 +106,11 @@ function dataArray(p5, fn){ *
*
*/ - p5.DataArray = 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; - } - } - }; + p5.DataArray = DataArray; } export default dataArray; +export { DataArray } if(typeof p5 !== 'undefined'){ dataArray(p5, p5.prototype); diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index c9eb6bba62..856d5d02ae 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -6,1622 +6,1635 @@ import * as constants from '../core/constants'; import { checkWebGLCapabilities } from './p5.Texture'; import { readPixelsWebGL, readPixelWebGL } from './p5.RendererGL'; +import { Camera } from './p5.Camera'; +import { Texture } from './p5.Texture'; + +class FramebufferCamera extends Camera { + constructor(framebuffer) { + super(framebuffer.target._renderer); + this.fbo = framebuffer; + + // WebGL textures are upside-down compared to textures that come from + // images and graphics. Framebuffer cameras need to invert their y + // axes when being rendered to so that the texture comes out rightway up + // when read in shaders or image(). + this.yScale = -1; + } + + _computeCameraDefaultSettings() { + super._computeCameraDefaultSettings(); + this.defaultAspectRatio = this.fbo.width / this.fbo.height; + this.defaultCameraFOV = + 2 * Math.atan(this.fbo.height / 2 / this.defaultEyeZ); + } +} -function framebuffer(p5, fn){ - /** - * A p5.Camera attached to a - * p5.Framebuffer. - * - * @class p5.FramebufferCamera - * @param {p5.Framebuffer} framebuffer The framebuffer this camera is - * attached to - * @private - */ - p5.FramebufferCamera = class FramebufferCamera extends p5.Camera { - constructor(framebuffer) { - super(framebuffer.target._renderer); - this.fbo = framebuffer; - - // WebGL textures are upside-down compared to textures that come from - // images and graphics. Framebuffer cameras need to invert their y - // axes when being rendered to so that the texture comes out rightway up - // when read in shaders or image(). - this.yScale = -1; - } +class FramebufferTexture { + constructor(framebuffer, property) { + this.framebuffer = framebuffer; + this.property = property; + } - _computeCameraDefaultSettings() { - super._computeCameraDefaultSettings(); - this.defaultAspectRatio = this.fbo.width / this.fbo.height; - this.defaultCameraFOV = - 2 * Math.atan(this.fbo.height / 2 / this.defaultEyeZ); - } - }; + get width() { + return this.framebuffer.width * this.framebuffer.density; + } - /** - * A p5.Texture corresponding to a property of a - * p5.Framebuffer. - * - * @class p5.FramebufferTexture - * @param {p5.Framebuffer} framebuffer The framebuffer represented by this - * texture - * @param {String} property The property of the framebuffer represented by - * this texture, either `color` or `depth` - * @private - */ - p5.FramebufferTexture = class FramebufferTexture { - constructor(framebuffer, property) { - this.framebuffer = framebuffer; - this.property = property; - } + get height() { + return this.framebuffer.height * this.framebuffer.density; + } + + rawTexture() { + return this.framebuffer[this.property]; + } +} - get width() { - return this.framebuffer.width * this.framebuffer.density; +class Framebuffer { + constructor(target, settings = {}) { + this.target = target; + this.target._renderer.framebuffers.add(this); + + this._isClipApplied = false; + + this.pixels = []; + + this.format = settings.format || constants.UNSIGNED_BYTE; + this.channels = settings.channels || ( + target._renderer._pInst._glAttributes.alpha + ? constants.RGBA + : constants.RGB + ); + this.useDepth = settings.depth === undefined ? true : settings.depth; + this.depthFormat = settings.depthFormat || constants.FLOAT; + this.textureFiltering = settings.textureFiltering || constants.LINEAR; + if (settings.antialias === undefined) { + this.antialiasSamples = target._renderer._pInst._glAttributes.antialias + ? 2 + : 0; + } else if (typeof settings.antialias === 'number') { + this.antialiasSamples = settings.antialias; + } else { + this.antialiasSamples = settings.antialias ? 2 : 0; + } + this.antialias = this.antialiasSamples > 0; + if (this.antialias && target.webglVersion !== constants.WEBGL2) { + console.warn('Antialiasing is unsupported in a WebGL 1 context'); + this.antialias = false; + } + this.density = settings.density || target.pixelDensity(); + const gl = target._renderer.GL; + this.gl = gl; + if (settings.width && settings.height) { + const dimensions = + target._renderer._adjustDimensions(settings.width, settings.height); + this.width = dimensions.adjustedWidth; + this.height = dimensions.adjustedHeight; + this._autoSized = false; + } else { + if ((settings.width === undefined) !== (settings.height === undefined)) { + console.warn( + 'Please supply both width and height for a framebuffer to give it a ' + + 'size. Only one was given, so the framebuffer will match the size ' + + 'of its canvas.' + ); + } + this.width = target.width; + this.height = target.height; + this._autoSized = true; } + this._checkIfFormatsAvailable(); - get height() { - return this.framebuffer.height * this.framebuffer.density; + if (settings.stencil && !this.useDepth) { + console.warn('A stencil buffer can only be used if also using depth. Since the framebuffer has no depth buffer, the stencil buffer will be ignored.'); } + this.useStencil = this.useDepth && + (settings.stencil === undefined ? true : settings.stencil); - rawTexture() { - return this.framebuffer[this.property]; + this.framebuffer = gl.createFramebuffer(); + if (!this.framebuffer) { + throw new Error('Unable to create a framebuffer'); + } + if (this.antialias) { + this.aaFramebuffer = gl.createFramebuffer(); + if (!this.aaFramebuffer) { + throw new Error('Unable to create a framebuffer for antialiasing'); + } } - }; + + this._recreateTextures(); + + const prevCam = this.target._renderer.states.curCamera; + this.defaultCamera = this.createCamera(); + this.filterCamera = this.createCamera(); + this.target._renderer.states.curCamera = prevCam; + + this.draw(() => this.target._renderer.clear()); + } /** - * A class to describe a high-performance drawing surface for textures. + * Resizes the framebuffer to a given width and height. * - * Each `p5.Framebuffer` object provides a dedicated drawing surface called - * a *framebuffer*. They're similar to - * p5.Graphics objects but can run much faster. - * Performance is improved because the framebuffer shares the same WebGL - * context as the canvas used to create it. + * The parameters, `width` and `height`, set the dimensions of the + * framebuffer. For example, calling `myBuffer.resize(300, 500)` resizes + * the framebuffer to 300×500 pixels, then sets `myBuffer.width` to 300 + * and `myBuffer.height` 500. * - * `p5.Framebuffer` objects have all the drawing features of the main - * canvas. Drawing instructions meant for the framebuffer must be placed - * between calls to - * myBuffer.begin() and - * myBuffer.end(). The resulting image - * can be applied as a texture by passing the `p5.Framebuffer` object to the - * texture() function, as in `texture(myBuffer)`. - * It can also be displayed on the main canvas by passing it to the - * image() function, as in `image(myBuffer, 0, 0)`. + * @param {Number} width width of the framebuffer. + * @param {Number} height height of the framebuffer. * - * Note: createFramebuffer() is the - * recommended way to create an instance of this class. + * @example + *
+ * + * let myBuffer; * - * @class p5.Framebuffer - * @param {p5.Graphics|p5} target sketch instance or - * p5.Graphics - * object. - * @param {Object} [settings] configuration options. + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * describe('A multicolor sphere on a white surface. The image grows larger or smaller when the user moves the mouse, revealing a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Draw to the p5.Framebuffer object. + * myBuffer.begin(); + * background(255); + * normalMaterial(); + * sphere(20); + * myBuffer.end(); + * + * // Display the p5.Framebuffer object. + * image(myBuffer, -50, -50); + * } + * + * // Resize the p5.Framebuffer object when the + * // user moves the mouse. + * function mouseMoved() { + * myBuffer.resize(mouseX, mouseY); + * } + * + *
*/ - p5.Framebuffer = class Framebuffer { - constructor(target, settings = {}) { - this.target = target; - this.target._renderer.framebuffers.add(this); - - this._isClipApplied = false; - - this.pixels = []; - - this.format = settings.format || constants.UNSIGNED_BYTE; - this.channels = settings.channels || ( - target._renderer._pInst._glAttributes.alpha - ? constants.RGBA - : constants.RGB - ); - this.useDepth = settings.depth === undefined ? true : settings.depth; - this.depthFormat = settings.depthFormat || constants.FLOAT; - this.textureFiltering = settings.textureFiltering || constants.LINEAR; - if (settings.antialias === undefined) { - this.antialiasSamples = target._renderer._pInst._glAttributes.antialias - ? 2 - : 0; - } else if (typeof settings.antialias === 'number') { - this.antialiasSamples = settings.antialias; - } else { - this.antialiasSamples = settings.antialias ? 2 : 0; - } - this.antialias = this.antialiasSamples > 0; - if (this.antialias && target.webglVersion !== constants.WEBGL2) { - console.warn('Antialiasing is unsupported in a WebGL 1 context'); - this.antialias = false; - } - this.density = settings.density || target.pixelDensity(); - const gl = target._renderer.GL; - this.gl = gl; - if (settings.width && settings.height) { - const dimensions = - target._renderer._adjustDimensions(settings.width, settings.height); - this.width = dimensions.adjustedWidth; - this.height = dimensions.adjustedHeight; - this._autoSized = false; - } else { - if ((settings.width === undefined) !== (settings.height === undefined)) { - console.warn( - 'Please supply both width and height for a framebuffer to give it a ' + - 'size. Only one was given, so the framebuffer will match the size ' + - 'of its canvas.' - ); - } - this.width = target.width; - this.height = target.height; - this._autoSized = true; - } - this._checkIfFormatsAvailable(); - - if (settings.stencil && !this.useDepth) { - console.warn('A stencil buffer can only be used if also using depth. Since the framebuffer has no depth buffer, the stencil buffer will be ignored.'); - } - this.useStencil = this.useDepth && - (settings.stencil === undefined ? true : settings.stencil); - - this.framebuffer = gl.createFramebuffer(); - if (!this.framebuffer) { - throw new Error('Unable to create a framebuffer'); - } - if (this.antialias) { - this.aaFramebuffer = gl.createFramebuffer(); - if (!this.aaFramebuffer) { - throw new Error('Unable to create a framebuffer for antialiasing'); - } - } - - this._recreateTextures(); + resize(width, height) { + this._autoSized = false; + const dimensions = + this.target._renderer._adjustDimensions(width, height); + width = dimensions.adjustedWidth; + height = dimensions.adjustedHeight; + this.width = width; + this.height = height; + this._handleResize(); + } - const prevCam = this.target._renderer.states.curCamera; - this.defaultCamera = this.createCamera(); - this.filterCamera = this.createCamera(); - this.target._renderer.states.curCamera = prevCam; - - this.draw(() => this.target.clear()); - } - - /** - * Resizes the framebuffer to a given width and height. - * - * The parameters, `width` and `height`, set the dimensions of the - * framebuffer. For example, calling `myBuffer.resize(300, 500)` resizes - * the framebuffer to 300×500 pixels, then sets `myBuffer.width` to 300 - * and `myBuffer.height` 500. - * - * @param {Number} width width of the framebuffer. - * @param {Number} height height of the framebuffer. - * - * @example - *
- * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * describe('A multicolor sphere on a white surface. The image grows larger or smaller when the user moves the mouse, revealing a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Draw to the p5.Framebuffer object. - * myBuffer.begin(); - * background(255); - * normalMaterial(); - * sphere(20); - * myBuffer.end(); - * - * // Display the p5.Framebuffer object. - * image(myBuffer, -50, -50); - * } - * - * // Resize the p5.Framebuffer object when the - * // user moves the mouse. - * function mouseMoved() { - * myBuffer.resize(mouseX, mouseY); - * } - * - *
- */ - resize(width, height) { + /** + * Sets the framebuffer's pixel density or returns its current density. + * + * Computer displays are grids of little lights called pixels. A display's + * pixel density describes how many pixels it packs into an area. Displays + * with smaller pixels have a higher pixel density and create sharper + * images. + * + * The parameter, `density`, is optional. If a number is passed, as in + * `myBuffer.pixelDensity(1)`, it sets the framebuffer's pixel density. By + * default, the framebuffer's pixel density will match that of the canvas + * where it was created. All canvases default to match the display's pixel + * density. + * + * Calling `myBuffer.pixelDensity()` without an argument returns its current + * pixel density. + * + * @param {Number} [density] pixel density to set. + * @returns {Number} current pixel density. + * + * @example + *
+ * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * describe("A white circle on a gray canvas. The circle's edge become fuzzy while the user presses and holds the mouse."); + * } + * + * function draw() { + * // Draw to the p5.Framebuffer object. + * myBuffer.begin(); + * background(200); + * circle(0, 0, 40); + * myBuffer.end(); + * + * // Display the p5.Framebuffer object. + * image(myBuffer, -50, -50); + * } + * + * // Decrease the pixel density when the user + * // presses the mouse. + * function mousePressed() { + * myBuffer.pixelDensity(1); + * } + * + * // Increase the pixel density when the user + * // releases the mouse. + * function mouseReleased() { + * myBuffer.pixelDensity(2); + * } + * + *
+ * + *
+ * + * let myBuffer; + * let myFont; + * + * // Load a font and create a p5.Font object. + * function preload() { + * myFont = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * // Get the p5.Framebuffer object's pixel density. + * let d = myBuffer.pixelDensity(); + * + * // Style the text. + * textAlign(CENTER, CENTER); + * textFont(myFont); + * textSize(16); + * fill(0); + * + * // Display the pixel density. + * text(`Density: ${d}`, 0, 0); + * + * describe(`The text "Density: ${d}" written in black on a gray background.`); + * } + * + *
+ */ + pixelDensity(density) { + if (density) { this._autoSized = false; - const dimensions = - this.target._renderer._adjustDimensions(width, height); - width = dimensions.adjustedWidth; - height = dimensions.adjustedHeight; - this.width = width; - this.height = height; + this.density = density; this._handleResize(); + } else { + return this.density; } + } - /** - * Sets the framebuffer's pixel density or returns its current density. - * - * Computer displays are grids of little lights called pixels. A display's - * pixel density describes how many pixels it packs into an area. Displays - * with smaller pixels have a higher pixel density and create sharper - * images. - * - * The parameter, `density`, is optional. If a number is passed, as in - * `myBuffer.pixelDensity(1)`, it sets the framebuffer's pixel density. By - * default, the framebuffer's pixel density will match that of the canvas - * where it was created. All canvases default to match the display's pixel - * density. - * - * Calling `myBuffer.pixelDensity()` without an argument returns its current - * pixel density. - * - * @param {Number} [density] pixel density to set. - * @returns {Number} current pixel density. - * - * @example - *
- * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * describe("A white circle on a gray canvas. The circle's edge become fuzzy while the user presses and holds the mouse."); - * } - * - * function draw() { - * // Draw to the p5.Framebuffer object. - * myBuffer.begin(); - * background(200); - * circle(0, 0, 40); - * myBuffer.end(); - * - * // Display the p5.Framebuffer object. - * image(myBuffer, -50, -50); - * } - * - * // Decrease the pixel density when the user - * // presses the mouse. - * function mousePressed() { - * myBuffer.pixelDensity(1); - * } - * - * // Increase the pixel density when the user - * // releases the mouse. - * function mouseReleased() { - * myBuffer.pixelDensity(2); - * } - * - *
- * - *
- * - * let myBuffer; - * let myFont; - * - * // Load a font and create a p5.Font object. - * function preload() { - * myFont = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * // Get the p5.Framebuffer object's pixel density. - * let d = myBuffer.pixelDensity(); - * - * // Style the text. - * textAlign(CENTER, CENTER); - * textFont(myFont); - * textSize(16); - * fill(0); - * - * // Display the pixel density. - * text(`Density: ${d}`, 0, 0); - * - * describe(`The text "Density: ${d}" written in black on a gray background.`); - * } - * - *
- */ - pixelDensity(density) { - if (density) { - this._autoSized = false; - this.density = density; - this._handleResize(); - } else { - return this.density; - } + /** + * Toggles the framebuffer's autosizing mode or returns the current mode. + * + * By default, the framebuffer automatically resizes to match the canvas + * that created it. Calling `myBuffer.autoSized(false)` disables this + * behavior and calling `myBuffer.autoSized(true)` re-enables it. + * + * Calling `myBuffer.autoSized()` without an argument returns `true` if + * the framebuffer automatically resizes and `false` if not. + * + * @param {Boolean} [autoSized] whether to automatically resize the framebuffer to match the canvas. + * @returns {Boolean} current autosize setting. + * + * @example + *
+ * + * // Double-click to toggle the autosizing mode. + * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * describe('A multicolor sphere on a gray background. The image resizes when the user moves the mouse.'); + * } + * + * function draw() { + * background(50); + * + * // Draw to the p5.Framebuffer object. + * myBuffer.begin(); + * background(200); + * normalMaterial(); + * sphere(width / 4); + * myBuffer.end(); + * + * // Display the p5.Framebuffer object. + * image(myBuffer, -width / 2, -height / 2); + * } + * + * // Resize the canvas when the user moves the mouse. + * function mouseMoved() { + * let w = constrain(mouseX, 0, 100); + * let h = constrain(mouseY, 0, 100); + * resizeCanvas(w, h); + * } + * + * // Toggle autoSizing when the user double-clicks. + * // Note: opened an issue to fix(?) this. + * function doubleClicked() { + * let isAuto = myBuffer.autoSized(); + * myBuffer.autoSized(!isAuto); + * } + * + *
+ */ + autoSized(autoSized) { + if (autoSized === undefined) { + return this._autoSized; + } else { + this._autoSized = autoSized; + this._handleResize(); } + } - /** - * Toggles the framebuffer's autosizing mode or returns the current mode. - * - * By default, the framebuffer automatically resizes to match the canvas - * that created it. Calling `myBuffer.autoSized(false)` disables this - * behavior and calling `myBuffer.autoSized(true)` re-enables it. - * - * Calling `myBuffer.autoSized()` without an argument returns `true` if - * the framebuffer automatically resizes and `false` if not. - * - * @param {Boolean} [autoSized] whether to automatically resize the framebuffer to match the canvas. - * @returns {Boolean} current autosize setting. - * - * @example - *
- * - * // Double-click to toggle the autosizing mode. - * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * describe('A multicolor sphere on a gray background. The image resizes when the user moves the mouse.'); - * } - * - * function draw() { - * background(50); - * - * // Draw to the p5.Framebuffer object. - * myBuffer.begin(); - * background(200); - * normalMaterial(); - * sphere(width / 4); - * myBuffer.end(); - * - * // Display the p5.Framebuffer object. - * image(myBuffer, -width / 2, -height / 2); - * } - * - * // Resize the canvas when the user moves the mouse. - * function mouseMoved() { - * let w = constrain(mouseX, 0, 100); - * let h = constrain(mouseY, 0, 100); - * resizeCanvas(w, h); - * } - * - * // Toggle autoSizing when the user double-clicks. - * // Note: opened an issue to fix(?) this. - * function doubleClicked() { - * let isAuto = myBuffer.autoSized(); - * myBuffer.autoSized(!isAuto); - * } - * - *
- */ - autoSized(autoSized) { - if (autoSized === undefined) { - return this._autoSized; - } else { - this._autoSized = autoSized; - this._handleResize(); - } + /** + * Checks the capabilities of the current WebGL environment to see if the + * settings supplied by the user are capable of being fulfilled. If they + * are not, warnings will be logged and the settings will be changed to + * something close that can be fulfilled. + * + * @private + */ + _checkIfFormatsAvailable() { + const gl = this.gl; + + if ( + this.useDepth && + this.target.webglVersion === constants.WEBGL && + !gl.getExtension('WEBGL_depth_texture') + ) { + console.warn( + 'Unable to create depth textures in this environment. Falling back ' + + 'to a framebuffer without depth.' + ); + this.useDepth = false; } - /** - * Checks the capabilities of the current WebGL environment to see if the - * settings supplied by the user are capable of being fulfilled. If they - * are not, warnings will be logged and the settings will be changed to - * something close that can be fulfilled. - * - * @private - */ - _checkIfFormatsAvailable() { - const gl = this.gl; - - if ( - this.useDepth && - this.target.webglVersion === constants.WEBGL && - !gl.getExtension('WEBGL_depth_texture') - ) { - console.warn( - 'Unable to create depth textures in this environment. Falling back ' + - 'to a framebuffer without depth.' - ); - this.useDepth = false; - } - - if ( - this.useDepth && - this.target.webglVersion === constants.WEBGL && - this.depthFormat === constants.FLOAT - ) { - console.warn( - 'FLOAT depth format is unavailable in WebGL 1. ' + - 'Defaulting to UNSIGNED_INT.' - ); - this.depthFormat = constants.UNSIGNED_INT; - } + if ( + this.useDepth && + this.target.webglVersion === constants.WEBGL && + this.depthFormat === constants.FLOAT + ) { + console.warn( + 'FLOAT depth format is unavailable in WebGL 1. ' + + 'Defaulting to UNSIGNED_INT.' + ); + this.depthFormat = constants.UNSIGNED_INT; + } - if (![ - constants.UNSIGNED_BYTE, - constants.FLOAT, - constants.HALF_FLOAT - ].includes(this.format)) { - console.warn( - 'Unknown Framebuffer format. ' + - 'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' + - 'Defaulting to UNSIGNED_BYTE.' - ); - this.format = constants.UNSIGNED_BYTE; - } - if (this.useDepth && ![ - constants.UNSIGNED_INT, - constants.FLOAT - ].includes(this.depthFormat)) { - console.warn( - 'Unknown Framebuffer depth format. ' + - 'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.' - ); - this.depthFormat = constants.FLOAT; - } + if (![ + constants.UNSIGNED_BYTE, + constants.FLOAT, + constants.HALF_FLOAT + ].includes(this.format)) { + console.warn( + 'Unknown Framebuffer format. ' + + 'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' + + 'Defaulting to UNSIGNED_BYTE.' + ); + this.format = constants.UNSIGNED_BYTE; + } + if (this.useDepth && ![ + constants.UNSIGNED_INT, + constants.FLOAT + ].includes(this.depthFormat)) { + console.warn( + 'Unknown Framebuffer depth format. ' + + 'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.' + ); + this.depthFormat = constants.FLOAT; + } - const support = checkWebGLCapabilities(this.target._renderer); - if (!support.float && this.format === constants.FLOAT) { - console.warn( - 'This environment does not support FLOAT textures. ' + - 'Falling back to UNSIGNED_BYTE.' - ); - this.format = constants.UNSIGNED_BYTE; - } - if ( - this.useDepth && - !support.float && - this.depthFormat === constants.FLOAT - ) { - console.warn( - 'This environment does not support FLOAT depth textures. ' + - 'Falling back to UNSIGNED_INT.' - ); - this.depthFormat = constants.UNSIGNED_INT; - } - if (!support.halfFloat && this.format === constants.HALF_FLOAT) { - console.warn( - 'This environment does not support HALF_FLOAT textures. ' + - 'Falling back to UNSIGNED_BYTE.' - ); - this.format = constants.UNSIGNED_BYTE; - } + const support = checkWebGLCapabilities(this.target._renderer); + if (!support.float && this.format === constants.FLOAT) { + console.warn( + 'This environment does not support FLOAT textures. ' + + 'Falling back to UNSIGNED_BYTE.' + ); + this.format = constants.UNSIGNED_BYTE; + } + if ( + this.useDepth && + !support.float && + this.depthFormat === constants.FLOAT + ) { + console.warn( + 'This environment does not support FLOAT depth textures. ' + + 'Falling back to UNSIGNED_INT.' + ); + this.depthFormat = constants.UNSIGNED_INT; + } + if (!support.halfFloat && this.format === constants.HALF_FLOAT) { + console.warn( + 'This environment does not support HALF_FLOAT textures. ' + + 'Falling back to UNSIGNED_BYTE.' + ); + this.format = constants.UNSIGNED_BYTE; + } - if ( - this.channels === constants.RGB && - [constants.FLOAT, constants.HALF_FLOAT].includes(this.format) - ) { - console.warn( - 'FLOAT and HALF_FLOAT formats do not work cross-platform with only ' + - 'RGB channels. Falling back to RGBA.' - ); - this.channels = constants.RGBA; - } + if ( + this.channels === constants.RGB && + [constants.FLOAT, constants.HALF_FLOAT].includes(this.format) + ) { + console.warn( + 'FLOAT and HALF_FLOAT formats do not work cross-platform with only ' + + 'RGB channels. Falling back to RGBA.' + ); + this.channels = constants.RGBA; } + } - /** - * Creates new textures and renderbuffers given the current size of the - * framebuffer. - * - * @private - */ - _recreateTextures() { - const gl = this.gl; + /** + * Creates new textures and renderbuffers given the current size of the + * framebuffer. + * + * @private + */ + _recreateTextures() { + const gl = this.gl; - this._updateSize(); + this._updateSize(); - const prevBoundTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); - const prevBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + const prevBoundTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); + const prevBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); - const colorTexture = gl.createTexture(); - if (!colorTexture) { - throw new Error('Unable to create color texture'); + const colorTexture = gl.createTexture(); + if (!colorTexture) { + throw new Error('Unable to create color texture'); + } + gl.bindTexture(gl.TEXTURE_2D, colorTexture); + const colorFormat = this._glColorFormat(); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + colorFormat.internalFormat, + this.width * this.density, + this.height * this.density, + 0, + colorFormat.format, + colorFormat.type, + null + ); + this.colorTexture = colorTexture; + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + colorTexture, + 0 + ); + + if (this.useDepth) { + // Create the depth texture + const depthTexture = gl.createTexture(); + if (!depthTexture) { + throw new Error('Unable to create depth texture'); } - gl.bindTexture(gl.TEXTURE_2D, colorTexture); - const colorFormat = this._glColorFormat(); + const depthFormat = this._glDepthFormat(); + gl.bindTexture(gl.TEXTURE_2D, depthTexture); gl.texImage2D( gl.TEXTURE_2D, 0, - colorFormat.internalFormat, + depthFormat.internalFormat, this.width * this.density, this.height * this.density, 0, - colorFormat.format, - colorFormat.type, + depthFormat.format, + depthFormat.type, null ); - this.colorTexture = colorTexture; - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + gl.framebufferTexture2D( gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, + this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, - colorTexture, + depthTexture, 0 ); + this.depthTexture = depthTexture; + } - if (this.useDepth) { - // Create the depth texture - const depthTexture = gl.createTexture(); - if (!depthTexture) { - throw new Error('Unable to create depth texture'); - } - const depthFormat = this._glDepthFormat(); - gl.bindTexture(gl.TEXTURE_2D, depthTexture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - depthFormat.internalFormat, - this.width * this.density, - this.height * this.density, + // Create separate framebuffer for antialiasing + if (this.antialias) { + this.colorRenderbuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, this.colorRenderbuffer); + gl.renderbufferStorageMultisample( + gl.RENDERBUFFER, + Math.max( 0, - depthFormat.format, - depthFormat.type, - null - ); - - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, - gl.TEXTURE_2D, - depthTexture, - 0 - ); - this.depthTexture = depthTexture; - } + Math.min(this.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) + ), + colorFormat.internalFormat, + this.width * this.density, + this.height * this.density + ); - // Create separate framebuffer for antialiasing - if (this.antialias) { - this.colorRenderbuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, this.colorRenderbuffer); + if (this.useDepth) { + const depthFormat = this._glDepthFormat(); + this.depthRenderbuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthRenderbuffer); gl.renderbufferStorageMultisample( gl.RENDERBUFFER, Math.max( 0, Math.min(this.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) ), - colorFormat.internalFormat, + depthFormat.internalFormat, this.width * this.density, this.height * this.density ); + } - if (this.useDepth) { - const depthFormat = this._glDepthFormat(); - this.depthRenderbuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthRenderbuffer); - gl.renderbufferStorageMultisample( - gl.RENDERBUFFER, - Math.max( - 0, - Math.min(this.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) - ), - depthFormat.internalFormat, - this.width * this.density, - this.height * this.density - ); - } - - gl.bindFramebuffer(gl.FRAMEBUFFER, this.aaFramebuffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.aaFramebuffer); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.RENDERBUFFER, + this.colorRenderbuffer + ); + if (this.useDepth) { gl.framebufferRenderbuffer( gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, + this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, - this.colorRenderbuffer - ); - if (this.useDepth) { - gl.framebufferRenderbuffer( - gl.FRAMEBUFFER, - this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, - gl.RENDERBUFFER, - this.depthRenderbuffer - ); - } - } - - if (this.useDepth) { - this.depth = new p5.FramebufferTexture(this, 'depthTexture'); - const depthFilter = gl.NEAREST; - this.depthP5Texture = new p5.Texture( - this.target._renderer, - this.depth, - { - minFilter: depthFilter, - magFilter: depthFilter - } + this.depthRenderbuffer ); - this.target._renderer.textures.set(this.depth, this.depthP5Texture); } + } - this.color = new p5.FramebufferTexture(this, 'colorTexture'); - const filter = this.textureFiltering === constants.LINEAR - ? gl.LINEAR - : gl.NEAREST; - this.colorP5Texture = new p5.Texture( + if (this.useDepth) { + this.depth = new FramebufferTexture(this, 'depthTexture'); + const depthFilter = gl.NEAREST; + this.depthP5Texture = new Texture( this.target._renderer, - this.color, + this.depth, { - minFilter: filter, - magFilter: filter + minFilter: depthFilter, + magFilter: depthFilter } ); - this.target._renderer.textures.set(this.color, this.colorP5Texture); - - gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture); - gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer); + this.target._renderer.textures.set(this.depth, this.depthP5Texture); } - /** - * To create a WebGL texture, one needs to supply three pieces of information: - * the type (the data type each channel will be stored as, e.g. int or float), - * the format (the color channels that will each be stored in the previously - * specified type, e.g. rgb or rgba), and the internal format (the specifics - * of how data for each channel, in the aforementioned type, will be packed - * together, such as how many bits to use, e.g. RGBA32F or RGB565.) - * - * The format and channels asked for by the user hint at what these values - * need to be, and the WebGL version affects what options are avaiable. - * This method returns the values for these three properties, given the - * framebuffer's settings. - * - * @private - */ - _glColorFormat() { - let type, format, internalFormat; - const gl = this.gl; - - if (this.format === constants.FLOAT) { - type = gl.FLOAT; - } else if (this.format === constants.HALF_FLOAT) { - type = this.target.webglVersion === constants.WEBGL2 - ? gl.HALF_FLOAT - : gl.getExtension('OES_texture_half_float').HALF_FLOAT_OES; - } else { - type = gl.UNSIGNED_BYTE; - } - - if (this.channels === constants.RGBA) { - format = gl.RGBA; - } else { - format = gl.RGB; + this.color = new FramebufferTexture(this, 'colorTexture'); + const filter = this.textureFiltering === constants.LINEAR + ? gl.LINEAR + : gl.NEAREST; + this.colorP5Texture = new Texture( + this.target._renderer, + this.color, + { + minFilter: filter, + magFilter: filter } + ); + this.target._renderer.textures.set(this.color, this.colorP5Texture); - if (this.target.webglVersion === constants.WEBGL2) { - // https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html - const table = { - [gl.FLOAT]: { - [gl.RGBA]: gl.RGBA32F - // gl.RGB32F is not available in Firefox without an alpha channel - }, - [gl.HALF_FLOAT]: { - [gl.RGBA]: gl.RGBA16F - // gl.RGB16F is not available in Firefox without an alpha channel - }, - [gl.UNSIGNED_BYTE]: { - [gl.RGBA]: gl.RGBA8, // gl.RGBA4 - [gl.RGB]: gl.RGB8 // gl.RGB565 - } - }; - internalFormat = table[type][format]; - } else if (this.format === constants.HALF_FLOAT) { - internalFormat = gl.RGBA; - } else { - internalFormat = format; - } + gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture); + gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer); + } - return { internalFormat, format, type }; + /** + * To create a WebGL texture, one needs to supply three pieces of information: + * the type (the data type each channel will be stored as, e.g. int or float), + * the format (the color channels that will each be stored in the previously + * specified type, e.g. rgb or rgba), and the internal format (the specifics + * of how data for each channel, in the aforementioned type, will be packed + * together, such as how many bits to use, e.g. RGBA32F or RGB565.) + * + * The format and channels asked for by the user hint at what these values + * need to be, and the WebGL version affects what options are avaiable. + * This method returns the values for these three properties, given the + * framebuffer's settings. + * + * @private + */ + _glColorFormat() { + let type, format, internalFormat; + const gl = this.gl; + + if (this.format === constants.FLOAT) { + type = gl.FLOAT; + } else if (this.format === constants.HALF_FLOAT) { + type = this.target.webglVersion === constants.WEBGL2 + ? gl.HALF_FLOAT + : gl.getExtension('OES_texture_half_float').HALF_FLOAT_OES; + } else { + type = gl.UNSIGNED_BYTE; } - /** - * To create a WebGL texture, one needs to supply three pieces of information: - * the type (the data type each channel will be stored as, e.g. int or float), - * the format (the color channels that will each be stored in the previously - * specified type, e.g. rgb or rgba), and the internal format (the specifics - * of how data for each channel, in the aforementioned type, will be packed - * together, such as how many bits to use, e.g. RGBA32F or RGB565.) - * - * This method takes into account the settings asked for by the user and - * returns values for these three properties that can be used for the - * texture storing depth information. - * - * @private - */ - _glDepthFormat() { - let type, format, internalFormat; - const gl = this.gl; + if (this.channels === constants.RGBA) { + format = gl.RGBA; + } else { + format = gl.RGB; + } - if (this.useStencil) { - if (this.depthFormat === constants.FLOAT) { - type = gl.FLOAT_32_UNSIGNED_INT_24_8_REV; - } else if (this.target.webglVersion === constants.WEBGL2) { - type = gl.UNSIGNED_INT_24_8; - } else { - type = gl.getExtension('WEBGL_depth_texture').UNSIGNED_INT_24_8_WEBGL; - } - } else { - if (this.depthFormat === constants.FLOAT) { - type = gl.FLOAT; - } else { - type = gl.UNSIGNED_INT; + if (this.target.webglVersion === constants.WEBGL2) { + // https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html + const table = { + [gl.FLOAT]: { + [gl.RGBA]: gl.RGBA32F + // gl.RGB32F is not available in Firefox without an alpha channel + }, + [gl.HALF_FLOAT]: { + [gl.RGBA]: gl.RGBA16F + // gl.RGB16F is not available in Firefox without an alpha channel + }, + [gl.UNSIGNED_BYTE]: { + [gl.RGBA]: gl.RGBA8, // gl.RGBA4 + [gl.RGB]: gl.RGB8 // gl.RGB565 } - } + }; + internalFormat = table[type][format]; + } else if (this.format === constants.HALF_FLOAT) { + internalFormat = gl.RGBA; + } else { + internalFormat = format; + } - if (this.useStencil) { - format = gl.DEPTH_STENCIL; - } else { - format = gl.DEPTH_COMPONENT; - } + return { internalFormat, format, type }; + } - if (this.useStencil) { - if (this.depthFormat === constants.FLOAT) { - internalFormat = gl.DEPTH32F_STENCIL8; - } else if (this.target.webglVersion === constants.WEBGL2) { - internalFormat = gl.DEPTH24_STENCIL8; - } else { - internalFormat = gl.DEPTH_STENCIL; - } + /** + * To create a WebGL texture, one needs to supply three pieces of information: + * the type (the data type each channel will be stored as, e.g. int or float), + * the format (the color channels that will each be stored in the previously + * specified type, e.g. rgb or rgba), and the internal format (the specifics + * of how data for each channel, in the aforementioned type, will be packed + * together, such as how many bits to use, e.g. RGBA32F or RGB565.) + * + * This method takes into account the settings asked for by the user and + * returns values for these three properties that can be used for the + * texture storing depth information. + * + * @private + */ + _glDepthFormat() { + let type, format, internalFormat; + const gl = this.gl; + + if (this.useStencil) { + if (this.depthFormat === constants.FLOAT) { + type = gl.FLOAT_32_UNSIGNED_INT_24_8_REV; } else if (this.target.webglVersion === constants.WEBGL2) { - if (this.depthFormat === constants.FLOAT) { - internalFormat = gl.DEPTH_COMPONENT32F; - } else { - internalFormat = gl.DEPTH_COMPONENT24; - } + type = gl.UNSIGNED_INT_24_8; } else { - internalFormat = gl.DEPTH_COMPONENT; + type = gl.getExtension('WEBGL_depth_texture').UNSIGNED_INT_24_8_WEBGL; + } + } else { + if (this.depthFormat === constants.FLOAT) { + type = gl.FLOAT; + } else { + type = gl.UNSIGNED_INT; } - - return { internalFormat, format, type }; } - /** - * A method that will be called when recreating textures. If the framebuffer - * is auto-sized, it will update its width, height, and density properties. - * - * @private - */ - _updateSize() { - if (this._autoSized) { - this.width = this.target.width; - this.height = this.target.height; - this.density = this.target.pixelDensity(); - } + if (this.useStencil) { + format = gl.DEPTH_STENCIL; + } else { + format = gl.DEPTH_COMPONENT; } - /** - * Called when the canvas that the framebuffer is attached to resizes. If the - * framebuffer is auto-sized, it will update its textures to match the new - * size. - * - * @private - */ - _canvasSizeChanged() { - if (this._autoSized) { - this._handleResize(); + if (this.useStencil) { + if (this.depthFormat === constants.FLOAT) { + internalFormat = gl.DEPTH32F_STENCIL8; + } else if (this.target.webglVersion === constants.WEBGL2) { + internalFormat = gl.DEPTH24_STENCIL8; + } else { + internalFormat = gl.DEPTH_STENCIL; } + } else if (this.target.webglVersion === constants.WEBGL2) { + if (this.depthFormat === constants.FLOAT) { + internalFormat = gl.DEPTH_COMPONENT32F; + } else { + internalFormat = gl.DEPTH_COMPONENT24; + } + } else { + internalFormat = gl.DEPTH_COMPONENT; } - /** - * Called when the size of the framebuffer has changed (either by being - * manually updated or from auto-size updates when its canvas changes size.) - * Old textures and renderbuffers will be deleted, and then recreated with the - * new size. - * - * @private - */ - _handleResize() { - const oldColor = this.color; - const oldDepth = this.depth; - const oldColorRenderbuffer = this.colorRenderbuffer; - const oldDepthRenderbuffer = this.depthRenderbuffer; - - this._deleteTexture(oldColor); - if (oldDepth) this._deleteTexture(oldDepth); - const gl = this.gl; - if (oldColorRenderbuffer) gl.deleteRenderbuffer(oldColorRenderbuffer); - if (oldDepthRenderbuffer) gl.deleteRenderbuffer(oldDepthRenderbuffer); + return { internalFormat, format, type }; + } - this._recreateTextures(); - this.defaultCamera._resize(); + /** + * A method that will be called when recreating textures. If the framebuffer + * is auto-sized, it will update its width, height, and density properties. + * + * @private + */ + _updateSize() { + if (this._autoSized) { + this.width = this.target._renderer.width; + this.height = this.target._renderer.height; + this.density = this.target.pixelDensity(); } + } - /** - * Creates a new - * p5.Camera object to use with the framebuffer. - * - * 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)`. - * - * Framebuffer cameras should be created between calls to - * myBuffer.begin() and - * myBuffer.end() like so: - * - * ```js - * let myCamera; - * - * myBuffer.begin(); - * - * // Create the camera for the framebuffer. - * myCamera = myBuffer.createCamera(); - * - * myBuffer.end(); - * ``` - * - * Calling setCamera() updates the - * framebuffer's projection using the camera. - * resetMatrix() must also be called for the - * view to change properly: - * - * ```js - * myBuffer.begin(); - * - * // Set the camera for the framebuffer. - * setCamera(myCamera); - * - * // Reset all transformations. - * resetMatrix(); - * - * // Draw stuff... - * - * myBuffer.end(); - * ``` - * - * @returns {p5.Camera} new camera. - * - * @example - *
- * - * // Double-click to toggle between cameras. - * - * let myBuffer; - * let cam1; - * let cam2; - * let usingCam1 = true; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * // Create the cameras between begin() and end(). - * myBuffer.begin(); - * - * // Create the first camera. - * // Keep its default settings. - * cam1 = myBuffer.createCamera(); - * - * // Create the second camera. - * // Place it at the top-left. - * // Point it at the origin. - * cam2 = myBuffer.createCamera(); - * cam2.setPosition(400, -400, 800); - * cam2.lookAt(0, 0, 0); - * - * myBuffer.end(); - * - * describe( - * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.' - * ); - * } - * - * function draw() { - * // Draw to the p5.Framebuffer object. - * myBuffer.begin(); - * background(200); - * - * // Set the camera. - * if (usingCam1 === true) { - * setCamera(cam1); - * } else { - * setCamera(cam2); - * } - * - * // Reset all transformations. - * resetMatrix(); - * - * // Draw the box. - * box(); - * - * myBuffer.end(); - * - * // Display the p5.Framebuffer object. - * image(myBuffer, -50, -50); - * } - * - * // Toggle the current camera when the user double-clicks. - * function doubleClicked() { - * if (usingCam1 === true) { - * usingCam1 = false; - * } else { - * usingCam1 = true; - * } - * } - * - *
- */ - createCamera() { - const cam = new p5.FramebufferCamera(this); - cam._computeCameraDefaultSettings(); - cam._setDefaultCamera(); - this.target._renderer.states.curCamera = cam; - return cam; + /** + * Called when the canvas that the framebuffer is attached to resizes. If the + * framebuffer is auto-sized, it will update its textures to match the new + * size. + * + * @private + */ + _canvasSizeChanged() { + if (this._autoSized) { + this._handleResize(); } + } - /** - * Given a raw texture wrapper, delete its stored texture from WebGL memory, - * and remove it from p5's list of active textures. - * - * @param {p5.FramebufferTexture} texture - * @private - */ - _deleteTexture(texture) { - const gl = this.gl; - gl.deleteTexture(texture.rawTexture()); + /** + * Called when the size of the framebuffer has changed (either by being + * manually updated or from auto-size updates when its canvas changes size.) + * Old textures and renderbuffers will be deleted, and then recreated with the + * new size. + * + * @private + */ + _handleResize() { + const oldColor = this.color; + const oldDepth = this.depth; + const oldColorRenderbuffer = this.colorRenderbuffer; + const oldDepthRenderbuffer = this.depthRenderbuffer; + + this._deleteTexture(oldColor); + if (oldDepth) this._deleteTexture(oldDepth); + const gl = this.gl; + if (oldColorRenderbuffer) gl.deleteRenderbuffer(oldColorRenderbuffer); + if (oldDepthRenderbuffer) gl.deleteRenderbuffer(oldDepthRenderbuffer); + + this._recreateTextures(); + this.defaultCamera._resize(); + } - this.target._renderer.textures.delete(texture); - } + /** + * Creates a new + * p5.Camera object to use with the framebuffer. + * + * 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)`. + * + * Framebuffer cameras should be created between calls to + * myBuffer.begin() and + * myBuffer.end() like so: + * + * ```js + * let myCamera; + * + * myBuffer.begin(); + * + * // Create the camera for the framebuffer. + * myCamera = myBuffer.createCamera(); + * + * myBuffer.end(); + * ``` + * + * Calling setCamera() updates the + * framebuffer's projection using the camera. + * resetMatrix() must also be called for the + * view to change properly: + * + * ```js + * myBuffer.begin(); + * + * // Set the camera for the framebuffer. + * setCamera(myCamera); + * + * // Reset all transformations. + * resetMatrix(); + * + * // Draw stuff... + * + * myBuffer.end(); + * ``` + * + * @returns {p5.Camera} new camera. + * + * @example + *
+ * + * // Double-click to toggle between cameras. + * + * let myBuffer; + * let cam1; + * let cam2; + * let usingCam1 = true; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * // Create the cameras between begin() and end(). + * myBuffer.begin(); + * + * // Create the first camera. + * // Keep its default settings. + * cam1 = myBuffer.createCamera(); + * + * // Create the second camera. + * // Place it at the top-left. + * // Point it at the origin. + * cam2 = myBuffer.createCamera(); + * cam2.setPosition(400, -400, 800); + * cam2.lookAt(0, 0, 0); + * + * myBuffer.end(); + * + * describe( + * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.' + * ); + * } + * + * function draw() { + * // Draw to the p5.Framebuffer object. + * myBuffer.begin(); + * background(200); + * + * // Set the camera. + * if (usingCam1 === true) { + * setCamera(cam1); + * } else { + * setCamera(cam2); + * } + * + * // Reset all transformations. + * resetMatrix(); + * + * // Draw the box. + * box(); + * + * myBuffer.end(); + * + * // Display the p5.Framebuffer object. + * image(myBuffer, -50, -50); + * } + * + * // Toggle the current camera when the user double-clicks. + * function doubleClicked() { + * if (usingCam1 === true) { + * usingCam1 = false; + * } else { + * usingCam1 = true; + * } + * } + * + *
+ */ + createCamera() { + const cam = new FramebufferCamera(this); + cam._computeCameraDefaultSettings(); + cam._setDefaultCamera(); + this.target._renderer.states.curCamera = cam; + return cam; + } - /** - * Deletes the framebuffer from GPU memory. - * - * Calling `myBuffer.remove()` frees the GPU memory used by the framebuffer. - * The framebuffer also uses a bit of memory on the CPU which can be freed - * like so: - * - * ```js - * // Delete the framebuffer from GPU memory. - * myBuffer.remove(); - * - * // Delete the framebuffer from CPU memory. - * myBuffer = undefined; - * ``` - * - * Note: All variables that reference the framebuffer must be assigned - * the value `undefined` to delete the framebuffer from CPU memory. If any - * variable still refers to the framebuffer, then it won't be garbage - * collected. - * - * @example - *
- * - * // Double-click to remove the p5.Framebuffer object. - * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create an options object. - * let options = { width: 60, height: 60 }; - * - * // Create a p5.Framebuffer object and - * // configure it using options. - * myBuffer = createFramebuffer(options); - * - * describe('A white circle at the center of a dark gray square disappears when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Display the p5.Framebuffer object if - * // it's available. - * if (myBuffer) { - * // Draw to the p5.Framebuffer object. - * myBuffer.begin(); - * background(100); - * circle(0, 0, 20); - * myBuffer.end(); - * - * image(myBuffer, -30, -30); - * } - * } - * - * // Remove the p5.Framebuffer object when the - * // the user double-clicks. - * function doubleClicked() { - * // Delete the framebuffer from GPU memory. - * myBuffer.remove(); - * - * // Delete the framebuffer from CPU memory. - * myBuffer = undefined; - * } - * - *
- */ - remove() { - const gl = this.gl; - this._deleteTexture(this.color); - if (this.depth) this._deleteTexture(this.depth); - gl.deleteFramebuffer(this.framebuffer); - if (this.aaFramebuffer) { - gl.deleteFramebuffer(this.aaFramebuffer); - } - if (this.depthRenderbuffer) { - gl.deleteRenderbuffer(this.depthRenderbuffer); - } - if (this.colorRenderbuffer) { - gl.deleteRenderbuffer(this.colorRenderbuffer); - } - this.target._renderer.framebuffers.delete(this); - } + /** + * Given a raw texture wrapper, delete its stored texture from WebGL memory, + * and remove it from p5's list of active textures. + * + * @param {p5.FramebufferTexture} texture + * @private + */ + _deleteTexture(texture) { + const gl = this.gl; + gl.deleteTexture(texture.rawTexture()); - /** - * Begins drawing shapes to the framebuffer. - * - * `myBuffer.begin()` and myBuffer.end() - * allow shapes to be drawn to the framebuffer. `myBuffer.begin()` begins - * drawing to the framebuffer and - * myBuffer.end() stops drawing to the - * framebuffer. Changes won't be visible until the framebuffer is displayed - * as an image or texture. - * - * @example - *
- * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); - * } - * - * function draw() { - * background(200); - * - * // Start drawing to the p5.Framebuffer object. - * myBuffer.begin(); - * - * background(50); - * rotateY(frameCount * 0.01); - * normalMaterial(); - * torus(30); - * - * // Stop drawing to the p5.Framebuffer object. - * myBuffer.end(); - * - * // Display the p5.Framebuffer object while - * // the user presses the mouse. - * if (mouseIsPressed === true) { - * image(myBuffer, -50, -50); - * } - * } - * - *
- */ - begin() { - this.prevFramebuffer = this.target._renderer.activeFramebuffer(); - if (this.prevFramebuffer) { - this.prevFramebuffer._beforeEnd(); - } - this.target._renderer.activeFramebuffers.push(this); - this._beforeBegin(); - this.target.push(); - // Apply the framebuffer's camera. This does almost what - // RendererGL.reset() does, but this does not try to clear any buffers; - // it only sets the camera. - this.target.setCamera(this.defaultCamera); - this.target.resetMatrix(); - this.target._renderer.states.uViewMatrix - .set(this.target._renderer.states.curCamera.cameraMatrix); - this.target._renderer.states.uModelMatrix.reset(); - this.target._renderer._applyStencilTestIfClipping(); - } + this.target._renderer.textures.delete(texture); + } - /** - * When making a p5.Framebuffer active so that it may be drawn to, this method - * returns the underlying WebGL framebuffer that needs to be active to - * support this. Antialiased framebuffers first write to a multisampled - * renderbuffer, while other framebuffers can write directly to their main - * framebuffers. - * - * @private - */ - _framebufferToBind() { - if (this.antialias) { - // If antialiasing, draw to an antialiased renderbuffer rather - // than directly to the texture. In end() we will copy from the - // renderbuffer to the texture. - return this.aaFramebuffer; - } else { - return this.framebuffer; - } + /** + * Deletes the framebuffer from GPU memory. + * + * Calling `myBuffer.remove()` frees the GPU memory used by the framebuffer. + * The framebuffer also uses a bit of memory on the CPU which can be freed + * like so: + * + * ```js + * // Delete the framebuffer from GPU memory. + * myBuffer.remove(); + * + * // Delete the framebuffer from CPU memory. + * myBuffer = undefined; + * ``` + * + * Note: All variables that reference the framebuffer must be assigned + * the value `undefined` to delete the framebuffer from CPU memory. If any + * variable still refers to the framebuffer, then it won't be garbage + * collected. + * + * @example + *
+ * + * // Double-click to remove the p5.Framebuffer object. + * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create an options object. + * let options = { width: 60, height: 60 }; + * + * // Create a p5.Framebuffer object and + * // configure it using options. + * myBuffer = createFramebuffer(options); + * + * describe('A white circle at the center of a dark gray square disappears when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Display the p5.Framebuffer object if + * // it's available. + * if (myBuffer) { + * // Draw to the p5.Framebuffer object. + * myBuffer.begin(); + * background(100); + * circle(0, 0, 20); + * myBuffer.end(); + * + * image(myBuffer, -30, -30); + * } + * } + * + * // Remove the p5.Framebuffer object when the + * // the user double-clicks. + * function doubleClicked() { + * // Delete the framebuffer from GPU memory. + * myBuffer.remove(); + * + * // Delete the framebuffer from CPU memory. + * myBuffer = undefined; + * } + * + *
+ */ + remove() { + const gl = this.gl; + this._deleteTexture(this.color); + if (this.depth) this._deleteTexture(this.depth); + gl.deleteFramebuffer(this.framebuffer); + if (this.aaFramebuffer) { + gl.deleteFramebuffer(this.aaFramebuffer); + } + if (this.depthRenderbuffer) { + gl.deleteRenderbuffer(this.depthRenderbuffer); } + if (this.colorRenderbuffer) { + gl.deleteRenderbuffer(this.colorRenderbuffer); + } + this.target._renderer.framebuffers.delete(this); + } - /** - * Ensures that the framebuffer is ready to be drawn to - * - * @private - */ - _beforeBegin() { - const gl = this.gl; - gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebufferToBind()); - this.target._renderer.viewport( - this.width * this.density, - this.height * this.density - ); + /** + * Begins drawing shapes to the framebuffer. + * + * `myBuffer.begin()` and myBuffer.end() + * allow shapes to be drawn to the framebuffer. `myBuffer.begin()` begins + * drawing to the framebuffer and + * myBuffer.end() stops drawing to the + * framebuffer. Changes won't be visible until the framebuffer is displayed + * as an image or texture. + * + * @example + *
+ * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); + * } + * + * function draw() { + * background(200); + * + * // Start drawing to the p5.Framebuffer object. + * myBuffer.begin(); + * + * background(50); + * rotateY(frameCount * 0.01); + * normalMaterial(); + * torus(30); + * + * // Stop drawing to the p5.Framebuffer object. + * myBuffer.end(); + * + * // Display the p5.Framebuffer object while + * // the user presses the mouse. + * if (mouseIsPressed === true) { + * image(myBuffer, -50, -50); + * } + * } + * + *
+ */ + begin() { + this.prevFramebuffer = this.target._renderer.activeFramebuffer(); + if (this.prevFramebuffer) { + this.prevFramebuffer._beforeEnd(); } + this.target._renderer.activeFramebuffers.push(this); + this._beforeBegin(); + this.target._renderer.push(); + // Apply the framebuffer's camera. This does almost what + // RendererGL.reset() does, but this does not try to clear any buffers; + // it only sets the camera. + // this.target._renderer.setCamera(this.defaultCamera); + this.target._renderer.states.curCamera = this.defaultCamera; + // set the projection matrix (which is not normally updated each frame) + this.target._renderer.states.uPMatrix.set(this.defaultCamera.projMatrix); + this.target._renderer.states.uViewMatrix.set(this.defaultCamera.cameraMatrix); + + this.target._renderer.resetMatrix(); + this.target._renderer.states.uViewMatrix + .set(this.target._renderer.states.curCamera.cameraMatrix); + this.target._renderer.states.uModelMatrix.reset(); + this.target._renderer._applyStencilTestIfClipping(); + } - /** - * Ensures that the framebuffer is ready to be read by other framebuffers. - * - * @private - */ - _beforeEnd() { - if (this.antialias) { - const gl = this.gl; - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.aaFramebuffer); - gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer); - const partsToCopy = [ - [gl.COLOR_BUFFER_BIT, this.colorP5Texture.glMagFilter] - ]; - if (this.useDepth) { - partsToCopy.push( - [gl.DEPTH_BUFFER_BIT, this.depthP5Texture.glMagFilter] - ); - } - for (const [flag, filter] of partsToCopy) { - gl.blitFramebuffer( - 0, 0, - this.width * this.density, this.height * this.density, - 0, 0, - this.width * this.density, this.height * this.density, - flag, - filter - ); - } - } + /** + * When making a p5.Framebuffer active so that it may be drawn to, this method + * returns the underlying WebGL framebuffer that needs to be active to + * support this. Antialiased framebuffers first write to a multisampled + * renderbuffer, while other framebuffers can write directly to their main + * framebuffers. + * + * @private + */ + _framebufferToBind() { + if (this.antialias) { + // If antialiasing, draw to an antialiased renderbuffer rather + // than directly to the texture. In end() we will copy from the + // renderbuffer to the texture. + return this.aaFramebuffer; + } else { + return this.framebuffer; } + } + + /** + * Ensures that the framebuffer is ready to be drawn to + * + * @private + */ + _beforeBegin() { + const gl = this.gl; + gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebufferToBind()); + this.target._renderer.viewport( + this.width * this.density, + this.height * this.density + ); + } - /** - * Stops drawing shapes to the framebuffer. - * - * myBuffer.begin() and `myBuffer.end()` - * allow shapes to be drawn to the framebuffer. - * myBuffer.begin() begins drawing to - * the framebuffer and `myBuffer.end()` stops drawing to the framebuffer. - * Changes won't be visible until the framebuffer is displayed as an image - * or texture. - * - * @example - *
- * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); - * } - * - * function draw() { - * background(200); - * - * // Start drawing to the p5.Framebuffer object. - * myBuffer.begin(); - * - * background(50); - * rotateY(frameCount * 0.01); - * normalMaterial(); - * torus(30); - * - * // Stop drawing to the p5.Framebuffer object. - * myBuffer.end(); - * - * // Display the p5.Framebuffer object while - * // the user presses the mouse. - * if (mouseIsPressed === true) { - * image(myBuffer, -50, -50); - * } - * } - * - *
- */ - end() { + /** + * Ensures that the framebuffer is ready to be read by other framebuffers. + * + * @private + */ + _beforeEnd() { + if (this.antialias) { const gl = this.gl; - this.target.pop(); - const fbo = this.target._renderer.activeFramebuffers.pop(); - if (fbo !== this) { - throw new Error("It looks like you've called end() while another Framebuffer is active."); + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.aaFramebuffer); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer); + const partsToCopy = [ + [gl.COLOR_BUFFER_BIT, this.colorP5Texture.glMagFilter] + ]; + if (this.useDepth) { + partsToCopy.push( + [gl.DEPTH_BUFFER_BIT, this.depthP5Texture.glMagFilter] + ); } - this._beforeEnd(); - if (this.prevFramebuffer) { - this.prevFramebuffer._beforeBegin(); - } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - this.target._renderer.viewport( - this.target._renderer._origViewport.width, - this.target._renderer._origViewport.height + for (const [flag, filter] of partsToCopy) { + gl.blitFramebuffer( + 0, 0, + this.width * this.density, this.height * this.density, + 0, 0, + this.width * this.density, this.height * this.density, + flag, + filter ); } - this.target._renderer._applyStencilTestIfClipping(); } + } - /** - * Draws to the framebuffer by calling a function that contains drawing - * instructions. - * - * The parameter, `callback`, is a function with the drawing instructions - * for the framebuffer. For example, calling `myBuffer.draw(myFunction)` - * will call a function named `myFunction()` to draw to the framebuffer. - * Doing so has the same effect as the following: - * - * ```js - * myBuffer.begin(); - * myFunction(); - * myBuffer.end(); - * ``` - * - * @param {Function} callback function that draws to the framebuffer. - * - * @example - *
- * - * // Click the canvas to display the framebuffer. - * - * let myBuffer; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); - * } - * - * function draw() { - * background(200); - * - * // Draw to the p5.Framebuffer object. - * myBuffer.draw(bagel); - * - * // Display the p5.Framebuffer object while - * // the user presses the mouse. - * if (mouseIsPressed === true) { - * image(myBuffer, -50, -50); - * } - * } - * - * // Draw a rotating, multicolor torus. - * function bagel() { - * background(50); - * rotateY(frameCount * 0.01); - * normalMaterial(); - * torus(30); - * } - * - *
- */ - draw(callback) { - this.begin(); - callback(); - this.end(); + /** + * Stops drawing shapes to the framebuffer. + * + * myBuffer.begin() and `myBuffer.end()` + * allow shapes to be drawn to the framebuffer. + * myBuffer.begin() begins drawing to + * the framebuffer and `myBuffer.end()` stops drawing to the framebuffer. + * Changes won't be visible until the framebuffer is displayed as an image + * or texture. + * + * @example + *
+ * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); + * } + * + * function draw() { + * background(200); + * + * // Start drawing to the p5.Framebuffer object. + * myBuffer.begin(); + * + * background(50); + * rotateY(frameCount * 0.01); + * normalMaterial(); + * torus(30); + * + * // Stop drawing to the p5.Framebuffer object. + * myBuffer.end(); + * + * // Display the p5.Framebuffer object while + * // the user presses the mouse. + * if (mouseIsPressed === true) { + * image(myBuffer, -50, -50); + * } + * } + * + *
+ */ + end() { + const gl = this.gl; + this.target._renderer.pop(); + const fbo = this.target._renderer.activeFramebuffers.pop(); + if (fbo !== this) { + throw new Error("It looks like you've called end() while another Framebuffer is active."); } - - /** - * Loads the current value of each pixel in the framebuffer into its - * pixels array. - * - * `myBuffer.loadPixels()` must be called before reading from or writing to - * myBuffer.pixels. - * - * @method loadPixels - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Framebuffer object. - * let myBuffer = createFramebuffer(); - * - * // Load the pixels array. - * myBuffer.loadPixels(); - * - * // Get the number of pixels in the - * // top half of the framebuffer. - * let numPixels = myBuffer.pixels.length / 2; - * - * // Set the framebuffer's top half to pink. - * for (let i = 0; i < numPixels; i += 4) { - * myBuffer.pixels[i] = 255; - * myBuffer.pixels[i + 1] = 102; - * myBuffer.pixels[i + 2] = 204; - * myBuffer.pixels[i + 3] = 255; - * } - * - * // Update the pixels array. - * myBuffer.updatePixels(); - * - * // Draw the p5.Framebuffer object to the canvas. - * image(myBuffer, -50, -50); - * - * describe('A pink rectangle above a gray rectangle.'); - * } - * - *
- */ - loadPixels() { - const gl = this.gl; - const prevFramebuffer = this.target._renderer.activeFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); - const colorFormat = this._glColorFormat(); - this.pixels = readPixelsWebGL( - this.pixels, - gl, - this.framebuffer, - 0, - 0, - this.width * this.density, - this.height * this.density, - colorFormat.format, - colorFormat.type + this._beforeEnd(); + if (this.prevFramebuffer) { + this.prevFramebuffer._beforeBegin(); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + this.target._renderer.viewport( + this.target._renderer._origViewport.width, + this.target._renderer._origViewport.height ); - if (prevFramebuffer) { - gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer._framebufferToBind()); - } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } } + this.target._renderer._applyStencilTestIfClipping(); + } - /** - * Gets a pixel or a region of pixels from the framebuffer. - * - * `myBuffer.get()` is easy to use but it's not as fast as - * myBuffer.pixels. Use - * myBuffer.pixels to read many pixel - * values. - * - * The version of `myBuffer.get()` with no parameters returns the entire - * framebuffer as a a p5.Image object. - * - * The version of `myBuffer.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 `myBuffer.get()` with four parameters interprets them as - * coordinates and dimensions. It returns a subsection of the framebuffer 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. - * - * @param {Number} x x-coordinate of the pixel. Defaults to 0. - * @param {Number} y y-coordinate of the pixel. Defaults to 0. - * @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. - */ - /** - * @return {p5.Image} entire framebuffer as a p5.Image object. - */ - /** - * @param {Number} x - * @param {Number} y - * @return {Number[]} color of the pixel at `(x, y)` as an array of color values `[R, G, B, A]`. - */ - get(x, y, w, h) { - p5._validateParameters('p5.Framebuffer.get', arguments); - const colorFormat = this._glColorFormat(); - if (x === undefined && y === undefined) { - x = 0; - y = 0; - w = this.width; - h = this.height; - } else if (w === undefined && h === undefined) { - if (x < 0 || y < 0 || x >= this.width || y >= this.height) { - console.warn( - 'The x and y values passed to p5.Framebuffer.get are outside of its range and will be clamped.' - ); - x = this.target.constrain(x, 0, this.width - 1); - y = this.target.constrain(y, 0, this.height - 1); - } + /** + * Draws to the framebuffer by calling a function that contains drawing + * instructions. + * + * The parameter, `callback`, is a function with the drawing instructions + * for the framebuffer. For example, calling `myBuffer.draw(myFunction)` + * will call a function named `myFunction()` to draw to the framebuffer. + * Doing so has the same effect as the following: + * + * ```js + * myBuffer.begin(); + * myFunction(); + * myBuffer.end(); + * ``` + * + * @param {Function} callback function that draws to the framebuffer. + * + * @example + *
+ * + * // Click the canvas to display the framebuffer. + * + * let myBuffer; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Framebuffer object. + * myBuffer = createFramebuffer(); + * + * describe('An empty gray canvas. The canvas gets darker and a rotating, multicolor torus appears while the user presses and holds the mouse.'); + * } + * + * function draw() { + * background(200); + * + * // Draw to the p5.Framebuffer object. + * myBuffer.draw(bagel); + * + * // Display the p5.Framebuffer object while + * // the user presses the mouse. + * if (mouseIsPressed === true) { + * image(myBuffer, -50, -50); + * } + * } + * + * // Draw a rotating, multicolor torus. + * function bagel() { + * background(50); + * rotateY(frameCount * 0.01); + * normalMaterial(); + * torus(30); + * } + * + *
+ */ + draw(callback) { + this.begin(); + callback(); + this.end(); + } + + /** + * Loads the current value of each pixel in the framebuffer into its + * pixels array. + * + * `myBuffer.loadPixels()` must be called before reading from or writing to + * myBuffer.pixels. + * + * @method loadPixels + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Create a p5.Framebuffer object. + * let myBuffer = createFramebuffer(); + * + * // Load the pixels array. + * myBuffer.loadPixels(); + * + * // Get the number of pixels in the + * // top half of the framebuffer. + * let numPixels = myBuffer.pixels.length / 2; + * + * // Set the framebuffer's top half to pink. + * for (let i = 0; i < numPixels; i += 4) { + * myBuffer.pixels[i] = 255; + * myBuffer.pixels[i + 1] = 102; + * myBuffer.pixels[i + 2] = 204; + * myBuffer.pixels[i + 3] = 255; + * } + * + * // Update the pixels array. + * myBuffer.updatePixels(); + * + * // Draw the p5.Framebuffer object to the canvas. + * image(myBuffer, -50, -50); + * + * describe('A pink rectangle above a gray rectangle.'); + * } + * + *
+ */ + loadPixels() { + const gl = this.gl; + const prevFramebuffer = this.target._renderer.activeFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + const colorFormat = this._glColorFormat(); + this.pixels = readPixelsWebGL( + this.pixels, + gl, + this.framebuffer, + 0, + 0, + this.width * this.density, + this.height * this.density, + colorFormat.format, + colorFormat.type + ); + if (prevFramebuffer) { + gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer._framebufferToBind()); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } + } - return readPixelWebGL( - this.gl, - this.framebuffer, - x * this.density, - y * this.density, - colorFormat.format, - colorFormat.type + /** + * Gets a pixel or a region of pixels from the framebuffer. + * + * `myBuffer.get()` is easy to use but it's not as fast as + * myBuffer.pixels. Use + * myBuffer.pixels to read many pixel + * values. + * + * The version of `myBuffer.get()` with no parameters returns the entire + * framebuffer as a a p5.Image object. + * + * The version of `myBuffer.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 `myBuffer.get()` with four parameters interprets them as + * coordinates and dimensions. It returns a subsection of the framebuffer 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. + * + * @param {Number} x x-coordinate of the pixel. Defaults to 0. + * @param {Number} y y-coordinate of the pixel. Defaults to 0. + * @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. + */ + /** + * @return {p5.Image} entire framebuffer as a p5.Image object. + */ + /** + * @param {Number} x + * @param {Number} y + * @return {Number[]} color of the pixel at `(x, y)` as an array of color values `[R, G, B, A]`. + */ + get(x, y, w, h) { + p5._validateParameters('p5.Framebuffer.get', arguments); + const colorFormat = this._glColorFormat(); + if (x === undefined && y === undefined) { + x = 0; + y = 0; + w = this.width; + h = this.height; + } else if (w === undefined && h === undefined) { + if (x < 0 || y < 0 || x >= this.width || y >= this.height) { + console.warn( + 'The x and y values passed to p5.Framebuffer.get are outside of its range and will be clamped.' ); + x = this.target.constrain(x, 0, this.width - 1); + y = this.target.constrain(y, 0, this.height - 1); } - x = this.target.constrain(x, 0, this.width - 1); - y = this.target.constrain(y, 0, this.height - 1); - w = this.target.constrain(w, 1, this.width - x); - h = this.target.constrain(h, 1, this.height - y); - - const rawData = readPixelsWebGL( - undefined, + return readPixelWebGL( this.gl, this.framebuffer, x * this.density, y * this.density, - w * this.density, - h * this.density, colorFormat.format, colorFormat.type ); - // Framebuffer data might be either a Uint8Array or Float32Array - // depending on its format, and it may or may not have an alpha channel. - // To turn it into an image, we have to normalize the data into a - // Uint8ClampedArray with alpha. - const fullData = new Uint8ClampedArray( - w * h * this.density * this.density * 4 - ); + } - // Default channels that aren't in the framebuffer (e.g. alpha, if the - // framebuffer is in RGB mode instead of RGBA) to 255 - fullData.fill(255); - - const channels = colorFormat.type === this.gl.RGB ? 3 : 4; - for (let y = 0; y < h * this.density; y++) { - for (let x = 0; x < w * this.density; x++) { - for (let channel = 0; channel < 4; channel++) { - const idx = (y * w * this.density + x) * 4 + channel; - if (channel < channels) { - // Find the index of this pixel in `rawData`, which might have a - // different number of channels - const rawDataIdx = channels === 4 - ? idx - : (y * w * this.density + x) * channels + channel; - fullData[idx] = rawData[rawDataIdx]; - } + x = this.target.constrain(x, 0, this.width - 1); + y = this.target.constrain(y, 0, this.height - 1); + w = this.target.constrain(w, 1, this.width - x); + h = this.target.constrain(h, 1, this.height - y); + + const rawData = readPixelsWebGL( + undefined, + this.gl, + this.framebuffer, + x * this.density, + y * this.density, + w * this.density, + h * this.density, + colorFormat.format, + colorFormat.type + ); + // Framebuffer data might be either a Uint8Array or Float32Array + // depending on its format, and it may or may not have an alpha channel. + // To turn it into an image, we have to normalize the data into a + // Uint8ClampedArray with alpha. + const fullData = new Uint8ClampedArray( + w * h * this.density * this.density * 4 + ); + + // Default channels that aren't in the framebuffer (e.g. alpha, if the + // framebuffer is in RGB mode instead of RGBA) to 255 + fullData.fill(255); + + const channels = colorFormat.type === this.gl.RGB ? 3 : 4; + for (let y = 0; y < h * this.density; y++) { + for (let x = 0; x < w * this.density; x++) { + for (let channel = 0; channel < 4; channel++) { + const idx = (y * w * this.density + x) * 4 + channel; + if (channel < channels) { + // Find the index of this pixel in `rawData`, which might have a + // different number of channels + const rawDataIdx = channels === 4 + ? idx + : (y * w * this.density + x) * channels + channel; + fullData[idx] = rawData[rawDataIdx]; } } } + } + + // Create an image from the data + const region = new p5.Image(w * this.density, h * this.density); + region.imageData = region.canvas.getContext('2d').createImageData( + region.width, + region.height + ); + region.imageData.data.set(fullData); + region.pixels = region.imageData.data; + region.updatePixels(); + if (this.density !== 1) { + // TODO: support get() at a pixel density > 1 + region.resize(w, h); + } + return region; + } - // Create an image from the data - const region = new p5.Image(w * this.density, h * this.density); - region.imageData = region.canvas.getContext('2d').createImageData( - region.width, - region.height + /** + * Updates the framebuffer with the RGBA values in the + * pixels array. + * + * `myBuffer.updatePixels()` only needs to be called after changing values + * in the myBuffer.pixels array. Such + * changes can be made directly after calling + * myBuffer.loadPixels(). + * + * @method updatePixels + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Create a p5.Framebuffer object. + * let myBuffer = createFramebuffer(); + * + * // Load the pixels array. + * myBuffer.loadPixels(); + * + * // Get the number of pixels in the + * // top half of the framebuffer. + * let numPixels = myBuffer.pixels.length / 2; + * + * // Set the framebuffer's top half to pink. + * for (let i = 0; i < numPixels; i += 4) { + * myBuffer.pixels[i] = 255; + * myBuffer.pixels[i + 1] = 102; + * myBuffer.pixels[i + 2] = 204; + * myBuffer.pixels[i + 3] = 255; + * } + * + * // Update the pixels array. + * myBuffer.updatePixels(); + * + * // Draw the p5.Framebuffer object to the canvas. + * image(myBuffer, -50, -50); + * + * describe('A pink rectangle above a gray rectangle.'); + * } + * + *
+ */ + updatePixels() { + const gl = this.gl; + this.colorP5Texture.bindTexture(); + const colorFormat = this._glColorFormat(); + + const channels = colorFormat.format === gl.RGBA ? 4 : 3; + const len = + this.width * this.height * this.density * this.density * channels; + const TypedArrayClass = colorFormat.type === gl.UNSIGNED_BYTE + ? Uint8Array + : Float32Array; + if ( + !(this.pixels instanceof TypedArrayClass) || this.pixels.length !== len + ) { + throw new Error( + 'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().' ); - region.imageData.data.set(fullData); - region.pixels = region.imageData.data; - region.updatePixels(); - if (this.density !== 1) { - // TODO: support get() at a pixel density > 1 - region.resize(w, h); - } - return region; } - /** - * Updates the framebuffer with the RGBA values in the - * pixels array. - * - * `myBuffer.updatePixels()` only needs to be called after changing values - * in the myBuffer.pixels array. Such - * changes can be made directly after calling - * myBuffer.loadPixels(). - * - * @method updatePixels - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Framebuffer object. - * let myBuffer = createFramebuffer(); - * - * // Load the pixels array. - * myBuffer.loadPixels(); - * - * // Get the number of pixels in the - * // top half of the framebuffer. - * let numPixels = myBuffer.pixels.length / 2; - * - * // Set the framebuffer's top half to pink. - * for (let i = 0; i < numPixels; i += 4) { - * myBuffer.pixels[i] = 255; - * myBuffer.pixels[i + 1] = 102; - * myBuffer.pixels[i + 2] = 204; - * myBuffer.pixels[i + 3] = 255; - * } - * - * // Update the pixels array. - * myBuffer.updatePixels(); - * - * // Draw the p5.Framebuffer object to the canvas. - * image(myBuffer, -50, -50); - * - * describe('A pink rectangle above a gray rectangle.'); - * } - * - *
- */ - updatePixels() { - const gl = this.gl; - this.colorP5Texture.bindTexture(); - const colorFormat = this._glColorFormat(); - - const channels = colorFormat.format === gl.RGBA ? 4 : 3; - const len = - this.width * this.height * this.density * this.density * channels; - const TypedArrayClass = colorFormat.type === gl.UNSIGNED_BYTE - ? Uint8Array - : Float32Array; - if ( - !(this.pixels instanceof TypedArrayClass) || this.pixels.length !== len - ) { - throw new Error( - 'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().' - ); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + colorFormat.internalFormat, + this.width * this.density, + this.height * this.density, + 0, + colorFormat.format, + colorFormat.type, + this.pixels + ); + this.colorP5Texture.unbindTexture(); + + const prevFramebuffer = this.target._renderer.activeFramebuffer(); + if (this.antialias) { + // We need to make sure the antialiased framebuffer also has the updated + // pixels so that if more is drawn to it, it goes on top of the updated + // pixels instead of replacing them. + // We can't blit the framebuffer to the multisampled antialias + // framebuffer to leave both in the same state, so instead we have + // to use image() to put the framebuffer texture onto the antialiased + // framebuffer. + this.begin(); + this.target._renderer.push(); + this.target._renderer.imageMode(this.target.CENTER); + this.target._renderer.resetMatrix(); + this.target._renderer.noStroke(); + this.target._renderer.clear(); + this.target._renderer.image(this, 0, 0); + this.target._renderer.pop(); + if (this.useDepth) { + gl.clearDepth(1); + gl.clear(gl.DEPTH_BUFFER_BIT); } - - gl.texImage2D( - gl.TEXTURE_2D, - 0, - colorFormat.internalFormat, - this.width * this.density, - this.height * this.density, - 0, - colorFormat.format, - colorFormat.type, - this.pixels - ); - this.colorP5Texture.unbindTexture(); - - const prevFramebuffer = this.target._renderer.activeFramebuffer(); - if (this.antialias) { - // We need to make sure the antialiased framebuffer also has the updated - // pixels so that if more is drawn to it, it goes on top of the updated - // pixels instead of replacing them. - // We can't blit the framebuffer to the multisampled antialias - // framebuffer to leave both in the same state, so instead we have - // to use image() to put the framebuffer texture onto the antialiased - // framebuffer. - this.begin(); - this.target.push(); - this.target.imageMode(this.target.CENTER); - this.target.resetMatrix(); - this.target.noStroke(); - this.target.clear(); - this.target.image(this, 0, 0); - this.target.pop(); - if (this.useDepth) { - gl.clearDepth(1); - gl.clear(gl.DEPTH_BUFFER_BIT); - } - this.end(); + this.end(); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + if (this.useDepth) { + gl.clearDepth(1); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + if (prevFramebuffer) { + gl.bindFramebuffer( + gl.FRAMEBUFFER, + prevFramebuffer._framebufferToBind() + ); } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); - if (this.useDepth) { - gl.clearDepth(1); - gl.clear(gl.DEPTH_BUFFER_BIT); - } - if (prevFramebuffer) { - gl.bindFramebuffer( - gl.FRAMEBUFFER, - prevFramebuffer._framebufferToBind() - ); - } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } + gl.bindFramebuffer(gl.FRAMEBUFFER, null); } } - }; + } +} + +function framebuffer(p5, fn){ + /** + * A p5.Camera attached to a + * p5.Framebuffer. + * + * @class p5.FramebufferCamera + * @param {p5.Framebuffer} framebuffer The framebuffer this camera is + * attached to + * @private + */ + p5.FramebufferCamera = FramebufferCamera; + + /** + * A p5.Texture corresponding to a property of a + * p5.Framebuffer. + * + * @class p5.FramebufferTexture + * @param {p5.Framebuffer} framebuffer The framebuffer represented by this + * texture + * @param {String} property The property of the framebuffer represented by + * this texture, either `color` or `depth` + * @private + */ + p5.FramebufferTexture = FramebufferTexture; + + /** + * A class to describe a high-performance drawing surface for textures. + * + * Each `p5.Framebuffer` object provides a dedicated drawing surface called + * a *framebuffer*. They're similar to + * p5.Graphics objects but can run much faster. + * Performance is improved because the framebuffer shares the same WebGL + * context as the canvas used to create it. + * + * `p5.Framebuffer` objects have all the drawing features of the main + * canvas. Drawing instructions meant for the framebuffer must be placed + * between calls to + * myBuffer.begin() and + * myBuffer.end(). The resulting image + * can be applied as a texture by passing the `p5.Framebuffer` object to the + * texture() function, as in `texture(myBuffer)`. + * It can also be displayed on the main canvas by passing it to the + * image() function, as in `image(myBuffer, 0, 0)`. + * + * Note: createFramebuffer() is the + * recommended way to create an instance of this class. + * + * @class p5.Framebuffer + * @param {p5.Graphics|p5} target sketch instance or + * p5.Graphics + * object. + * @param {Object} [settings] configuration options. + */ + p5.Framebuffer = Framebuffer; /** * An object that stores the framebuffer's color data. @@ -1832,6 +1845,7 @@ function framebuffer(p5, fn){ } export default framebuffer; +export { FramebufferTexture, FramebufferCamera, Framebuffer }; if(typeof p5 !== 'undefined'){ framebuffer(p5, p5.prototype); diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 6cd433015d..0809fd8248 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -9,180 +9,944 @@ //some of the functions are adjusted from Three.js(http://threejs.org) import * as constants from '../core/constants'; +import { DataArray } from './p5.DataArray'; + +class Geometry { + constructor(detailX, detailY, callback) { + 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; + + if (callback instanceof Function) { + callback.call(this); + } + } -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. + * 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 p5.Vector( + Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE); + let maxVertex = new p5.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 p5.Vector(maxVertex.x - minVertex.x, + maxVertex.y - minVertex.y, maxVertex.z - minVertex.z); + let offset = new p5.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() { + 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. * - * @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. + * `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 *
* - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * * function setup() { * createCanvas(100, 100, WEBGL); * + * background(200); + * * // Create a p5.Geometry object. - * myGeometry = new p5.Geometry(); + * // Set its internal color to red. + * beginGeometry(); + * fill(255, 0, 0); + * plane(20); + * let myGeometry = endGeometry(); * - * // 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); + * // Style the shape. + * noStroke(); * - * // Add the vertices to the p5.Geometry object's vertices array. - * myGeometry.vertices.push(v0, v1, v2); + * // Draw the p5.Geometry object (center). + * model(myGeometry); * - * describe('A white triangle drawn on a gray background.'); - * } + * // Translate the origin to the bottom-right. + * translate(25, 25, 0); * - * function draw() { - * background(200); + * // Try to fill the geometry with green. + * fill(0, 255, 0); * - * // Enable orbiting with the mouse. - * orbitControl(); + * // Draw the geometry again (bottom-right). + * model(myGeometry); * - * // Draw the p5.Geometry object. + * // 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 *
* - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * + * let myModel; + * let saveBtn; * function setup() { - * createCanvas(100, 100, WEBGL); + * createCanvas(200, 200, WEBGL); + * myModel = buildGeometry(() => { + * for (let i = 0; i < 5; i++) { + * push(); + * translate( + * random(-75, 75), + * random(-75, 75), + * random(-75, 75) + * ); + * sphere(random(5, 50)); + * pop(); + * } + * }); * - * // Create a p5.Geometry object using a callback function. - * myGeometry = new p5.Geometry(1, 1, createShape); + * saveBtn = createButton('Save .obj'); + * saveBtn.mousePressed(() => myModel.saveObj()); * - * describe('A white triangle drawn on a gray background.'); + * describe('A few spheres rotating in space'); * } * * 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]); + * 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' }); + fn.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. * - *
- * - * // Click and drag the mouse to view the scene from different angles. + * The exported .stl file will include the faces, vertices, and normals of the `p5.Geometry`. * - * let myGeometry; + * 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. Options can include a boolean `binary` property, which + * controls whether or not a binary .stl file is saved. It defaults to false. + * @example + *
+ * + * let myModel; + * let saveBtn1; + * let saveBtn2; * function setup() { - * createCanvas(100, 100, WEBGL); + * createCanvas(200, 200, WEBGL); + * myModel = buildGeometry(() => { + * for (let i = 0; i < 5; i++) { + * push(); + * translate( + * random(-75, 75), + * random(-75, 75), + * random(-75, 75) + * ); + * sphere(random(5, 50)); + * pop(); + * } + * }); * - * // Create a p5.Geometry object using a callback function. - * myGeometry = new p5.Geometry(1, 1, createShape); + * 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 white triangle drawn on a gray background.'); + * describe('A few spheres rotating in space'); * } * * 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(); + * 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 = p5.Vector.sub(this.vertices[f[1]], this.vertices[f[0]]); + const V = p5.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 p5.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' }); + fn.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; + * + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * 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] + * ``` + * + * @for p5.Geometry + * + * @example + *
+ * + * let img; + * + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * 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); + * } + * + *
+ * + *
+ * + * // 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 = p5.Vector.sub(vB, vA); + const ac = p5.Vector.sub(vC, vA); + const n = p5.Vector.cross(ab, ac); + const ln = p5.Vector.mag(n); + let sinAlpha = ln / (p5.Vector.mag(ab) * p5.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. * - * // 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); + * // Create a p5.Geometry object. + * beginGeometry(); + * torus(); + * myGeometry = endGeometry(); + * + * // 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() { @@ -194,804 +958,940 @@ function geometry(p5, fn){ * // Turn on the lights. * lights(); * - * // Style the p5.Geometry object. - * strokeWeight(0.2); + * // Rotate the coordinate system. + * rotateX(1); * - * // Draw the p5.Geometry object. - * model(myGeometry); - * } + * // Style the helix. + * stroke(0); * - * function createShape() { - * // "this" refers to the p5.Geometry object being created. + * // Display the helix. + * model(myGeometry); * - * // 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; + * // Style the normal vectors. + * stroke(255, 0, 0); * - * for (let j = 0; j <= this.detailY; j += 1) { - * // Calculate the "vertical" point along the strip. - * let v = yOffset + yInterval * j; + * // Iterate over the vertices and vertexNormals arrays. + * for (let i = 0; i < myGeometry.vertices.length; i += 1) { * - * for (let i = 0; i <= this.detailX; i += 1) { - * // Calculate the angle of rotation around the strip. - * let u = i * xInterval; + * // Get the vertex p5.Vector object. + * let v = myGeometry.vertices[i]; * - * // 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; + * // Get the vertex normal p5.Vector object. + * let n = myGeometry.vertexNormals[i]; * - * // Create a p5.Vector object to position the vertex. - * let vert = createVector(x, y, z); + * // Calculate a point along the vertex normal. + * let p = p5.Vector.mult(n, 5); * - * // Add the vertex to the p5.Geometry object's vertices array. - * this.vertices.push(vert); - * } + * // Draw the vertex normal as a red line. + * push(); + * translate(v); + * line(0, 0, 0, p.x, p.y, p.z); + * pop(); * } - * - * // Compute the faces array. - * this.computeFaces(); - * - * // Compute the surface normals to help with lighting. - * this.computeNormals(); * } * *
- */ - p5.Geometry = class Geometry { - constructor(detailX, detailY, callback) { - this.vertices = []; - - this.boundingBoxCache = null; - - - //an array containing every vertex for stroke drawing - this.lineVertices = new p5.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 p5.DataArray(); - this.lineTangentsOut = new p5.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 p5.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 p5.DataArray(); - this.detailX = detailX !== undefined ? detailX : 1; - this.detailY = detailY !== undefined ? detailY : 1; - this.dirtyFlags = {}; - - this._hasFillTransparency = undefined; - this._hasStrokeTransparency = undefined; - - 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; + * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * - * // Create a new p5.Geometry object with random spheres. - * particles = buildGeometry(createParticles); + * // Create a p5.Geometry object using a callback function. + * myGeometry = new p5.Geometry(); * - * describe('Ten white spheres placed randomly against a gray background. A box encloses the spheres.'); + * // 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(50); + * background(200); * * // Enable orbiting with the mouse. * orbitControl(); * - * // Turn on the lights. - * lights(); + * // Add a white point light. + * pointLight(255, 255, 255, 0, 0, 10); * - * // Style the particles. + * // Style the p5.Geometry object. * noStroke(); - * fill(255); + * fill(255, 0, 0); * - * // Draw the particles. - * model(particles); + * // Draw the p5.Geometry object. + * model(myGeometry); + * } + * + *
* - * // Calculate the bounding box. - * let bbox = particles.calculateBoundingBox(); + *
+ * + * // Click and drag the mouse to view the scene from different angles. * - * // Translate to the bounding box's center. - * translate(bbox.offset.x, bbox.offset.y, bbox.offset.z); + * let myGeometry; * - * // Style the bounding box. - * stroke(255); - * noFill(); + * function setup() { + * createCanvas(100, 100, WEBGL); * - * // Draw the bounding box. - * box(bbox.size.x, bbox.size.y, bbox.size.z); - * } + * // Create a p5.Geometry object. + * myGeometry = buildGeometry(createShape); * - * 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); + * // Compute normals using default (FLAT) shading. + * myGeometry.computeNormals(FLAT); * - * push(); - * // Translate to the particle's coordinates. - * translate(x, y, z); - * // Draw the particle. - * sphere(3); - * pop(); - * } + * describe('A white, helical structure drawn on a dark gray background. Its faces appear faceted.'); * } - * - *
- */ - calculateBoundingBox() { - if (this.boundingBoxCache) { - return this.boundingBoxCache; // Return cached result if available - } - - let minVertex = new p5.Vector( - Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE); - let maxVertex = new p5.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 p5.Vector(maxVertex.x - minVertex.x, - maxVertex.y - minVertex.y, maxVertex.z - minVertex.z); - let offset = new p5.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() { - 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. - * beginGeometry(); - * fill(255, 0, 0); - * plane(20); - * let myGeometry = endGeometry(); - * - * // 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(() => { - * 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' }); - fn.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. Options can include a boolean `binary` property, which - * controls whether or not a binary .stl file is saved. It defaults to false. - * @example - *
- * - * let myModel; - * let saveBtn1; - * let saveBtn2; - * function setup() { - * createCanvas(200, 200, WEBGL); - * myModel = buildGeometry(() => { - * 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 = p5.Vector.sub(this.vertices[f[1]], this.vertices[f[0]]); - const V = p5.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 p5.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' }); - fn.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. + * function draw() { + * background(50); * - * 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: + * // Enable orbiting with the mouse. + * orbitControl(); * - * ```js - * // Print the original texture coordinates. - * // Output: [0, 0, 1, 0, 0, 1, 1, 1] - * console.log(myGeometry.uvs); + * // Turn on the lights. + * lights(); * - * // Flip the u-coordinates. - * myGeometry.flipU(); + * // Rotate the coordinate system. + * rotateX(1); * - * // Print the flipped texture coordinates. - * // Output: [1, 0, 0, 0, 1, 1, 0, 1] - * console.log(myGeometry.uvs); + * // Style the helix. + * noStroke(); * - * // Notice the swaps: - * // Top vertices: [0, 0, 1, 0] --> [1, 0, 0, 0] - * // Bottom vertices: [0, 1, 1, 1] --> [1, 1, 0, 1] - * ``` + * // Display the helix. + * model(myGeometry); + * } * - * @for p5.Geometry + * 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 *
* - * let img; + * // Click and drag the mouse to view the scene from different angles. * - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } + * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * - * background(200); + * // Create a p5.Geometry object. + * myGeometry = buildGeometry(createShape); * - * // Create p5.Geometry objects. - * let geom1 = buildGeometry(createShape); - * let geom2 = buildGeometry(createShape); + * // Compute normals using smooth shading. + * myGeometry.computeNormals(SMOOTH); * - * // Flip geom2's U texture coordinates. - * geom2.flipU(); + * describe('A white, helical structure drawn on a dark gray background.'); + * } * - * // Left (original). - * push(); - * translate(-25, 0, 0); - * texture(img); - * noStroke(); - * model(geom1); - * pop(); + * function draw() { + * background(50); * - * // Right (flipped). - * push(); - * translate(25, 0, 0); - * texture(img); + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Rotate the coordinate system. + * rotateX(1); + * + * // Style the helix. * noStroke(); - * model(geom2); - * pop(); * - * describe( - * 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.' - * ); + * // Display the helix. + * model(myGeometry); * } * * function createShape() { - * plane(40); + * // 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(); * } * *
- */ - 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); + *
+ * + * // Click and drag the mouse to view the scene from different angles. * - * // Flip the v-coordinates. - * myGeometry.flipV(); + * let myGeometry; * - * // Print the flipped texture coordinates. - * // Output: [0, 1, 1, 1, 0, 0, 1, 0] - * console.log(myGeometry.uvs); + * function setup() { + * createCanvas(100, 100, WEBGL); * - * // Notice the swaps: - * // Left vertices: [0, 0] <--> [1, 0] - * // Right vertices: [1, 0] <--> [1, 1] - * ``` + * // Create a p5.Geometry object. + * myGeometry = buildGeometry(createShape); * - * @for p5.Geometry + * // Create an options object. + * let options = { roundToPrecision: 5 }; * - * @example - *
- * - * let img; + * // Compute normals using smooth shading. + * myGeometry.computeNormals(SMOOTH, options); * - * function preload() { - * img = loadImage('assets/laDefense.jpg'); + * describe('A white, helical structure drawn on a dark gray background.'); * } * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); + * function draw() { + * background(50); * - * // Create p5.Geometry objects. - * let geom1 = buildGeometry(createShape); - * let geom2 = buildGeometry(createShape); + * // Enable orbiting with the mouse. + * orbitControl(); * - * // Flip geom2's V texture coordinates. - * geom2.flipV(); + * // Turn on the lights. + * lights(); * - * // Left (original). - * push(); - * translate(-25, 0, 0); - * texture(img); - * noStroke(); - * model(geom1); - * pop(); + * // Rotate the coordinate system. + * rotateX(1); * - * // Right (flipped). - * push(); - * translate(25, 0, 0); - * texture(img); + * // Style the helix. * noStroke(); - * model(geom2); - * pop(); * - * describe( - * 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.' - * ); + * // Display the helix. + * model(myGeometry); * } * * function createShape() { - * plane(40); + * // 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(); * } * *
*/ - flipV() { - this.uvs = this.uvs.flat().map((val, index) => { - if (index % 2 === 0) { - return val; - } else { - return 1 - val; + computeNormals(shadingType = constants.FLAT, { roundToPrecision = 3 } = {}) { + const vertexNormals = this.vertexNormals; + let vertices = this.vertices; + const faces = this.faces; + let iv; + + if (shadingType === constants.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 p5.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 = p5.Vector.add( + this.vertexNormals[i * offset], + this.vertexNormals[i * offset + this.detailX] + ); + + temp = p5.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 p5.Vector(0, 0, 0); + for (let i = 0; i < this.detailX; i++) { + sum.add(this.vertexNormals[i]); + } + sum = p5.Vector.div(sum, this.detailX); + + for (let i = 0; i < this.detailX; i++) { + this.vertexNormals[i] = sum; + } + + //average the south pole + sum = new p5.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 = p5.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; + } + + /** + * 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 begin = this.vertices[currEdge[0]]; + const end = this.vertices[currEdge[1]]; + 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 = end + .copy() + .sub(begin) + .normalize(); + const dirOK = dir.magSq() > 0; + if (dirOK) { + this._addSegment(begin, end, fromColor, toColor, dir); + } + + 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 { + // 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 (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), + fromColor + ); + 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: fromColor + }); + } + 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); + this.lineVertexColors.push( + ...fromColor, + ...toColor, + ...fromColor, + ...toColor, + ...toColor, + ...fromColor + ); + return this; + } - /** - * Computes the geometry's faces using its vertices. + /** + * 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. + * beginGeometry(); + * torus(1, 0.25); + * myGeometry = endGeometry(); + * + * // 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 = p5.Vector.lerp(maxPosition, minPosition, 0.5); + const dist = p5.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. + * + * An 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, x) + pow(y, y) + pow(z, z)); + * 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 = materialShader().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. + * beginGeometry(); + * fill('hotpink'); + * sphere(45, 50, 50); + * geo = endGeometry(); + * + * // 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); + * } + * + *
+ * + * @method vertexProperty + * @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(){ + 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) { + const size = data.length ? data.length : 1; + 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]; + } +}; + +function geometry(p5, fn){ + /** + * A class to describe a 3D shape. * - * 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. + * 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 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. + * 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. * - * 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]`. + * 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. * - * Note: `myGeometry.computeFaces()` only works when geometries have four or more vertices. + * 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. * - * @chainable + * @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 *
@@ -1009,16 +1909,12 @@ function geometry(p5, fn){ * // 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); + * let v2 = createVector(40, 0, 0); * - * // Compute myGeometry's faces array. - * myGeometry.computeFaces(); + * // Add the vertices to the p5.Geometry object's vertices array. + * myGeometry.vertices.push(v0, v1, v2); * - * describe('A red square drawn on a gray background.'); + * describe('A white triangle drawn on a gray background.'); * } * * function draw() { @@ -1027,13 +1923,6 @@ function geometry(p5, fn){ * // 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); * } @@ -1052,7 +1941,7 @@ function geometry(p5, fn){ * // 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.'); + * describe('A white triangle drawn on a gray background.'); * } * * function draw() { @@ -1061,13 +1950,6 @@ function geometry(p5, fn){ * // 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); * } @@ -1076,1029 +1958,150 @@ function geometry(p5, fn){ * // 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); + * 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, v3); + * this.vertices.push(v0, v1, v2); * - * // Compute the faces array. - * this.computeFaces(); + * // 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]); * } * *
- */ - 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 = p5.Vector.sub(vB, vA); - const ac = p5.Vector.sub(vC, vA); - const n = p5.Vector.cross(ab, ac); - const ln = p5.Vector.mag(n); - let sinAlpha = ln / (p5.Vector.mag(ab) * p5.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. - * beginGeometry(); - * torus(); - * myGeometry = endGeometry(); - * - * // 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(); - * } - * } - * - *
- * - *
- * - * // 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); - * } - * - *
- * - *
- * - * // 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(); - * } - * - *
- * - *
- * - * // 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(); - * } - * - *
- * - *
- * - * // 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 = constants.FLAT, { roundToPrecision = 3 } = {}) { - const vertexNormals = this.vertexNormals; - let vertices = this.vertices; - const faces = this.faces; - let iv; - - if (shadingType === constants.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 p5.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 = p5.Vector.add( - this.vertexNormals[i * offset], - this.vertexNormals[i * offset + this.detailX] - ); - - temp = p5.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 p5.Vector(0, 0, 0); - for (let i = 0; i < this.detailX; i++) { - sum.add(this.vertexNormals[i]); - } - sum = p5.Vector.div(sum, this.detailX); - - for (let i = 0; i < this.detailX; i++) { - this.vertexNormals[i] = sum; - } - - //average the south pole - sum = new p5.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 = p5.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; - } - - /** - * 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 begin = this.vertices[currEdge[0]]; - const end = this.vertices[currEdge[1]]; - 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 = end - .copy() - .sub(begin) - .normalize(); - const dirOK = dir.magSq() > 0; - if (dirOK) { - this._addSegment(begin, end, fromColor, toColor, dir); - } - - 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 { - // 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 (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), - fromColor - ); - 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: fromColor - }); - } - 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); - 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 *
* + * // Click and drag the mouse to view the scene from different angles. + * * let myGeometry; * * function setup() { * createCanvas(100, 100, WEBGL); * - * // Create a very small torus. - * beginGeometry(); - * torus(1, 0.25); - * myGeometry = endGeometry(); - * - * // Normalize the torus so its vertices fill - * // the range [-100, 100]. - * myGeometry.normalize(); + * // Create a p5.Geometry object using a callback function. + * myGeometry = new p5.Geometry(1, 1, createShape); * - * describe('A white torus rotates slowly against a dark gray background.'); + * describe('A white triangle drawn on a gray background.'); * } * * function draw() { - * background(50); - * - * // Turn on the lights. - * lights(); - * - * // Rotate around the y-axis. - * rotateY(frameCount * 0.01); + * background(200); * - * // Style the torus. - * noStroke(); + * // Enable orbiting with the mouse. + * orbitControl(); * - * // Draw the torus. + * // Draw the p5.Geometry object. * 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 = p5.Vector.lerp(maxPosition, minPosition, 0.5); - const dist = p5.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. * - * An 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(). + * 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); * - * 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()`. + * // "this" refers to the p5.Geometry object being created. * - * 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. + * // Add the vertices to the p5.Geometry object's vertices array. + * this.vertices.push(v0, v1, v2); * - * 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. + * // 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]); * - * See also the global vertexProperty() function. + * // Compute the surface normals to help with lighting. + * this.computeNormals(); + * } + * + *
* - * @example *
* - * let geo; + * // Click and drag the mouse to view the scene from different angles. * - * function cartesianToSpherical(x, y, z) { - * let r = sqrt(pow(x, x) + pow(y, y) + pow(z, z)); - * let theta = acos(z / r); - * let phi = atan2(y, x); - * return { theta, phi }; - * } + * // 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); * - * // Modify the material shader to display roughness. - * const myShader = materialShader().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. - * beginGeometry(); - * fill('hotpink'); - * sphere(45, 50, 50); - * geo = endGeometry(); + * // Create the p5.Geometry object. + * // Set detailX to 48 and detailY to 2. + * // >>> try changing them. + * myGeometry = new p5.Geometry(48, 2, createShape); + * } * - * // Set the roughness value for every vertex. - * for (let v of geo.vertices){ + * function draw() { + * background(50); * - * // convert coordinates to spherical coordinates - * let spherical = cartesianToSpherical(v.x, v.y, v.z); + * // Enable orbiting with the mouse. + * orbitControl(); * - * // Set the custom roughness vertex property. - * let roughness = noise(spherical.theta*5, spherical.phi*5); - * geo.vertexProperty('aRoughness', roughness); - * } + * // Turn on the lights. + * lights(); * - * // Use the custom shader. - * shader(myShader); + * // Style the p5.Geometry object. + * strokeWeight(0.2); * - * describe('A rough pink sphere rotating on a blue background.'); + * // Draw the p5.Geometry object. + * model(myGeometry); * } * - * function draw() { - * // Set some styles and lighting - * background('lightblue'); - * noStroke(); + * 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; * - * specularMaterial(255,125,100); - * shininess(2); + * // Create a p5.Vector object to position the vertex. + * let vert = createVector(x, y, z); * - * directionalLight('white', -1, 1, -1); - * ambientLight(320); + * // Add the vertex to the p5.Geometry object's vertices array. + * this.vertices.push(vert); + * } + * } * - * rotateY(millis()*0.001); + * // Compute the faces array. + * this.computeFaces(); * - * // Draw the geometry - * model(geo); + * // Compute the surface normals to help with lighting. + * this.computeNormals(); * } * *
- * - * @method vertexProperty - * @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(){ - 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) { - const size = data.length ? data.length : 1; - 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]; - } - }; + p5.Geometry = Geometry; /** * An array with the geometry's vertices. @@ -2492,6 +2495,7 @@ function geometry(p5, fn){ } export default geometry; +export { Geometry }; if(typeof p5 !== 'undefined'){ geometry(p5, p5.prototype); diff --git a/src/webgl/p5.Matrix.js b/src/webgl/p5.Matrix.js index 8c41715238..9355bebac0 100644 --- a/src/webgl/p5.Matrix.js +++ b/src/webgl/p5.Matrix.js @@ -7,982 +7,987 @@ * Reference/Global_Objects/SIMD */ -function matrix(p5, fn){ - 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; - } - - /** - * A class to describe a 4×4 matrix - * for model and view matrix manipulation in the p5js webgl renderer. - * @class p5.Matrix - * @private - * @param {Array} [mat4] column-major array literal of our 4×4 matrix - */ - p5.Matrix = class Matrix { - constructor(...args){ - - // This is default behavior when object - // instantiated using createMatrix() - // @todo implement createMatrix() in core/math.js - if (args.length && args[args.length - 1] instanceof p5) { - this.p5 = args[args.length - 1]; - } - - if (args[0] === 'mat3') { - this.mat3 = Array.isArray(args[1]) - ? args[1] - : new GLMAT_ARRAY_TYPE([1, 0, 0, 0, 1, 0, 0, 0, 1]); - } else { - this.mat4 = Array.isArray(args[0]) - ? args[0] - : new GLMAT_ARRAY_TYPE( - [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); - } - return this; - } - - reset() { - if (this.mat3) { - this.mat3.set([1, 0, 0, 0, 1, 0, 0, 0, 1]); - } else if (this.mat4) { - this.mat4.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); - } - return this; - } - - /** - * Replace the entire contents of a 4x4 matrix. - * If providing an array or a p5.Matrix, the values will be copied without - * referencing the source object. - * Can also provide 16 numbers as individual arguments. - * - * @param {p5.Matrix|Float32Array|Number[]} [inMatrix] the input p5.Matrix or - * an Array of length 16 - * @chainable - */ - /** - * @param {Number[]} elements 16 numbers passed by value to avoid - * array copying. - * @chainable - */ - set(inMatrix) { - let refArray = arguments; - if (inMatrix instanceof p5.Matrix) { - refArray = inMatrix.mat4; - } else if (isMatrixArray(inMatrix)) { - refArray = inMatrix; - } - if (refArray.length !== 16) { - p5._friendlyError( - `Expected 16 values but received ${refArray.length}.`, - 'p5.Matrix.set' - ); - return this; - } - for (let i = 0; i < 16; i++) { - this.mat4[i] = refArray[i]; - } - return this; - } +import { Vector } from '../math/p5.Vector'; - /** - * Gets a copy of the vector, returns a p5.Matrix object. - * - * @return {p5.Matrix} the copy of the p5.Matrix object - */ - get() { - return new p5.Matrix(this.mat4, this.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. - * - * @return {p5.Matrix} the result matrix - */ - copy() { - if (this.mat3 !== undefined) { - const copied3x3 = new p5.Matrix('mat3', this.p5); - copied3x3.mat3[0] = this.mat3[0]; - copied3x3.mat3[1] = this.mat3[1]; - copied3x3.mat3[2] = this.mat3[2]; - copied3x3.mat3[3] = this.mat3[3]; - copied3x3.mat3[4] = this.mat3[4]; - copied3x3.mat3[5] = this.mat3[5]; - copied3x3.mat3[6] = this.mat3[6]; - copied3x3.mat3[7] = this.mat3[7]; - copied3x3.mat3[8] = this.mat3[8]; - return copied3x3; - } - const copied = new p5.Matrix(this.p5); - copied.mat4[0] = this.mat4[0]; - copied.mat4[1] = this.mat4[1]; - copied.mat4[2] = this.mat4[2]; - copied.mat4[3] = this.mat4[3]; - copied.mat4[4] = this.mat4[4]; - copied.mat4[5] = this.mat4[5]; - copied.mat4[6] = this.mat4[6]; - copied.mat4[7] = this.mat4[7]; - copied.mat4[8] = this.mat4[8]; - copied.mat4[9] = this.mat4[9]; - copied.mat4[10] = this.mat4[10]; - copied.mat4[11] = this.mat4[11]; - copied.mat4[12] = this.mat4[12]; - copied.mat4[13] = this.mat4[13]; - copied.mat4[14] = this.mat4[14]; - copied.mat4[15] = this.mat4[15]; - return copied; - } - - clone() { - return this.copy(); - } +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; +} - /** - * return an identity matrix - * @return {p5.Matrix} the result matrix - */ - static identity(pInst){ - return new p5.Matrix(pInst); - } +class Matrix { + constructor(...args){ - /** - * transpose according to a given matrix - * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be - * based on to transpose - * @chainable - */ - transpose(a) { - let a01, a02, a03, a12, a13, a23; - if (a instanceof p5.Matrix) { - a01 = a.mat4[1]; - a02 = a.mat4[2]; - a03 = a.mat4[3]; - a12 = a.mat4[6]; - a13 = a.mat4[7]; - a23 = a.mat4[11]; - - this.mat4[0] = a.mat4[0]; - this.mat4[1] = a.mat4[4]; - this.mat4[2] = a.mat4[8]; - this.mat4[3] = a.mat4[12]; - this.mat4[4] = a01; - this.mat4[5] = a.mat4[5]; - this.mat4[6] = a.mat4[9]; - this.mat4[7] = a.mat4[13]; - this.mat4[8] = a02; - this.mat4[9] = a12; - this.mat4[10] = a.mat4[10]; - this.mat4[11] = a.mat4[14]; - this.mat4[12] = a03; - this.mat4[13] = a13; - this.mat4[14] = a23; - this.mat4[15] = a.mat4[15]; - } else if (isMatrixArray(a)) { - a01 = a[1]; - a02 = a[2]; - a03 = a[3]; - a12 = a[6]; - a13 = a[7]; - a23 = a[11]; - - this.mat4[0] = a[0]; - this.mat4[1] = a[4]; - this.mat4[2] = a[8]; - this.mat4[3] = a[12]; - this.mat4[4] = a01; - this.mat4[5] = a[5]; - this.mat4[6] = a[9]; - this.mat4[7] = a[13]; - this.mat4[8] = a02; - this.mat4[9] = a12; - this.mat4[10] = a[10]; - this.mat4[11] = a[14]; - this.mat4[12] = a03; - this.mat4[13] = a13; - this.mat4[14] = a23; - this.mat4[15] = a[15]; - } - return this; - } + // This is default behavior when object + // instantiated using createMatrix() + // @todo implement createMatrix() in core/math.js + // if (args.length && args[args.length - 1] instanceof p5) { + // this.p5 = args[args.length - 1]; + // } - /** - * invert matrix according to a give matrix - * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be - * based on to invert - * @chainable - */ - invert(a) { - let a00, a01, a02, a03, a10, a11, a12, a13; - let a20, a21, a22, a23, a30, a31, a32, a33; - if (a instanceof p5.Matrix) { - a00 = a.mat4[0]; - a01 = a.mat4[1]; - a02 = a.mat4[2]; - a03 = a.mat4[3]; - a10 = a.mat4[4]; - a11 = a.mat4[5]; - a12 = a.mat4[6]; - a13 = a.mat4[7]; - a20 = a.mat4[8]; - a21 = a.mat4[9]; - a22 = a.mat4[10]; - a23 = a.mat4[11]; - a30 = a.mat4[12]; - a31 = a.mat4[13]; - a32 = a.mat4[14]; - a33 = a.mat4[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.mat4[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; - this.mat4[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; - this.mat4[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; - this.mat4[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; - this.mat4[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; - this.mat4[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; - this.mat4[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; - this.mat4[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; - this.mat4[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; - this.mat4[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; - this.mat4[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; - this.mat4[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; - this.mat4[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; - this.mat4[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; - this.mat4[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; - this.mat4[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; + if (args[0] === 'mat3') { + this.mat3 = Array.isArray(args[1]) + ? args[1] + : new GLMAT_ARRAY_TYPE([1, 0, 0, 0, 1, 0, 0, 0, 1]); + } else { + this.mat4 = Array.isArray(args[0]) + ? args[0] + : new GLMAT_ARRAY_TYPE( + [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); + } + return this; + } - return this; + reset() { + if (this.mat3) { + this.mat3.set([1, 0, 0, 0, 1, 0, 0, 0, 1]); + } else if (this.mat4) { + this.mat4.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); } + return this; + } - /** - * Inverts a 3×3 matrix - * @chainable - */ - 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; + /** + * Replace the entire contents of a 4x4 matrix. + * If providing an array or a p5.Matrix, the values will be copied without + * referencing the source object. + * Can also provide 16 numbers as individual arguments. + * + * @param {p5.Matrix|Float32Array|Number[]} [inMatrix] the input p5.Matrix or + * an Array of length 16 + * @chainable + */ + /** + * @param {Number[]} elements 16 numbers passed by value to avoid + * array copying. + * @chainable + */ + set(inMatrix) { + let refArray = arguments; + if (inMatrix instanceof Matrix) { + refArray = inMatrix.mat4; + } else if (isMatrixArray(inMatrix)) { + refArray = inMatrix; + } + if (refArray.length !== 16) { + p5._friendlyError( + `Expected 16 values but received ${refArray.length}.`, + 'p5.Matrix.set' + ); 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 - * @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; + for (let i = 0; i < 16; i++) { + this.mat4[i] = refArray[i]; } + return this; + } - /** - * converts a 4×4 matrix to its 3×3 inverse transform - * commonly used in MVMatrix to NMatrix conversions. - * @param {p5.Matrix} mat4 the matrix to be based on to invert - * @chainable - * @todo finish implementation - */ - inverseTranspose({ mat4 }) { - if (this.mat3 === undefined) { - p5._friendlyError('sorry, this function only works with mat3'); - } else { - //convert mat4 -> mat3 - this.mat3[0] = mat4[0]; - this.mat3[1] = mat4[1]; - this.mat3[2] = mat4[2]; - this.mat3[3] = mat4[4]; - this.mat3[4] = mat4[5]; - this.mat3[5] = mat4[6]; - this.mat3[6] = mat4[8]; - this.mat3[7] = mat4[9]; - this.mat3[8] = mat4[10]; - } - - const inverse = this.invert3x3(); - // check inverse succeeded - if (inverse) { - inverse.transpose3x3(this.mat3); - } else { - // in case of singularity, just zero the matrix - for (let i = 0; i < 9; i++) { - this.mat3[i] = 0; - } - } - return this; - } + /** + * Gets a copy of the vector, returns a p5.Matrix object. + * + * @return {p5.Matrix} the copy of the p5.Matrix object + */ + get() { + return new Matrix(this.mat4, this.p5); + } - /** - * inspired by Toji's mat4 determinant - * @return {Number} Determinant of our 4×4 matrix - */ - determinant() { - const d00 = this.mat4[0] * this.mat4[5] - this.mat4[1] * this.mat4[4], - d01 = this.mat4[0] * this.mat4[6] - this.mat4[2] * this.mat4[4], - d02 = this.mat4[0] * this.mat4[7] - this.mat4[3] * this.mat4[4], - d03 = this.mat4[1] * this.mat4[6] - this.mat4[2] * this.mat4[5], - d04 = this.mat4[1] * this.mat4[7] - this.mat4[3] * this.mat4[5], - d05 = this.mat4[2] * this.mat4[7] - this.mat4[3] * this.mat4[6], - d06 = this.mat4[8] * this.mat4[13] - this.mat4[9] * this.mat4[12], - d07 = this.mat4[8] * this.mat4[14] - this.mat4[10] * this.mat4[12], - d08 = this.mat4[8] * this.mat4[15] - this.mat4[11] * this.mat4[12], - d09 = this.mat4[9] * this.mat4[14] - this.mat4[10] * this.mat4[13], - d10 = this.mat4[9] * this.mat4[15] - this.mat4[11] * this.mat4[13], - d11 = this.mat4[10] * this.mat4[15] - this.mat4[11] * this.mat4[14]; - - // Calculate the determinant - return d00 * d11 - d01 * d10 + d02 * d09 + - d03 * d08 - d04 * d07 + d05 * d06; - } + /** + * 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. + * + * @return {p5.Matrix} the result matrix + */ + copy() { + if (this.mat3 !== undefined) { + const copied3x3 = new Matrix('mat3', this.p5); + copied3x3.mat3[0] = this.mat3[0]; + copied3x3.mat3[1] = this.mat3[1]; + copied3x3.mat3[2] = this.mat3[2]; + copied3x3.mat3[3] = this.mat3[3]; + copied3x3.mat3[4] = this.mat3[4]; + copied3x3.mat3[5] = this.mat3[5]; + copied3x3.mat3[6] = this.mat3[6]; + copied3x3.mat3[7] = this.mat3[7]; + copied3x3.mat3[8] = this.mat3[8]; + return copied3x3; + } + const copied = new Matrix(this.p5); + copied.mat4[0] = this.mat4[0]; + copied.mat4[1] = this.mat4[1]; + copied.mat4[2] = this.mat4[2]; + copied.mat4[3] = this.mat4[3]; + copied.mat4[4] = this.mat4[4]; + copied.mat4[5] = this.mat4[5]; + copied.mat4[6] = this.mat4[6]; + copied.mat4[7] = this.mat4[7]; + copied.mat4[8] = this.mat4[8]; + copied.mat4[9] = this.mat4[9]; + copied.mat4[10] = this.mat4[10]; + copied.mat4[11] = this.mat4[11]; + copied.mat4[12] = this.mat4[12]; + copied.mat4[13] = this.mat4[13]; + copied.mat4[14] = this.mat4[14]; + copied.mat4[15] = this.mat4[15]; + return copied; + } - /** - * multiply two mat4s - * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix - * we want to multiply by - * @chainable - */ - mult(multMatrix) { - let _src; - - if (multMatrix === this || multMatrix === this.mat4) { - _src = this.copy().mat4; // only need to allocate in this rare case - } else if (multMatrix instanceof p5.Matrix) { - _src = multMatrix.mat4; - } else if (isMatrixArray(multMatrix)) { - _src = multMatrix; - } else if (arguments.length === 16) { - _src = arguments; - } else { - return; // nothing to do. - } + clone() { + return this.copy(); + } - // each row is used for the multiplier - let b0 = this.mat4[0], - b1 = this.mat4[1], - b2 = this.mat4[2], - b3 = this.mat4[3]; - this.mat4[0] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; - this.mat4[1] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; - this.mat4[2] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; - this.mat4[3] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; - - b0 = this.mat4[4]; - b1 = this.mat4[5]; - b2 = this.mat4[6]; - b3 = this.mat4[7]; - this.mat4[4] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; - this.mat4[5] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; - this.mat4[6] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; - this.mat4[7] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; - - b0 = this.mat4[8]; - b1 = this.mat4[9]; - b2 = this.mat4[10]; - b3 = this.mat4[11]; - this.mat4[8] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; - this.mat4[9] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; - this.mat4[10] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; - this.mat4[11] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; - - b0 = this.mat4[12]; - b1 = this.mat4[13]; - b2 = this.mat4[14]; - b3 = this.mat4[15]; - this.mat4[12] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; - this.mat4[13] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; - this.mat4[14] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; - this.mat4[15] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; + /** + * return an identity matrix + * @return {p5.Matrix} the result matrix + */ + static identity(pInst){ + return new Matrix(pInst); + } - return this; - } + /** + * transpose according to a given matrix + * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be + * based on to transpose + * @chainable + */ + transpose(a) { + let a01, a02, a03, a12, a13, a23; + if (a instanceof p5.Matrix) { + a01 = a.mat4[1]; + a02 = a.mat4[2]; + a03 = a.mat4[3]; + a12 = a.mat4[6]; + a13 = a.mat4[7]; + a23 = a.mat4[11]; + + this.mat4[0] = a.mat4[0]; + this.mat4[1] = a.mat4[4]; + this.mat4[2] = a.mat4[8]; + this.mat4[3] = a.mat4[12]; + this.mat4[4] = a01; + this.mat4[5] = a.mat4[5]; + this.mat4[6] = a.mat4[9]; + this.mat4[7] = a.mat4[13]; + this.mat4[8] = a02; + this.mat4[9] = a12; + this.mat4[10] = a.mat4[10]; + this.mat4[11] = a.mat4[14]; + this.mat4[12] = a03; + this.mat4[13] = a13; + this.mat4[14] = a23; + this.mat4[15] = a.mat4[15]; + } else if (isMatrixArray(a)) { + a01 = a[1]; + a02 = a[2]; + a03 = a[3]; + a12 = a[6]; + a13 = a[7]; + a23 = a[11]; + + this.mat4[0] = a[0]; + this.mat4[1] = a[4]; + this.mat4[2] = a[8]; + this.mat4[3] = a[12]; + this.mat4[4] = a01; + this.mat4[5] = a[5]; + this.mat4[6] = a[9]; + this.mat4[7] = a[13]; + this.mat4[8] = a02; + this.mat4[9] = a12; + this.mat4[10] = a[10]; + this.mat4[11] = a[14]; + this.mat4[12] = a03; + this.mat4[13] = a13; + this.mat4[14] = a23; + this.mat4[15] = a[15]; + } + return this; + } - apply(multMatrix) { - let _src; - - if (multMatrix === this || multMatrix === this.mat4) { - _src = this.copy().mat4; // only need to allocate in this rare case - } else if (multMatrix instanceof p5.Matrix) { - _src = multMatrix.mat4; - } else if (isMatrixArray(multMatrix)) { - _src = multMatrix; - } else if (arguments.length === 16) { - _src = arguments; - } else { - return; // nothing to do. - } + /** + * invert matrix according to a give matrix + * @param {p5.Matrix|Float32Array|Number[]} a the matrix to be + * based on to invert + * @chainable + */ + invert(a) { + let a00, a01, a02, a03, a10, a11, a12, a13; + let a20, a21, a22, a23, a30, a31, a32, a33; + if (a instanceof p5.Matrix) { + a00 = a.mat4[0]; + a01 = a.mat4[1]; + a02 = a.mat4[2]; + a03 = a.mat4[3]; + a10 = a.mat4[4]; + a11 = a.mat4[5]; + a12 = a.mat4[6]; + a13 = a.mat4[7]; + a20 = a.mat4[8]; + a21 = a.mat4[9]; + a22 = a.mat4[10]; + a23 = a.mat4[11]; + a30 = a.mat4[12]; + a31 = a.mat4[13]; + a32 = a.mat4[14]; + a33 = a.mat4[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.mat4[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + this.mat4[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + this.mat4[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + this.mat4[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; + this.mat4[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + this.mat4[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + this.mat4[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + this.mat4[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; + this.mat4[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + this.mat4[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + this.mat4[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + this.mat4[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; + this.mat4[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; + this.mat4[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; + this.mat4[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; + this.mat4[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; + + return this; + } - const mat4 = this.mat4; - - // 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; + /** + * Inverts a 3×3 matrix + * @chainable + */ + 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; + } - 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 + * @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; + } - /** - * scales a p5.Matrix by scalars or a vector - * @param {p5.Vector|Float32Array|Number[]} s vector to scale by - * @chainable - */ - scale(x, y, z) { - if (x instanceof p5.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 + /** + * converts a 4×4 matrix to its 3×3 inverse transform + * commonly used in MVMatrix to NMatrix conversions. + * @param {p5.Matrix} mat4 the matrix to be based on to invert + * @chainable + * @todo finish implementation + */ + inverseTranspose({ mat4 }) { + if (this.mat3 === undefined) { + p5._friendlyError('sorry, this function only works with mat3'); + } else { + //convert mat4 -> mat3 + this.mat3[0] = mat4[0]; + this.mat3[1] = mat4[1]; + this.mat3[2] = mat4[2]; + this.mat3[3] = mat4[4]; + this.mat3[4] = mat4[5]; + this.mat3[5] = mat4[6]; + this.mat3[6] = mat4[8]; + this.mat3[7] = mat4[9]; + this.mat3[8] = mat4[10]; + } + + const inverse = this.invert3x3(); + // check inverse succeeded + if (inverse) { + inverse.transpose3x3(this.mat3); + } else { + // in case of singularity, just zero the matrix + for (let i = 0; i < 9; i++) { + this.mat3[i] = 0; } - - this.mat4[0] *= x; - this.mat4[1] *= x; - this.mat4[2] *= x; - this.mat4[3] *= x; - this.mat4[4] *= y; - this.mat4[5] *= y; - this.mat4[6] *= y; - this.mat4[7] *= y; - this.mat4[8] *= z; - this.mat4[9] *= z; - this.mat4[10] *= z; - this.mat4[11] *= z; - - return this; } + return this; + } - /** - * rotate our Matrix around an axis by the given angle. - * @param {Number} a The angle of rotation in radians - * @param {p5.Vector|Number[]} axis the axis(es) to rotate around - * @chainable - * inspired by Toji's gl-matrix lib, mat4 rotation - */ - rotate(a, x, y, z) { - if (x instanceof p5.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.mat4[0]; - const a01 = this.mat4[1]; - const a02 = this.mat4[2]; - const a03 = this.mat4[3]; - const a10 = this.mat4[4]; - const a11 = this.mat4[5]; - const a12 = this.mat4[6]; - const a13 = this.mat4[7]; - const a20 = this.mat4[8]; - const a21 = this.mat4[9]; - const a22 = this.mat4[10]; - const a23 = this.mat4[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.mat4[0] = a00 * b00 + a10 * b01 + a20 * b02; - this.mat4[1] = a01 * b00 + a11 * b01 + a21 * b02; - this.mat4[2] = a02 * b00 + a12 * b01 + a22 * b02; - this.mat4[3] = a03 * b00 + a13 * b01 + a23 * b02; - this.mat4[4] = a00 * b10 + a10 * b11 + a20 * b12; - this.mat4[5] = a01 * b10 + a11 * b11 + a21 * b12; - this.mat4[6] = a02 * b10 + a12 * b11 + a22 * b12; - this.mat4[7] = a03 * b10 + a13 * b11 + a23 * b12; - this.mat4[8] = a00 * b20 + a10 * b21 + a20 * b22; - this.mat4[9] = a01 * b20 + a11 * b21 + a21 * b22; - this.mat4[10] = a02 * b20 + a12 * b21 + a22 * b22; - this.mat4[11] = a03 * b20 + a13 * b21 + a23 * b22; + /** + * inspired by Toji's mat4 determinant + * @return {Number} Determinant of our 4×4 matrix + */ + determinant() { + const d00 = this.mat4[0] * this.mat4[5] - this.mat4[1] * this.mat4[4], + d01 = this.mat4[0] * this.mat4[6] - this.mat4[2] * this.mat4[4], + d02 = this.mat4[0] * this.mat4[7] - this.mat4[3] * this.mat4[4], + d03 = this.mat4[1] * this.mat4[6] - this.mat4[2] * this.mat4[5], + d04 = this.mat4[1] * this.mat4[7] - this.mat4[3] * this.mat4[5], + d05 = this.mat4[2] * this.mat4[7] - this.mat4[3] * this.mat4[6], + d06 = this.mat4[8] * this.mat4[13] - this.mat4[9] * this.mat4[12], + d07 = this.mat4[8] * this.mat4[14] - this.mat4[10] * this.mat4[12], + d08 = this.mat4[8] * this.mat4[15] - this.mat4[11] * this.mat4[12], + d09 = this.mat4[9] * this.mat4[14] - this.mat4[10] * this.mat4[13], + d10 = this.mat4[9] * this.mat4[15] - this.mat4[11] * this.mat4[13], + d11 = this.mat4[10] * this.mat4[15] - this.mat4[11] * this.mat4[14]; + + // Calculate the determinant + return d00 * d11 - d01 * d10 + d02 * d09 + + d03 * d08 - d04 * d07 + d05 * d06; + } - return this; - } + /** + * multiply two mat4s + * @param {p5.Matrix|Float32Array|Number[]} multMatrix The matrix + * we want to multiply by + * @chainable + */ + mult(multMatrix) { + let _src; + + if (multMatrix === this || multMatrix === this.mat4) { + _src = this.copy().mat4; // only need to allocate in this rare case + } else if (multMatrix instanceof Matrix) { + _src = multMatrix.mat4; + } else if (isMatrixArray(multMatrix)) { + _src = multMatrix; + } else if (arguments.length === 16) { + _src = arguments; + } else { + return; // nothing to do. + } + + // each row is used for the multiplier + let b0 = this.mat4[0], + b1 = this.mat4[1], + b2 = this.mat4[2], + b3 = this.mat4[3]; + this.mat4[0] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; + this.mat4[1] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; + this.mat4[2] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; + this.mat4[3] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; + + b0 = this.mat4[4]; + b1 = this.mat4[5]; + b2 = this.mat4[6]; + b3 = this.mat4[7]; + this.mat4[4] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; + this.mat4[5] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; + this.mat4[6] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; + this.mat4[7] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; + + b0 = this.mat4[8]; + b1 = this.mat4[9]; + b2 = this.mat4[10]; + b3 = this.mat4[11]; + this.mat4[8] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; + this.mat4[9] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; + this.mat4[10] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; + this.mat4[11] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; + + b0 = this.mat4[12]; + b1 = this.mat4[13]; + b2 = this.mat4[14]; + b3 = this.mat4[15]; + this.mat4[12] = b0 * _src[0] + b1 * _src[4] + b2 * _src[8] + b3 * _src[12]; + this.mat4[13] = b0 * _src[1] + b1 * _src[5] + b2 * _src[9] + b3 * _src[13]; + this.mat4[14] = b0 * _src[2] + b1 * _src[6] + b2 * _src[10] + b3 * _src[14]; + this.mat4[15] = b0 * _src[3] + b1 * _src[7] + b2 * _src[11] + b3 * _src[15]; + + return this; + } - /** - * @todo finish implementing this method! - * translates - * @param {Number[]} v vector to translate by - * @chainable - */ - translate(v) { - const x = v[0], - y = v[1], - z = v[2] || 0; - this.mat4[12] += this.mat4[0] * x + this.mat4[4] * y + this.mat4[8] * z; - this.mat4[13] += this.mat4[1] * x + this.mat4[5] * y + this.mat4[9] * z; - this.mat4[14] += this.mat4[2] * x + this.mat4[6] * y + this.mat4[10] * z; - this.mat4[15] += this.mat4[3] * x + this.mat4[7] * y + this.mat4[11] * z; - } + apply(multMatrix) { + let _src; + + if (multMatrix === this || multMatrix === this.mat4) { + _src = this.copy().mat4; // only need to allocate in this rare case + } else if (multMatrix instanceof Matrix) { + _src = multMatrix.mat4; + } else if (isMatrixArray(multMatrix)) { + _src = multMatrix; + } else if (arguments.length === 16) { + _src = arguments; + } else { + return; // nothing to do. + } + + const mat4 = this.mat4; + + // 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; + } - rotateX(a) { - this.rotate(a, 1, 0, 0); - } - rotateY(a) { - this.rotate(a, 0, 1, 0); - } - rotateZ(a) { - this.rotate(a, 0, 0, 1); - } + /** + * scales a p5.Matrix by scalars or a vector + * @param {p5.Vector|Float32Array|Number[]} s vector to scale by + * @chainable + */ + 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.mat4[0] *= x; + this.mat4[1] *= x; + this.mat4[2] *= x; + this.mat4[3] *= x; + this.mat4[4] *= y; + this.mat4[5] *= y; + this.mat4[6] *= y; + this.mat4[7] *= y; + this.mat4[8] *= z; + this.mat4[9] *= z; + this.mat4[10] *= z; + this.mat4[11] *= z; + + return this; + } - /** - * sets the perspective matrix - * @param {Number} fovy [description] - * @param {Number} aspect [description] - * @param {Number} near near clipping plane - * @param {Number} far far clipping plane - * @chainable - */ - perspective(fovy, aspect, near, far) { - const f = 1.0 / Math.tan(fovy / 2), - nf = 1 / (near - far); - - this.mat4[0] = f / aspect; - this.mat4[1] = 0; - this.mat4[2] = 0; - this.mat4[3] = 0; - this.mat4[4] = 0; - this.mat4[5] = f; - this.mat4[6] = 0; - this.mat4[7] = 0; - this.mat4[8] = 0; - this.mat4[9] = 0; - this.mat4[10] = (far + near) * nf; - this.mat4[11] = -1; - this.mat4[12] = 0; - this.mat4[13] = 0; - this.mat4[14] = 2 * far * near * nf; - this.mat4[15] = 0; + /** + * rotate our Matrix around an axis by the given angle. + * @param {Number} a The angle of rotation in radians + * @param {p5.Vector|Number[]} axis the axis(es) to rotate around + * @chainable + * inspired by Toji's gl-matrix lib, mat4 rotation + */ + rotate(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.mat4[0]; + const a01 = this.mat4[1]; + const a02 = this.mat4[2]; + const a03 = this.mat4[3]; + const a10 = this.mat4[4]; + const a11 = this.mat4[5]; + const a12 = this.mat4[6]; + const a13 = this.mat4[7]; + const a20 = this.mat4[8]; + const a21 = this.mat4[9]; + const a22 = this.mat4[10]; + const a23 = this.mat4[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.mat4[0] = a00 * b00 + a10 * b01 + a20 * b02; + this.mat4[1] = a01 * b00 + a11 * b01 + a21 * b02; + this.mat4[2] = a02 * b00 + a12 * b01 + a22 * b02; + this.mat4[3] = a03 * b00 + a13 * b01 + a23 * b02; + this.mat4[4] = a00 * b10 + a10 * b11 + a20 * b12; + this.mat4[5] = a01 * b10 + a11 * b11 + a21 * b12; + this.mat4[6] = a02 * b10 + a12 * b11 + a22 * b12; + this.mat4[7] = a03 * b10 + a13 * b11 + a23 * b12; + this.mat4[8] = a00 * b20 + a10 * b21 + a20 * b22; + this.mat4[9] = a01 * b20 + a11 * b21 + a21 * b22; + this.mat4[10] = a02 * b20 + a12 * b21 + a22 * b22; + this.mat4[11] = a03 * b20 + a13 * b21 + a23 * b22; + + return this; + } - return this; - } + /** + * @todo finish implementing this method! + * translates + * @param {Number[]} v vector to translate by + * @chainable + */ + translate(v) { + const x = v[0], + y = v[1], + z = v[2] || 0; + this.mat4[12] += this.mat4[0] * x + this.mat4[4] * y + this.mat4[8] * z; + this.mat4[13] += this.mat4[1] * x + this.mat4[5] * y + this.mat4[9] * z; + this.mat4[14] += this.mat4[2] * x + this.mat4[6] * y + this.mat4[10] * z; + this.mat4[15] += this.mat4[3] * x + this.mat4[7] * y + this.mat4[11] * z; + } - /** - * sets the ortho matrix - * @param {Number} left [description] - * @param {Number} right [description] - * @param {Number} bottom [description] - * @param {Number} top [description] - * @param {Number} near near clipping plane - * @param {Number} far far clipping plane - * @chainable - */ - ortho(left, right, bottom, top, near, far) { - const lr = 1 / (left - right), - bt = 1 / (bottom - top), - nf = 1 / (near - far); - this.mat4[0] = -2 * lr; - this.mat4[1] = 0; - this.mat4[2] = 0; - this.mat4[3] = 0; - this.mat4[4] = 0; - this.mat4[5] = -2 * bt; - this.mat4[6] = 0; - this.mat4[7] = 0; - this.mat4[8] = 0; - this.mat4[9] = 0; - this.mat4[10] = 2 * nf; - this.mat4[11] = 0; - this.mat4[12] = (left + right) * lr; - this.mat4[13] = (top + bottom) * bt; - this.mat4[14] = (far + near) * nf; - this.mat4[15] = 1; + rotateX(a) { + this.rotate(a, 1, 0, 0); + } + rotateY(a) { + this.rotate(a, 0, 1, 0); + } + rotateZ(a) { + this.rotate(a, 0, 0, 1); + } - return this; - } + /** + * sets the perspective matrix + * @param {Number} fovy [description] + * @param {Number} aspect [description] + * @param {Number} near near clipping plane + * @param {Number} far far clipping plane + * @chainable + */ + perspective(fovy, aspect, near, far) { + const f = 1.0 / Math.tan(fovy / 2), + nf = 1 / (near - far); + + this.mat4[0] = f / aspect; + this.mat4[1] = 0; + this.mat4[2] = 0; + this.mat4[3] = 0; + this.mat4[4] = 0; + this.mat4[5] = f; + this.mat4[6] = 0; + this.mat4[7] = 0; + this.mat4[8] = 0; + this.mat4[9] = 0; + this.mat4[10] = (far + near) * nf; + this.mat4[11] = -1; + this.mat4[12] = 0; + this.mat4[13] = 0; + this.mat4[14] = 2 * far * near * nf; + this.mat4[15] = 0; + + return this; + } - /** - * apply a matrix to a vector with x,y,z,w components - * get the results in the form of an array - * @param {Number} - * @return {Number[]} - */ - multiplyVec4(x, y, z, w) { - const result = new Array(4); - const m = this.mat4; + /** + * sets the ortho matrix + * @param {Number} left [description] + * @param {Number} right [description] + * @param {Number} bottom [description] + * @param {Number} top [description] + * @param {Number} near near clipping plane + * @param {Number} far far clipping plane + * @chainable + */ + ortho(left, right, bottom, top, near, far) { + const lr = 1 / (left - right), + bt = 1 / (bottom - top), + nf = 1 / (near - far); + this.mat4[0] = -2 * lr; + this.mat4[1] = 0; + this.mat4[2] = 0; + this.mat4[3] = 0; + this.mat4[4] = 0; + this.mat4[5] = -2 * bt; + this.mat4[6] = 0; + this.mat4[7] = 0; + this.mat4[8] = 0; + this.mat4[9] = 0; + this.mat4[10] = 2 * nf; + this.mat4[11] = 0; + this.mat4[12] = (left + right) * lr; + this.mat4[13] = (top + bottom) * bt; + this.mat4[14] = (far + near) * nf; + this.mat4[15] = 1; + + return this; + } - 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; + /** + * apply a matrix to a vector with x,y,z,w components + * get the results in the form of an array + * @param {Number} + * @return {Number[]} + */ + multiplyVec4(x, y, z, w) { + const result = new Array(4); + const m = this.mat4; - return result; - } + 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; - /** - * 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. - * - * @param {p5.Vector} - * @return {p5.Vector} - */ - multiplyPoint({ x, y, z }) { - const array = this.multiplyVec4(x, y, z, 1); - return new p5.Vector(array[0], array[1], array[2]); - } + return result; + } - /** - * 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. - * - * @param {p5.Vector} - * @return {p5.Vector} - */ - 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 p5.Vector(array[0], array[1], array[2]); - } + /** + * 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. + * + * @param {p5.Vector} + * @return {p5.Vector} + */ + 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 0. - * Returns a vector consisting of the first - * through third components of the result. - * - * @param {p5.Vector} - * @return {p5.Vector} - */ - multiplyDirection({ x, y, z }) { - const array = this.multiplyVec4(x, y, z, 0); - return new p5.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. + * + * @param {p5.Vector} + * @return {p5.Vector} + */ + 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]); + } - /** - * 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 - * @chainable - */ - mult3x3(multMatrix) { - let _src; - - if (multMatrix === this || multMatrix === this.mat3) { - _src = this.copy().mat3; // only need to allocate in this rare case - } else if (multMatrix instanceof p5.Matrix) { - _src = multMatrix.mat3; - } else if (isMatrixArray(multMatrix)) { - _src = multMatrix; - } else if (arguments.length === 9) { - _src = arguments; - } else { - return; // nothing to do. - } + /** + * 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. + * + * @param {p5.Vector} + * @return {p5.Vector} + */ + multiplyDirection({ x, y, z }) { + const array = this.multiplyVec4(x, y, z, 0); + return new Vector(array[0], array[1], array[2]); + } - // 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]; + /** + * 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 + * @chainable + */ + mult3x3(multMatrix) { + let _src; + + if (multMatrix === this || multMatrix === this.mat3) { + _src = this.copy().mat3; // only need to allocate in this rare case + } else if (multMatrix instanceof Matrix) { + _src = multMatrix.mat3; + } else if (isMatrixArray(multMatrix)) { + _src = multMatrix; + } else if (arguments.length === 9) { + _src = arguments; + } else { + return; // nothing to do. + } + + // 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; + } - return this; - } + /** + * This function is only for 3x3 matrices. + * A function that returns a column vector of a 3x3 matrix. + * + * @param {Number} columnIndex matrix column number + * @return {p5.Vector} + */ + column(columnIndex) { + return new Vector( + this.mat3[3 * columnIndex], + this.mat3[3 * columnIndex + 1], + this.mat3[3 * columnIndex + 2] + ); + } - /** - * This function is only for 3x3 matrices. - * A function that returns a column vector of a 3x3 matrix. - * - * @param {Number} columnIndex matrix column number - * @return {p5.Vector} - */ - column(columnIndex) { - return new p5.Vector( - this.mat3[3 * columnIndex], - this.mat3[3 * columnIndex + 1], - this.mat3[3 * columnIndex + 2] - ); - } + /** + * This function is only for 3x3 matrices. + * A function that returns a row vector of a 3x3 matrix. + * + * @param {Number} rowIndex matrix row number + * @return {p5.Vector} + */ + row(rowIndex) { + return new Vector( + this.mat3[rowIndex], + this.mat3[rowIndex + 3], + this.mat3[rowIndex + 6] + ); + } - /** - * This function is only for 3x3 matrices. - * A function that returns a row vector of a 3x3 matrix. - * - * @param {Number} rowIndex matrix row number - * @return {p5.Vector} - */ - row(rowIndex) { - return new p5.Vector( - this.mat3[rowIndex], - this.mat3[rowIndex + 3], - this.mat3[rowIndex + 6] - ); + /** + * Returns the diagonal elements of the matrix in the form of an array. + * A 3x3 matrix will return an array of length 3. + * A 4x4 matrix will return an array of length 4. + * + * @return {Number[]} An array obtained by arranging the diagonal elements + * of the matrix in ascending order of index + */ + diagonal() { + if (this.mat3 !== undefined) { + return [this.mat3[0], this.mat3[4], this.mat3[8]]; } + return [this.mat4[0], this.mat4[5], this.mat4[10], this.mat4[15]]; + } - /** - * Returns the diagonal elements of the matrix in the form of an array. - * A 3x3 matrix will return an array of length 3. - * A 4x4 matrix will return an array of length 4. - * - * @return {Number[]} An array obtained by arranging the diagonal elements - * of the matrix in ascending order of index - */ - diagonal() { - if (this.mat3 !== undefined) { - return [this.mat3[0], this.mat3[4], this.mat3[8]]; - } - return [this.mat4[0], this.mat4[5], this.mat4[10], this.mat4[15]]; - } + /** + * This function is only for 3x3 matrices. + * Takes a vector and returns the vector resulting from multiplying to + * that vector by this matrix from left. + * + * @param {p5.Vector} multVector the vector to which this matrix applies + * @param {p5.Vector} [target] The vector to receive the result + * @return {p5.Vector} + */ + 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; + } - /** - * This function is only for 3x3 matrices. - * Takes a vector and returns the vector resulting from multiplying to - * that vector by this matrix from left. - * - * @param {p5.Vector} multVector the vector to which this matrix applies - * @param {p5.Vector} [target] The vector to receive the result - * @return {p5.Vector} - */ - 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; - } + /** + * This function is only for 4x4 matrices. + * Creates a 3x3 matrix whose entries are the top left 3x3 part and returns it. + * + * @return {p5.Matrix} + */ + createSubMatrix3x3() { + const result = new Matrix('mat3'); + result.mat3[0] = this.mat4[0]; + result.mat3[1] = this.mat4[1]; + result.mat3[2] = this.mat4[2]; + result.mat3[3] = this.mat4[4]; + result.mat3[4] = this.mat4[5]; + result.mat3[5] = this.mat4[6]; + result.mat3[6] = this.mat4[8]; + result.mat3[7] = this.mat4[9]; + result.mat3[8] = this.mat4[10]; + return result; + } - /** - * This function is only for 4x4 matrices. - * Creates a 3x3 matrix whose entries are the top left 3x3 part and returns it. - * - * @return {p5.Matrix} - */ - createSubMatrix3x3() { - const result = new p5.Matrix('mat3'); - result.mat3[0] = this.mat4[0]; - result.mat3[1] = this.mat4[1]; - result.mat3[2] = this.mat4[2]; - result.mat3[3] = this.mat4[4]; - result.mat3[4] = this.mat4[5]; - result.mat3[5] = this.mat4[6]; - result.mat3[6] = this.mat4[8]; - result.mat3[7] = this.mat4[9]; - result.mat3[8] = this.mat4[10]; - return result; - } + /** + * 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 +//]; +}; - /** - * PRIVATE +function matrix(p5, fn){ + /** + * A class to describe a 4×4 matrix + * for model and view matrix manipulation in the p5js webgl renderer. + * @class p5.Matrix + * @private + * @param {Array} [mat4] column-major array literal of our 4×4 matrix */ - // 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 - //]; - }; + p5.Matrix = Matrix } export default matrix; +export { Matrix }; if(typeof p5 !== 'undefined'){ matrix(p5, p5.prototype); diff --git a/src/webgl/p5.Quat.js b/src/webgl/p5.Quat.js index 1eaf41b7f3..7ecb773bff 100644 --- a/src/webgl/p5.Quat.js +++ b/src/webgl/p5.Quat.js @@ -3,6 +3,82 @@ * @submodule Quaternion */ +import { Vector } from '../math/p5.Vector'; + +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) { + /* eslint-disable max-len */ + 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 + ); + /* eslint-enable max-len */ + } + + /** + * 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 + * @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 @@ -17,82 +93,11 @@ function quat(p5, fn){ * @param {Number} [z] z component of imaginary part of quaternion * @private */ - p5.Quat = class { - constructor(w, x, y, z) { - this.w = w; - this.vec = new p5.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 p5.Vector(x, y, z).normalize().mult(Math.sin(angle/2)); - return new p5.Quat(w, vec.x, vec.y, vec.z); - } - - conjugate() { - return new p5.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) { - /* eslint-disable max-len */ - return new p5.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 - ); - /* eslint-enable max-len */ - } - - /** - * 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 - * @param {p5.Vector} [p] vector to rotate on the axis quaternion - */ - rotateVector(p) { - return p5.Vector.mult( p, this.w*this.w - this.vec.dot(this.vec) ) - .add( p5.Vector.mult( this.vec, 2 * p.dot(this.vec) ) ) - .add( p5.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(); - } - }; + p5.Quat = Quat; } export default quat; +export { Quat }; if(typeof p5 !== 'undefined'){ quat(p5, p5.prototype); diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index 99850f4713..e2f597a832 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -1,77 +1,80 @@ -function renderBuffer(p5, fn){ - p5.RenderBuffer = class { - constructor(size, src, dst, attr, renderer, map){ - this.size = size; // the number of FLOATs in each vertex - this.src = src; // the name of the model's source array - this.dst = dst; // the name of the geometry's buffer - this.attr = attr; // the name of the vertex attribute - this._renderer = renderer; - this.map = map; // optional, a transformation function to apply to src - } +class RenderBuffer { + constructor(size, src, dst, attr, renderer, map){ + this.size = size; // the number of FLOATs in each vertex + this.src = src; // the name of the model's source array + this.dst = dst; // the name of the geometry's buffer + this.attr = attr; // the name of the vertex attribute + this._renderer = renderer; + this.map = map; // optional, a transformation function to apply to src + } - /** - * Enables and binds the buffers used by shader when the appropriate data exists in geometry. - * Must always be done prior to drawing geometry in WebGL. - * @param {p5.Geometry} geometry Geometry that is going to be drawn - * @param {p5.Shader} shader Active shader - * @private - */ - _prepareBuffer(geometry, shader) { - const attributes = shader.attributes; - const gl = this._renderer.GL; - let model; - if (geometry.model) { - model = geometry.model; - } else { - model = geometry; - } + /** + * Enables and binds the buffers used by shader when the appropriate data exists in geometry. + * Must always be done prior to drawing geometry in WebGL. + * @param {p5.Geometry} geometry Geometry that is going to be drawn + * @param {p5.Shader} shader Active shader + * @private + */ + _prepareBuffer(geometry, shader) { + const attributes = shader.attributes; + const gl = this._renderer.GL; + let model; + if (geometry.model) { + model = geometry.model; + } else { + model = geometry; + } - // loop through each of the buffer definitions - const attr = attributes[this.attr]; - if (!attr) { - return; - } - // check if the model has the appropriate source array - let buffer = geometry[this.dst]; - const src = model[this.src]; - if (!src){ - return; + // loop through each of the buffer definitions + const attr = attributes[this.attr]; + if (!attr) { + return; + } + // check if the model has the appropriate source array + let buffer = geometry[this.dst]; + const src = model[this.src]; + if (!src){ + return; + } + if (src.length > 0) { + // check if we need to create the GL buffer + const createBuffer = !buffer; + if (createBuffer) { + // create and remember the buffer + geometry[this.dst] = buffer = gl.createBuffer(); } - if (src.length > 0) { - // check if we need to create the GL buffer - const createBuffer = !buffer; - if (createBuffer) { - // create and remember the buffer - geometry[this.dst] = buffer = gl.createBuffer(); - } - // bind the buffer - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + // bind the buffer + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - // check if we need to fill the buffer with data - if (createBuffer || model.dirtyFlags[this.src] !== false) { - const map = this.map; - // get the values from the model, possibly transformed - const values = map ? map(src) : src; - // fill the buffer with the values - this._renderer._bindBuffer(buffer, gl.ARRAY_BUFFER, values); - // mark the model's source array as clean - model.dirtyFlags[this.src] = false; - } - // enable the attribute - shader.enableAttrib(attr, this.size); - } else { - const loc = attr.location; - if (loc === -1 || !this._renderer.registerEnabled.has(loc)) { return; } - // Disable register corresponding to unused attribute - gl.disableVertexAttribArray(loc); - // Record register availability - this._renderer.registerEnabled.delete(loc); + // check if we need to fill the buffer with data + if (createBuffer || model.dirtyFlags[this.src] !== false) { + const map = this.map; + // get the values from the model, possibly transformed + const values = map ? map(src) : src; + // fill the buffer with the values + this._renderer._bindBuffer(buffer, gl.ARRAY_BUFFER, values); + // mark the model's source array as clean + model.dirtyFlags[this.src] = false; } + // enable the attribute + shader.enableAttrib(attr, this.size); + } else { + const loc = attr.location; + if (loc === -1 || !this._renderer.registerEnabled.has(loc)) { return; } + // Disable register corresponding to unused attribute + gl.disableVertexAttribArray(loc); + // Record register availability + this._renderer.registerEnabled.delete(loc); } - }; + } +}; + +function renderBuffer(p5, fn){ + p5.RenderBuffer = RenderBuffer; } export default renderBuffer; +export { RenderBuffer }; if(typeof p5 !== 'undefined'){ renderBuffer(p5, p5.prototype); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index d55a68dd0d..45deab33b1 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1,6 +1,16 @@ import * as constants from '../core/constants'; import GeometryBuilder from './GeometryBuilder'; import libtess from 'libtess'; // Fixed with exporting module from libtess +import { Renderer } from '../core/p5.Renderer'; +import { Matrix } from './p5.Matrix'; +import { Camera } from './p5.Camera'; +import { Vector } from '../math/p5.Vector'; +import { RenderBuffer } from './p5.RenderBuffer'; +import { Geometry } from './p5.Geometry'; +import { DataArray } from './p5.DataArray'; +import { Shader } from './p5.Shader'; +import { Image } from '../image/p5.Image'; +import { Texture } from './p5.Texture'; import lightingShader from './shaders/lighting.glsl'; import webgl2CompatibilityShader from './shaders/webgl2Compatibility.glsl'; @@ -35,2376 +45,3553 @@ import filterInvertFrag from './shaders/filters/invert.frag'; import filterThresholdFrag from './shaders/filters/threshold.frag'; import filterShaderVert from './shaders/filters/default.vert'; -function rendererGL(p5, fn){ - const STROKE_CAP_ENUM = {}; - const STROKE_JOIN_ENUM = {}; - let lineDefs = ''; - const defineStrokeCapEnum = function (key, val) { - lineDefs += `#define STROKE_CAP_${key} ${val}\n`; - STROKE_CAP_ENUM[constants[key]] = val; - }; - const defineStrokeJoinEnum = function (key, val) { - lineDefs += `#define STROKE_JOIN_${key} ${val}\n`; - STROKE_JOIN_ENUM[constants[key]] = val; - }; +const STROKE_CAP_ENUM = {}; +const STROKE_JOIN_ENUM = {}; +let lineDefs = ''; +const defineStrokeCapEnum = function (key, val) { + lineDefs += `#define STROKE_CAP_${key} ${val}\n`; + STROKE_CAP_ENUM[constants[key]] = val; +}; +const defineStrokeJoinEnum = function (key, val) { + lineDefs += `#define STROKE_JOIN_${key} ${val}\n`; + STROKE_JOIN_ENUM[constants[key]] = val; +}; + + +// Define constants in line shaders for each type of cap/join, and also record +// the values in JS objects +defineStrokeCapEnum('ROUND', 0); +defineStrokeCapEnum('PROJECT', 1); +defineStrokeCapEnum('SQUARE', 2); +defineStrokeJoinEnum('ROUND', 0); +defineStrokeJoinEnum('MITER', 1); +defineStrokeJoinEnum('BEVEL', 2); + +const defaultShaders = { + immediateVert, + vertexColorVert, + vertexColorFrag, + normalVert, + normalFrag, + basicFrag, + sphereMappingFrag, + lightVert: + lightingShader + + lightVert, + lightTextureFrag, + phongVert, + phongFrag: + lightingShader + + phongFrag, + fontVert, + fontFrag, + lineVert: + lineDefs + lineVert, + lineFrag: + lineDefs + lineFrag, + pointVert, + pointFrag, + imageLightVert, + imageLightDiffusedFrag, + imageLightSpecularFrag +}; +let sphereMapping = defaultShaders.sphereMappingFrag; +for (const key in defaultShaders) { + defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key]; +} +const filterShaderFrags = { + [constants.GRAY]: filterGrayFrag, + [constants.ERODE]: filterErodeFrag, + [constants.DILATE]: filterDilateFrag, + [constants.BLUR]: filterBlurFrag, + [constants.POSTERIZE]: filterPosterizeFrag, + [constants.OPAQUE]: filterOpaqueFrag, + [constants.INVERT]: filterInvertFrag, + [constants.THRESHOLD]: filterThresholdFrag +}; - // Define constants in line shaders for each type of cap/join, and also record - // the values in JS objects - defineStrokeCapEnum('ROUND', 0); - defineStrokeCapEnum('PROJECT', 1); - defineStrokeCapEnum('SQUARE', 2); - defineStrokeJoinEnum('ROUND', 0); - defineStrokeJoinEnum('MITER', 1); - defineStrokeJoinEnum('BEVEL', 2); - - const defaultShaders = { - immediateVert, - vertexColorVert, - vertexColorFrag, - normalVert, - normalFrag, - basicFrag, - sphereMappingFrag, - lightVert: - lightingShader + - lightVert, - lightTextureFrag, - phongVert, - phongFrag: - lightingShader + - phongFrag, - fontVert, - fontFrag, - lineVert: - lineDefs + lineVert, - lineFrag: - lineDefs + lineFrag, - pointVert, - pointFrag, - imageLightVert, - imageLightDiffusedFrag, - imageLightSpecularFrag - }; - let sphereMapping = defaultShaders.sphereMappingFrag; - for (const key in defaultShaders) { - defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key]; - } - - const filterShaderFrags = { - [constants.GRAY]: filterGrayFrag, - [constants.ERODE]: filterErodeFrag, - [constants.DILATE]: filterDilateFrag, - [constants.BLUR]: filterBlurFrag, - [constants.POSTERIZE]: filterPosterizeFrag, - [constants.OPAQUE]: filterOpaqueFrag, - [constants.INVERT]: filterInvertFrag, - [constants.THRESHOLD]: filterThresholdFrag - }; +/** + * 3D graphics class + * @private + * @class p5.RendererGL + * @extends p5.Renderer + * @todo extend class to include public method for offscreen + * rendering (FBO). + */ +class RendererGL extends Renderer { + constructor(pInst, w, h, isMainCanvas, elt, attr) { + super(pInst, w, h, isMainCanvas); + + // Create new canvas + this.canvas = this.elt = elt || document.createElement('canvas'); + this._initContext(); + // This redundant property is useful in reminding you that you are + // interacting with WebGLRenderingContext, still worth considering future removal + this.GL = this.drawingContext; + this._pInst.drawingContext = this.drawingContext; + + if (this._isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = 'none'; + } + this.elt.id = 'defaultCanvas0'; + this.elt.classList.add('p5Canvas'); + + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; + + this.width = w; + this.height = h; + + // Set canvas size + this.elt.width = w * this._pixelDensity; + this.elt.height = h * this._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; + this._origViewport = { + width: this.GL.drawingBufferWidth, + height: this.GL.drawingBufferHeight + }; + this.viewport( + this._origViewport.width, + this._origViewport.height + ); + + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName('main').length === 0) { + let m = document.createElement('main'); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName('main')[0].appendChild(this.elt); + } + + this._setAttributeDefaults(pInst); + this.isP3D = true; //lets us know we're in 3d mode + + // When constructing a new Geometry, this will represent the builder + this.geometryBuilder = undefined; + + // Push/pop state + this.states.uModelMatrix = new Matrix(); + this.states.uViewMatrix = new Matrix(); + this.states.uMVMatrix = new Matrix(); + this.states.uPMatrix = new Matrix(); + this.states.uNMatrix = new Matrix('mat3'); + this.states.curMatrix = new Matrix('mat3'); + + this.states.curCamera = new Camera(this); + + this.states.enableLighting = false; + this.states.ambientLightColors = []; + this.states.specularColors = [1, 1, 1]; + this.states.directionalLightDirections = []; + this.states.directionalLightDiffuseColors = []; + this.states.directionalLightSpecularColors = []; + this.states.pointLightPositions = []; + this.states.pointLightDiffuseColors = []; + this.states.pointLightSpecularColors = []; + this.states.spotLightPositions = []; + this.states.spotLightDirections = []; + this.states.spotLightDiffuseColors = []; + this.states.spotLightSpecularColors = []; + this.states.spotLightAngle = []; + this.states.spotLightConc = []; + this.states.activeImageLight = null; + + this.states.curFillColor = [1, 1, 1, 1]; + this.states.curAmbientColor = [1, 1, 1, 1]; + this.states.curSpecularColor = [0, 0, 0, 0]; + this.states.curEmissiveColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 1]; + + this.states.curBlendMode = constants.BLEND; + + this.states._hasSetAmbient = false; + this.states._useSpecularMaterial = false; + this.states._useEmissiveMaterial = false; + this.states._useNormalMaterial = false; + this.states._useShininess = 1; + this.states._useMetalness = 0; + + this.states.tint = [255, 255, 255, 255]; + + this.states.constantAttenuation = 1; + this.states.linearAttenuation = 0; + this.states.quadraticAttenuation = 0; + + this.states._currentNormal = new Vector(0, 0, 1); + + this.states.drawMode = constants.FILL; + + this.states._tex = null; + + // erasing + this._isErasing = false; + + // clipping + this._clipDepths = []; + this._isClipApplied = false; + this._stencilTestOn = false; + + this.mixedAmbientLight = []; + this.mixedSpecularColor = []; + + // p5.framebuffer for this are calculated in getDiffusedTexture function + this.diffusedTextures = new Map(); + // p5.framebuffer for this are calculated in getSpecularTexture function + this.specularTextures = new Map(); + + this.preEraseBlend = undefined; + this._cachedBlendMode = undefined; + this._cachedFillStyle = [1, 1, 1, 1]; + this._cachedStrokeStyle = [0, 0, 0, 1]; + if (this.webglVersion === constants.WEBGL2) { + this.blendExt = this.GL; + } else { + this.blendExt = this.GL.getExtension('EXT_blend_minmax'); + } + this._isBlending = false; + + this._useLineColor = false; + this._useVertexColor = false; + + this.registerEnabled = new Set(); + + // Camera + this.states.curCamera._computeCameraDefaultSettings(); + this.states.curCamera._setDefaultCamera(); + + // FilterCamera + this.filterCamera = new Camera(this); + this.filterCamera._computeCameraDefaultSettings(); + this.filterCamera._setDefaultCamera(); + // Information about the previous frame's touch object + // for executing orbitControl() + this.prevTouches = []; + // Velocity variable for use with orbitControl() + this.zoomVelocity = 0; + this.rotateVelocity = new Vector(0, 0); + this.moveVelocity = new Vector(0, 0); + // Flags for recording the state of zooming, rotation and moving + this.executeZoom = false; + this.executeRotateAndMove = false; + + this.states.specularShader = undefined; + this.sphereMapping = undefined; + this.states.diffusedShader = undefined; + this._defaultLightShader = undefined; + this._defaultImmediateModeShader = undefined; + this._defaultNormalShader = undefined; + this._defaultColorShader = undefined; + this._defaultPointShader = undefined; + + this.states.userFillShader = undefined; + this.states.userStrokeShader = undefined; + this.states.userPointShader = undefined; + + this._useUserVertexProperties = undefined; + + // Default drawing is done in Retained Mode + // Geometry and Material hashes stored here + this.retainedMode = { + geometry: {}, + buffers: { + stroke: [ + new RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + new RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + ], + fill: [ + new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + new RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + new RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + //new BufferDef(3, 'vertexSpeculars', 'specularBuffer', 'aSpecularColor'), + new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + text: [ + new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + user:[] + } + }; + + // Immediate Mode + // Geometry and Material hashes stored here + this.immediateMode = { + geometry: new Geometry(), + shapeMode: constants.TRIANGLE_FAN, + contourIndices: [], + _bezierVertex: [], + _quadraticVertex: [], + _curveVertex: [], + buffers: { + fill: [ + new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + new RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + new RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + stroke: [ + new RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + new RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + ], + point: this.GL.createBuffer(), + user:[] + } + }; + + this.pointSize = 5.0; //default point size + this.curStrokeWeight = 1; + this.curStrokeCap = constants.ROUND; + this.curStrokeJoin = constants.ROUND; + + // map of texture sources to textures created in this gl context via this.getTexture(src) + this.textures = new Map(); + + // set of framebuffers in use + this.framebuffers = new Set(); + // stack of active framebuffers + this.activeFramebuffers = []; + + // for post processing step + this.states.filterShader = undefined; + this.filterLayer = undefined; + this.filterLayerTemp = undefined; + this.defaultFilterShaders = {}; + + this.textureMode = constants.IMAGE; + // default wrap settings + this.textureWrapX = constants.CLAMP; + this.textureWrapY = constants.CLAMP; + this.states._tex = null; + this._curveTightness = 6; + + // lookUpTable for coefficients needed to be calculated for bezierVertex, same are used for curveVertex + this._lookUpTableBezier = []; + // lookUpTable for coefficients needed to be calculated for quadraticVertex + this._lookUpTableQuadratic = []; + + // current curveDetail in the Bezier lookUpTable + this._lutBezierDetail = 0; + // current curveDetail in the Quadratic lookUpTable + this._lutQuadraticDetail = 0; + + // Used to distinguish between user calls to vertex() and internal calls + this.isProcessingVertices = false; + this._tessy = this._initTessy(); + + this.fontInfos = {}; + + this._curShader = undefined; + } /** - * @module Rendering - * @submodule Rendering - * @for p5 + * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added + * to the geometry and then returned when + * endGeometry() is called. One can also use + * buildGeometry() to pass a function that + * draws shapes. + * + * If you need to draw complex shapes every frame which don't change over time, + * combining them upfront with `beginGeometry()` and `endGeometry()` and then + * drawing that will run faster than repeatedly drawing the individual pieces. */ + beginGeometry() { + if (this.geometryBuilder) { + throw new Error('It looks like `beginGeometry()` is being called while another p5.Geometry is already being build.'); + } + this.geometryBuilder = new GeometryBuilder(this); + this.geometryBuilder.prevFillColor = [...this.states.curFillColor]; + this.states.curFillColor = [-1, -1, -1, -1]; + } + /** - * Set attributes for the WebGL Drawing context. - * This is a way of adjusting how the WebGL - * renderer works to fine-tune the display and performance. - * - * Note that this will reinitialize the drawing context - * if called after the WebGL canvas is made. - * - * If an object is passed as the parameter, all attributes - * not declared in the object will be set to defaults. - * - * The available attributes are: - *
- * alpha - indicates if the canvas contains an alpha buffer - * default is true - * - * depth - indicates whether the drawing buffer has a depth buffer - * of at least 16 bits - default is true - * - * stencil - indicates whether the drawing buffer has a stencil buffer - * of at least 8 bits - * - * antialias - indicates whether or not to perform anti-aliasing - * default is false (true in Safari) - * - * premultipliedAlpha - indicates that the page compositor will assume - * the drawing buffer contains colors with pre-multiplied alpha - * default is true - * - * preserveDrawingBuffer - if true the buffers will not be cleared and - * and will preserve their values until cleared or overwritten by author - * (note that p5 clears automatically on draw loop) - * default is true - * - * perPixelLighting - if true, per-pixel lighting will be used in the - * lighting shader otherwise per-vertex lighting is used. - * default is true. - * - * version - either 1 or 2, to specify which WebGL version to ask for. By - * default, WebGL 2 will be requested. If WebGL2 is not available, it will - * fall back to WebGL 1. You can check what version is used with by looking at - * the global `webglVersion` property. - * - * @method setAttributes - * @for p5 - * @param {String} key Name of attribute - * @param {Boolean} value New value of named attribute - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * } - * - * function draw() { - * background(255); - * push(); - * rotateZ(frameCount * 0.02); - * rotateX(frameCount * 0.02); - * rotateY(frameCount * 0.02); - * fill(0, 0, 0); - * box(50); - * pop(); - * } - * - *
- *
- * Now with the antialias attribute set to true. - *
- *
- * - * function setup() { - * setAttributes('antialias', true); - * createCanvas(100, 100, WEBGL); - * } - * - * function draw() { - * background(255); - * push(); - * rotateZ(frameCount * 0.02); - * rotateX(frameCount * 0.02); - * rotateY(frameCount * 0.02); - * fill(0, 0, 0); - * box(50); - * pop(); - * } - * - *
- * - *
- * - * // press the mouse button to disable perPixelLighting - * function setup() { - * createCanvas(100, 100, WEBGL); - * noStroke(); - * fill(255); - * } - * - * let lights = [ - * { c: '#f00', t: 1.12, p: 1.91, r: 0.2 }, - * { c: '#0f0', t: 1.21, p: 1.31, r: 0.2 }, - * { c: '#00f', t: 1.37, p: 1.57, r: 0.2 }, - * { c: '#ff0', t: 1.12, p: 1.91, r: 0.7 }, - * { c: '#0ff', t: 1.21, p: 1.31, r: 0.7 }, - * { c: '#f0f', t: 1.37, p: 1.57, r: 0.7 } - * ]; - * - * function draw() { - * let t = millis() / 1000 + 1000; - * background(0); - * directionalLight(color('#222'), 1, 1, 1); - * - * for (let i = 0; i < lights.length; i++) { - * let light = lights[i]; - * pointLight( - * color(light.c), - * p5.Vector.fromAngles(t * light.t, t * light.p, width * light.r) - * ); - * } - * - * specularMaterial(255); - * sphere(width * 0.1); - * - * rotateX(t * 0.77); - * rotateY(t * 0.83); - * rotateZ(t * 0.91); - * torus(width * 0.3, width * 0.07, 24, 10); - * } + * Finishes creating a new p5.Geometry that was + * started using beginGeometry(). One can also + * use buildGeometry() to pass a function that + * draws shapes. * - * function mousePressed() { - * setAttributes('perPixelLighting', false); - * noStroke(); - * fill(255); - * } - * function mouseReleased() { - * setAttributes('perPixelLighting', true); - * noStroke(); - * fill(255); - * } - * - *
- * - * @alt a rotating cube with smoother edges + * @returns {p5.Geometry} The model that was built. */ + endGeometry() { + if (!this.geometryBuilder) { + throw new Error('Make sure you call beginGeometry() before endGeometry()!'); + } + const geometry = this.geometryBuilder.finish(); + this.states.curFillColor = this.geometryBuilder.prevFillColor; + this.geometryBuilder = undefined; + return geometry; + } + /** - * @method setAttributes - * @for p5 - * @param {Object} obj object with key-value pairs + * Creates a new p5.Geometry that contains all + * the shapes drawn in a provided callback function. The returned combined shape + * can then be drawn all at once using model(). + * + * If you need to draw complex shapes every frame which don't change over time, + * combining them with `buildGeometry()` once and then drawing that will run + * faster than repeatedly drawing the individual pieces. + * + * One can also draw shapes directly between + * beginGeometry() and + * endGeometry() instead of using a callback + * function. + * @param {Function} callback A function that draws shapes. + * @returns {p5.Geometry} The model that was built from the callback function. */ - fn.setAttributes = function (key, value) { - if (typeof this._glAttributes === 'undefined') { - console.log( - 'You are trying to use setAttributes on a p5.Graphics object ' + - 'that does not use a WEBGL renderer.' + buildGeometry(callback) { + this.beginGeometry(); + callback(); + return this.endGeometry(); + } + + ////////////////////////////////////////////// + // Setting + ////////////////////////////////////////////// + + _setAttributeDefaults(pInst) { + // See issue #3850, safer to enable AA in Safari + const applyAA = navigator.userAgent.toLowerCase().includes('safari'); + const defaults = { + alpha: true, + depth: true, + stencil: true, + antialias: applyAA, + premultipliedAlpha: true, + preserveDrawingBuffer: true, + perPixelLighting: true, + version: 2 + }; + if (pInst._glAttributes === null) { + pInst._glAttributes = defaults; + } else { + pInst._glAttributes = Object.assign(defaults, pInst._glAttributes); + } + return; + } + + _initContext() { + if (this._pInst._glAttributes?.version !== 1) { + // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context + this.drawingContext = + this.canvas.getContext('webgl2', this._pInst._glAttributes); + } + this.webglVersion = + this.drawingContext ? constants.WEBGL2 : constants.WEBGL; + // If this is the main canvas, make sure the global `webglVersion` is set + this._pInst.webglVersion = this.webglVersion; + if (!this.drawingContext) { + // If we were unable to create a WebGL2 context (either because it was + // disabled via `setAttributes({ version: 1 })` or because the device + // doesn't support it), fall back to a WebGL1 context + this.drawingContext = + this.canvas.getContext('webgl', this._pInst._glAttributes) || + this.canvas.getContext('experimental-webgl', this._pInst._glAttributes); + } + if (this.drawingContext === null) { + throw new Error('Error creating webgl context'); + } else { + const gl = this.drawingContext; + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + // Make sure all images are loaded into the canvas premultiplied so that + // they match the way we render colors. This will make framebuffer textures + // be encoded the same way as textures from everything else. + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + this._viewport = this.drawingContext.getParameter( + this.drawingContext.VIEWPORT ); - return; } - let unchanged = true; - if (typeof value !== 'undefined') { - //first time modifying the attributes - if (this._glAttributes === null) { - this._glAttributes = {}; - } - if (this._glAttributes[key] !== value) { - //changing value of previously altered attribute - this._glAttributes[key] = value; - unchanged = false; - } - //setting all attributes with some change - } else if (key instanceof Object) { - if (this._glAttributes !== key) { - this._glAttributes = key; - unchanged = false; - } - } - //@todo_FES - if (!this._renderer.isP3D || unchanged) { - return; + } + + _getMaxTextureSize() { + const gl = this.drawingContext; + return gl.getParameter(gl.MAX_TEXTURE_SIZE); + } + + _adjustDimensions(width, height) { + if (!this._maxTextureSize) { + this._maxTextureSize = this._getMaxTextureSize(); + } + let maxTextureSize = this._maxTextureSize; + + let maxAllowedPixelDimensions = Math.floor( + maxTextureSize / this._pixelDensity + ); + let adjustedWidth = Math.min( + width, maxAllowedPixelDimensions + ); + let adjustedHeight = Math.min( + height, maxAllowedPixelDimensions + ); + + if (adjustedWidth !== width || adjustedHeight !== height) { + console.warn( + 'Warning: The requested width/height exceeds hardware limits. ' + + `Adjusting dimensions to width: ${adjustedWidth}, height: ${adjustedHeight}.` + ); } - if (!this._setupDone) { - for (const x in this._renderer.retainedMode.geometry) { - if (this._renderer.retainedMode.geometry.hasOwnProperty(x)) { - p5._friendlyError( - 'Sorry, Could not set the attributes, you need to call setAttributes() ' + - 'before calling the other drawing methods in setup()' - ); - return; - } + return { adjustedWidth, adjustedHeight }; + } + + //This is helper function to reset the context anytime the attributes + //are changed with setAttributes() + + _resetContext(options, callback) { + const w = this.width; + const h = this.height; + const defaultId = this.canvas.id; + const isPGraphics = this._pInst instanceof p5.Graphics; + + if (isPGraphics) { + const pg = this._pInst; + pg.canvas.parentNode.removeChild(pg.canvas); + pg.canvas = document.createElement('canvas'); + const node = pg._pInst._userNode || document.body; + node.appendChild(pg.canvas); + p5.Element.call(pg, pg.canvas, pg._pInst); + pg.width = w; + pg.height = h; + } else { + let c = this.canvas; + if (c) { + c.parentNode.removeChild(c); + } + c = document.createElement('canvas'); + c.id = defaultId; + if (this._pInst._userNode) { + this._pInst._userNode.appendChild(c); + } else { + document.body.appendChild(c); } + this._pInst.canvas = c; + this.canvas = c; } - this.push(); - this._renderer._resetContext(); - this.pop(); + const renderer = new p5.RendererGL( + this._pInst, + w, + h, + !isPGraphics, + this._pInst.canvas, + ); + this._pInst._renderer = renderer; - if (this._renderer.states.curCamera) { - this._renderer.states.curCamera._renderer = this._renderer; + renderer._applyDefaults(); + + if (typeof callback === 'function') { + //setTimeout with 0 forces the task to the back of the queue, this ensures that + //we finish switching out the renderer + setTimeout(() => { + callback.apply(window._renderer, options); + }, 0); } - }; + } + + + _update() { + // reset model view and apply initial camera transform + // (containing only look at info; no projection). + this.states.uModelMatrix.reset(); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + + // reset light data for new frame. + + this.states.ambientLightColors.length = 0; + this.states.specularColors = [1, 1, 1]; + + this.states.directionalLightDirections.length = 0; + this.states.directionalLightDiffuseColors.length = 0; + this.states.directionalLightSpecularColors.length = 0; + + this.states.pointLightPositions.length = 0; + this.states.pointLightDiffuseColors.length = 0; + this.states.pointLightSpecularColors.length = 0; + + this.states.spotLightPositions.length = 0; + this.states.spotLightDirections.length = 0; + this.states.spotLightDiffuseColors.length = 0; + this.states.spotLightSpecularColors.length = 0; + this.states.spotLightAngle.length = 0; + this.states.spotLightConc.length = 0; + + this.states._enableLighting = false; + + //reset tint value for new frame + this.states.tint = [255, 255, 255, 255]; + + //Clear depth every frame + this.GL.clearStencil(0); + this.GL.clear(this.GL.DEPTH_BUFFER_BIT | this.GL.STENCIL_BUFFER_BIT); + this.GL.disable(this.GL.STENCIL_TEST); + } /** - * 3D graphics class - * @private - * @class p5.RendererGL - * @extends p5.Renderer - * @todo extend class to include public method for offscreen - * rendering (FBO). - */ - p5.RendererGL = class RendererGL extends p5.Renderer { - constructor(pInst, w, h, isMainCanvas, elt, attr) { - super(pInst, w, h, isMainCanvas); - - // Create new canvas - this.canvas = this.elt = elt || document.createElement('canvas'); - this._initContext(); - // This redundant property is useful in reminding you that you are - // interacting with WebGLRenderingContext, still worth considering future removal - this.GL = this.drawingContext; - this._pInst.drawingContext = this.drawingContext; - - if (this._isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - } - this.elt.id = 'defaultCanvas0'; - this.elt.classList.add('p5Canvas'); - - const dimensions = this._adjustDimensions(w, h); - w = dimensions.adjustedWidth; - h = dimensions.adjustedHeight; - - this.width = w; - this.height = h; - - // Set canvas size - this.elt.width = w * this._pixelDensity; - this.elt.height = h * this._pixelDensity; - this.elt.style.width = `${w}px`; - this.elt.style.height = `${h}px`; - this._origViewport = { - width: this.GL.drawingBufferWidth, - height: this.GL.drawingBufferHeight + * [background description] + */ + background(...args) { + const _col = this._pInst.color(...args); + const _r = _col.levels[0] / 255; + const _g = _col.levels[1] / 255; + const _b = _col.levels[2] / 255; + const _a = _col.levels[3] / 255; + this.clear(_r, _g, _b, _a); + } + + ////////////////////////////////////////////// + // COLOR + ////////////////////////////////////////////// + /** + * Basic fill material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @chainable + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * noStroke(); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube spinning + */ + fill(v1, v2, v3, a) { + //see material.js for more info on color blending in webgl + const color = fn.color.apply(this._pInst, arguments); + this.states.curFillColor = color._array; + this.states.drawMode = constants.FILL; + this.states._useNormalMaterial = false; + this.states._tex = null; + } + + /** + * Basic stroke material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * stroke(240, 150, 150); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube with pink outline spinning + */ + stroke(r, g, b, a) { + const color = fn.color.apply(this._pInst, arguments); + this.states.curStrokeColor = color._array; + } + + strokeCap(cap) { + this.curStrokeCap = cap; + } + + strokeJoin(join) { + this.curStrokeJoin = join; + } + getFilterLayer() { + if (!this.filterLayer) { + this.filterLayer = this._pInst.createFramebuffer(); + } + return this.filterLayer; + } + getFilterLayerTemp() { + if (!this.filterLayerTemp) { + this.filterLayerTemp = this._pInst.createFramebuffer(); + } + return this.filterLayerTemp; + } + matchSize(fboToMatch, target) { + if ( + fboToMatch.width !== target.width || + fboToMatch.height !== target.height + ) { + fboToMatch.resize(target.width, target.height); + } + + if (fboToMatch.pixelDensity() !== target.pixelDensity()) { + fboToMatch.pixelDensity(target.pixelDensity()); + } + } + filter(...args) { + + let fbo = this.getFilterLayer(); + + // use internal shader for filter constants BLUR, INVERT, etc + let filterParameter = undefined; + let operation = undefined; + if (typeof args[0] === 'string') { + operation = args[0]; + let defaults = { + [constants.BLUR]: 3, + [constants.POSTERIZE]: 4, + [constants.THRESHOLD]: 0.5 }; - this.viewport( - this._origViewport.width, - this._origViewport.height + let useDefaultParam = operation in defaults && args[1] === undefined; + filterParameter = useDefaultParam ? defaults[operation] : args[1]; + + // Create and store shader for constants once on initial filter call. + // Need to store multiple in case user calls different filters, + // eg. filter(BLUR) then filter(GRAY) + if (!(operation in this.defaultFilterShaders)) { + this.defaultFilterShaders[operation] = new Shader( + fbo._renderer, + filterShaderVert, + filterShaderFrags[operation] + ); + } + this.states.filterShader = this.defaultFilterShaders[operation]; + + } + // use custom user-supplied shader + else { + this.states.filterShader = args[0]; + } + + // Setting the target to the framebuffer when applying a filter to a framebuffer. + + const target = this.activeFramebuffer() || this; + + // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. + this.matchSize(fbo, target); + + fbo.draw(() => this._pInst.clear()); // prevent undesirable feedback effects accumulating secretly. + + let texelSize = [ + 1 / (target.width * target.pixelDensity()), + 1 / (target.height * target.pixelDensity()) + ]; + + // apply blur shader with multiple passes. + if (operation === constants.BLUR) { + // Treating 'tmp' as a framebuffer. + const tmp = this.getFilterLayerTemp(); + // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. + this.matchSize(tmp, target); + // setup + this._pInst.push(); + this._pInst.noStroke(); + this._pInst.blendMode(constants.BLEND); + + // draw main to temp buffer + this._pInst.shader(this.states.filterShader); + this.states.filterShader.setUniform('texelSize', texelSize); + this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); + this.states.filterShader.setUniform('radius', Math.max(1, filterParameter)); + + // Horiz pass: draw `target` to `tmp` + tmp.draw(() => { + this.states.filterShader.setUniform('direction', [1, 0]); + this.states.filterShader.setUniform('tex0', target); + this._pInst.clear(); + this._pInst.shader(this.states.filterShader); + this._pInst.noLights(); + this._pInst.plane(target.width, target.height); + }); + + // Vert pass: draw `tmp` to `fbo` + fbo.draw(() => { + this.states.filterShader.setUniform('direction', [0, 1]); + this.states.filterShader.setUniform('tex0', tmp); + this._pInst.clear(); + this._pInst.shader(this.states.filterShader); + this._pInst.noLights(); + this._pInst.plane(target.width, target.height); + }); + + this._pInst.pop(); + } + // every other non-blur shader uses single pass + else { + fbo.draw(() => { + this._pInst.noStroke(); + this._pInst.blendMode(constants.BLEND); + this._pInst.shader(this.states.filterShader); + this.states.filterShader.setUniform('tex0', target); + this.states.filterShader.setUniform('texelSize', texelSize); + this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); + // filterParameter uniform only used for POSTERIZE, and THRESHOLD + // but shouldn't hurt to always set + this.states.filterShader.setUniform('filterParameter', filterParameter); + this._pInst.noLights(); + this._pInst.plane(target.width, target.height); + }); + + } + // draw fbo contents onto main renderer. + this._pInst.push(); + this._pInst.noStroke(); + this.clear(); + this._pInst.push(); + this._pInst.imageMode(constants.CORNER); + this._pInst.blendMode(constants.BLEND); + target.filterCamera._resize(); + this._pInst.setCamera(target.filterCamera); + this._pInst.resetMatrix(); + this._pInst.image(fbo, -target.width / 2, -target.height / 2, + target.width, target.height); + this._pInst.clearDepth(); + this._pInst.pop(); + this._pInst.pop(); + } + + // Pass this off to the host instance so that we can treat a renderer and a + // framebuffer the same in filter() + + pixelDensity(newDensity) { + if (newDensity) { + return this._pInst.pixelDensity(newDensity); + } + return this._pInst.pixelDensity(); + } + + blendMode(mode) { + if ( + mode === constants.DARKEST || + mode === constants.LIGHTEST || + mode === constants.ADD || + mode === constants.BLEND || + mode === constants.SUBTRACT || + mode === constants.SCREEN || + mode === constants.EXCLUSION || + mode === constants.REPLACE || + mode === constants.MULTIPLY || + mode === constants.REMOVE + ) + this.states.curBlendMode = mode; + else if ( + mode === constants.BURN || + mode === constants.OVERLAY || + mode === constants.HARD_LIGHT || + mode === constants.SOFT_LIGHT || + mode === constants.DODGE + ) { + console.warn( + 'BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.' ); + } + } - // Attach canvas element to DOM - if (this._pInst._userNode) { - // user input node case - this._pInst._userNode.appendChild(this.elt); - } else { - //create main element - if (document.getElementsByTagName('main').length === 0) { - let m = document.createElement('main'); - document.body.appendChild(m); - } - //append canvas to main - document.getElementsByTagName('main')[0].appendChild(this.elt); - } + erase(opacityFill, opacityStroke) { + if (!this._isErasing) { + this.preEraseBlend = this.states.curBlendMode; + this._isErasing = true; + this.blendMode(constants.REMOVE); + this._cachedFillStyle = this.states.curFillColor.slice(); + this.states.curFillColor = [1, 1, 1, opacityFill / 255]; + this._cachedStrokeStyle = this.states.curStrokeColor.slice(); + this.states.curStrokeColor = [1, 1, 1, opacityStroke / 255]; + } + } - this._setAttributeDefaults(pInst); - this.isP3D = true; //lets us know we're in 3d mode - - // When constructing a new p5.Geometry, this will represent the builder - this.geometryBuilder = undefined; - - // Push/pop state - this.states.uModelMatrix = new p5.Matrix(); - this.states.uViewMatrix = new p5.Matrix(); - this.states.uMVMatrix = new p5.Matrix(); - this.states.uPMatrix = new p5.Matrix(); - this.states.uNMatrix = new p5.Matrix('mat3'); - this.states.curMatrix = new p5.Matrix('mat3'); - - this.states.curCamera = new p5.Camera(this); - - this.states.enableLighting = false; - this.states.ambientLightColors = []; - this.states.specularColors = [1, 1, 1]; - this.states.directionalLightDirections = []; - this.states.directionalLightDiffuseColors = []; - this.states.directionalLightSpecularColors = []; - this.states.pointLightPositions = []; - this.states.pointLightDiffuseColors = []; - this.states.pointLightSpecularColors = []; - this.states.spotLightPositions = []; - this.states.spotLightDirections = []; - this.states.spotLightDiffuseColors = []; - this.states.spotLightSpecularColors = []; - this.states.spotLightAngle = []; - this.states.spotLightConc = []; - this.states.activeImageLight = null; - - this.states.curFillColor = [1, 1, 1, 1]; - this.states.curAmbientColor = [1, 1, 1, 1]; - this.states.curSpecularColor = [0, 0, 0, 0]; - this.states.curEmissiveColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 1]; - - this.states.curBlendMode = constants.BLEND; - - this.states._hasSetAmbient = false; - this.states._useSpecularMaterial = false; - this.states._useEmissiveMaterial = false; - this.states._useNormalMaterial = false; - this.states._useShininess = 1; - this.states._useMetalness = 0; - - this.states.tint = [255, 255, 255, 255]; - - this.states.constantAttenuation = 1; - this.states.linearAttenuation = 0; - this.states.quadraticAttenuation = 0; - - this.states._currentNormal = new p5.Vector(0, 0, 1); - - this.states.drawMode = constants.FILL; - - this.states._tex = null; - - // erasing + noErase() { + if (this._isErasing) { + // Restore colors + this.states.curFillColor = this._cachedFillStyle.slice(); + this.states.curStrokeColor = this._cachedStrokeStyle.slice(); + // Restore blend mode + this.states.curBlendMode = this.preEraseBlend; + this.blendMode(this.preEraseBlend); + // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode this._isErasing = false; + this._applyBlendMode(); + } + } - // clipping - this._clipDepths = []; - this._isClipApplied = false; - this._stencilTestOn = false; - - this.mixedAmbientLight = []; - this.mixedSpecularColor = []; - - // p5.framebuffer for this are calculated in getDiffusedTexture function - this.diffusedTextures = new Map(); - // p5.framebuffer for this are calculated in getSpecularTexture function - this.specularTextures = new Map(); - - this.preEraseBlend = undefined; - this._cachedBlendMode = undefined; - this._cachedFillStyle = [1, 1, 1, 1]; - this._cachedStrokeStyle = [0, 0, 0, 1]; - if (this.webglVersion === constants.WEBGL2) { - this.blendExt = this.GL; - } else { - this.blendExt = this.GL.getExtension('EXT_blend_minmax'); - } - this._isBlending = false; - - this._useLineColor = false; - this._useVertexColor = false; - - this.registerEnabled = new Set(); - - // Camera - this.states.curCamera._computeCameraDefaultSettings(); - this.states.curCamera._setDefaultCamera(); - - // FilterCamera - this.filterCamera = new p5.Camera(this); - this.filterCamera._computeCameraDefaultSettings(); - this.filterCamera._setDefaultCamera(); - // Information about the previous frame's touch object - // for executing orbitControl() - this.prevTouches = []; - // Velocity variable for use with orbitControl() - this.zoomVelocity = 0; - this.rotateVelocity = new p5.Vector(0, 0); - this.moveVelocity = new p5.Vector(0, 0); - // Flags for recording the state of zooming, rotation and moving - this.executeZoom = false; - this.executeRotateAndMove = false; - - this.states.specularShader = undefined; - this.sphereMapping = undefined; - this.states.diffusedShader = undefined; - this._defaultLightShader = undefined; - this._defaultImmediateModeShader = undefined; - this._defaultNormalShader = undefined; - this._defaultColorShader = undefined; - this._defaultPointShader = undefined; - - this.states.userFillShader = undefined; - this.states.userStrokeShader = undefined; - this.states.userPointShader = undefined; - - this._useUserVertexProperties = undefined; - - // Default drawing is done in Retained Mode - // Geometry and Material hashes stored here - this.retainedMode = { - geometry: {}, - buffers: { - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - fill: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - //new BufferDef(3, 'vertexSpeculars', 'specularBuffer', 'aSpecularColor'), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - text: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - user:[] - } - }; + drawTarget() { + return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this; + } - // Immediate Mode - // Geometry and Material hashes stored here - this.immediateMode = { - geometry: new p5.Geometry(), - shapeMode: constants.TRIANGLE_FAN, - contourIndices: [], - _bezierVertex: [], - _quadraticVertex: [], - _curveVertex: [], - buffers: { - fill: [ - new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new p5.RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - point: this.GL.createBuffer(), - user:[] - } - }; + beginClip(options = {}) { + super.beginClip(options); + + this.drawTarget()._isClipApplied = true; + + const gl = this.GL; + gl.clearStencil(0); + gl.clear(gl.STENCIL_BUFFER_BIT); + gl.enable(gl.STENCIL_TEST); + this._stencilTestOn = true; + gl.stencilFunc( + gl.ALWAYS, // the test + 1, // reference value + 0xff // mask + ); + gl.stencilOp( + gl.KEEP, // what to do if the stencil test fails + gl.KEEP, // what to do if the depth test fails + gl.REPLACE // what to do if both tests pass + ); + gl.disable(gl.DEPTH_TEST); + + this._pInst.push(); + this._pInst.resetShader(); + if (this.states.doFill) this._pInst.fill(0, 0); + if (this.states.doStroke) this._pInst.stroke(0, 0); + } - this.pointSize = 5.0; //default point size - this.curStrokeWeight = 1; - this.curStrokeCap = constants.ROUND; - this.curStrokeJoin = constants.ROUND; - - // map of texture sources to textures created in this gl context via this.getTexture(src) - this.textures = new Map(); - - // set of framebuffers in use - this.framebuffers = new Set(); - // stack of active framebuffers - this.activeFramebuffers = []; - - // for post processing step - this.states.filterShader = undefined; - this.filterLayer = undefined; - this.filterLayerTemp = undefined; - this.defaultFilterShaders = {}; - - this.textureMode = constants.IMAGE; - // default wrap settings - this.textureWrapX = constants.CLAMP; - this.textureWrapY = constants.CLAMP; - this.states._tex = null; - this._curveTightness = 6; - - // lookUpTable for coefficients needed to be calculated for bezierVertex, same are used for curveVertex - this._lookUpTableBezier = []; - // lookUpTable for coefficients needed to be calculated for quadraticVertex - this._lookUpTableQuadratic = []; - - // current curveDetail in the Bezier lookUpTable - this._lutBezierDetail = 0; - // current curveDetail in the Quadratic lookUpTable - this._lutQuadraticDetail = 0; - - // Used to distinguish between user calls to vertex() and internal calls - this.isProcessingVertices = false; - this._tessy = this._initTessy(); - - this.fontInfos = {}; - - this._curShader = undefined; - } - - /** - * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added - * to the geometry and then returned when - * endGeometry() is called. One can also use - * buildGeometry() to pass a function that - * draws shapes. - * - * If you need to draw complex shapes every frame which don't change over time, - * combining them upfront with `beginGeometry()` and `endGeometry()` and then - * drawing that will run faster than repeatedly drawing the individual pieces. - */ - beginGeometry() { - if (this.geometryBuilder) { - throw new Error('It looks like `beginGeometry()` is being called while another p5.Geometry is already being build.'); - } - this.geometryBuilder = new GeometryBuilder(this); - this.geometryBuilder.prevFillColor = [...this.states.curFillColor]; - this.states.curFillColor = [-1, -1, -1, -1]; + endClip() { + this._pInst.pop(); + + const gl = this.GL; + gl.stencilOp( + gl.KEEP, // what to do if the stencil test fails + gl.KEEP, // what to do if the depth test fails + gl.KEEP // what to do if both tests pass + ); + gl.stencilFunc( + this._clipInvert ? gl.EQUAL : gl.NOTEQUAL, // the test + 0, // reference value + 0xff // mask + ); + gl.enable(gl.DEPTH_TEST); + + // Mark the depth at which the clip has been applied so that we can clear it + // when we pop past this depth + this._clipDepths.push(this._pushPopDepth); + + super.endClip(); + } + + _clearClip() { + this.GL.clearStencil(1); + this.GL.clear(this.GL.STENCIL_BUFFER_BIT); + if (this._clipDepths.length > 0) { + this._clipDepths.pop(); } + this.drawTarget()._isClipApplied = false; + } - /** - * Finishes creating a new p5.Geometry that was - * started using beginGeometry(). One can also - * use buildGeometry() to pass a function that - * draws shapes. - * - * @returns {p5.Geometry} The model that was built. - */ - endGeometry() { - if (!this.geometryBuilder) { - throw new Error('Make sure you call beginGeometry() before endGeometry()!'); - } - const geometry = this.geometryBuilder.finish(); - this.states.curFillColor = this.geometryBuilder.prevFillColor; - this.geometryBuilder = undefined; - return geometry; + /** + * Change weight of stroke + * @param {Number} stroke weight to be used for drawing + * @example + *
+ * + * function setup() { + * createCanvas(200, 400, WEBGL); + * setAttributes('antialias', true); + * } + * + * function draw() { + * background(0); + * noStroke(); + * translate(0, -100, 0); + * stroke(240, 150, 150); + * fill(100, 100, 240); + * push(); + * strokeWeight(8); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * sphere(75); + * pop(); + * push(); + * translate(0, 200, 0); + * strokeWeight(1); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * sphere(75); + * pop(); + * } + * + *
+ * + * @alt + * black canvas with two purple rotating spheres with pink + * outlines the sphere on top has much heavier outlines, + */ + strokeWeight(w) { + if (this.curStrokeWeight !== w) { + this.pointSize = w; + this.curStrokeWeight = w; } + } - /** - * Creates a new p5.Geometry that contains all - * the shapes drawn in a provided callback function. The returned combined shape - * can then be drawn all at once using model(). - * - * If you need to draw complex shapes every frame which don't change over time, - * combining them with `buildGeometry()` once and then drawing that will run - * faster than repeatedly drawing the individual pieces. - * - * One can also draw shapes directly between - * beginGeometry() and - * endGeometry() instead of using a callback - * function. - * @param {Function} callback A function that draws shapes. - * @returns {p5.Geometry} The model that was built from the callback function. - */ - buildGeometry(callback) { - this.beginGeometry(); - callback(); - return this.endGeometry(); - } - - ////////////////////////////////////////////// - // Setting - ////////////////////////////////////////////// - - _setAttributeDefaults(pInst) { - // See issue #3850, safer to enable AA in Safari - const applyAA = navigator.userAgent.toLowerCase().includes('safari'); - const defaults = { - alpha: true, - depth: true, - stencil: true, - antialias: applyAA, - premultipliedAlpha: true, - preserveDrawingBuffer: true, - perPixelLighting: true, - version: 2 - }; - if (pInst._glAttributes === null) { - pInst._glAttributes = defaults; - } else { - pInst._glAttributes = Object.assign(defaults, pInst._glAttributes); - } + // x,y are canvas-relative (pre-scaled by _pixelDensity) + _getPixel(x, y) { + const gl = this.GL; + return readPixelWebGL( + gl, + null, + x, + y, + gl.RGBA, + gl.UNSIGNED_BYTE, + this._pInst.height * this._pInst.pixelDensity() + ); + } + + /** + * Loads the pixels data for this canvas into the pixels[] attribute. + * Note that updatePixels() and set() do not work. + * Any pixel manipulation must be done directly to the pixels[] array. + * + * @private + */ + + loadPixels() { + const pixelsState = this._pixelsState; + + //@todo_FES + if (this._pInst._glAttributes.preserveDrawingBuffer !== true) { + console.log( + 'loadPixels only works in WebGL when preserveDrawingBuffer ' + 'is true.' + ); return; } - _initContext() { - if (this._pInst._glAttributes?.version !== 1) { - // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context - this.drawingContext = - this.canvas.getContext('webgl2', this._pInst._glAttributes); - } - this.webglVersion = - this.drawingContext ? constants.WEBGL2 : constants.WEBGL; - // If this is the main canvas, make sure the global `webglVersion` is set - this._pInst.webglVersion = this.webglVersion; - if (!this.drawingContext) { - // If we were unable to create a WebGL2 context (either because it was - // disabled via `setAttributes({ version: 1 })` or because the device - // doesn't support it), fall back to a WebGL1 context - this.drawingContext = - this.canvas.getContext('webgl', this._pInst._glAttributes) || - this.canvas.getContext('experimental-webgl', this._pInst._glAttributes); - } - if (this.drawingContext === null) { - throw new Error('Error creating webgl context'); - } else { - const gl = this.drawingContext; - gl.enable(gl.DEPTH_TEST); - gl.depthFunc(gl.LEQUAL); - gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - // Make sure all images are loaded into the canvas premultiplied so that - // they match the way we render colors. This will make framebuffer textures - // be encoded the same way as textures from everything else. - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - this._viewport = this.drawingContext.getParameter( - this.drawingContext.VIEWPORT + const pd = this._pixelDensity; + const gl = this.GL; + + pixelsState.pixels = + readPixelsWebGL( + pixelsState.pixels, + gl, + null, + 0, + 0, + this.width * pd, + this.height * pd, + gl.RGBA, + gl.UNSIGNED_BYTE, + this.height * pd + ); + } + + updatePixels() { + const fbo = this._getTempFramebuffer(); + fbo.pixels = this._pixelsState.pixels; + fbo.updatePixels(); + this._pInst.push(); + this._pInst.resetMatrix(); + this._pInst.clear(); + this._pInst.imageMode(constants.CENTER); + this._pInst.image(fbo, 0, 0); + this._pInst.pop(); + this.GL.clearDepth(1); + this.GL.clear(this.GL.DEPTH_BUFFER_BIT); + } + + /** + * @private + * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings + * of the renderer's canvas. It will be created if it does not yet exist, and + * reused if it does. + */ + _getTempFramebuffer() { + if (!this._tempFramebuffer) { + this._tempFramebuffer = this._pInst.createFramebuffer({ + format: constants.UNSIGNED_BYTE, + useDepth: this._pInst._glAttributes.depth, + depthFormat: constants.UNSIGNED_INT, + antialias: this._pInst._glAttributes.antialias + }); + } + return this._tempFramebuffer; + } + + + + ////////////////////////////////////////////// + // HASH | for geometry + ////////////////////////////////////////////// + + geometryInHash(gId) { + return this.retainedMode.geometry[gId] !== undefined; + } + + viewport(w, h) { + this._viewport = [0, 0, w, h]; + this.GL.viewport(0, 0, w, h); + } + + /** + * [resize description] + * @private + * @param {Number} w [description] + * @param {Number} h [description] + */ + resize(w, h) { + super.resize(w, h); + + // save canvas properties + const props = {}; + for (const key in this.drawingContext) { + const val = this.drawingContext[key]; + if (typeof val !== 'object' && typeof val !== 'function') { + props[key] = val; + } + } + + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; + + this.width = w; + this.height = h; + + this.canvas.width = w * this._pixelDensity; + this.canvas.height = h * this._pixelDensity; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; + this._origViewport = { + width: this.GL.drawingBufferWidth, + height: this.GL.drawingBufferHeight + }; + this.viewport( + this._origViewport.width, + this._origViewport.height + ); + + this.states.curCamera._resize(); + + //resize pixels buffer + const pixelsState = this._pixelsState; + if (typeof pixelsState.pixels !== 'undefined') { + pixelsState.pixels = + new Uint8Array( + this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 ); - } } - _getMaxTextureSize() { - const gl = this.drawingContext; - return gl.getParameter(gl.MAX_TEXTURE_SIZE); + for (const framebuffer of this.framebuffers) { + // Notify framebuffers of the resize so that any auto-sized framebuffers + // can also update their size + framebuffer._canvasSizeChanged(); } - _adjustDimensions(width, height) { - if (!this._maxTextureSize) { - this._maxTextureSize = this._getMaxTextureSize(); + // reset canvas properties + for (const savedKey in props) { + try { + this.drawingContext[savedKey] = props[savedKey]; + } catch (err) { + // ignore read-only property errors } - let maxTextureSize = this._maxTextureSize; + } + } - let maxAllowedPixelDimensions = Math.floor( - maxTextureSize / this._pixelDensity - ); - let adjustedWidth = Math.min( - width, maxAllowedPixelDimensions - ); - let adjustedHeight = Math.min( - height, maxAllowedPixelDimensions - ); + /** + * clears color and depth buffers + * with r,g,b,a + * @private + * @param {Number} r normalized red val. + * @param {Number} g normalized green val. + * @param {Number} b normalized blue val. + * @param {Number} a normalized alpha val. + */ + clear(...args) { + const _r = args[0] || 0; + const _g = args[1] || 0; + const _b = args[2] || 0; + let _a = args[3] || 0; + + const activeFramebuffer = this.activeFramebuffer(); + if ( + activeFramebuffer && + activeFramebuffer.format === constants.UNSIGNED_BYTE && + !activeFramebuffer.antialias && + _a === 0 + ) { + // Drivers on Intel Macs check for 0,0,0,0 exactly when drawing to a + // framebuffer and ignore the command if it's the only drawing command to + // the framebuffer. To work around it, we can set the alpha to a value so + // low that it still rounds down to 0, but that circumvents the buggy + // check in the driver. + _a = 1e-10; + } - if (adjustedWidth !== width || adjustedHeight !== height) { - console.warn( - 'Warning: The requested width/height exceeds hardware limits. ' + - `Adjusting dimensions to width: ${adjustedWidth}, height: ${adjustedHeight}.` - ); - } + this.GL.clearColor(_r * _a, _g * _a, _b * _a, _a); + this.GL.clearDepth(1); + this.GL.clear(this.GL.COLOR_BUFFER_BIT | this.GL.DEPTH_BUFFER_BIT); + } + + /** + * Resets all depth information so that nothing previously drawn will + * occlude anything subsequently drawn. + */ + clearDepth(depth = 1) { + this.GL.clearDepth(depth); + this.GL.clear(this.GL.DEPTH_BUFFER_BIT); + } - return { adjustedWidth, adjustedHeight }; + applyMatrix(a, b, c, d, e, f) { + if (arguments.length === 16) { + Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); + } else { + this.states.uModelMatrix.apply([ + a, b, 0, 0, + c, d, 0, 0, + 0, 0, 1, 0, + e, f, 0, 1 + ]); } + } - //This is helper function to reset the context anytime the attributes - //are changed with setAttributes() + /** + * [translate description] + * @private + * @param {Number} x [description] + * @param {Number} y [description] + * @param {Number} z [description] + * @chainable + * @todo implement handle for components or vector as args + */ + translate(x, y, z) { + if (x instanceof Vector) { + z = x.z; + y = x.y; + x = x.x; + } + this.states.uModelMatrix.translate([x, y, z]); + return this; + } - _resetContext(options, callback) { - const w = this.width; - const h = this.height; - const defaultId = this.canvas.id; - const isPGraphics = this._pInst instanceof p5.Graphics; + /** + * Scales the Model View Matrix by a vector + * @private + * @param {Number | p5.Vector | Array} x [description] + * @param {Number} [y] y-axis scalar + * @param {Number} [z] z-axis scalar + * @chainable + */ + scale(x, y, z) { + this.states.uModelMatrix.scale(x, y, z); + return this; + } + + rotate(rad, axis) { + if (typeof axis === 'undefined') { + return this.rotateZ(rad); + } + Matrix.prototype.rotate.apply(this.states.uModelMatrix, arguments); + return this; + } + + rotateX(rad) { + this.rotate(rad, 1, 0, 0); + return this; + } + + rotateY(rad) { + this.rotate(rad, 0, 1, 0); + return this; + } + + rotateZ(rad) { + this.rotate(rad, 0, 0, 1); + return this; + } - if (isPGraphics) { - const pg = this._pInst; - pg.canvas.parentNode.removeChild(pg.canvas); - pg.canvas = document.createElement('canvas'); - const node = pg._pInst._userNode || document.body; - node.appendChild(pg.canvas); - p5.Element.call(pg, pg.canvas, pg._pInst); - pg.width = w; - pg.height = h; + pop(...args) { + if ( + this._clipDepths.length > 0 && + this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] + ) { + this._clearClip(); + } + super.pop(...args); + this._applyStencilTestIfClipping(); + } + _applyStencilTestIfClipping() { + const drawTarget = this.drawTarget(); + if (drawTarget._isClipApplied !== this._stencilTestOn) { + if (drawTarget._isClipApplied) { + this.GL.enable(this.GL.STENCIL_TEST); + this._stencilTestOn = true; } else { - let c = this.canvas; - if (c) { - c.parentNode.removeChild(c); - } - c = document.createElement('canvas'); - c.id = defaultId; - if (this._pInst._userNode) { - this._pInst._userNode.appendChild(c); - } else { - document.body.appendChild(c); - } - this._pInst.canvas = c; - this.canvas = c; + this.GL.disable(this.GL.STENCIL_TEST); + this._stencilTestOn = false; } + } + } + resetMatrix() { + this.states.uModelMatrix.reset(); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + return this; + } + + ////////////////////////////////////////////// + // SHADER + ////////////////////////////////////////////// - const renderer = new p5.RendererGL( - this._pInst, - w, - h, - !isPGraphics, - this._pInst.canvas, + /* + * shaders are created and cached on a per-renderer basis, + * on the grounds that each renderer will have its own gl context + * and the shader must be valid in that context. + */ + + _getImmediateStrokeShader() { + // select the stroke shader to use + const stroke = this.states.userStrokeShader; + if (!stroke || !stroke.isStrokeShader()) { + return this._getLineShader(); + } + return stroke; + } + + + _getRetainedStrokeShader() { + return this._getImmediateStrokeShader(); + } + + _getSphereMapping(img) { + if (!this.sphereMapping) { + this.sphereMapping = this._pInst.createFilterShader( + sphereMapping ); - this._pInst._renderer = renderer; + } + this.states.uNMatrix.inverseTranspose(this.states.uViewMatrix); + this.states.uNMatrix.invert3x3(this.states.uNMatrix); + this.sphereMapping.setUniform('uFovY', this.states.curCamera.cameraFOV); + this.sphereMapping.setUniform('uAspect', this.states.curCamera.aspectRatio); + this.sphereMapping.setUniform('uNewNormalMatrix', this.states.uNMatrix.mat3); + this.sphereMapping.setUniform('uSampler', img); + return this.sphereMapping; + } - renderer._applyDefaults(); + /* + * selects which fill shader should be used based on renderer state, + * for use with begin/endShape and immediate vertex mode. + */ + _getImmediateFillShader() { + const fill = this.states.userFillShader; + if (this.states._useNormalMaterial) { + if (!fill || !fill.isNormalShader()) { + return this._getNormalShader(); + } + } + if (this.states._enableLighting) { + if (!fill || !fill.isLightShader()) { + return this._getLightShader(); + } + } else if (this.states._tex) { + if (!fill || !fill.isTextureShader()) { + return this._getLightShader(); + } + } else if (!fill /*|| !fill.isColorShader()*/) { + return this._getImmediateModeShader(); + } + return fill; + } - if (typeof callback === 'function') { - //setTimeout with 0 forces the task to the back of the queue, this ensures that - //we finish switching out the renderer - setTimeout(() => { - callback.apply(window._renderer, options); - }, 0); + /* + * selects which fill shader should be used based on renderer state + * for retained mode. + */ + _getRetainedFillShader() { + if (this.states._useNormalMaterial) { + return this._getNormalShader(); + } + + const fill = this.states.userFillShader; + if (this.states._enableLighting) { + if (!fill || !fill.isLightShader()) { + return this._getLightShader(); } + } else if (this.states._tex) { + if (!fill || !fill.isTextureShader()) { + return this._getLightShader(); + } + } else if (!fill /* || !fill.isColorShader()*/) { + return this._getColorShader(); } + return fill; + } + _getImmediatePointShader() { + // select the point shader to use + const point = this.states.userPointShader; + if (!point || !point.isPointShader()) { + return this._getPointShader(); + } + return point; + } + + _getRetainedLineShader() { + return this._getImmediateLineShader(); + } - _update() { - // reset model view and apply initial camera transform - // (containing only look at info; no projection). - this.states.uModelMatrix.reset(); - this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + baseMaterialShader() { + if (!this._pInst._glAttributes.perPixelLighting) { + throw new Error( + 'The material shader does not support hooks without perPixelLighting. Try turning it back on.' + ); + } + return this._getLightShader(); + } + + _getLightShader() { + if (!this._defaultLightShader) { + if (this._pInst._glAttributes.perPixelLighting) { + this._defaultLightShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'highp') + + defaultShaders.phongVert, + this._webGL2CompatibilityPrefix('frag', 'highp') + + defaultShaders.phongFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', + 'vec4 combineColors': `(ColorComponents components) { + vec4 color = vec4(0.); + color.rgb += components.diffuse * components.baseColor; + color.rgb += components.ambient * components.ambientColor; + color.rgb += components.specular * components.specularColor; + color.rgb += components.emissive; + color.a = components.opacity; + return color; + }`, + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } + ); + } else { + this._defaultLightShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'highp') + + defaultShaders.lightVert, + this._webGL2CompatibilityPrefix('frag', 'highp') + + defaultShaders.lightTextureFrag + ); + } + } - // reset light data for new frame. + return this._defaultLightShader; + } - this.states.ambientLightColors.length = 0; - this.states.specularColors = [1, 1, 1]; + _getImmediateModeShader() { + if (!this._defaultImmediateModeShader) { + this._defaultImmediateModeShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.immediateVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.vertexColorFrag + ); + } - this.states.directionalLightDirections.length = 0; - this.states.directionalLightDiffuseColors.length = 0; - this.states.directionalLightSpecularColors.length = 0; + return this._defaultImmediateModeShader; + } - this.states.pointLightPositions.length = 0; - this.states.pointLightDiffuseColors.length = 0; - this.states.pointLightSpecularColors.length = 0; + baseNormalShader() { + return this._getNormalShader(); + } - this.states.spotLightPositions.length = 0; - this.states.spotLightDirections.length = 0; - this.states.spotLightDiffuseColors.length = 0; - this.states.spotLightSpecularColors.length = 0; - this.states.spotLightAngle.length = 0; - this.states.spotLightConc.length = 0; + _getNormalShader() { + if (!this._defaultNormalShader) { + this._defaultNormalShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.normalVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.normalFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } + ); + } - this.states._enableLighting = false; + return this._defaultNormalShader; + } - //reset tint value for new frame - this.states.tint = [255, 255, 255, 255]; + baseColorShader() { + return this._getColorShader(); + } - //Clear depth every frame - this.GL.clearStencil(0); - this.GL.clear(this.GL.DEPTH_BUFFER_BIT | this.GL.STENCIL_BUFFER_BIT); - this.GL.disable(this.GL.STENCIL_TEST); + _getColorShader() { + if (!this._defaultColorShader) { + this._defaultColorShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.normalVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.basicFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', + 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', + 'vec2 getUV': '(vec2 uv) { return uv; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'void afterFragment': '() {}' + } + } + ); } - /** - * [background description] - */ - background(...args) { - const _col = this._pInst.color(...args); - const _r = _col.levels[0] / 255; - const _g = _col.levels[1] / 255; - const _b = _col.levels[2] / 255; - const _a = _col.levels[3] / 255; - this.clear(_r, _g, _b, _a); - } - - ////////////////////////////////////////////// - // COLOR - ////////////////////////////////////////////// - /** - * Basic fill material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } + return this._defaultColorShader; + } + + /** + * TODO(dave): un-private this when there is a way to actually override the + * shader used for points * - * function draw() { - * background(0); - * noStroke(); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
+ * Get the shader used when drawing points with `point()`. * - * @alt - * black canvas with purple cube spinning - */ - fill(v1, v2, v3, a) { - //see material.js for more info on color blending in webgl - const color = fn.color.apply(this._pInst, arguments); - this.states.curFillColor = color._array; - this.states.drawMode = constants.FILL; - this.states._useNormalMaterial = false; - this.states._tex = null; - } - - /** - * Basic stroke material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } + * You can call `pointShader().modify()` + * and change any of the following hooks: + * - `void beforeVertex`: Called at the start of the vertex shader. + * - `vec3 getLocalPosition`: Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * - `vec3 getWorldPosition`: Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * - `float getPointSize`: Update the size of the point. It takes in `float size` and must return a modified version. + * - `void afterVertex`: Called at the end of the vertex shader. + * - `void beforeFragment`: Called at the start of the fragment shader. + * - `bool shouldDiscard`: Points are drawn inside a square, with the corners discarded in the fragment shader to create a circle. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. + * - `vec4 getFinalColor`: Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * - `void afterFragment`: Called at the end of the fragment shader. * - * function draw() { - * background(0); - * stroke(240, 150, 150); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
+ * Call `pointShader().inspectHooks()` to see all the possible hooks and + * their default implementations. * - * @alt - * black canvas with purple cube with pink outline spinning + * @returns {p5.Shader} The `point()` shader + * @private() */ - stroke(r, g, b, a) { - const color = fn.color.apply(this._pInst, arguments); - this.states.curStrokeColor = color._array; + pointShader() { + return this._getPointShader(); + } + + _getPointShader() { + if (!this._defaultPointShader) { + this._defaultPointShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.pointVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.pointFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'float getPointSize': '(float size) { return size; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'bool shouldDiscard': '(bool outside) { return outside; }', + 'void afterFragment': '() {}' + } + } + ); } + return this._defaultPointShader; + } + + baseStrokeShader() { + return this._getLineShader(); + } - strokeCap(cap) { - this.curStrokeCap = cap; + _getLineShader() { + if (!this._defaultLineShader) { + this._defaultLineShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.lineVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.lineFrag, + { + vertex: { + 'void beforeVertex': '() {}', + 'vec3 getLocalPosition': '(vec3 position) { return position; }', + 'vec3 getWorldPosition': '(vec3 position) { return position; }', + 'float getStrokeWeight': '(float weight) { return weight; }', + 'vec2 getLineCenter': '(vec2 center) { return center; }', + 'vec2 getLinePosition': '(vec2 position) { return position; }', + 'vec4 getVertexColor': '(vec4 color) { return color; }', + 'void afterVertex': '() {}' + }, + fragment: { + 'void beforeFragment': '() {}', + 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', + 'vec4 getFinalColor': '(vec4 color) { return color; }', + 'bool shouldDiscard': '(bool outside) { return outside; }', + 'void afterFragment': '() {}' + } + } + ); } - strokeJoin(join) { - this.curStrokeJoin = join; + return this._defaultLineShader; + } + + _getFontShader() { + if (!this._defaultFontShader) { + if (this.webglVersion === constants.WEBGL) { + this.GL.getExtension('OES_standard_derivatives'); + } + this._defaultFontShader = new Shader( + this, + this._webGL2CompatibilityPrefix('vert', 'mediump') + + defaultShaders.fontVert, + this._webGL2CompatibilityPrefix('frag', 'mediump') + + defaultShaders.fontFrag + ); } - getFilterLayer() { - if (!this.filterLayer) { - this.filterLayer = this._pInst.createFramebuffer(); - } - return this.filterLayer; + return this._defaultFontShader; + } + + _webGL2CompatibilityPrefix( + shaderType, + floatPrecision + ) { + let code = ''; + if (this.webglVersion === constants.WEBGL2) { + code += '#version 300 es\n#define WEBGL2\n'; } - getFilterLayerTemp() { - if (!this.filterLayerTemp) { - this.filterLayerTemp = this._pInst.createFramebuffer(); - } - return this.filterLayerTemp; + if (shaderType === 'vert') { + code += '#define VERTEX_SHADER\n'; + } else if (shaderType === 'frag') { + code += '#define FRAGMENT_SHADER\n'; } - matchSize(fboToMatch, target) { - if ( - fboToMatch.width !== target.width || - fboToMatch.height !== target.height - ) { - fboToMatch.resize(target.width, target.height); - } + if (floatPrecision) { + code += `precision ${floatPrecision} float;\n`; + } + return code; + } - if (fboToMatch.pixelDensity() !== target.pixelDensity()) { - fboToMatch.pixelDensity(target.pixelDensity()); - } + _getEmptyTexture() { + if (!this._emptyTexture) { + // a plain white texture RGBA, full alpha, single pixel. + const im = new Image(1, 1); + im.set(0, 0, 255); + this._emptyTexture = new Texture(this, im); } - filter(...args) { + return this._emptyTexture; + } - let fbo = this.getFilterLayer(); + getTexture(input) { + let src = input; + if (src instanceof p5.Framebuffer) { + src = src.color; + } - // use internal shader for filter constants BLUR, INVERT, etc - let filterParameter = undefined; - let operation = undefined; - if (typeof args[0] === 'string') { - operation = args[0]; - let defaults = { - [constants.BLUR]: 3, - [constants.POSTERIZE]: 4, - [constants.THRESHOLD]: 0.5 - }; - let useDefaultParam = operation in defaults && args[1] === undefined; - filterParameter = useDefaultParam ? defaults[operation] : args[1]; - - // Create and store shader for constants once on initial filter call. - // Need to store multiple in case user calls different filters, - // eg. filter(BLUR) then filter(GRAY) - if (!(operation in this.defaultFilterShaders)) { - this.defaultFilterShaders[operation] = new p5.Shader( - fbo._renderer, - filterShaderVert, - filterShaderFrags[operation] - ); - } - this.states.filterShader = this.defaultFilterShaders[operation]; + const texture = this.textures.get(src); + if (texture) { + return texture; + } - } - // use custom user-supplied shader - else { - this.states.filterShader = args[0]; - } + const tex = new Texture(this, src); + this.textures.set(src, tex); + return tex; + } + /* + * used in imageLight, + * To create a blurry image from the input non blurry img, if it doesn't already exist + * Add it to the diffusedTexture map, + * Returns the blurry image + * maps a Image used by imageLight() to a p5.Framebuffer + */ + getDiffusedTexture(input) { + // if one already exists for a given input image + if (this.diffusedTextures.get(input) != null) { + return this.diffusedTextures.get(input); + } + // if not, only then create one + let newFramebuffer; + // hardcoded to 200px, because it's going to be blurry and smooth + let smallWidth = 200; + let width = smallWidth; + let height = Math.floor(smallWidth * (input.height / input.width)); + newFramebuffer = this._pInst.createFramebuffer({ + width, height, density: 1 + }); + // create framebuffer is like making a new sketch, all functions on main + // sketch it would be available on framebuffer + if (!this.states.diffusedShader) { + this.states.diffusedShader = this._pInst.createShader( + defaultShaders.imageLightVert, + defaultShaders.imageLightDiffusedFrag + ); + } + newFramebuffer.draw(() => { + this._pInst.shader(this.states.diffusedShader); + this.states.diffusedShader.setUniform('environmentMap', input); + this._pInst.noStroke(); + this._pInst.rectMode(constants.CENTER); + this._pInst.noLights(); + this._pInst.rect(0, 0, width, height); + }); + this.diffusedTextures.set(input, newFramebuffer); + return newFramebuffer; + } - // Setting the target to the framebuffer when applying a filter to a framebuffer. + /* + * used in imageLight, + * To create a texture from the input non blurry image, if it doesn't already exist + * Creating 8 different levels of textures according to different + * sizes and atoring them in `levels` array + * Creating a new Mipmap texture with that `levels` array + * Storing the texture for input image in map called `specularTextures` + * maps the input Image to a p5.MipmapTexture + */ + getSpecularTexture(input) { + // check if already exits (there are tex of diff resolution so which one to check) + // currently doing the whole array + if (this.specularTextures.get(input) != null) { + return this.specularTextures.get(input); + } + // Hardcoded size + const size = 512; + let tex; + const levels = []; + const framebuffer = this._pInst.createFramebuffer({ + width: size, height: size, density: 1 + }); + let count = Math.log(size) / Math.log(2); + if (!this.states.specularShader) { + this.states.specularShader = this._pInst.createShader( + defaultShaders.imageLightVert, + defaultShaders.imageLightSpecularFrag + ); + } + // currently only 8 levels + // This loop calculates 8 framebuffers of varying size of canvas + // and corresponding different roughness levels. + // Roughness increases with the decrease in canvas size, + // because rougher surfaces have less detailed/more blurry reflections. + for (let w = size; w >= 1; w /= 2) { + framebuffer.resize(w, w); + let currCount = Math.log(w) / Math.log(2); + let roughness = 1 - currCount / count; + framebuffer.draw(() => { + this._pInst.shader(this.states.specularShader); + this._pInst.clear(); + this.states.specularShader.setUniform('environmentMap', input); + this.states.specularShader.setUniform('roughness', roughness); + this._pInst.noStroke(); + this._pInst.noLights(); + this._pInst.plane(w, w); + }); + levels.push(framebuffer.get().drawingContext.getImageData(0, 0, w, w)); + } + // Free the Framebuffer + framebuffer.remove(); + tex = new p5.MipmapTexture(this, levels, {}); + this.specularTextures.set(input, tex); + return tex; + } - const target = this.activeFramebuffer() || this; + /** + * @private + * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if + * the main canvas is the current draw target. + */ + activeFramebuffer() { + return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null; + } - // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. - this.matchSize(fbo, target); + createFramebuffer(options) { + return new p5.Framebuffer(this, options); + } - fbo.draw(() => this._pInst.clear()); // prevent undesirable feedback effects accumulating secretly. + _setStrokeUniforms(baseStrokeShader) { + baseStrokeShader.bindShader(); - let texelSize = [ - 1 / (target.width * target.pixelDensity()), - 1 / (target.height * target.pixelDensity()) - ]; + // set the uniform values + baseStrokeShader.setUniform('uUseLineColor', this._useLineColor); + baseStrokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); + baseStrokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); + baseStrokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); + baseStrokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); + } - // apply blur shader with multiple passes. - if (operation === constants.BLUR) { - // Treating 'tmp' as a framebuffer. - const tmp = this.getFilterLayerTemp(); - // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. - this.matchSize(tmp, target); - // setup - this._pInst.push(); - this._pInst.noStroke(); - this._pInst.blendMode(constants.BLEND); + _setFillUniforms(fillShader) { + fillShader.bindShader(); - // draw main to temp buffer - this._pInst.shader(this.states.filterShader); - this.states.filterShader.setUniform('texelSize', texelSize); - this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); - this.states.filterShader.setUniform('radius', Math.max(1, filterParameter)); - - // Horiz pass: draw `target` to `tmp` - tmp.draw(() => { - this.states.filterShader.setUniform('direction', [1, 0]); - this.states.filterShader.setUniform('tex0', target); - this._pInst.clear(); - this._pInst.shader(this.states.filterShader); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); - }); - - // Vert pass: draw `tmp` to `fbo` - fbo.draw(() => { - this.states.filterShader.setUniform('direction', [0, 1]); - this.states.filterShader.setUniform('tex0', tmp); - this._pInst.clear(); - this._pInst.shader(this.states.filterShader); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); - }); - - this._pInst.pop(); - } - // every other non-blur shader uses single pass - else { - fbo.draw(() => { - this._pInst.noStroke(); - this._pInst.blendMode(constants.BLEND); - this._pInst.shader(this.states.filterShader); - this.states.filterShader.setUniform('tex0', target); - this.states.filterShader.setUniform('texelSize', texelSize); - this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); - // filterParameter uniform only used for POSTERIZE, and THRESHOLD - // but shouldn't hurt to always set - this.states.filterShader.setUniform('filterParameter', filterParameter); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); - }); + this.mixedSpecularColor = [...this.states.curSpecularColor]; - } - // draw fbo contents onto main renderer. - this._pInst.push(); - this._pInst.noStroke(); - this.clear(); - this._pInst.push(); - this._pInst.imageMode(constants.CORNER); - this._pInst.blendMode(constants.BLEND); - target.filterCamera._resize(); - this._pInst.setCamera(target.filterCamera); - this._pInst.resetMatrix(); - this._pInst.image(fbo, -target.width / 2, -target.height / 2, - target.width, target.height); - this._pInst.clearDepth(); - this._pInst.pop(); - this._pInst.pop(); + if (this.states._useMetalness > 0) { + this.mixedSpecularColor = this.mixedSpecularColor.map( + (mixedSpecularColor, index) => + this.states.curFillColor[index] * this.states._useMetalness + + mixedSpecularColor * (1 - this.states._useMetalness) + ); } - // Pass this off to the host instance so that we can treat a renderer and a - // framebuffer the same in filter() + // TODO: optimize + fillShader.setUniform('uUseVertexColor', this._useVertexColor); + fillShader.setUniform('uMaterialColor', this.states.curFillColor); + fillShader.setUniform('isTexture', !!this.states._tex); + if (this.states._tex) { + fillShader.setUniform('uSampler', this.states._tex); + } + fillShader.setUniform('uTint', this.states.tint); + + fillShader.setUniform('uHasSetAmbient', this.states._hasSetAmbient); + fillShader.setUniform('uAmbientMatColor', this.states.curAmbientColor); + fillShader.setUniform('uSpecularMatColor', this.mixedSpecularColor); + fillShader.setUniform('uEmissiveMatColor', this.states.curEmissiveColor); + fillShader.setUniform('uSpecular', this.states._useSpecularMaterial); + fillShader.setUniform('uEmissive', this.states._useEmissiveMaterial); + fillShader.setUniform('uShininess', this.states._useShininess); + fillShader.setUniform('uMetallic', this.states._useMetalness); + + this._setImageLightUniforms(fillShader); + + fillShader.setUniform('uUseLighting', this.states._enableLighting); + + const pointLightCount = this.states.pointLightDiffuseColors.length / 3; + fillShader.setUniform('uPointLightCount', pointLightCount); + fillShader.setUniform('uPointLightLocation', this.states.pointLightPositions); + fillShader.setUniform( + 'uPointLightDiffuseColors', + this.states.pointLightDiffuseColors + ); + fillShader.setUniform( + 'uPointLightSpecularColors', + this.states.pointLightSpecularColors + ); + + const directionalLightCount = this.states.directionalLightDiffuseColors.length / 3; + fillShader.setUniform('uDirectionalLightCount', directionalLightCount); + fillShader.setUniform('uLightingDirection', this.states.directionalLightDirections); + fillShader.setUniform( + 'uDirectionalDiffuseColors', + this.states.directionalLightDiffuseColors + ); + fillShader.setUniform( + 'uDirectionalSpecularColors', + this.states.directionalLightSpecularColors + ); + + // TODO: sum these here... + const ambientLightCount = this.states.ambientLightColors.length / 3; + this.mixedAmbientLight = [...this.states.ambientLightColors]; + + if (this.states._useMetalness > 0) { + this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors => { + let mixing = ambientColors - this.states._useMetalness; + return Math.max(0, mixing); + })); + } + fillShader.setUniform('uAmbientLightCount', ambientLightCount); + fillShader.setUniform('uAmbientColor', this.mixedAmbientLight); + + const spotLightCount = this.states.spotLightDiffuseColors.length / 3; + fillShader.setUniform('uSpotLightCount', spotLightCount); + fillShader.setUniform('uSpotLightAngle', this.states.spotLightAngle); + fillShader.setUniform('uSpotLightConc', this.states.spotLightConc); + fillShader.setUniform('uSpotLightDiffuseColors', this.states.spotLightDiffuseColors); + fillShader.setUniform( + 'uSpotLightSpecularColors', + this.states.spotLightSpecularColors + ); + fillShader.setUniform('uSpotLightLocation', this.states.spotLightPositions); + fillShader.setUniform('uSpotLightDirection', this.states.spotLightDirections); + + fillShader.setUniform('uConstantAttenuation', this.states.constantAttenuation); + fillShader.setUniform('uLinearAttenuation', this.states.linearAttenuation); + fillShader.setUniform('uQuadraticAttenuation', this.states.quadraticAttenuation); + + fillShader.bindTextures(); + } + + // getting called from _setFillUniforms + _setImageLightUniforms(shader) { + //set uniform values + shader.setUniform('uUseImageLight', this.states.activeImageLight != null); + // true + if (this.states.activeImageLight) { + // this.states.activeImageLight has image as a key + // look up the texture from the diffusedTexture map + let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); + shader.setUniform('environmentMapDiffused', diffusedLight); + let specularLight = this.getSpecularTexture(this.states.activeImageLight); - pixelDensity(newDensity) { - if (newDensity) { - return this._pInst.pixelDensity(newDensity); - } - return this._pInst.pixelDensity(); + shader.setUniform('environmentMapSpecular', specularLight); } + } - blendMode(mode) { - if ( - mode === constants.DARKEST || - mode === constants.LIGHTEST || - mode === constants.ADD || - mode === constants.BLEND || - mode === constants.SUBTRACT || - mode === constants.SCREEN || - mode === constants.EXCLUSION || - mode === constants.REPLACE || - mode === constants.MULTIPLY || - mode === constants.REMOVE - ) - this.states.curBlendMode = mode; - else if ( - mode === constants.BURN || - mode === constants.OVERLAY || - mode === constants.HARD_LIGHT || - mode === constants.SOFT_LIGHT || - mode === constants.DODGE - ) { - console.warn( - 'BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode.' - ); - } + _setPointUniforms(pointShader) { + pointShader.bindShader(); + + // set the uniform values + pointShader.setUniform('uMaterialColor', this.states.curStrokeColor); + // @todo is there an instance where this isn't stroke weight? + // should be they be same var? + pointShader.setUniform( + 'uPointSize', + this.pointSize * this._pixelDensity + ); + } + + /* Binds a buffer to the drawing context + * when passed more than two arguments it also updates or initializes + * the data associated with the buffer + */ + _bindBuffer( + buffer, + target, + values, + type, + usage + ) { + if (!target) target = this.GL.ARRAY_BUFFER; + this.GL.bindBuffer(target, buffer); + if (values !== undefined) { + let data = values; + if (values instanceof DataArray) { + data = values.dataArray(); + } else if (!(data instanceof (type || Float32Array))) { + data = new (type || Float32Array)(data); + } + this.GL.bufferData(target, data, usage || this.GL.STATIC_DRAW); } + } + + /////////////////////////////// + //// UTILITY FUNCTIONS + ////////////////////////////// + _arraysEqual(a, b) { + const aLength = a.length; + if (aLength !== b.length) return false; + return a.every((ai, i) => ai === b[i]); + } + + _isTypedArray(arr) { + return [ + Float32Array, + Float64Array, + Int16Array, + Uint16Array, + Uint32Array + ].some(x => arr instanceof x); + } + /** + * turn a two dimensional array into one dimensional array + * @private + * @param {Array} arr 2-dimensional array + * @return {Array} 1-dimensional array + * [[1, 2, 3],[4, 5, 6]] -> [1, 2, 3, 4, 5, 6] + */ + _flatten(arr) { + return arr.flat(); + } + + /** + * turn a p5.Vector Array into a one dimensional number array + * @private + * @param {p5.Vector[]} arr an array of p5.Vector + * @return {Number[]} a one dimensional array of numbers + * [p5.Vector(1, 2, 3), p5.Vector(4, 5, 6)] -> + * [1, 2, 3, 4, 5, 6] + */ + _vToNArray(arr) { + return arr.flatMap(item => [item.x, item.y, item.z]); + } + + // function to calculate BezierVertex Coefficients + _bezierCoefficients(t) { + const t2 = t * t; + const t3 = t2 * t; + const mt = 1 - t; + const mt2 = mt * mt; + const mt3 = mt2 * mt; + return [mt3, 3 * mt2 * t, 3 * mt * t2, t3]; + } + + // function to calculate QuadraticVertex Coefficients + _quadraticCoefficients(t) { + const t2 = t * t; + const mt = 1 - t; + const mt2 = mt * mt; + return [mt2, 2 * mt * t, t2]; + } - erase(opacityFill, opacityStroke) { - if (!this._isErasing) { - this.preEraseBlend = this.states.curBlendMode; - this._isErasing = true; - this.blendMode(constants.REMOVE); - this._cachedFillStyle = this.states.curFillColor.slice(); - this.states.curFillColor = [1, 1, 1, opacityFill / 255]; - this._cachedStrokeStyle = this.states.curStrokeColor.slice(); - this.states.curStrokeColor = [1, 1, 1, opacityStroke / 255]; + // function to convert Bezier coordinates to Catmull Rom Splines + _bezierToCatmull(w) { + const p1 = w[1]; + const p2 = w[1] + (w[2] - w[0]) / this._curveTightness; + const p3 = w[2] - (w[3] - w[1]) / this._curveTightness; + const p4 = w[2]; + const p = [p1, p2, p3, p4]; + return p; + } + _initTessy() { + this.tessyVertexSize = 12; + // function called for each vertex of tesselator output + function vertexCallback(data, polyVertArray) { + for (const element of data) { + polyVertArray.push(element); } } - noErase() { - if (this._isErasing) { - // Restore colors - this.states.curFillColor = this._cachedFillStyle.slice(); - this.states.curStrokeColor = this._cachedStrokeStyle.slice(); - // Restore blend mode - this.states.curBlendMode = this.preEraseBlend; - this.blendMode(this.preEraseBlend); - // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode - this._isErasing = false; - this._applyBlendMode(); + function begincallback(type) { + if (type !== libtess.primitiveType.GL_TRIANGLES) { + console.log(`expected TRIANGLES but got type: ${type}`); } } - drawTarget() { - return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this; + function errorcallback(errno) { + console.log('error callback'); + console.log(`error number: ${errno}`); } + // callback for when segments intersect and must be split + const combinecallback = (coords, data, weight) => { + const result = new Array(this.tessyVertexSize).fill(0); + for (let i = 0; i < weight.length; i++) { + for (let j = 0; j < result.length; j++) { + if (weight[i] === 0 || !data[i]) continue; + result[j] += data[i][j] * weight[i]; + } + } + return result; + }; - beginClip(options = {}) { - super.beginClip(options); + function edgeCallback(flag) { + // don't really care about the flag, but need no-strip/no-fan behavior + } - this.drawTarget()._isClipApplied = true; + const tessy = new libtess.GluTesselator(); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); + tessy.gluTessProperty( + libtess.gluEnum.GLU_TESS_WINDING_RULE, + libtess.windingRule.GLU_TESS_WINDING_NONZERO + ); - const gl = this.GL; - gl.clearStencil(0); - gl.clear(gl.STENCIL_BUFFER_BIT); - gl.enable(gl.STENCIL_TEST); - this._stencilTestOn = true; - gl.stencilFunc( - gl.ALWAYS, // the test - 1, // reference value - 0xff // mask - ); - gl.stencilOp( - gl.KEEP, // what to do if the stencil test fails - gl.KEEP, // what to do if the depth test fails - gl.REPLACE // what to do if both tests pass - ); - gl.disable(gl.DEPTH_TEST); + return tessy; + } - this._pInst.push(); - this._pInst.resetShader(); - if (this.states.doFill) this._pInst.fill(0, 0); - if (this.states.doStroke) this._pInst.stroke(0, 0); + _triangulate(contours) { + // libtess will take 3d verts and flatten to a plane for tesselation. + // libtess is capable of calculating a plane to tesselate on, but + // if all of the vertices have the same z values, we'll just + // assume the face is facing the camera, letting us skip any performance + // issues or bugs in libtess's automatic calculation. + const z = contours[0] ? contours[0][2] : undefined; + let allSameZ = true; + for (const contour of contours) { + for ( + let j = 0; + j < contour.length; + j += this.tessyVertexSize + ) { + if (contour[j + 2] !== z) { + allSameZ = false; + break; + } + } + } + if (allSameZ) { + this._tessy.gluTessNormal(0, 0, 1); + } else { + // Let libtess pick a plane for us + this._tessy.gluTessNormal(0, 0, 0); } - endClip() { - this._pInst.pop(); + const triangleVerts = []; + this._tessy.gluTessBeginPolygon(triangleVerts); - const gl = this.GL; - gl.stencilOp( - gl.KEEP, // what to do if the stencil test fails - gl.KEEP, // what to do if the depth test fails - gl.KEEP // what to do if both tests pass - ); - gl.stencilFunc( - this._clipInvert ? gl.EQUAL : gl.NOTEQUAL, // the test - 0, // reference value - 0xff // mask - ); - gl.enable(gl.DEPTH_TEST); + for (const contour of contours) { + this._tessy.gluTessBeginContour(); + for ( + let j = 0; + j < contour.length; + j += this.tessyVertexSize + ) { + const coords = contour.slice( + j, + j + this.tessyVertexSize + ); + this._tessy.gluTessVertex(coords, coords); + } + this._tessy.gluTessEndContour(); + } - // Mark the depth at which the clip has been applied so that we can clear it - // when we pop past this depth - this._clipDepths.push(this._pushPopDepth); + // finish polygon + this._tessy.gluTessEndPolygon(); - super.endClip(); - } + return triangleVerts; + } +}; - _clearClip() { - this.GL.clearStencil(1); - this.GL.clear(this.GL.STENCIL_BUFFER_BIT); - if (this._clipDepths.length > 0) { - this._clipDepths.pop(); - } - this.drawTarget()._isClipApplied = false; - } +function rendererGL(p5, fn){ + p5.RendererGL = RendererGL; - /** - * Change weight of stroke - * @param {Number} stroke weight to be used for drawing + /** + * @module Rendering + * @submodule Rendering + * @for p5 + */ + /** + * Set attributes for the WebGL Drawing context. + * This is a way of adjusting how the WebGL + * renderer works to fine-tune the display and performance. + * + * Note that this will reinitialize the drawing context + * if called after the WebGL canvas is made. + * + * If an object is passed as the parameter, all attributes + * not declared in the object will be set to defaults. + * + * The available attributes are: + *
+ * alpha - indicates if the canvas contains an alpha buffer + * default is true + * + * depth - indicates whether the drawing buffer has a depth buffer + * of at least 16 bits - default is true + * + * stencil - indicates whether the drawing buffer has a stencil buffer + * of at least 8 bits + * + * antialias - indicates whether or not to perform anti-aliasing + * default is false (true in Safari) + * + * premultipliedAlpha - indicates that the page compositor will assume + * the drawing buffer contains colors with pre-multiplied alpha + * default is true + * + * preserveDrawingBuffer - if true the buffers will not be cleared and + * and will preserve their values until cleared or overwritten by author + * (note that p5 clears automatically on draw loop) + * default is true + * + * perPixelLighting - if true, per-pixel lighting will be used in the + * lighting shader otherwise per-vertex lighting is used. + * default is true. + * + * version - either 1 or 2, to specify which WebGL version to ask for. By + * default, WebGL 2 will be requested. If WebGL2 is not available, it will + * fall back to WebGL 1. You can check what version is used with by looking at + * the global `webglVersion` property. + * + * @method setAttributes + * @for p5 + * @param {String} key Name of attribute + * @param {Boolean} value New value of named attribute * @example *
* * function setup() { - * createCanvas(200, 400, WEBGL); + * createCanvas(100, 100, WEBGL); + * } + * + * function draw() { + * background(255); + * push(); + * rotateZ(frameCount * 0.02); + * rotateX(frameCount * 0.02); + * rotateY(frameCount * 0.02); + * fill(0, 0, 0); + * box(50); + * pop(); + * } + * + *
+ *
+ * Now with the antialias attribute set to true. + *
+ *
+ * + * function setup() { * setAttributes('antialias', true); + * createCanvas(100, 100, WEBGL); + * } + * + * function draw() { + * background(255); + * push(); + * rotateZ(frameCount * 0.02); + * rotateX(frameCount * 0.02); + * rotateY(frameCount * 0.02); + * fill(0, 0, 0); + * box(50); + * pop(); + * } + * + *
+ * + *
+ * + * // press the mouse button to disable perPixelLighting + * function setup() { + * createCanvas(100, 100, WEBGL); + * noStroke(); + * fill(255); * } * + * let lights = [ + * { c: '#f00', t: 1.12, p: 1.91, r: 0.2 }, + * { c: '#0f0', t: 1.21, p: 1.31, r: 0.2 }, + * { c: '#00f', t: 1.37, p: 1.57, r: 0.2 }, + * { c: '#ff0', t: 1.12, p: 1.91, r: 0.7 }, + * { c: '#0ff', t: 1.21, p: 1.31, r: 0.7 }, + * { c: '#f0f', t: 1.37, p: 1.57, r: 0.7 } + * ]; + * * function draw() { + * let t = millis() / 1000 + 1000; * background(0); + * directionalLight(color('#222'), 1, 1, 1); + * + * for (let i = 0; i < lights.length; i++) { + * let light = lights[i]; + * pointLight( + * color(light.c), + * p5.Vector.fromAngles(t * light.t, t * light.p, width * light.r) + * ); + * } + * + * specularMaterial(255); + * sphere(width * 0.1); + * + * rotateX(t * 0.77); + * rotateY(t * 0.83); + * rotateZ(t * 0.91); + * torus(width * 0.3, width * 0.07, 24, 10); + * } + * + * function mousePressed() { + * setAttributes('perPixelLighting', false); * noStroke(); - * translate(0, -100, 0); - * stroke(240, 150, 150); - * fill(100, 100, 240); - * push(); - * strokeWeight(8); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * sphere(75); - * pop(); - * push(); - * translate(0, 200, 0); - * strokeWeight(1); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * sphere(75); - * pop(); + * fill(255); + * } + * function mouseReleased() { + * setAttributes('perPixelLighting', true); + * noStroke(); + * fill(255); * } * *
* - * @alt - * black canvas with two purple rotating spheres with pink - * outlines the sphere on top has much heavier outlines, + * @alt a rotating cube with smoother edges */ - strokeWeight(w) { - if (this.curStrokeWeight !== w) { - this.pointSize = w; - this.curStrokeWeight = w; - } - } - - // x,y are canvas-relative (pre-scaled by _pixelDensity) - _getPixel(x, y) { - const gl = this.GL; - return readPixelWebGL( - gl, - null, - x, - y, - gl.RGBA, - gl.UNSIGNED_BYTE, - this._pInst.height * this._pInst.pixelDensity() + /** + * @method setAttributes + * @for p5 + * @param {Object} obj object with key-value pairs + */ + fn.setAttributes = function (key, value) { + if (typeof this._glAttributes === 'undefined') { + console.log( + 'You are trying to use setAttributes on a p5.Graphics object ' + + 'that does not use a WEBGL renderer.' ); + return; } - - /** - * Loads the pixels data for this canvas into the pixels[] attribute. - * Note that updatePixels() and set() do not work. - * Any pixel manipulation must be done directly to the pixels[] array. - * - * @private - */ - - loadPixels() { - const pixelsState = this._pixelsState; - - //@todo_FES - if (this._pInst._glAttributes.preserveDrawingBuffer !== true) { - console.log( - 'loadPixels only works in WebGL when preserveDrawingBuffer ' + 'is true.' - ); - return; + let unchanged = true; + if (typeof value !== 'undefined') { + //first time modifying the attributes + if (this._glAttributes === null) { + this._glAttributes = {}; } - - const pd = this._pixelDensity; - const gl = this.GL; - - pixelsState.pixels = - readPixelsWebGL( - pixelsState.pixels, - gl, - null, - 0, - 0, - this.width * pd, - this.height * pd, - gl.RGBA, - gl.UNSIGNED_BYTE, - this.height * pd - ); - } - - updatePixels() { - const fbo = this._getTempFramebuffer(); - fbo.pixels = this._pixelsState.pixels; - fbo.updatePixels(); - this._pInst.push(); - this._pInst.resetMatrix(); - this._pInst.clear(); - this._pInst.imageMode(constants.CENTER); - this._pInst.image(fbo, 0, 0); - this._pInst.pop(); - this.GL.clearDepth(1); - this.GL.clear(this.GL.DEPTH_BUFFER_BIT); - } - - /** - * @private - * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings - * of the renderer's canvas. It will be created if it does not yet exist, and - * reused if it does. - */ - _getTempFramebuffer() { - if (!this._tempFramebuffer) { - this._tempFramebuffer = this._pInst.createFramebuffer({ - format: constants.UNSIGNED_BYTE, - useDepth: this._pInst._glAttributes.depth, - depthFormat: constants.UNSIGNED_INT, - antialias: this._pInst._glAttributes.antialias - }); + if (this._glAttributes[key] !== value) { + //changing value of previously altered attribute + this._glAttributes[key] = value; + unchanged = false; + } + //setting all attributes with some change + } else if (key instanceof Object) { + if (this._glAttributes !== key) { + this._glAttributes = key; + unchanged = false; } - return this._tempFramebuffer; - } - - - - ////////////////////////////////////////////// - // HASH | for geometry - ////////////////////////////////////////////// - - geometryInHash(gId) { - return this.retainedMode.geometry[gId] !== undefined; } - - viewport(w, h) { - this._viewport = [0, 0, w, h]; - this.GL.viewport(0, 0, w, h); + //@todo_FES + if (!this._renderer.isP3D || unchanged) { + return; } - /** - * [resize description] - * @private - * @param {Number} w [description] - * @param {Number} h [description] - */ - resize(w, h) { - super.resize(w, h); - - // save canvas properties - const props = {}; - for (const key in this.drawingContext) { - const val = this.drawingContext[key]; - if (typeof val !== 'object' && typeof val !== 'function') { - props[key] = val; - } - } - - const dimensions = this._adjustDimensions(w, h); - w = dimensions.adjustedWidth; - h = dimensions.adjustedHeight; - - this.width = w; - this.height = h; - - this.canvas.width = w * this._pixelDensity; - this.canvas.height = h * this._pixelDensity; - this.canvas.style.width = `${w}px`; - this.canvas.style.height = `${h}px`; - this._origViewport = { - width: this.GL.drawingBufferWidth, - height: this.GL.drawingBufferHeight - }; - this.viewport( - this._origViewport.width, - this._origViewport.height - ); - - this.states.curCamera._resize(); - - //resize pixels buffer - const pixelsState = this._pixelsState; - if (typeof pixelsState.pixels !== 'undefined') { - pixelsState.pixels = - new Uint8Array( - this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 + if (!this._setupDone) { + for (const x in this._renderer.retainedMode.geometry) { + if (this._renderer.retainedMode.geometry.hasOwnProperty(x)) { + p5._friendlyError( + 'Sorry, Could not set the attributes, you need to call setAttributes() ' + + 'before calling the other drawing methods in setup()' ); - } - - for (const framebuffer of this.framebuffers) { - // Notify framebuffers of the resize so that any auto-sized framebuffers - // can also update their size - framebuffer._canvasSizeChanged(); - } - - // reset canvas properties - for (const savedKey in props) { - try { - this.drawingContext[savedKey] = props[savedKey]; - } catch (err) { - // ignore read-only property errors + return; } } } - /** - * clears color and depth buffers - * with r,g,b,a - * @private - * @param {Number} r normalized red val. - * @param {Number} g normalized green val. - * @param {Number} b normalized blue val. - * @param {Number} a normalized alpha val. - */ - clear(...args) { - const _r = args[0] || 0; - const _g = args[1] || 0; - const _b = args[2] || 0; - let _a = args[3] || 0; - - const activeFramebuffer = this.activeFramebuffer(); - if ( - activeFramebuffer && - activeFramebuffer.format === constants.UNSIGNED_BYTE && - !activeFramebuffer.antialias && - _a === 0 - ) { - // Drivers on Intel Macs check for 0,0,0,0 exactly when drawing to a - // framebuffer and ignore the command if it's the only drawing command to - // the framebuffer. To work around it, we can set the alpha to a value so - // low that it still rounds down to 0, but that circumvents the buggy - // check in the driver. - _a = 1e-10; - } + this.push(); + this._renderer._resetContext(); + this.pop(); - this.GL.clearColor(_r * _a, _g * _a, _b * _a, _a); - this.GL.clearDepth(1); - this.GL.clear(this.GL.COLOR_BUFFER_BIT | this.GL.DEPTH_BUFFER_BIT); + if (this._renderer.states.curCamera) { + this._renderer.states.curCamera._renderer = this._renderer; } + }; - /** - * Resets all depth information so that nothing previously drawn will - * occlude anything subsequently drawn. - */ - clearDepth(depth = 1) { - this.GL.clearDepth(depth); - this.GL.clear(this.GL.DEPTH_BUFFER_BIT); - } + /** + * ensures that p5 is using a 3d renderer. throws an error if not. + */ + fn._assert3d = function (name) { + if (!this._renderer.isP3D) + throw new Error( + `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` + ); + }; - applyMatrix(a, b, c, d, e, f) { - if (arguments.length === 16) { - p5.Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); - } else { - this.states.uModelMatrix.apply([ - a, b, 0, 0, - c, d, 0, 0, - 0, 0, 1, 0, - e, f, 0, 1 - ]); - } - } + p5.renderers[constants.WEBGL] = p5.RendererGL; + p5.renderers[constants.WEBGL2] = p5.RendererGL; + RendererGL = p5.RendererGL; - /** - * [translate description] - * @private - * @param {Number} x [description] - * @param {Number} y [description] - * @param {Number} z [description] - * @chainable - * @todo implement handle for components or vector as args - */ - translate(x, y, z) { - if (x instanceof p5.Vector) { - z = x.z; - y = x.y; - x = x.x; - } - this.states.uModelMatrix.translate([x, y, z]); - return this; - } + /////////////////////// + /// 2D primitives + ///////////////////////// + // + // Note: Documentation is not generated on the p5.js website for functions on + // the p5.RendererGL prototype. - /** - * Scales the Model View Matrix by a vector + /** + * Draws a point, a coordinate in space at the dimension of one pixel, + * given x, y and z coordinates. The color of the point is determined + * by the current stroke, while the point size is determined by current + * stroke weight. * @private - * @param {Number | p5.Vector | Array} x [description] - * @param {Number} [y] y-axis scalar - * @param {Number} [z] z-axis scalar + * @param {Number} x x-coordinate of point + * @param {Number} y y-coordinate of point + * @param {Number} z z-coordinate of point * @chainable + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * } + * + * function draw() { + * background(50); + * stroke(255); + * strokeWeight(4); + * point(25, 0); + * strokeWeight(3); + * point(-25, 0); + * strokeWeight(2); + * point(0, 25); + * strokeWeight(1); + * point(0, -25); + * } + * + *
*/ - scale(x, y, z) { - this.states.uModelMatrix.scale(x, y, z); - return this; - } - - rotate(rad, axis) { - if (typeof axis === 'undefined') { - return this.rotateZ(rad); - } - p5.Matrix.prototype.rotate.apply(this.states.uModelMatrix, arguments); - return this; - } - - rotateX(rad) { - this.rotate(rad, 1, 0, 0); - return this; - } - - rotateY(rad) { - this.rotate(rad, 0, 1, 0); - return this; - } - - rotateZ(rad) { - this.rotate(rad, 0, 0, 1); - return this; - } - - pop(...args) { - if ( - this._clipDepths.length > 0 && - this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] - ) { - this._clearClip(); - } - super.pop(...args); - this._applyStencilTestIfClipping(); - } - _applyStencilTestIfClipping() { - const drawTarget = this.drawTarget(); - if (drawTarget._isClipApplied !== this._stencilTestOn) { - if (drawTarget._isClipApplied) { - this.GL.enable(this.GL.STENCIL_TEST); - this._stencilTestOn = true; - } else { - this.GL.disable(this.GL.STENCIL_TEST); - this._stencilTestOn = false; - } - } - } - resetMatrix() { - this.states.uModelMatrix.reset(); - this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); - return this; - } - - ////////////////////////////////////////////// - // SHADER - ////////////////////////////////////////////// - - /* - * shaders are created and cached on a per-renderer basis, - * on the grounds that each renderer will have its own gl context - * and the shader must be valid in that context. - */ - - _getImmediateStrokeShader() { - // select the stroke shader to use - const stroke = this.states.userStrokeShader; - if (!stroke || !stroke.isStrokeShader()) { - return this._getLineShader(); - } - return stroke; - } - - - _getRetainedStrokeShader() { - return this._getImmediateStrokeShader(); - } - - _getSphereMapping(img) { - if (!this.sphereMapping) { - this.sphereMapping = this._pInst.createFilterShader( - sphereMapping - ); - } - this.states.uNMatrix.inverseTranspose(this.states.uViewMatrix); - this.states.uNMatrix.invert3x3(this.states.uNMatrix); - this.sphereMapping.setUniform('uFovY', this.states.curCamera.cameraFOV); - this.sphereMapping.setUniform('uAspect', this.states.curCamera.aspectRatio); - this.sphereMapping.setUniform('uNewNormalMatrix', this.states.uNMatrix.mat3); - this.sphereMapping.setUniform('uSampler', img); - return this.sphereMapping; - } - - /* - * selects which fill shader should be used based on renderer state, - * for use with begin/endShape and immediate vertex mode. - */ - _getImmediateFillShader() { - const fill = this.states.userFillShader; - if (this.states._useNormalMaterial) { - if (!fill || !fill.isNormalShader()) { - return this._getNormalShader(); - } - } - if (this.states._enableLighting) { - if (!fill || !fill.isLightShader()) { - return this._getLightShader(); - } - } else if (this.states._tex) { - if (!fill || !fill.isTextureShader()) { - return this._getLightShader(); - } - } else if (!fill /*|| !fill.isColorShader()*/) { - return this._getImmediateModeShader(); - } - return fill; - } - - /* - * selects which fill shader should be used based on renderer state - * for retained mode. - */ - _getRetainedFillShader() { - if (this.states._useNormalMaterial) { - return this._getNormalShader(); - } - - const fill = this.states.userFillShader; - if (this.states._enableLighting) { - if (!fill || !fill.isLightShader()) { - return this._getLightShader(); - } - } else if (this.states._tex) { - if (!fill || !fill.isTextureShader()) { - return this._getLightShader(); - } - } else if (!fill /* || !fill.isColorShader()*/) { - return this._getColorShader(); - } - return fill; - } - - _getImmediatePointShader() { - // select the point shader to use - const point = this.states.userPointShader; - if (!point || !point.isPointShader()) { - return this._getPointShader(); - } - return point; - } - - _getRetainedLineShader() { - return this._getImmediateLineShader(); - } - - baseMaterialShader() { - if (!this._pInst._glAttributes.perPixelLighting) { - throw new Error( - 'The material shader does not support hooks without perPixelLighting. Try turning it back on.' - ); - } - return this._getLightShader(); - } - - _getLightShader() { - if (!this._defaultLightShader) { - if (this._pInst._glAttributes.perPixelLighting) { - this._defaultLightShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'highp') + - defaultShaders.phongVert, - this._webGL2CompatibilityPrefix('frag', 'highp') + - defaultShaders.phongFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', - 'vec4 combineColors': `(ColorComponents components) { - vec4 color = vec4(0.); - color.rgb += components.diffuse * components.baseColor; - color.rgb += components.ambient * components.ambientColor; - color.rgb += components.specular * components.specularColor; - color.rgb += components.emissive; - color.a = components.opacity; - return color; - }`, - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' - } - } - ); - } else { - this._defaultLightShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'highp') + - defaultShaders.lightVert, - this._webGL2CompatibilityPrefix('frag', 'highp') + - defaultShaders.lightTextureFrag - ); - } - } + p5.RendererGL.prototype.point = function(x, y, z = 0) { - return this._defaultLightShader; - } + const _vertex = []; + _vertex.push(new Vector(x, y, z)); + this._drawPoints(_vertex, this.immediateMode.buffers.point); - _getImmediateModeShader() { - if (!this._defaultImmediateModeShader) { - this._defaultImmediateModeShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.immediateVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.vertexColorFrag - ); - } + return this; + }; - return this._defaultImmediateModeShader; - } + p5.RendererGL.prototype.triangle = function(args) { + const x1 = args[0], + y1 = args[1]; + const x2 = args[2], + y2 = args[3]; + const x3 = args[4], + y3 = args[5]; + + const gId = 'tri'; + if (!this.geometryInHash(gId)) { + const _triangle = function() { + const vertices = []; + vertices.push(new Vector(0, 0, 0)); + vertices.push(new Vector(1, 0, 0)); + vertices.push(new Vector(0, 1, 0)); + this.edges = [[0, 1], [1, 2], [2, 0]]; + this.vertices = vertices; + this.faces = [[0, 1, 2]]; + this.uvs = [0, 0, 1, 0, 1, 1]; + }; + const triGeom = new Geometry(1, 1, _triangle); + triGeom._edgesToVertices(); + triGeom.computeNormals(); + this.createBuffers(gId, triGeom); + } + + // only one triangle is cached, one point is at the origin, and the + // two adjacent sides are tne unit vectors along the X & Y axes. + // + // this matrix multiplication transforms those two unit vectors + // onto the required vector prior to rendering, and moves the + // origin appropriately. + const uModelMatrix = this.states.uModelMatrix.copy(); + try { + // triangle orientation. + const orientation = Math.sign(x1*y2-x2*y1 + x2*y3-x3*y2 + x3*y1-x1*y3); + const mult = new Matrix([ + x2 - x1, y2 - y1, 0, 0, // the resulting unit X-axis + x3 - x1, y3 - y1, 0, 0, // the resulting unit Y-axis + 0, 0, orientation, 0, // the resulting unit Z-axis (Reflect the specified order of vertices) + x1, y1, 0, 1 // the resulting origin + ]).mult(this.states.uModelMatrix); + + this.states.uModelMatrix = mult; + + this.drawBuffers(gId); + } finally { + this.states.uModelMatrix = uModelMatrix; + } + + return this; + }; - baseNormalShader() { - return this._getNormalShader(); - } + p5.RendererGL.prototype.ellipse = function(args) { + this.arc( + args[0], + args[1], + args[2], + args[3], + 0, + constants.TWO_PI, + constants.OPEN, + args[4] + ); + }; - _getNormalShader() { - if (!this._defaultNormalShader) { - this._defaultNormalShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.normalVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.normalFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' - } + p5.RendererGL.prototype.arc = function(...args) { + const x = args[0]; + const y = args[1]; + const width = args[2]; + const height = args[3]; + const start = args[4]; + const stop = args[5]; + const mode = args[6]; + const detail = args[7] || 25; + + let shape; + let gId; + + // check if it is an ellipse or an arc + if (Math.abs(stop - start) >= constants.TWO_PI) { + shape = 'ellipse'; + gId = `${shape}|${detail}|`; + } else { + shape = 'arc'; + gId = `${shape}|${start}|${stop}|${mode}|${detail}|`; + } + + if (!this.geometryInHash(gId)) { + const _arc = function() { + + // if the start and stop angles are not the same, push vertices to the array + if (start.toFixed(10) !== stop.toFixed(10)) { + // if the mode specified is PIE or null, push the mid point of the arc in vertices + if (mode === constants.PIE || typeof mode === 'undefined') { + this.vertices.push(new Vector(0.5, 0.5, 0)); + this.uvs.push([0.5, 0.5]); } - ); - } - return this._defaultNormalShader; - } + // vertices for the perimeter of the circle + for (let i = 0; i <= detail; i++) { + const u = i / detail; + const theta = (stop - start) * u + start; - baseColorShader() { - return this._getColorShader(); - } + const _x = 0.5 + Math.cos(theta) / 2; + const _y = 0.5 + Math.sin(theta) / 2; - _getColorShader() { - if (!this._defaultColorShader) { - this._defaultColorShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.normalVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.basicFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'vec3 getLocalNormal': '(vec3 normal) { return normal; }', - 'vec3 getWorldNormal': '(vec3 normal) { return normal; }', - 'vec2 getUV': '(vec2 uv) { return uv; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'void afterFragment': '() {}' + this.vertices.push(new Vector(_x, _y, 0)); + this.uvs.push([_x, _y]); + + if (i < detail - 1) { + this.faces.push([0, i + 1, i + 2]); + this.edges.push([i + 1, i + 2]); } } - ); - } - return this._defaultColorShader; - } + // check the mode specified in order to push vertices and faces, different for each mode + switch (mode) { + case constants.PIE: + this.faces.push([ + 0, + this.vertices.length - 2, + this.vertices.length - 1 + ]); + this.edges.push([0, 1]); + this.edges.push([ + this.vertices.length - 2, + this.vertices.length - 1 + ]); + this.edges.push([0, this.vertices.length - 1]); + break; + + case constants.CHORD: + this.edges.push([0, 1]); + this.edges.push([0, this.vertices.length - 1]); + break; + + case constants.OPEN: + this.edges.push([0, 1]); + break; + + default: + this.faces.push([ + 0, + this.vertices.length - 2, + this.vertices.length - 1 + ]); + this.edges.push([ + this.vertices.length - 2, + this.vertices.length - 1 + ]); + } + } + }; - /** - * TODO(dave): un-private this when there is a way to actually override the - * shader used for points - * - * Get the shader used when drawing points with `point()`. - * - * You can call `pointShader().modify()` - * and change any of the following hooks: - * - `void beforeVertex`: Called at the start of the vertex shader. - * - `vec3 getLocalPosition`: Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. - * - `vec3 getWorldPosition`: Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. - * - `float getPointSize`: Update the size of the point. It takes in `float size` and must return a modified version. - * - `void afterVertex`: Called at the end of the vertex shader. - * - `void beforeFragment`: Called at the start of the fragment shader. - * - `bool shouldDiscard`: Points are drawn inside a square, with the corners discarded in the fragment shader to create a circle. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. - * - `vec4 getFinalColor`: Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. - * - `void afterFragment`: Called at the end of the fragment shader. - * - * Call `pointShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @returns {p5.Shader} The `point()` shader - * @private() - */ - pointShader() { - return this._getPointShader(); - } + const arcGeom = new Geometry(detail, 1, _arc); + arcGeom.computeNormals(); - _getPointShader() { - if (!this._defaultPointShader) { - this._defaultPointShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.pointVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.pointFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'float getPointSize': '(float size) { return size; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'bool shouldDiscard': '(bool outside) { return outside; }', - 'void afterFragment': '() {}' - } - } + if (detail <= 50) { + arcGeom._edgesToVertices(arcGeom); + } else if (this.states.doStroke) { + console.log( + `Cannot apply a stroke to an ${shape} with more than 50 detail` ); } - return this._defaultPointShader; - } - baseStrokeShader() { - return this._getLineShader(); + this.createBuffers(gId, arcGeom); } - _getLineShader() { - if (!this._defaultLineShader) { - this._defaultLineShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.lineVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.lineFrag, - { - vertex: { - 'void beforeVertex': '() {}', - 'vec3 getLocalPosition': '(vec3 position) { return position; }', - 'vec3 getWorldPosition': '(vec3 position) { return position; }', - 'float getStrokeWeight': '(float weight) { return weight; }', - 'vec2 getLineCenter': '(vec2 center) { return center; }', - 'vec2 getLinePosition': '(vec2 position) { return position; }', - 'vec4 getVertexColor': '(vec4 color) { return color; }', - 'void afterVertex': '() {}' - }, - fragment: { - 'void beforeFragment': '() {}', - 'Inputs getPixelInputs': '(Inputs inputs) { return inputs; }', - 'vec4 getFinalColor': '(vec4 color) { return color; }', - 'bool shouldDiscard': '(bool outside) { return outside; }', - 'void afterFragment': '() {}' - } - } - ); - } + const uModelMatrix = this.states.uModelMatrix.copy(); - return this._defaultLineShader; - } + try { + this.states.uModelMatrix.translate([x, y, 0]); + this.states.uModelMatrix.scale(width, height, 1); - _getFontShader() { - if (!this._defaultFontShader) { - if (this.webglVersion === constants.WEBGL) { - this.GL.getExtension('OES_standard_derivatives'); - } - this._defaultFontShader = new p5.Shader( - this, - this._webGL2CompatibilityPrefix('vert', 'mediump') + - defaultShaders.fontVert, - this._webGL2CompatibilityPrefix('frag', 'mediump') + - defaultShaders.fontFrag - ); - } - return this._defaultFontShader; + this.drawBuffers(gId); + } finally { + this.states.uModelMatrix = uModelMatrix; } - _webGL2CompatibilityPrefix( - shaderType, - floatPrecision - ) { - let code = ''; - if (this.webglVersion === constants.WEBGL2) { - code += '#version 300 es\n#define WEBGL2\n'; + return this; + }; + + p5.RendererGL.prototype.rect = function(args) { + const x = args[0]; + const y = args[1]; + const width = args[2]; + const height = args[3]; + + if (typeof args[4] === 'undefined') { + // Use the retained mode for drawing rectangle, + // if args for rounding rectangle is not provided by user. + const perPixelLighting = this._pInst._glAttributes.perPixelLighting; + const detailX = args[4] || (perPixelLighting ? 1 : 24); + const detailY = args[5] || (perPixelLighting ? 1 : 16); + const gId = `rect|${detailX}|${detailY}`; + if (!this.geometryInHash(gId)) { + const _rect = function() { + for (let i = 0; i <= this.detailY; i++) { + const v = i / this.detailY; + for (let j = 0; j <= this.detailX; j++) { + const u = j / this.detailX; + const p = new Vector(u, v, 0); + this.vertices.push(p); + this.uvs.push(u, v); + } + } + // using stroke indices to avoid stroke over face(s) of rectangle + if (detailX > 0 && detailY > 0) { + this.edges = [ + [0, detailX], + [detailX, (detailX + 1) * (detailY + 1) - 1], + [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY], + [(detailX + 1) * detailY, 0] + ]; + } + }; + const rectGeom = new Geometry(detailX, detailY, _rect); + rectGeom + .computeFaces() + .computeNormals() + ._edgesToVertices(); + this.createBuffers(gId, rectGeom); + } + + // only a single rectangle (of a given detail) is cached: a square with + // opposite corners at (0,0) & (1,1). + // + // before rendering, this square is scaled & moved to the required location. + const uModelMatrix = this.states.uModelMatrix.copy(); + try { + this.states.uModelMatrix.translate([x, y, 0]); + this.states.uModelMatrix.scale(width, height, 1); + + this.drawBuffers(gId); + } finally { + this.states.uModelMatrix = uModelMatrix; + } + } else { + // Use Immediate mode to round the rectangle corner, + // if args for rounding corners is provided by user + let tl = args[4]; + let tr = typeof args[5] === 'undefined' ? tl : args[5]; + let br = typeof args[6] === 'undefined' ? tr : args[6]; + let bl = typeof args[7] === 'undefined' ? br : args[7]; + + let a = x; + let b = y; + let c = width; + let d = height; + + c += a; + d += b; + + if (a > c) { + const temp = a; + a = c; + c = temp; + } + + if (b > d) { + const temp = b; + b = d; + d = temp; + } + + const maxRounding = Math.min((c - a) / 2, (d - b) / 2); + if (tl > maxRounding) tl = maxRounding; + if (tr > maxRounding) tr = maxRounding; + if (br > maxRounding) br = maxRounding; + if (bl > maxRounding) bl = maxRounding; + + let x1 = a; + let y1 = b; + let x2 = c; + let y2 = d; + + this.beginShape(); + if (tr !== 0) { + this.vertex(x2 - tr, y1); + this.quadraticVertex(x2, y1, x2, y1 + tr); + } else { + this.vertex(x2, y1); } - if (shaderType === 'vert') { - code += '#define VERTEX_SHADER\n'; - } else if (shaderType === 'frag') { - code += '#define FRAGMENT_SHADER\n'; + if (br !== 0) { + this.vertex(x2, y2 - br); + this.quadraticVertex(x2, y2, x2 - br, y2); + } else { + this.vertex(x2, y2); } - if (floatPrecision) { - code += `precision ${floatPrecision} float;\n`; + if (bl !== 0) { + this.vertex(x1 + bl, y2); + this.quadraticVertex(x1, y2, x1, y2 - bl); + } else { + this.vertex(x1, y2); } - return code; - } - - _getEmptyTexture() { - if (!this._emptyTexture) { - // a plain white texture RGBA, full alpha, single pixel. - const im = new p5.Image(1, 1); - im.set(0, 0, 255); - this._emptyTexture = new p5.Texture(this, im); + if (tl !== 0) { + this.vertex(x1, y1 + tl); + this.quadraticVertex(x1, y1, x1 + tl, y1); + } else { + this.vertex(x1, y1); } - return this._emptyTexture; - } - getTexture(input) { - let src = input; - if (src instanceof p5.Framebuffer) { - src = src.color; + this.immediateMode.geometry.uvs.length = 0; + for (const vert of this.immediateMode.geometry.vertices) { + const u = (vert.x - x1) / width; + const v = (vert.y - y1) / height; + this.immediateMode.geometry.uvs.push(u, v); } - const texture = this.textures.get(src); - if (texture) { - return texture; - } + this.endShape(constants.CLOSE); + } + return this; + }; - const tex = new p5.Texture(this, src); - this.textures.set(src, tex); - return tex; - } - /* - * used in imageLight, - * To create a blurry image from the input non blurry img, if it doesn't already exist - * Add it to the diffusedTexture map, - * Returns the blurry image - * maps a p5.Image used by imageLight() to a p5.Framebuffer - */ - getDiffusedTexture(input) { - // if one already exists for a given input image - if (this.diffusedTextures.get(input) != null) { - return this.diffusedTextures.get(input); - } - // if not, only then create one - let newFramebuffer; - // hardcoded to 200px, because it's going to be blurry and smooth - let smallWidth = 200; - let width = smallWidth; - let height = Math.floor(smallWidth * (input.height / input.width)); - newFramebuffer = this._pInst.createFramebuffer({ - width, height, density: 1 - }); - // create framebuffer is like making a new sketch, all functions on main - // sketch it would be available on framebuffer - if (!this.states.diffusedShader) { - this.states.diffusedShader = this._pInst.createShader( - defaultShaders.imageLightVert, - defaultShaders.imageLightDiffusedFrag - ); - } - newFramebuffer.draw(() => { - this._pInst.shader(this.states.diffusedShader); - this.states.diffusedShader.setUniform('environmentMap', input); - this._pInst.noStroke(); - this._pInst.rectMode(constants.CENTER); - this._pInst.noLights(); - this._pInst.rect(0, 0, width, height); - }); - this.diffusedTextures.set(input, newFramebuffer); - return newFramebuffer; - } - - /* - * used in imageLight, - * To create a texture from the input non blurry image, if it doesn't already exist - * Creating 8 different levels of textures according to different - * sizes and atoring them in `levels` array - * Creating a new Mipmap texture with that `levels` array - * Storing the texture for input image in map called `specularTextures` - * maps the input p5.Image to a p5.MipmapTexture - */ - getSpecularTexture(input) { - // check if already exits (there are tex of diff resolution so which one to check) - // currently doing the whole array - if (this.specularTextures.get(input) != null) { - return this.specularTextures.get(input); - } - // Hardcoded size - const size = 512; - let tex; - const levels = []; - const framebuffer = this._pInst.createFramebuffer({ - width: size, height: size, density: 1 + /* eslint-disable max-len */ + p5.RendererGL.prototype.quad = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, detailX=2, detailY=2) { + /* eslint-enable max-len */ + + const gId = + `quad|${x1}|${y1}|${z1}|${x2}|${y2}|${z2}|${x3}|${y3}|${z3}|${x4}|${y4}|${z4}|${detailX}|${detailY}`; + + if (!this.geometryInHash(gId)) { + const quadGeom = new Geometry(detailX, detailY, function() { + //algorithm adapted from c++ to js + //https://stackoverflow.com/questions/16989181/whats-the-correct-way-to-draw-a-distorted-plane-in-opengl/16993202#16993202 + let xRes = 1.0 / (this.detailX - 1); + let yRes = 1.0 / (this.detailY - 1); + for (let y = 0; y < this.detailY; y++) { + for (let x = 0; x < this.detailX; x++) { + let pctx = x * xRes; + let pcty = y * yRes; + + let linePt0x = (1 - pcty) * x1 + pcty * x4; + let linePt0y = (1 - pcty) * y1 + pcty * y4; + let linePt0z = (1 - pcty) * z1 + pcty * z4; + let linePt1x = (1 - pcty) * x2 + pcty * x3; + let linePt1y = (1 - pcty) * y2 + pcty * y3; + let linePt1z = (1 - pcty) * z2 + pcty * z3; + + let ptx = (1 - pctx) * linePt0x + pctx * linePt1x; + let pty = (1 - pctx) * linePt0y + pctx * linePt1y; + let ptz = (1 - pctx) * linePt0z + pctx * linePt1z; + + this.vertices.push(new Vector(ptx, pty, ptz)); + this.uvs.push([pctx, pcty]); + } + } }); - let count = Math.log(size) / Math.log(2); - if (!this.states.specularShader) { - this.states.specularShader = this._pInst.createShader( - defaultShaders.imageLightVert, - defaultShaders.imageLightSpecularFrag - ); + + quadGeom.faces = []; + for(let y = 0; y < detailY-1; y++){ + for(let x = 0; x < detailX-1; x++){ + let pt0 = x + y * detailX; + let pt1 = (x + 1) + y * detailX; + let pt2 = (x + 1) + (y + 1) * detailX; + let pt3 = x + (y + 1) * detailX; + quadGeom.faces.push([pt0, pt1, pt2]); + quadGeom.faces.push([pt0, pt2, pt3]); + } } - // currently only 8 levels - // This loop calculates 8 framebuffers of varying size of canvas - // and corresponding different roughness levels. - // Roughness increases with the decrease in canvas size, - // because rougher surfaces have less detailed/more blurry reflections. - for (let w = size; w >= 1; w /= 2) { - framebuffer.resize(w, w); - let currCount = Math.log(w) / Math.log(2); - let roughness = 1 - currCount / count; - framebuffer.draw(() => { - this._pInst.shader(this.states.specularShader); - this._pInst.clear(); - this.states.specularShader.setUniform('environmentMap', input); - this.states.specularShader.setUniform('roughness', roughness); - this._pInst.noStroke(); - this._pInst.noLights(); - this._pInst.plane(w, w); - }); - levels.push(framebuffer.get().drawingContext.getImageData(0, 0, w, w)); + quadGeom.computeNormals(); + quadGeom.edges.length = 0; + const vertexOrder = [0, 2, 3, 1]; + for (let i = 0; i < vertexOrder.length; i++) { + const startVertex = vertexOrder[i]; + const endVertex = vertexOrder[(i + 1) % vertexOrder.length]; + quadGeom.edges.push([startVertex, endVertex]); } - // Free the Framebuffer - framebuffer.remove(); - tex = new p5.MipmapTexture(this, levels, {}); - this.specularTextures.set(input, tex); - return tex; - } - - /** - * @private - * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if - * the main canvas is the current draw target. - */ - activeFramebuffer() { - return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null; + quadGeom._edgesToVertices(); + this.createBuffers(gId, quadGeom); } + this.drawBuffers(gId); + return this; + }; - createFramebuffer(options) { - return new p5.Framebuffer(this, options); + //this implementation of bezier curve + //is based on Bernstein polynomial + // pretier-ignore + p5.RendererGL.prototype.bezier = function( + x1, + y1, + z1, // x2 + x2, // y2 + y2, // x3 + z2, // y3 + x3, // x4 + y3, // y4 + z3, + x4, + y4, + z4 + ) { + if (arguments.length === 8) { + y4 = y3; + x4 = x3; + y3 = z2; + x3 = y2; + y2 = x2; + x2 = z1; + z1 = z2 = z3 = z4 = 0; + } + const bezierDetail = this._pInst._bezierDetail || 20; //value of Bezier detail + this.beginShape(); + for (let i = 0; i <= bezierDetail; i++) { + const c1 = Math.pow(1 - i / bezierDetail, 3); + const c2 = 3 * (i / bezierDetail) * Math.pow(1 - i / bezierDetail, 2); + const c3 = 3 * Math.pow(i / bezierDetail, 2) * (1 - i / bezierDetail); + const c4 = Math.pow(i / bezierDetail, 3); + this.vertex( + x1 * c1 + x2 * c2 + x3 * c3 + x4 * c4, + y1 * c1 + y2 * c2 + y3 * c3 + y4 * c4, + z1 * c1 + z2 * c2 + z3 * c3 + z4 * c4 + ); } + this.endShape(); + return this; + }; - _setStrokeUniforms(baseStrokeShader) { - baseStrokeShader.bindShader(); + // pretier-ignore + p5.RendererGL.prototype.curve = function( + x1, + y1, + z1, // x2 + x2, // y2 + y2, // x3 + z2, // y3 + x3, // x4 + y3, // y4 + z3, + x4, + y4, + z4 + ) { + if (arguments.length === 8) { + x4 = x3; + y4 = y3; + x3 = y2; + y3 = x2; + x2 = z1; + y2 = x2; + z1 = z2 = z3 = z4 = 0; + } + const curveDetail = this._pInst._curveDetail; + this.beginShape(); + for (let i = 0; i <= curveDetail; i++) { + const c1 = Math.pow(i / curveDetail, 3) * 0.5; + const c2 = Math.pow(i / curveDetail, 2) * 0.5; + const c3 = i / curveDetail * 0.5; + const c4 = 0.5; + const vx = + c1 * (-x1 + 3 * x2 - 3 * x3 + x4) + + c2 * (2 * x1 - 5 * x2 + 4 * x3 - x4) + + c3 * (-x1 + x3) + + c4 * (2 * x2); + const vy = + c1 * (-y1 + 3 * y2 - 3 * y3 + y4) + + c2 * (2 * y1 - 5 * y2 + 4 * y3 - y4) + + c3 * (-y1 + y3) + + c4 * (2 * y2); + const vz = + c1 * (-z1 + 3 * z2 - 3 * z3 + z4) + + c2 * (2 * z1 - 5 * z2 + 4 * z3 - z4) + + c3 * (-z1 + z3) + + c4 * (2 * z2); + this.vertex(vx, vy, vz); + } + this.endShape(); + return this; + }; - // set the uniform values - baseStrokeShader.setUniform('uUseLineColor', this._useLineColor); - baseStrokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); - baseStrokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); - baseStrokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); - baseStrokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); - } + /** + * Draw a line given two points + * @private + * @param {Number} x0 x-coordinate of first vertex + * @param {Number} y0 y-coordinate of first vertex + * @param {Number} z0 z-coordinate of first vertex + * @param {Number} x1 x-coordinate of second vertex + * @param {Number} y1 y-coordinate of second vertex + * @param {Number} z1 z-coordinate of second vertex + * @chainable + * @example + *
+ * + * //draw a line + * function setup() { + * createCanvas(100, 100, WEBGL); + * } + * + * function draw() { + * background(200); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * // Use fill instead of stroke to change the color of shape. + * fill(255, 0, 0); + * line(10, 10, 0, 60, 60, 20); + * } + * + *
+ */ + p5.RendererGL.prototype.line = function(...args) { + if (args.length === 6) { + this.beginShape(constants.LINES); + this.vertex(args[0], args[1], args[2]); + this.vertex(args[3], args[4], args[5]); + this.endShape(); + } else if (args.length === 4) { + this.beginShape(constants.LINES); + this.vertex(args[0], args[1], 0); + this.vertex(args[2], args[3], 0); + this.endShape(); + } + return this; + }; - _setFillUniforms(fillShader) { - fillShader.bindShader(); + p5.RendererGL.prototype.bezierVertex = function(...args) { + if (this.immediateMode._bezierVertex.length === 0) { + throw Error('vertex() must be used once before calling bezierVertex()'); + } else { + let w_x = []; + let w_y = []; + let w_z = []; + let t, _x, _y, _z, i, k, m; + // variable i for bezierPoints, k for components, and m for anchor points. + const argLength = args.length; - this.mixedSpecularColor = [...this.states.curSpecularColor]; + t = 0; - if (this.states._useMetalness > 0) { - this.mixedSpecularColor = this.mixedSpecularColor.map( - (mixedSpecularColor, index) => - this.states.curFillColor[index] * this.states._useMetalness + - mixedSpecularColor * (1 - this.states._useMetalness) - ); + if ( + this._lookUpTableBezier.length === 0 || + this._lutBezierDetail !== this._pInst._curveDetail + ) { + this._lookUpTableBezier = []; + this._lutBezierDetail = this._pInst._curveDetail; + const step = 1 / this._lutBezierDetail; + let start = 0; + let end = 1; + let j = 0; + while (start < 1) { + t = parseFloat(start.toFixed(6)); + this._lookUpTableBezier[j] = this._bezierCoefficients(t); + if (end.toFixed(6) === step.toFixed(6)) { + t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); + ++j; + this._lookUpTableBezier[j] = this._bezierCoefficients(t); + break; + } + start += step; + end -= step; + ++j; + } } - // TODO: optimize - fillShader.setUniform('uUseVertexColor', this._useVertexColor); - fillShader.setUniform('uMaterialColor', this.states.curFillColor); - fillShader.setUniform('isTexture', !!this.states._tex); - if (this.states._tex) { - fillShader.setUniform('uSampler', this.states._tex); + const LUTLength = this._lookUpTableBezier.length; + const immediateGeometry = this.immediateMode.geometry; + + // fillColors[0]: start point color + // fillColors[1],[2]: control point color + // fillColors[3]: end point color + const fillColors = []; + for (m = 0; m < 4; m++) fillColors.push([]); + fillColors[0] = immediateGeometry.vertexColors.slice(-4); + fillColors[3] = this.states.curFillColor.slice(); + + // Do the same for strokeColor. + const strokeColors = []; + for (m = 0; m < 4; m++) strokeColors.push([]); + strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); + strokeColors[3] = this.states.curStrokeColor.slice(); + + // Do the same for custom vertex properties + const userVertexProperties = {}; + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + userVertexProperties[propName] = []; + for (m = 0; m < 4; m++) userVertexProperties[propName].push([]); + userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); + userVertexProperties[propName][3] = prop.getCurrentData(); + } + + if (argLength === 6) { + this.isBezier = true; + + w_x = [this.immediateMode._bezierVertex[0], args[0], args[2], args[4]]; + w_y = [this.immediateMode._bezierVertex[1], args[1], args[3], args[5]]; + // The ratio of the distance between the start point, the two control- + // points, and the end point determines the intermediate color. + let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1]); + let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); + let d2 = Math.hypot(w_x[2]-w_x[3], w_y[2]-w_y[3]); + const totalLength = d0 + d1 + d2; + d0 /= totalLength; + d2 /= totalLength; + for (k = 0; k < 4; k++) { + fillColors[1].push( + fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 + ); + fillColors[2].push( + fillColors[0][k] * d2 + fillColors[3][k] * (1-d2) + ); + strokeColors[1].push( + strokeColors[0][k] * (1-d0) + strokeColors[3][k] * d0 + ); + strokeColors[2].push( + strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) + ); + } + for (const propName in immediateGeometry.userVertexProperties){ + const size = immediateGeometry.userVertexProperties[propName].getDataSize(); + for (k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 + ); + userVertexProperties[propName][2].push( + userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 + ); + } + } + + for (let i = 0; i < LUTLength; i++) { + // Interpolate colors using control points + this.states.curFillColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 0]; + _x = _y = 0; + for (let m = 0; m < 4; m++) { + for (let k = 0; k < 4; k++) { + this.states.curFillColor[k] += + this._lookUpTableBezier[i][m] * fillColors[m][k]; + this.states.curStrokeColor[k] += + this._lookUpTableBezier[i][m] * strokeColors[m][k]; + } + _x += w_x[m] * this._lookUpTableBezier[i][m]; + _y += w_y[m] * this._lookUpTableBezier[i][m]; + } + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 4; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } + this.vertex(_x, _y); + } + // so that we leave currentColor with the last value the user set it to + this.states.curFillColor = fillColors[3]; + this.states.curStrokeColor = strokeColors[3]; + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + prop.setCurrentData(userVertexProperties[propName][2]); + } + this.immediateMode._bezierVertex[0] = args[4]; + this.immediateMode._bezierVertex[1] = args[5]; + } else if (argLength === 9) { + this.isBezier = true; + + w_x = [this.immediateMode._bezierVertex[0], args[0], args[3], args[6]]; + w_y = [this.immediateMode._bezierVertex[1], args[1], args[4], args[7]]; + w_z = [this.immediateMode._bezierVertex[2], args[2], args[5], args[8]]; + // The ratio of the distance between the start point, the two control- + // points, and the end point determines the intermediate color. + let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1], w_z[0]-w_z[1]); + let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2], w_z[1]-w_z[2]); + let d2 = Math.hypot(w_x[2]-w_x[3], w_y[2]-w_y[3], w_z[2]-w_z[3]); + const totalLength = d0 + d1 + d2; + d0 /= totalLength; + d2 /= totalLength; + for (let k = 0; k < 4; k++) { + fillColors[1].push( + fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 + ); + fillColors[2].push( + fillColors[0][k] * d2 + fillColors[3][k] * (1-d2) + ); + strokeColors[1].push( + strokeColors[0][k] * (1-d0) + strokeColors[3][k] * d0 + ); + strokeColors[2].push( + strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) + ); + } + for (const propName in immediateGeometry.userVertexProperties){ + const size = immediateGeometry.userVertexProperties[propName].getDataSize(); + for (k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 + ); + userVertexProperties[propName][2].push( + userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 + ); + } + } + for (let i = 0; i < LUTLength; i++) { + // Interpolate colors using control points + this.states.curFillColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 0]; + _x = _y = _z = 0; + for (m = 0; m < 4; m++) { + for (k = 0; k < 4; k++) { + this.states.curFillColor[k] += + this._lookUpTableBezier[i][m] * fillColors[m][k]; + this.states.curStrokeColor[k] += + this._lookUpTableBezier[i][m] * strokeColors[m][k]; + } + _x += w_x[m] * this._lookUpTableBezier[i][m]; + _y += w_y[m] * this._lookUpTableBezier[i][m]; + _z += w_z[m] * this._lookUpTableBezier[i][m]; + } + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 4; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } + this.vertex(_x, _y, _z); + } + // so that we leave currentColor with the last value the user set it to + this.states.curFillColor = fillColors[3]; + this.states.curStrokeColor = strokeColors[3]; + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + prop.setCurrentData(userVertexProperties[propName][2]); + } + this.immediateMode._bezierVertex[0] = args[6]; + this.immediateMode._bezierVertex[1] = args[7]; + this.immediateMode._bezierVertex[2] = args[8]; } - fillShader.setUniform('uTint', this.states.tint); - - fillShader.setUniform('uHasSetAmbient', this.states._hasSetAmbient); - fillShader.setUniform('uAmbientMatColor', this.states.curAmbientColor); - fillShader.setUniform('uSpecularMatColor', this.mixedSpecularColor); - fillShader.setUniform('uEmissiveMatColor', this.states.curEmissiveColor); - fillShader.setUniform('uSpecular', this.states._useSpecularMaterial); - fillShader.setUniform('uEmissive', this.states._useEmissiveMaterial); - fillShader.setUniform('uShininess', this.states._useShininess); - fillShader.setUniform('uMetallic', this.states._useMetalness); - - this._setImageLightUniforms(fillShader); - - fillShader.setUniform('uUseLighting', this.states._enableLighting); - - const pointLightCount = this.states.pointLightDiffuseColors.length / 3; - fillShader.setUniform('uPointLightCount', pointLightCount); - fillShader.setUniform('uPointLightLocation', this.states.pointLightPositions); - fillShader.setUniform( - 'uPointLightDiffuseColors', - this.states.pointLightDiffuseColors - ); - fillShader.setUniform( - 'uPointLightSpecularColors', - this.states.pointLightSpecularColors - ); + } + }; - const directionalLightCount = this.states.directionalLightDiffuseColors.length / 3; - fillShader.setUniform('uDirectionalLightCount', directionalLightCount); - fillShader.setUniform('uLightingDirection', this.states.directionalLightDirections); - fillShader.setUniform( - 'uDirectionalDiffuseColors', - this.states.directionalLightDiffuseColors - ); - fillShader.setUniform( - 'uDirectionalSpecularColors', - this.states.directionalLightSpecularColors - ); + p5.RendererGL.prototype.quadraticVertex = function(...args) { + if (this.immediateMode._quadraticVertex.length === 0) { + throw Error('vertex() must be used once before calling quadraticVertex()'); + } else { + let w_x = []; + let w_y = []; + let w_z = []; + let t, _x, _y, _z, i, k, m; + // variable i for bezierPoints, k for components, and m for anchor points. + const argLength = args.length; - // TODO: sum these here... - const ambientLightCount = this.states.ambientLightColors.length / 3; - this.mixedAmbientLight = [...this.states.ambientLightColors]; + t = 0; - if (this.states._useMetalness > 0) { - this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors => { - let mixing = ambientColors - this.states._useMetalness; - return Math.max(0, mixing); - })); + if ( + this._lookUpTableQuadratic.length === 0 || + this._lutQuadraticDetail !== this._pInst._curveDetail + ) { + this._lookUpTableQuadratic = []; + this._lutQuadraticDetail = this._pInst._curveDetail; + const step = 1 / this._lutQuadraticDetail; + let start = 0; + let end = 1; + let j = 0; + while (start < 1) { + t = parseFloat(start.toFixed(6)); + this._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); + if (end.toFixed(6) === step.toFixed(6)) { + t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); + ++j; + this._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); + break; + } + start += step; + end -= step; + ++j; + } } - fillShader.setUniform('uAmbientLightCount', ambientLightCount); - fillShader.setUniform('uAmbientColor', this.mixedAmbientLight); - - const spotLightCount = this.states.spotLightDiffuseColors.length / 3; - fillShader.setUniform('uSpotLightCount', spotLightCount); - fillShader.setUniform('uSpotLightAngle', this.states.spotLightAngle); - fillShader.setUniform('uSpotLightConc', this.states.spotLightConc); - fillShader.setUniform('uSpotLightDiffuseColors', this.states.spotLightDiffuseColors); - fillShader.setUniform( - 'uSpotLightSpecularColors', - this.states.spotLightSpecularColors - ); - fillShader.setUniform('uSpotLightLocation', this.states.spotLightPositions); - fillShader.setUniform('uSpotLightDirection', this.states.spotLightDirections); - fillShader.setUniform('uConstantAttenuation', this.states.constantAttenuation); - fillShader.setUniform('uLinearAttenuation', this.states.linearAttenuation); - fillShader.setUniform('uQuadraticAttenuation', this.states.quadraticAttenuation); + const LUTLength = this._lookUpTableQuadratic.length; + const immediateGeometry = this.immediateMode.geometry; + + // fillColors[0]: start point color + // fillColors[1]: control point color + // fillColors[2]: end point color + const fillColors = []; + for (m = 0; m < 3; m++) fillColors.push([]); + fillColors[0] = immediateGeometry.vertexColors.slice(-4); + fillColors[2] = this.states.curFillColor.slice(); + + // Do the same for strokeColor. + const strokeColors = []; + for (m = 0; m < 3; m++) strokeColors.push([]); + strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); + strokeColors[2] = this.states.curStrokeColor.slice(); + + // Do the same for user defined vertex properties + const userVertexProperties = {}; + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + userVertexProperties[propName] = []; + for (m = 0; m < 3; m++) userVertexProperties[propName].push([]); + userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); + userVertexProperties[propName][2] = prop.getCurrentData(); + } + + if (argLength === 4) { + this.isQuadratic = true; + + w_x = [this.immediateMode._quadraticVertex[0], args[0], args[2]]; + w_y = [this.immediateMode._quadraticVertex[1], args[1], args[3]]; + + // The ratio of the distance between the start point, the control- + // point, and the end point determines the intermediate color. + let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1]); + let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); + const totalLength = d0 + d1; + d0 /= totalLength; + for (let k = 0; k < 4; k++) { + fillColors[1].push( + fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 + ); + strokeColors[1].push( + strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 + ); + } + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + for (let k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 + ); + } + } - fillShader.bindTextures(); - } + for (let i = 0; i < LUTLength; i++) { + // Interpolate colors using control points + this.states.curFillColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 0]; + _x = _y = 0; + for (let m = 0; m < 3; m++) { + for (let k = 0; k < 4; k++) { + this.states.curFillColor[k] += + this._lookUpTableQuadratic[i][m] * fillColors[m][k]; + this.states.curStrokeColor[k] += + this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; + } + _x += w_x[m] * this._lookUpTableQuadratic[i][m]; + _y += w_y[m] * this._lookUpTableQuadratic[i][m]; + } - // getting called from _setFillUniforms - _setImageLightUniforms(shader) { - //set uniform values - shader.setUniform('uUseImageLight', this.states.activeImageLight != null); - // true - if (this.states.activeImageLight) { - // this.states.activeImageLight has image as a key - // look up the texture from the diffusedTexture map - let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); - shader.setUniform('environmentMapDiffused', diffusedLight); - let specularLight = this.getSpecularTexture(this.states.activeImageLight); + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 3; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } + this.vertex(_x, _y); + } - shader.setUniform('environmentMapSpecular', specularLight); - } - } + // so that we leave currentColor with the last value the user set it to + this.states.curFillColor = fillColors[2]; + this.states.curStrokeColor = strokeColors[2]; + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + prop.setCurrentData(userVertexProperties[propName][2]); + } + this.immediateMode._quadraticVertex[0] = args[2]; + this.immediateMode._quadraticVertex[1] = args[3]; + } else if (argLength === 6) { + this.isQuadratic = true; + + w_x = [this.immediateMode._quadraticVertex[0], args[0], args[3]]; + w_y = [this.immediateMode._quadraticVertex[1], args[1], args[4]]; + w_z = [this.immediateMode._quadraticVertex[2], args[2], args[5]]; + + // The ratio of the distance between the start point, the control- + // point, and the end point determines the intermediate color. + let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1], w_z[0]-w_z[1]); + let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2], w_z[1]-w_z[2]); + const totalLength = d0 + d1; + d0 /= totalLength; + for (k = 0; k < 4; k++) { + fillColors[1].push( + fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 + ); + strokeColors[1].push( + strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 + ); + } - _setPointUniforms(pointShader) { - pointShader.bindShader(); + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + for (let k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 + ); + } + } - // set the uniform values - pointShader.setUniform('uMaterialColor', this.states.curStrokeColor); - // @todo is there an instance where this isn't stroke weight? - // should be they be same var? - pointShader.setUniform( - 'uPointSize', - this.pointSize * this._pixelDensity - ); + for (i = 0; i < LUTLength; i++) { + // Interpolate colors using control points + this.states.curFillColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 0]; + _x = _y = _z = 0; + for (m = 0; m < 3; m++) { + for (k = 0; k < 4; k++) { + this.states.curFillColor[k] += + this._lookUpTableQuadratic[i][m] * fillColors[m][k]; + this.states.curStrokeColor[k] += + this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; + } + _x += w_x[m] * this._lookUpTableQuadratic[i][m]; + _y += w_y[m] * this._lookUpTableQuadratic[i][m]; + _z += w_z[m] * this._lookUpTableQuadratic[i][m]; + } + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 3; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } + this.vertex(_x, _y, _z); + } + + // so that we leave currentColor with the last value the user set it to + this.states.curFillColor = fillColors[2]; + this.states.curStrokeColor = strokeColors[2]; + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + prop.setCurrentData(userVertexProperties[propName][2]); + } + this.immediateMode._quadraticVertex[0] = args[3]; + this.immediateMode._quadraticVertex[1] = args[4]; + this.immediateMode._quadraticVertex[2] = args[5]; + } } + }; - /* Binds a buffer to the drawing context - * when passed more than two arguments it also updates or initializes - * the data associated with the buffer - */ - _bindBuffer( - buffer, - target, - values, - type, - usage + p5.RendererGL.prototype.curveVertex = function(...args) { + let w_x = []; + let w_y = []; + let w_z = []; + let t, _x, _y, _z, i; + t = 0; + const argLength = args.length; + + if ( + this._lookUpTableBezier.length === 0 || + this._lutBezierDetail !== this._pInst._curveDetail ) { - if (!target) target = this.GL.ARRAY_BUFFER; - this.GL.bindBuffer(target, buffer); - if (values !== undefined) { - let data = values; - if (values instanceof p5.DataArray) { - data = values.dataArray(); - } else if (!(data instanceof (type || Float32Array))) { - data = new (type || Float32Array)(data); + this._lookUpTableBezier = []; + this._lutBezierDetail = this._pInst._curveDetail; + const step = 1 / this._lutBezierDetail; + let start = 0; + let end = 1; + let j = 0; + while (start < 1) { + t = parseFloat(start.toFixed(6)); + this._lookUpTableBezier[j] = this._bezierCoefficients(t); + if (end.toFixed(6) === step.toFixed(6)) { + t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); + ++j; + this._lookUpTableBezier[j] = this._bezierCoefficients(t); + break; } - this.GL.bufferData(target, data, usage || this.GL.STATIC_DRAW); + start += step; + end -= step; + ++j; } } - /////////////////////////////// - //// UTILITY FUNCTIONS - ////////////////////////////// - _arraysEqual(a, b) { - const aLength = a.length; - if (aLength !== b.length) return false; - return a.every((ai, i) => ai === b[i]); - } - - _isTypedArray(arr) { - return [ - Float32Array, - Float64Array, - Int16Array, - Uint16Array, - Uint32Array - ].some(x => arr instanceof x); - } - /** - * turn a two dimensional array into one dimensional array - * @private - * @param {Array} arr 2-dimensional array - * @return {Array} 1-dimensional array - * [[1, 2, 3],[4, 5, 6]] -> [1, 2, 3, 4, 5, 6] - */ - _flatten(arr) { - return arr.flat(); - } - - /** - * turn a p5.Vector Array into a one dimensional number array - * @private - * @param {p5.Vector[]} arr an array of p5.Vector - * @return {Number[]} a one dimensional array of numbers - * [p5.Vector(1, 2, 3), p5.Vector(4, 5, 6)] -> - * [1, 2, 3, 4, 5, 6] - */ - _vToNArray(arr) { - return arr.flatMap(item => [item.x, item.y, item.z]); - } - - // function to calculate BezierVertex Coefficients - _bezierCoefficients(t) { - const t2 = t * t; - const t3 = t2 * t; - const mt = 1 - t; - const mt2 = mt * mt; - const mt3 = mt2 * mt; - return [mt3, 3 * mt2 * t, 3 * mt * t2, t3]; - } - - // function to calculate QuadraticVertex Coefficients - _quadraticCoefficients(t) { - const t2 = t * t; - const mt = 1 - t; - const mt2 = mt * mt; - return [mt2, 2 * mt * t, t2]; - } - - // function to convert Bezier coordinates to Catmull Rom Splines - _bezierToCatmull(w) { - const p1 = w[1]; - const p2 = w[1] + (w[2] - w[0]) / this._curveTightness; - const p3 = w[2] - (w[3] - w[1]) / this._curveTightness; - const p4 = w[2]; - const p = [p1, p2, p3, p4]; - return p; - } - _initTessy() { - this.tessyVertexSize = 12; - // function called for each vertex of tesselator output - function vertexCallback(data, polyVertArray) { - for (const element of data) { - polyVertArray.push(element); + const LUTLength = this._lookUpTableBezier.length; + + if (argLength === 2) { + this.immediateMode._curveVertex.push(args[0]); + this.immediateMode._curveVertex.push(args[1]); + if (this.immediateMode._curveVertex.length === 8) { + this.isCurve = true; + w_x = this._bezierToCatmull([ + this.immediateMode._curveVertex[0], + this.immediateMode._curveVertex[2], + this.immediateMode._curveVertex[4], + this.immediateMode._curveVertex[6] + ]); + w_y = this._bezierToCatmull([ + this.immediateMode._curveVertex[1], + this.immediateMode._curveVertex[3], + this.immediateMode._curveVertex[5], + this.immediateMode._curveVertex[7] + ]); + for (i = 0; i < LUTLength; i++) { + _x = + w_x[0] * this._lookUpTableBezier[i][0] + + w_x[1] * this._lookUpTableBezier[i][1] + + w_x[2] * this._lookUpTableBezier[i][2] + + w_x[3] * this._lookUpTableBezier[i][3]; + _y = + w_y[0] * this._lookUpTableBezier[i][0] + + w_y[1] * this._lookUpTableBezier[i][1] + + w_y[2] * this._lookUpTableBezier[i][2] + + w_y[3] * this._lookUpTableBezier[i][3]; + this.vertex(_x, _y); + } + for (i = 0; i < argLength; i++) { + this.immediateMode._curveVertex.shift(); } } - - function begincallback(type) { - if (type !== libtess.primitiveType.GL_TRIANGLES) { - console.log(`expected TRIANGLES but got type: ${type}`); + } else if (argLength === 3) { + this.immediateMode._curveVertex.push(args[0]); + this.immediateMode._curveVertex.push(args[1]); + this.immediateMode._curveVertex.push(args[2]); + if (this.immediateMode._curveVertex.length === 12) { + this.isCurve = true; + w_x = this._bezierToCatmull([ + this.immediateMode._curveVertex[0], + this.immediateMode._curveVertex[3], + this.immediateMode._curveVertex[6], + this.immediateMode._curveVertex[9] + ]); + w_y = this._bezierToCatmull([ + this.immediateMode._curveVertex[1], + this.immediateMode._curveVertex[4], + this.immediateMode._curveVertex[7], + this.immediateMode._curveVertex[10] + ]); + w_z = this._bezierToCatmull([ + this.immediateMode._curveVertex[2], + this.immediateMode._curveVertex[5], + this.immediateMode._curveVertex[8], + this.immediateMode._curveVertex[11] + ]); + for (i = 0; i < LUTLength; i++) { + _x = + w_x[0] * this._lookUpTableBezier[i][0] + + w_x[1] * this._lookUpTableBezier[i][1] + + w_x[2] * this._lookUpTableBezier[i][2] + + w_x[3] * this._lookUpTableBezier[i][3]; + _y = + w_y[0] * this._lookUpTableBezier[i][0] + + w_y[1] * this._lookUpTableBezier[i][1] + + w_y[2] * this._lookUpTableBezier[i][2] + + w_y[3] * this._lookUpTableBezier[i][3]; + _z = + w_z[0] * this._lookUpTableBezier[i][0] + + w_z[1] * this._lookUpTableBezier[i][1] + + w_z[2] * this._lookUpTableBezier[i][2] + + w_z[3] * this._lookUpTableBezier[i][3]; + this.vertex(_x, _y, _z); + } + for (i = 0; i < argLength; i++) { + this.immediateMode._curveVertex.shift(); } } + } + }; - function errorcallback(errno) { - console.log('error callback'); - console.log(`error number: ${errno}`); - } - // callback for when segments intersect and must be split - const combinecallback = (coords, data, weight) => { - const result = new Array(this.tessyVertexSize).fill(0); - for (let i = 0; i < weight.length; i++) { - for (let j = 0; j < result.length; j++) { - if (weight[i] === 0 || !data[i]) continue; - result[j] += data[i][j] * weight[i]; - } - } - return result; - }; + p5.RendererGL.prototype.image = function( + img, + sx, + sy, + sWidth, + sHeight, + dx, + dy, + dWidth, + dHeight + ) { + if (this._isErasing) { + this.blendMode(this._cachedBlendMode); + } - function edgeCallback(flag) { - // don't really care about the flag, but need no-strip/no-fan behavior - } + this._pInst.push(); - const tessy = new libtess.GluTesselator(); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); - tessy.gluTessProperty( - libtess.gluEnum.GLU_TESS_WINDING_RULE, - libtess.windingRule.GLU_TESS_WINDING_NONZERO - ); + this._pInst.noLights(); + this._pInst.noStroke(); - return tessy; - } - - _triangulate(contours) { - // libtess will take 3d verts and flatten to a plane for tesselation. - // libtess is capable of calculating a plane to tesselate on, but - // if all of the vertices have the same z values, we'll just - // assume the face is facing the camera, letting us skip any performance - // issues or bugs in libtess's automatic calculation. - const z = contours[0] ? contours[0][2] : undefined; - let allSameZ = true; - for (const contour of contours) { - for ( - let j = 0; - j < contour.length; - j += this.tessyVertexSize - ) { - if (contour[j + 2] !== z) { - allSameZ = false; - break; - } - } - } - if (allSameZ) { - this._tessy.gluTessNormal(0, 0, 1); - } else { - // Let libtess pick a plane for us - this._tessy.gluTessNormal(0, 0, 0); - } + this._pInst.texture(img); + this._pInst.textureMode(constants.NORMAL); - const triangleVerts = []; - this._tessy.gluTessBeginPolygon(triangleVerts); - - for (const contour of contours) { - this._tessy.gluTessBeginContour(); - for ( - let j = 0; - j < contour.length; - j += this.tessyVertexSize - ) { - const coords = contour.slice( - j, - j + this.tessyVertexSize - ); - this._tessy.gluTessVertex(coords, coords); - } - this._tessy.gluTessEndContour(); - } + let u0 = 0; + if (sx <= img.width) { + u0 = sx / img.width; + } - // finish polygon - this._tessy.gluTessEndPolygon(); + let u1 = 1; + if (sx + sWidth <= img.width) { + u1 = (sx + sWidth) / img.width; + } - return triangleVerts; + let v0 = 0; + if (sy <= img.height) { + v0 = sy / img.height; } - }; - /** - * ensures that p5 is using a 3d renderer. throws an error if not. - */ - fn._assert3d = function (name) { - if (!this._renderer.isP3D) - throw new Error( - `${name}() is only supported in WEBGL mode. If you'd like to use 3D graphics and WebGL, see https://p5js.org/examples/form-3d-primitives.html for more information.` - ); - }; - p5.renderers[constants.WEBGL] = p5.RendererGL; - p5.renderers[constants.WEBGL2] = p5.RendererGL; + let v1 = 1; + if (sy + sHeight <= img.height) { + v1 = (sy + sHeight) / img.height; + } + + this.beginShape(); + this.vertex(dx, dy, 0, u0, v0); + this.vertex(dx + dWidth, dy, 0, u1, v0); + this.vertex(dx + dWidth, dy + dHeight, 0, u1, v1); + this.vertex(dx, dy + dHeight, 0, u0, v1); + this.endShape(constants.CLOSE); + + this._pInst.pop(); + + if (this._isErasing) { + this.blendMode(constants.REMOVE); + } + }; } /** @@ -2520,7 +3707,8 @@ export function readPixelWebGL( } export default rendererGL; +export { RendererGL }; if(typeof p5 !== 'undefined'){ rendererGL(p5, p5.prototype); -} \ No newline at end of file +} diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 68cb21131d..ff3934fd6a 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -6,6 +6,1292 @@ * @requires core */ +import { Texture } from './p5.Texture'; + +class Shader { + constructor(renderer, vertSrc, fragSrc, options = {}) { + // TODO: adapt this to not take ids, but rather, + // to take the source for a vertex and fragment shader + // to enable custom shaders at some later date + this._renderer = renderer; + this._vertSrc = vertSrc; + this._fragSrc = fragSrc; + this._vertShader = -1; + this._fragShader = -1; + this._glProgram = 0; + this._loadedAttributes = false; + this.attributes = {}; + this._loadedUniforms = false; + this.uniforms = {}; + this._bound = false; + this.samplers = []; + this.hooks = { + // These should be passed in by `.modify()` instead of being manually + // passed in. + + // Stores uniforms + default values. + uniforms: options.uniforms || {}, + + // Stores custom uniform + helper declarations as a string. + declarations: options.declarations, + + // Stores helper functions to prepend to shaders. + helpers: options.helpers || {}, + + // Stores the hook implementations + vertex: options.vertex || {}, + fragment: options.fragment || {}, + + // Stores whether or not the hook implementation has been modified + // from the default. This is supplied automatically by calling + // yourShader.modify(...). + modified: { + vertex: (options.modified && options.modified.vertex) || {}, + fragment: (options.modified && options.modified.fragment) || {} + } + }; + } + + shaderSrc(src, shaderType) { + const main = 'void main'; + const [preMain, postMain] = src.split(main); + + let hooks = ''; + for (const key in this.hooks.uniforms) { + hooks += `uniform ${key};\n`; + } + if (this.hooks.declarations) { + hooks += this.hooks.declarations + '\n'; + } + if (this.hooks[shaderType].declarations) { + hooks += this.hooks[shaderType].declarations + '\n'; + } + for (const hookDef in this.hooks.helpers) { + hooks += `${hookDef}${this.hooks.helpers[hookDef]}\n`; + } + for (const hookDef in this.hooks[shaderType]) { + if (hookDef === 'declarations') continue; + const [hookType, hookName] = hookDef.split(' '); + + // Add a #define so that if the shader wants to use preprocessor directives to + // optimize away the extra function calls in main, it can do so + if (this.hooks.modified[shaderType][hookDef]) { + hooks += '#define AUGMENTED_HOOK_' + hookName + '\n'; + } + + hooks += + hookType + ' HOOK_' + hookName + this.hooks[shaderType][hookDef] + '\n'; + } + + return preMain + hooks + main + postMain; + } + + /** + * Shaders are written in GLSL, but + * there are different versions of GLSL that it might be written in. + * + * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. + * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. + * + * @returns {String} The GLSL version used by the shader. + */ + version() { + const match = /#version (.+)$/.exec(this.vertSrc()); + if (match) { + return match[1]; + } else { + return '100 es'; + } + } + + vertSrc() { + return this.shaderSrc(this._vertSrc, 'vertex'); + } + + fragSrc() { + return this.shaderSrc(this._fragSrc, 'fragment'); + } + + /** + * Logs the hooks available in this shader, and their current implementation. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. This method logs those values to the console, letting you know what + * you are able to use in a call to + * `modify()`. + * + * For example, this shader will produce the following output: + * + * ```js + * myShader = baseMaterialShader().modify({ + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * myShader.inspectHooks(); + * ``` + * + * ``` + * ==== Vertex shader hooks: ==== + * void beforeVertex() {} + * vec3 getLocalPosition(vec3 position) { return position; } + * [MODIFIED] vec3 getWorldPosition(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * } + * vec3 getLocalNormal(vec3 normal) { return normal; } + * vec3 getWorldNormal(vec3 normal) { return normal; } + * vec2 getUV(vec2 uv) { return uv; } + * vec4 getVertexColor(vec4 color) { return color; } + * void afterVertex() {} + * + * ==== Fragment shader hooks: ==== + * void beforeFragment() {} + * Inputs getPixelInputs(Inputs inputs) { return inputs; } + * vec4 combineColors(ColorComponents components) { + * vec4 color = vec4(0.); + * color.rgb += components.diffuse * components.baseColor; + * color.rgb += components.ambient * components.ambientColor; + * color.rgb += components.specular * components.specularColor; + * color.rgb += components.emissive; + * color.a = components.opacity; + * return color; + * } + * vec4 getFinalColor(vec4 color) { return color; } + * void afterFragment() {} + * ``` + * + * @beta + */ + inspectHooks() { + console.log('==== Vertex shader hooks: ===='); + for (const key in this.hooks.vertex) { + console.log( + (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.vertex[key] + ); + } + console.log(''); + console.log('==== Fragment shader hooks: ===='); + for (const key in this.hooks.fragment) { + console.log( + (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.fragment[key] + ); + } + console.log(''); + console.log('==== Helper functions: ===='); + for (const key in this.hooks.helpers) { + console.log( + key + + this.hooks.helpers[key] + ); + } + } + + /** + * Returns a new shader, based on the original, but with custom snippets + * of shader code replacing default behaviour. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. You can inspect the different hooks available by calling + * `yourShader.inspectHooks()`. You can + * also read the reference for the default material, normal material, color, line, and point shaders to + * see what hooks they have available. + * + * `modify()` takes one parameter, `hooks`, an object with the hooks you want + * to override. Each key of the `hooks` object is the name + * of a hook, and the value is a string with the GLSL code for your hook. + * + * If you supply functions that aren't existing hooks, they will get added at the start of + * the shader as helper functions so that you can use them in your hooks. + * + * To add new uniforms to your shader, you can pass in a `uniforms` object containing + * the type and name of the uniform as the key, and a default value or function returning + * a default value as its value. These will be automatically set when the shader is set + * with `shader(yourShader)`. + * + * You can also add a `declarations` key, where the value is a GLSL string declaring + * custom uniform variables, globals, and functions shared + * between hooks. To add declarations just in a vertex or fragment shader, add + * `vertexDeclarations` and `fragmentDeclarations` keys. + * + * @beta + * @param {Object} [hooks] The hooks in the shader to replace. + * @returns {p5.Shader} + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * // Manually specifying a uniform + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * myShader.setUniform('time', millis()); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ + modify(hooks) { + p5._validateParameters('p5.Shader.modify', arguments); + const newHooks = { + vertex: {}, + fragment: {}, + helpers: {} + }; + for (const key in hooks) { + if (key === 'declarations') continue; + if (key === 'uniforms') continue; + if (key === 'vertexDeclarations') { + newHooks.vertex.declarations = + (newHooks.vertex.declarations || '') + '\n' + hooks[key]; + } else if (key === 'fragmentDeclarations') { + newHooks.fragment.declarations = + (newHooks.fragment.declarations || '') + '\n' + hooks[key]; + } else if (this.hooks.vertex[key]) { + newHooks.vertex[key] = hooks[key]; + } else if (this.hooks.fragment[key]) { + newHooks.fragment[key] = hooks[key]; + } else { + newHooks.helpers[key] = hooks[key]; + } + } + const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); + const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); + for (const key in newHooks.vertex || {}) { + if (key === 'declarations') continue; + modifiedVertex[key] = true; + } + for (const key in newHooks.fragment || {}) { + if (key === 'declarations') continue; + modifiedFragment[key] = true; + } + + return new p5.Shader(this._renderer, this._vertSrc, this._fragSrc, { + declarations: + (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), + uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), + fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), + vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), + helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), + modified: { + vertex: modifiedVertex, + fragment: modifiedFragment + } + }); + } + + /** + * Creates, compiles, and links the shader based on its + * sources for the vertex and fragment shaders (provided + * to the constructor). Populates known attributes and + * uniforms from the shader. + * @chainable + * @private + */ + init() { + if (this._glProgram === 0 /* or context is stale? */) { + const gl = this._renderer.GL; + + // @todo: once custom shading is allowed, + // friendly error messages should be used here to share + // compiler and linker errors. + + //set up the shader by + // 1. creating and getting a gl id for the shader program, + // 2. compliling its vertex & fragment sources, + // 3. linking the vertex and fragment shaders + this._vertShader = gl.createShader(gl.VERTEX_SHADER); + //load in our default vertex shader + gl.shaderSource(this._vertShader, this.vertSrc()); + gl.compileShader(this._vertShader); + // if our vertex shader failed compilation? + if (!gl.getShaderParameter(this._vertShader, gl.COMPILE_STATUS)) { + const glError = gl.getShaderInfoLog(this._vertShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { + p5._friendlyError( + `Yikes! An error occurred compiling the vertex shader:${glError}` + ); + } + return null; + } + + this._fragShader = gl.createShader(gl.FRAGMENT_SHADER); + //load in our material frag shader + gl.shaderSource(this._fragShader, this.fragSrc()); + gl.compileShader(this._fragShader); + // if our frag shader failed compilation? + if (!gl.getShaderParameter(this._fragShader, gl.COMPILE_STATUS)) { + const glError = gl.getShaderInfoLog(this._fragShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { + p5._friendlyError( + `Darn! An error occurred compiling the fragment shader:${glError}` + ); + } + return null; + } + + this._glProgram = gl.createProgram(); + gl.attachShader(this._glProgram, this._vertShader); + gl.attachShader(this._glProgram, this._fragShader); + gl.linkProgram(this._glProgram); + if (!gl.getProgramParameter(this._glProgram, gl.LINK_STATUS)) { + p5._friendlyError( + `Snap! Error linking shader program: ${gl.getProgramInfoLog( + this._glProgram + )}` + ); + } + + this._loadAttributes(); + this._loadUniforms(); + } + return this; + } + + /** + * @private + */ + setDefaultUniforms() { + for (const key in this.hooks.uniforms) { + const [, name] = key.split(' '); + const initializer = this.hooks.uniforms[key]; + let value; + if (initializer instanceof Function) { + value = initializer(); + } else { + value = initializer; + } + + if (value !== undefined && value !== null) { + this.setUniform(name, value); + } + } + } + + /** + * Copies the shader from one drawing context to another. + * + * Each `p5.Shader` object must be compiled by calling + * shader() before it can run. Compilation happens + * in a drawing context which is usually the main canvas or an instance of + * p5.Graphics. A shader can only be used in the + * context where it was compiled. The `copyToContext()` method compiles the + * shader again and copies it to another drawing context where it can be + * reused. + * + * The parameter, `context`, is the drawing context where the shader will be + * used. The shader can be copied to an instance of + * p5.Graphics, as in + * `myShader.copyToContext(pg)`. The shader can also be copied from a + * p5.Graphics object to the main canvas using + * the `window` variable, as in `myShader.copyToContext(window)`. + * + * Note: A p5.Shader object created with + * createShader(), + * createFilterShader(), or + * loadShader() + * can be used directly with a p5.Framebuffer + * object created with + * createFramebuffer(). Both objects + * have the same context as the main canvas. + * + * @param {p5|p5.Graphics} context WebGL context for the copied shader. + * @returns {p5.Shader} new shader compiled for the target context. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * varying vec2 vTexCoord; + * + * void main() { + * vec2 uv = vTexCoord; + * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); + * gl_FragColor = vec4(color, 1.0);\ + * } + * `; + * + * let pg; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Create a p5.Shader object. + * let original = createShader(vertSrc, fragSrc); + * + * // Compile the p5.Shader object. + * shader(original); + * + * // Create a p5.Graphics object. + * pg = createGraphics(50, 50, WEBGL); + * + * // Copy the original shader to the p5.Graphics object. + * let copied = original.copyToContext(pg); + * + * // Apply the copied shader to the p5.Graphics object. + * pg.shader(copied); + * + * // Style the display surface. + * pg.noStroke(); + * + * // Add a display surface for the shader. + * pg.plane(50, 50); + * + * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Draw the p5.Graphics object to the main canvas. + * image(pg, -25, -25); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * varying vec2 vTexCoord; + * + * void main() { + * vec2 uv = vTexCoord; + * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); + * gl_FragColor = vec4(color, 1.0); + * } + * `; + * + * let copied; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Graphics object. + * let pg = createGraphics(25, 25, WEBGL); + * + * // Create a p5.Shader object. + * let original = pg.createShader(vertSrc, fragSrc); + * + * // Compile the p5.Shader object. + * pg.shader(original); + * + * // Copy the original shader to the main canvas. + * copied = original.copyToContext(window); + * + * // Apply the copied shader to the main canvas. + * shader(copied); + * + * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Rotate around the x-, y-, and z-axes. + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * rotateZ(frameCount * 0.01); + * + * // Draw the box. + * box(50); + * } + * + *
+ */ + copyToContext(context) { + const shader = new p5.Shader( + context._renderer, + this._vertSrc, + this._fragSrc + ); + shader.ensureCompiledOnContext(context); + return shader; + } + + /** + * @private + */ + ensureCompiledOnContext(context) { + if (this._glProgram !== 0 && this._renderer !== context._renderer) { + throw new Error( + 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' + ); + } else if (this._glProgram === 0) { + this._renderer = context._renderer; + this.init(); + } + } + + /** + * Queries the active attributes for this shader and loads + * their names and locations into the attributes array. + * @private + */ + _loadAttributes() { + if (this._loadedAttributes) { + return; + } + + this.attributes = {}; + + const gl = this._renderer.GL; + + const numAttributes = gl.getProgramParameter( + this._glProgram, + gl.ACTIVE_ATTRIBUTES + ); + for (let i = 0; i < numAttributes; ++i) { + const attributeInfo = gl.getActiveAttrib(this._glProgram, i); + const name = attributeInfo.name; + const location = gl.getAttribLocation(this._glProgram, name); + const attribute = {}; + attribute.name = name; + attribute.location = location; + attribute.index = i; + attribute.type = attributeInfo.type; + attribute.size = attributeInfo.size; + this.attributes[name] = attribute; + } + + this._loadedAttributes = true; + } + + /** + * Queries the active uniforms for this shader and loads + * their names and locations into the uniforms array. + * @private + */ + _loadUniforms() { + if (this._loadedUniforms) { + return; + } + + const gl = this._renderer.GL; + + // Inspect shader and cache uniform info + const numUniforms = gl.getProgramParameter( + this._glProgram, + gl.ACTIVE_UNIFORMS + ); + + let samplerIndex = 0; + for (let i = 0; i < numUniforms; ++i) { + const uniformInfo = gl.getActiveUniform(this._glProgram, i); + const uniform = {}; + uniform.location = gl.getUniformLocation( + this._glProgram, + uniformInfo.name + ); + uniform.size = uniformInfo.size; + let uniformName = uniformInfo.name; + //uniforms that are arrays have their name returned as + //someUniform[0] which is a bit silly so we trim it + //off here. The size property tells us that its an array + //so we dont lose any information by doing this + if (uniformInfo.size > 1) { + uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); + } + uniform.name = uniformName; + uniform.type = uniformInfo.type; + uniform._cachedData = undefined; + if (uniform.type === gl.SAMPLER_2D) { + uniform.samplerIndex = samplerIndex; + samplerIndex++; + this.samplers.push(uniform); + } + + uniform.isArray = + uniformInfo.size > 1 || + uniform.type === gl.FLOAT_MAT3 || + uniform.type === gl.FLOAT_MAT4 || + uniform.type === gl.FLOAT_VEC2 || + uniform.type === gl.FLOAT_VEC3 || + uniform.type === gl.FLOAT_VEC4 || + uniform.type === gl.INT_VEC2 || + uniform.type === gl.INT_VEC4 || + uniform.type === gl.INT_VEC3; + + this.uniforms[uniformName] = uniform; + } + this._loadedUniforms = true; + } + + compile() { + // TODO + } + + /** + * initializes (if needed) and binds the shader program. + * @private + */ + bindShader() { + this.init(); + if (!this._bound) { + this.useProgram(); + this._bound = true; + + this._setMatrixUniforms(); + + this.setUniform('uViewport', this._renderer._viewport); + } + } + + /** + * @chainable + * @private + */ + unbindShader() { + if (this._bound) { + this.unbindTextures(); + //this._renderer.GL.useProgram(0); ?? + this._bound = false; + } + return this; + } + + bindTextures() { + const gl = this._renderer.GL; + + for (const uniform of this.samplers) { + let tex = uniform.texture; + if (tex === undefined) { + // user hasn't yet supplied a texture for this slot. + // (or there may not be one--maybe just lighting), + // so we supply a default texture instead. + tex = this._renderer._getEmptyTexture(); + } + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + tex.bindTexture(); + tex.update(); + gl.uniform1i(uniform.location, uniform.samplerIndex); + } + } + + updateTextures() { + for (const uniform of this.samplers) { + const tex = uniform.texture; + if (tex) { + tex.update(); + } + } + } + + unbindTextures() { + for (const uniform of this.samplers) { + this.setUniform(uniform.name, this._renderer._getEmptyTexture()); + } + } + + _setMatrixUniforms() { + const modelMatrix = this._renderer.states.uModelMatrix; + const viewMatrix = this._renderer.states.uViewMatrix; + const projectionMatrix = this._renderer.states.uPMatrix; + const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); + this._renderer.states.uMVMatrix = modelViewMatrix; + + const modelViewProjectionMatrix = modelViewMatrix.copy(); + modelViewProjectionMatrix.mult(projectionMatrix); + + if (this.isStrokeShader()) { + this.setUniform( + 'uPerspective', + this._renderer.states.curCamera.useLinePerspective ? 1 : 0 + ); + } + this.setUniform('uViewMatrix', viewMatrix.mat4); + this.setUniform('uProjectionMatrix', projectionMatrix.mat4); + this.setUniform('uModelMatrix', modelMatrix.mat4); + this.setUniform('uModelViewMatrix', modelViewMatrix.mat4); + this.setUniform( + 'uModelViewProjectionMatrix', + modelViewProjectionMatrix.mat4 + ); + if (this.uniforms.uNormalMatrix) { + this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); + this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); + } + if (this.uniforms.uCameraRotation) { + this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); + this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); + } + } + + /** + * @chainable + * @private + */ + useProgram() { + const gl = this._renderer.GL; + if (this._renderer._curShader !== this) { + gl.useProgram(this._glProgram); + this._renderer._curShader = this; + } + return this; + } + + /** + * Sets the shader’s uniform (global) variables. + * + * Shader programs run on the computer’s graphics processing unit (GPU). + * They live in part of the computer’s memory that’s completely separate + * from the sketch that runs them. Uniforms are global variables within a + * shader program. They provide a way to pass values from a sketch running + * on the CPU to a shader program running on the GPU. + * + * The first parameter, `uniformName`, is a string with the uniform’s name. + * For the shader above, `uniformName` would be `'r'`. + * + * The second parameter, `data`, is the value that should be used to set the + * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set + * the `r` uniform in the shader above to `0.5`. data should match the + * uniform’s type. Numbers, strings, booleans, arrays, and many types of + * images can all be passed to a shader with `setUniform()`. + * + * @chainable + * @param {String} uniformName name of the uniform. Must match the name + * used in the vertex and fragment shaders. + * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} + * data value to assign to the uniform. Must match the uniform’s data type. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * uniform float r; + * + * void main() { + * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); + * } + * `; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * let myShader = createShader(vertSrc, fragSrc); + * + * // Apply the p5.Shader object. + * shader(myShader); + * + * // Set the r uniform to 0.5. + * myShader.setUniform('r', 0.5); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface for the shader. + * plane(100, 100); + * + * describe('A cyan square.'); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * uniform float r; + * + * void main() { + * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); + * } + * `; + * + * let myShader; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * myShader = createShader(vertSrc, fragSrc); + * + * // Compile and apply the p5.Shader object. + * shader(myShader); + * + * describe('A square oscillates color between cyan and white.'); + * } + * + * function draw() { + * background(200); + * + * // Style the drawing surface. + * noStroke(); + * + * // Update the r uniform. + * let nextR = 0.5 * (sin(frameCount * 0.01) + 1); + * myShader.setUniform('r', nextR); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision highp float; + * uniform vec2 p; + * uniform float r; + * const int numIterations = 500; + * varying vec2 vTexCoord; + * + * void main() { + * vec2 c = p + gl_FragCoord.xy * r; + * vec2 z = c; + * float n = 0.0; + * + * for (int i = numIterations; i > 0; i--) { + * if (z.x * z.x + z.y * z.y > 4.0) { + * n = float(i) / float(numIterations); + * break; + * } + * + * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; + * } + * + * gl_FragColor = vec4( + * 0.5 - cos(n * 17.0) / 2.0, + * 0.5 - cos(n * 13.0) / 2.0, + * 0.5 - cos(n * 23.0) / 2.0, + * 1.0 + * ); + * } + * `; + * + * let mandelbrot; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * mandelbrot = createShader(vertSrc, fragSrc); + * + * // Compile and apply the p5.Shader object. + * shader(mandelbrot); + * + * // Set the shader uniform p to an array. + * // p is the center point of the Mandelbrot image. + * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); + * + * describe('A fractal image zooms in and out of focus.'); + * } + * + * function draw() { + * // Set the shader uniform r to a value that oscillates + * // between 0 and 0.005. + * // r is the size of the image in Mandelbrot-space. + * let radius = 0.005 * (sin(frameCount * 0.01) + 1); + * mandelbrot.setUniform('r', radius); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * } + * + *
+ */ + setUniform(uniformName, data) { + const uniform = this.uniforms[uniformName]; + if (!uniform) { + return; + } + const gl = this._renderer.GL; + + if (uniform.isArray) { + if ( + uniform._cachedData && + this._renderer._arraysEqual(uniform._cachedData, data) + ) { + return; + } else { + uniform._cachedData = data.slice(0); + } + } else if (uniform._cachedData && uniform._cachedData === data) { + return; + } else { + if (Array.isArray(data)) { + uniform._cachedData = data.slice(0); + } else { + uniform._cachedData = data; + } + } + + const location = uniform.location; + + this.useProgram(); + + switch (uniform.type) { + case gl.BOOL: + if (data === true) { + gl.uniform1i(location, 1); + } else { + gl.uniform1i(location, 0); + } + break; + case gl.INT: + if (uniform.size > 1) { + data.length && gl.uniform1iv(location, data); + } else { + gl.uniform1i(location, data); + } + break; + case gl.FLOAT: + if (uniform.size > 1) { + data.length && gl.uniform1fv(location, data); + } else { + gl.uniform1f(location, data); + } + break; + case gl.FLOAT_MAT3: + gl.uniformMatrix3fv(location, false, data); + break; + case gl.FLOAT_MAT4: + gl.uniformMatrix4fv(location, false, data); + break; + case gl.FLOAT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2fv(location, data); + } else { + gl.uniform2f(location, data[0], data[1]); + } + break; + case gl.FLOAT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3fv(location, data); + } else { + gl.uniform3f(location, data[0], data[1], data[2]); + } + break; + case gl.FLOAT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4fv(location, data); + } else { + gl.uniform4f(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.INT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2iv(location, data); + } else { + gl.uniform2i(location, data[0], data[1]); + } + break; + case gl.INT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3iv(location, data); + } else { + gl.uniform3i(location, data[0], data[1], data[2]); + } + break; + case gl.INT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4iv(location, data); + } else { + gl.uniform4i(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.SAMPLER_2D: + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + uniform.texture = + data instanceof Texture ? data : this._renderer.getTexture(data); + gl.uniform1i(location, uniform.samplerIndex); + if (uniform.texture.src.gifProperties) { + uniform.texture.src._animateGif(this._renderer._pInst); + } + break; + //@todo complete all types + } + return this; + } + + /* NONE OF THIS IS FAST OR EFFICIENT BUT BEAR WITH ME + * + * these shader "type" query methods are used by various + * facilities of the renderer to determine if changing + * the shader type for the required action (for example, + * do we need to load the default lighting shader if the + * current shader cannot handle lighting?) + * + **/ + + isLightShader() { + return [ + this.attributes.aNormal, + this.uniforms.uUseLighting, + this.uniforms.uAmbientLightCount, + this.uniforms.uDirectionalLightCount, + this.uniforms.uPointLightCount, + this.uniforms.uAmbientColor, + this.uniforms.uDirectionalDiffuseColors, + this.uniforms.uDirectionalSpecularColors, + this.uniforms.uPointLightLocation, + this.uniforms.uPointLightDiffuseColors, + this.uniforms.uPointLightSpecularColors, + this.uniforms.uLightingDirection, + this.uniforms.uSpecular + ].some(x => x !== undefined); + } + + isNormalShader() { + return this.attributes.aNormal !== undefined; + } + + isTextureShader() { + return this.samplers.length > 0; + } + + isColorShader() { + return ( + this.attributes.aVertexColor !== undefined || + this.uniforms.uMaterialColor !== undefined + ); + } + + isTexLightShader() { + return this.isLightShader() && this.isTextureShader(); + } + + isStrokeShader() { + return this.uniforms.uStrokeWeight !== undefined; + } + + /** + * @chainable + * @private + */ + enableAttrib(attr, size, type, normalized, stride, offset) { + if (attr) { + if ( + typeof IS_MINIFIED === 'undefined' && + this.attributes[attr.name] !== attr + ) { + console.warn( + `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.` + ); + } + const loc = attr.location; + if (loc !== -1) { + const gl = this._renderer.GL; + // Enable register even if it is disabled + if (!this._renderer.registerEnabled.has(loc)) { + gl.enableVertexAttribArray(loc); + // Record register availability + this._renderer.registerEnabled.add(loc); + } + this._renderer.GL.vertexAttribPointer( + loc, + size, + type || gl.FLOAT, + normalized || false, + stride || 0, + offset || 0 + ); + } + } + return this; + } + + /** + * Once all buffers have been bound, this checks to see if there are any + * remaining active attributes, likely left over from previous renders, + * and disables them so that they don't affect rendering. + * @private + */ + disableRemainingAttributes() { + for (const location of this._renderer.registerEnabled.values()) { + if ( + !Object.keys(this.attributes).some( + key => this.attributes[key].location === location + ) + ) { + this._renderer.GL.disableVertexAttribArray(location); + this._renderer.registerEnabled.delete(location); + } + } + } +}; + function shader(p5, fn){ /** * A class to describe a shader program. @@ -150,1292 +1436,11 @@ function shader(p5, fn){ * *
*/ - p5.Shader = class Shader { - constructor(renderer, vertSrc, fragSrc, options = {}) { - // TODO: adapt this to not take ids, but rather, - // to take the source for a vertex and fragment shader - // to enable custom shaders at some later date - this._renderer = renderer; - this._vertSrc = vertSrc; - this._fragSrc = fragSrc; - this._vertShader = -1; - this._fragShader = -1; - this._glProgram = 0; - this._loadedAttributes = false; - this.attributes = {}; - this._loadedUniforms = false; - this.uniforms = {}; - this._bound = false; - this.samplers = []; - this.hooks = { - // These should be passed in by `.modify()` instead of being manually - // passed in. - - // Stores uniforms + default values. - uniforms: options.uniforms || {}, - - // Stores custom uniform + helper declarations as a string. - declarations: options.declarations, - - // Stores helper functions to prepend to shaders. - helpers: options.helpers || {}, - - // Stores the hook implementations - vertex: options.vertex || {}, - fragment: options.fragment || {}, - - // Stores whether or not the hook implementation has been modified - // from the default. This is supplied automatically by calling - // yourShader.modify(...). - modified: { - vertex: (options.modified && options.modified.vertex) || {}, - fragment: (options.modified && options.modified.fragment) || {} - } - }; - } - - shaderSrc(src, shaderType) { - const main = 'void main'; - const [preMain, postMain] = src.split(main); - - let hooks = ''; - for (const key in this.hooks.uniforms) { - hooks += `uniform ${key};\n`; - } - if (this.hooks.declarations) { - hooks += this.hooks.declarations + '\n'; - } - if (this.hooks[shaderType].declarations) { - hooks += this.hooks[shaderType].declarations + '\n'; - } - for (const hookDef in this.hooks.helpers) { - hooks += `${hookDef}${this.hooks.helpers[hookDef]}\n`; - } - for (const hookDef in this.hooks[shaderType]) { - if (hookDef === 'declarations') continue; - const [hookType, hookName] = hookDef.split(' '); - - // Add a #define so that if the shader wants to use preprocessor directives to - // optimize away the extra function calls in main, it can do so - if (this.hooks.modified[shaderType][hookDef]) { - hooks += '#define AUGMENTED_HOOK_' + hookName + '\n'; - } - - hooks += - hookType + ' HOOK_' + hookName + this.hooks[shaderType][hookDef] + '\n'; - } - - return preMain + hooks + main + postMain; - } - - /** - * Shaders are written in GLSL, but - * there are different versions of GLSL that it might be written in. - * - * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. - * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. - * - * @returns {String} The GLSL version used by the shader. - */ - version() { - const match = /#version (.+)$/.exec(this.vertSrc()); - if (match) { - return match[1]; - } else { - return '100 es'; - } - } - - vertSrc() { - return this.shaderSrc(this._vertSrc, 'vertex'); - } - - fragSrc() { - return this.shaderSrc(this._fragSrc, 'fragment'); - } - - /** - * Logs the hooks available in this shader, and their current implementation. - * - * Each shader may let you override bits of its behavior. Each bit is called - * a *hook.* A hook is either for the *vertex* shader, if it affects the - * position of vertices, or in the *fragment* shader, if it affects the pixel - * color. This method logs those values to the console, letting you know what - * you are able to use in a call to - * `modify()`. - * - * For example, this shader will produce the following output: - * - * ```js - * myShader = baseMaterialShader().modify({ - * declarations: 'uniform float time;', - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * myShader.inspectHooks(); - * ``` - * - * ``` - * ==== Vertex shader hooks: ==== - * void beforeVertex() {} - * vec3 getLocalPosition(vec3 position) { return position; } - * [MODIFIED] vec3 getWorldPosition(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * } - * vec3 getLocalNormal(vec3 normal) { return normal; } - * vec3 getWorldNormal(vec3 normal) { return normal; } - * vec2 getUV(vec2 uv) { return uv; } - * vec4 getVertexColor(vec4 color) { return color; } - * void afterVertex() {} - * - * ==== Fragment shader hooks: ==== - * void beforeFragment() {} - * Inputs getPixelInputs(Inputs inputs) { return inputs; } - * vec4 combineColors(ColorComponents components) { - * vec4 color = vec4(0.); - * color.rgb += components.diffuse * components.baseColor; - * color.rgb += components.ambient * components.ambientColor; - * color.rgb += components.specular * components.specularColor; - * color.rgb += components.emissive; - * color.a = components.opacity; - * return color; - * } - * vec4 getFinalColor(vec4 color) { return color; } - * void afterFragment() {} - * ``` - * - * @beta - */ - inspectHooks() { - console.log('==== Vertex shader hooks: ===='); - for (const key in this.hooks.vertex) { - console.log( - (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.vertex[key] - ); - } - console.log(''); - console.log('==== Fragment shader hooks: ===='); - for (const key in this.hooks.fragment) { - console.log( - (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.fragment[key] - ); - } - console.log(''); - console.log('==== Helper functions: ===='); - for (const key in this.hooks.helpers) { - console.log( - key + - this.hooks.helpers[key] - ); - } - } - - /** - * Returns a new shader, based on the original, but with custom snippets - * of shader code replacing default behaviour. - * - * Each shader may let you override bits of its behavior. Each bit is called - * a *hook.* A hook is either for the *vertex* shader, if it affects the - * position of vertices, or in the *fragment* shader, if it affects the pixel - * color. You can inspect the different hooks available by calling - * `yourShader.inspectHooks()`. You can - * also read the reference for the default material, normal material, color, line, and point shaders to - * see what hooks they have available. - * - * `modify()` takes one parameter, `hooks`, an object with the hooks you want - * to override. Each key of the `hooks` object is the name - * of a hook, and the value is a string with the GLSL code for your hook. - * - * If you supply functions that aren't existing hooks, they will get added at the start of - * the shader as helper functions so that you can use them in your hooks. - * - * To add new uniforms to your shader, you can pass in a `uniforms` object containing - * the type and name of the uniform as the key, and a default value or function returning - * a default value as its value. These will be automatically set when the shader is set - * with `shader(yourShader)`. - * - * You can also add a `declarations` key, where the value is a GLSL string declaring - * custom uniform variables, globals, and functions shared - * between hooks. To add declarations just in a vertex or fragment shader, add - * `vertexDeclarations` and `fragmentDeclarations` keys. - * - * @beta - * @param {Object} [hooks] The hooks in the shader to replace. - * @returns {p5.Shader} - * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * // Manually specifying a uniform - * declarations: 'uniform float time;', - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * myShader.setUniform('time', millis()); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- */ - modify(hooks) { - p5._validateParameters('p5.Shader.modify', arguments); - const newHooks = { - vertex: {}, - fragment: {}, - helpers: {} - }; - for (const key in hooks) { - if (key === 'declarations') continue; - if (key === 'uniforms') continue; - if (key === 'vertexDeclarations') { - newHooks.vertex.declarations = - (newHooks.vertex.declarations || '') + '\n' + hooks[key]; - } else if (key === 'fragmentDeclarations') { - newHooks.fragment.declarations = - (newHooks.fragment.declarations || '') + '\n' + hooks[key]; - } else if (this.hooks.vertex[key]) { - newHooks.vertex[key] = hooks[key]; - } else if (this.hooks.fragment[key]) { - newHooks.fragment[key] = hooks[key]; - } else { - newHooks.helpers[key] = hooks[key]; - } - } - const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); - const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); - for (const key in newHooks.vertex || {}) { - if (key === 'declarations') continue; - modifiedVertex[key] = true; - } - for (const key in newHooks.fragment || {}) { - if (key === 'declarations') continue; - modifiedFragment[key] = true; - } - - return new p5.Shader(this._renderer, this._vertSrc, this._fragSrc, { - declarations: - (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), - uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), - fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), - vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), - helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), - modified: { - vertex: modifiedVertex, - fragment: modifiedFragment - } - }); - } - - /** - * Creates, compiles, and links the shader based on its - * sources for the vertex and fragment shaders (provided - * to the constructor). Populates known attributes and - * uniforms from the shader. - * @chainable - * @private - */ - init() { - if (this._glProgram === 0 /* or context is stale? */) { - const gl = this._renderer.GL; - - // @todo: once custom shading is allowed, - // friendly error messages should be used here to share - // compiler and linker errors. - - //set up the shader by - // 1. creating and getting a gl id for the shader program, - // 2. compliling its vertex & fragment sources, - // 3. linking the vertex and fragment shaders - this._vertShader = gl.createShader(gl.VERTEX_SHADER); - //load in our default vertex shader - gl.shaderSource(this._vertShader, this.vertSrc()); - gl.compileShader(this._vertShader); - // if our vertex shader failed compilation? - if (!gl.getShaderParameter(this._vertShader, gl.COMPILE_STATUS)) { - const glError = gl.getShaderInfoLog(this._vertShader); - if (typeof IS_MINIFIED !== 'undefined') { - console.error(glError); - } else { - p5._friendlyError( - `Yikes! An error occurred compiling the vertex shader:${glError}` - ); - } - return null; - } - - this._fragShader = gl.createShader(gl.FRAGMENT_SHADER); - //load in our material frag shader - gl.shaderSource(this._fragShader, this.fragSrc()); - gl.compileShader(this._fragShader); - // if our frag shader failed compilation? - if (!gl.getShaderParameter(this._fragShader, gl.COMPILE_STATUS)) { - const glError = gl.getShaderInfoLog(this._fragShader); - if (typeof IS_MINIFIED !== 'undefined') { - console.error(glError); - } else { - p5._friendlyError( - `Darn! An error occurred compiling the fragment shader:${glError}` - ); - } - return null; - } - - this._glProgram = gl.createProgram(); - gl.attachShader(this._glProgram, this._vertShader); - gl.attachShader(this._glProgram, this._fragShader); - gl.linkProgram(this._glProgram); - if (!gl.getProgramParameter(this._glProgram, gl.LINK_STATUS)) { - p5._friendlyError( - `Snap! Error linking shader program: ${gl.getProgramInfoLog( - this._glProgram - )}` - ); - } - - this._loadAttributes(); - this._loadUniforms(); - } - return this; - } - - /** - * @private - */ - setDefaultUniforms() { - for (const key in this.hooks.uniforms) { - const [, name] = key.split(' '); - const initializer = this.hooks.uniforms[key]; - let value; - if (initializer instanceof Function) { - value = initializer(); - } else { - value = initializer; - } - - if (value !== undefined && value !== null) { - this.setUniform(name, value); - } - } - } - - /** - * Copies the shader from one drawing context to another. - * - * Each `p5.Shader` object must be compiled by calling - * shader() before it can run. Compilation happens - * in a drawing context which is usually the main canvas or an instance of - * p5.Graphics. A shader can only be used in the - * context where it was compiled. The `copyToContext()` method compiles the - * shader again and copies it to another drawing context where it can be - * reused. - * - * The parameter, `context`, is the drawing context where the shader will be - * used. The shader can be copied to an instance of - * p5.Graphics, as in - * `myShader.copyToContext(pg)`. The shader can also be copied from a - * p5.Graphics object to the main canvas using - * the `window` variable, as in `myShader.copyToContext(window)`. - * - * Note: A p5.Shader object created with - * createShader(), - * createFilterShader(), or - * loadShader() - * can be used directly with a p5.Framebuffer - * object created with - * createFramebuffer(). Both objects - * have the same context as the main canvas. - * - * @param {p5|p5.Graphics} context WebGL context for the copied shader. - * @returns {p5.Shader} new shader compiled for the target context. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * varying vec2 vTexCoord; - * - * void main() { - * vec2 uv = vTexCoord; - * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); - * gl_FragColor = vec4(color, 1.0);\ - * } - * `; - * - * let pg; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Shader object. - * let original = createShader(vertSrc, fragSrc); - * - * // Compile the p5.Shader object. - * shader(original); - * - * // Create a p5.Graphics object. - * pg = createGraphics(50, 50, WEBGL); - * - * // Copy the original shader to the p5.Graphics object. - * let copied = original.copyToContext(pg); - * - * // Apply the copied shader to the p5.Graphics object. - * pg.shader(copied); - * - * // Style the display surface. - * pg.noStroke(); - * - * // Add a display surface for the shader. - * pg.plane(50, 50); - * - * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Draw the p5.Graphics object to the main canvas. - * image(pg, -25, -25); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * varying vec2 vTexCoord; - * - * void main() { - * vec2 uv = vTexCoord; - * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); - * gl_FragColor = vec4(color, 1.0); - * } - * `; - * - * let copied; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Graphics object. - * let pg = createGraphics(25, 25, WEBGL); - * - * // Create a p5.Shader object. - * let original = pg.createShader(vertSrc, fragSrc); - * - * // Compile the p5.Shader object. - * pg.shader(original); - * - * // Copy the original shader to the main canvas. - * copied = original.copyToContext(window); - * - * // Apply the copied shader to the main canvas. - * shader(copied); - * - * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Rotate around the x-, y-, and z-axes. - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * rotateZ(frameCount * 0.01); - * - * // Draw the box. - * box(50); - * } - * - *
- */ - copyToContext(context) { - const shader = new p5.Shader( - context._renderer, - this._vertSrc, - this._fragSrc - ); - shader.ensureCompiledOnContext(context); - return shader; - } - - /** - * @private - */ - ensureCompiledOnContext(context) { - if (this._glProgram !== 0 && this._renderer !== context._renderer) { - throw new Error( - 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' - ); - } else if (this._glProgram === 0) { - this._renderer = context._renderer; - this.init(); - } - } - - /** - * Queries the active attributes for this shader and loads - * their names and locations into the attributes array. - * @private - */ - _loadAttributes() { - if (this._loadedAttributes) { - return; - } - - this.attributes = {}; - - const gl = this._renderer.GL; - - const numAttributes = gl.getProgramParameter( - this._glProgram, - gl.ACTIVE_ATTRIBUTES - ); - for (let i = 0; i < numAttributes; ++i) { - const attributeInfo = gl.getActiveAttrib(this._glProgram, i); - const name = attributeInfo.name; - const location = gl.getAttribLocation(this._glProgram, name); - const attribute = {}; - attribute.name = name; - attribute.location = location; - attribute.index = i; - attribute.type = attributeInfo.type; - attribute.size = attributeInfo.size; - this.attributes[name] = attribute; - } - - this._loadedAttributes = true; - } - - /** - * Queries the active uniforms for this shader and loads - * their names and locations into the uniforms array. - * @private - */ - _loadUniforms() { - if (this._loadedUniforms) { - return; - } - - const gl = this._renderer.GL; - - // Inspect shader and cache uniform info - const numUniforms = gl.getProgramParameter( - this._glProgram, - gl.ACTIVE_UNIFORMS - ); - - let samplerIndex = 0; - for (let i = 0; i < numUniforms; ++i) { - const uniformInfo = gl.getActiveUniform(this._glProgram, i); - const uniform = {}; - uniform.location = gl.getUniformLocation( - this._glProgram, - uniformInfo.name - ); - uniform.size = uniformInfo.size; - let uniformName = uniformInfo.name; - //uniforms that are arrays have their name returned as - //someUniform[0] which is a bit silly so we trim it - //off here. The size property tells us that its an array - //so we dont lose any information by doing this - if (uniformInfo.size > 1) { - uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); - } - uniform.name = uniformName; - uniform.type = uniformInfo.type; - uniform._cachedData = undefined; - if (uniform.type === gl.SAMPLER_2D) { - uniform.samplerIndex = samplerIndex; - samplerIndex++; - this.samplers.push(uniform); - } - - uniform.isArray = - uniformInfo.size > 1 || - uniform.type === gl.FLOAT_MAT3 || - uniform.type === gl.FLOAT_MAT4 || - uniform.type === gl.FLOAT_VEC2 || - uniform.type === gl.FLOAT_VEC3 || - uniform.type === gl.FLOAT_VEC4 || - uniform.type === gl.INT_VEC2 || - uniform.type === gl.INT_VEC4 || - uniform.type === gl.INT_VEC3; - - this.uniforms[uniformName] = uniform; - } - this._loadedUniforms = true; - } - - compile() { - // TODO - } - - /** - * initializes (if needed) and binds the shader program. - * @private - */ - bindShader() { - this.init(); - if (!this._bound) { - this.useProgram(); - this._bound = true; - - this._setMatrixUniforms(); - - this.setUniform('uViewport', this._renderer._viewport); - } - } - - /** - * @chainable - * @private - */ - unbindShader() { - if (this._bound) { - this.unbindTextures(); - //this._renderer.GL.useProgram(0); ?? - this._bound = false; - } - return this; - } - - bindTextures() { - const gl = this._renderer.GL; - - for (const uniform of this.samplers) { - let tex = uniform.texture; - if (tex === undefined) { - // user hasn't yet supplied a texture for this slot. - // (or there may not be one--maybe just lighting), - // so we supply a default texture instead. - tex = this._renderer._getEmptyTexture(); - } - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - tex.bindTexture(); - tex.update(); - gl.uniform1i(uniform.location, uniform.samplerIndex); - } - } - - updateTextures() { - for (const uniform of this.samplers) { - const tex = uniform.texture; - if (tex) { - tex.update(); - } - } - } - - unbindTextures() { - for (const uniform of this.samplers) { - this.setUniform(uniform.name, this._renderer._getEmptyTexture()); - } - } - - _setMatrixUniforms() { - const modelMatrix = this._renderer.states.uModelMatrix; - const viewMatrix = this._renderer.states.uViewMatrix; - const projectionMatrix = this._renderer.states.uPMatrix; - const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); - this._renderer.states.uMVMatrix = modelViewMatrix; - - const modelViewProjectionMatrix = modelViewMatrix.copy(); - modelViewProjectionMatrix.mult(projectionMatrix); - - if (this.isStrokeShader()) { - this.setUniform( - 'uPerspective', - this._renderer.states.curCamera.useLinePerspective ? 1 : 0 - ); - } - this.setUniform('uViewMatrix', viewMatrix.mat4); - this.setUniform('uProjectionMatrix', projectionMatrix.mat4); - this.setUniform('uModelMatrix', modelMatrix.mat4); - this.setUniform('uModelViewMatrix', modelViewMatrix.mat4); - this.setUniform( - 'uModelViewProjectionMatrix', - modelViewProjectionMatrix.mat4 - ); - if (this.uniforms.uNormalMatrix) { - this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); - this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); - } - if (this.uniforms.uCameraRotation) { - this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); - this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); - } - } - - /** - * @chainable - * @private - */ - useProgram() { - const gl = this._renderer.GL; - if (this._renderer._curShader !== this) { - gl.useProgram(this._glProgram); - this._renderer._curShader = this; - } - return this; - } - - /** - * Sets the shader’s uniform (global) variables. - * - * Shader programs run on the computer’s graphics processing unit (GPU). - * They live in part of the computer’s memory that’s completely separate - * from the sketch that runs them. Uniforms are global variables within a - * shader program. They provide a way to pass values from a sketch running - * on the CPU to a shader program running on the GPU. - * - * The first parameter, `uniformName`, is a string with the uniform’s name. - * For the shader above, `uniformName` would be `'r'`. - * - * The second parameter, `data`, is the value that should be used to set the - * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set - * the `r` uniform in the shader above to `0.5`. data should match the - * uniform’s type. Numbers, strings, booleans, arrays, and many types of - * images can all be passed to a shader with `setUniform()`. - * - * @chainable - * @param {String} uniformName name of the uniform. Must match the name - * used in the vertex and fragment shaders. - * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} - * data value to assign to the uniform. Must match the uniform’s data type. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * uniform float r; - * - * void main() { - * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); - * } - * `; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * let myShader = createShader(vertSrc, fragSrc); - * - * // Apply the p5.Shader object. - * shader(myShader); - * - * // Set the r uniform to 0.5. - * myShader.setUniform('r', 0.5); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface for the shader. - * plane(100, 100); - * - * describe('A cyan square.'); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * uniform float r; - * - * void main() { - * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); - * } - * `; - * - * let myShader; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * myShader = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(myShader); - * - * describe('A square oscillates color between cyan and white.'); - * } - * - * function draw() { - * background(200); - * - * // Style the drawing surface. - * noStroke(); - * - * // Update the r uniform. - * let nextR = 0.5 * (sin(frameCount * 0.01) + 1); - * myShader.setUniform('r', nextR); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision highp float; - * uniform vec2 p; - * uniform float r; - * const int numIterations = 500; - * varying vec2 vTexCoord; - * - * void main() { - * vec2 c = p + gl_FragCoord.xy * r; - * vec2 z = c; - * float n = 0.0; - * - * for (int i = numIterations; i > 0; i--) { - * if (z.x * z.x + z.y * z.y > 4.0) { - * n = float(i) / float(numIterations); - * break; - * } - * - * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; - * } - * - * gl_FragColor = vec4( - * 0.5 - cos(n * 17.0) / 2.0, - * 0.5 - cos(n * 13.0) / 2.0, - * 0.5 - cos(n * 23.0) / 2.0, - * 1.0 - * ); - * } - * `; - * - * let mandelbrot; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * mandelbrot = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(mandelbrot); - * - * // Set the shader uniform p to an array. - * // p is the center point of the Mandelbrot image. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); - * - * describe('A fractal image zooms in and out of focus.'); - * } - * - * function draw() { - * // Set the shader uniform r to a value that oscillates - * // between 0 and 0.005. - * // r is the size of the image in Mandelbrot-space. - * let radius = 0.005 * (sin(frameCount * 0.01) + 1); - * mandelbrot.setUniform('r', radius); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * } - * - *
- */ - setUniform(uniformName, data) { - const uniform = this.uniforms[uniformName]; - if (!uniform) { - return; - } - const gl = this._renderer.GL; - - if (uniform.isArray) { - if ( - uniform._cachedData && - this._renderer._arraysEqual(uniform._cachedData, data) - ) { - return; - } else { - uniform._cachedData = data.slice(0); - } - } else if (uniform._cachedData && uniform._cachedData === data) { - return; - } else { - if (Array.isArray(data)) { - uniform._cachedData = data.slice(0); - } else { - uniform._cachedData = data; - } - } - - const location = uniform.location; - - this.useProgram(); - - switch (uniform.type) { - case gl.BOOL: - if (data === true) { - gl.uniform1i(location, 1); - } else { - gl.uniform1i(location, 0); - } - break; - case gl.INT: - if (uniform.size > 1) { - data.length && gl.uniform1iv(location, data); - } else { - gl.uniform1i(location, data); - } - break; - case gl.FLOAT: - if (uniform.size > 1) { - data.length && gl.uniform1fv(location, data); - } else { - gl.uniform1f(location, data); - } - break; - case gl.FLOAT_MAT3: - gl.uniformMatrix3fv(location, false, data); - break; - case gl.FLOAT_MAT4: - gl.uniformMatrix4fv(location, false, data); - break; - case gl.FLOAT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2fv(location, data); - } else { - gl.uniform2f(location, data[0], data[1]); - } - break; - case gl.FLOAT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3fv(location, data); - } else { - gl.uniform3f(location, data[0], data[1], data[2]); - } - break; - case gl.FLOAT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4fv(location, data); - } else { - gl.uniform4f(location, data[0], data[1], data[2], data[3]); - } - break; - case gl.INT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2iv(location, data); - } else { - gl.uniform2i(location, data[0], data[1]); - } - break; - case gl.INT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3iv(location, data); - } else { - gl.uniform3i(location, data[0], data[1], data[2]); - } - break; - case gl.INT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4iv(location, data); - } else { - gl.uniform4i(location, data[0], data[1], data[2], data[3]); - } - break; - case gl.SAMPLER_2D: - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - uniform.texture = - data instanceof p5.Texture ? data : this._renderer.getTexture(data); - gl.uniform1i(location, uniform.samplerIndex); - if (uniform.texture.src.gifProperties) { - uniform.texture.src._animateGif(this._renderer._pInst); - } - break; - //@todo complete all types - } - return this; - } - - /* NONE OF THIS IS FAST OR EFFICIENT BUT BEAR WITH ME - * - * these shader "type" query methods are used by various - * facilities of the renderer to determine if changing - * the shader type for the required action (for example, - * do we need to load the default lighting shader if the - * current shader cannot handle lighting?) - * - **/ - - isLightShader() { - return [ - this.attributes.aNormal, - this.uniforms.uUseLighting, - this.uniforms.uAmbientLightCount, - this.uniforms.uDirectionalLightCount, - this.uniforms.uPointLightCount, - this.uniforms.uAmbientColor, - this.uniforms.uDirectionalDiffuseColors, - this.uniforms.uDirectionalSpecularColors, - this.uniforms.uPointLightLocation, - this.uniforms.uPointLightDiffuseColors, - this.uniforms.uPointLightSpecularColors, - this.uniforms.uLightingDirection, - this.uniforms.uSpecular - ].some(x => x !== undefined); - } - - isNormalShader() { - return this.attributes.aNormal !== undefined; - } - - isTextureShader() { - return this.samplers.length > 0; - } - - isColorShader() { - return ( - this.attributes.aVertexColor !== undefined || - this.uniforms.uMaterialColor !== undefined - ); - } - - isTexLightShader() { - return this.isLightShader() && this.isTextureShader(); - } - - isStrokeShader() { - return this.uniforms.uStrokeWeight !== undefined; - } - - /** - * @chainable - * @private - */ - enableAttrib(attr, size, type, normalized, stride, offset) { - if (attr) { - if ( - typeof IS_MINIFIED === 'undefined' && - this.attributes[attr.name] !== attr - ) { - console.warn( - `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.` - ); - } - const loc = attr.location; - if (loc !== -1) { - const gl = this._renderer.GL; - // Enable register even if it is disabled - if (!this._renderer.registerEnabled.has(loc)) { - gl.enableVertexAttribArray(loc); - // Record register availability - this._renderer.registerEnabled.add(loc); - } - this._renderer.GL.vertexAttribPointer( - loc, - size, - type || gl.FLOAT, - normalized || false, - stride || 0, - offset || 0 - ); - } - } - return this; - } - - /** - * Once all buffers have been bound, this checks to see if there are any - * remaining active attributes, likely left over from previous renders, - * and disables them so that they don't affect rendering. - * @private - */ - disableRemainingAttributes() { - for (const location of this._renderer.registerEnabled.values()) { - if ( - !Object.keys(this.attributes).some( - key => this.attributes[key].location === location - ) - ) { - this._renderer.GL.disableVertexAttribArray(location); - this._renderer.registerEnabled.delete(location); - } - } - } - }; + p5.Shader = Shader; } export default shader; +export { Shader }; if(typeof p5 !== 'undefined'){ shader(p5, p5.prototype); diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index f311dcc185..6bebe1662f 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -8,451 +8,459 @@ // import p5 from '../core/main'; import * as constants from '../core/constants'; +import { Element } from '../core/p5.Element'; +import { Renderer } from '../core/p5.Renderer'; +import { MediaElement } from '../dom/dom'; +import { Image } from '../image/p5.Image'; +import { Graphics } from '../core/p5.Graphics'; +import { FramebufferTexture } from './p5.Framebuffer'; + +class Texture { + constructor (renderer, obj, settings) { + this._renderer = renderer; + + const gl = this._renderer.GL; + + settings = settings || {}; + + this.src = obj; + this.glTex = undefined; + this.glTarget = gl.TEXTURE_2D; + this.glFormat = settings.format || gl.RGBA; + this.mipmaps = false; + this.glMinFilter = settings.minFilter || gl.LINEAR; + this.glMagFilter = settings.magFilter || gl.LINEAR; + this.glWrapS = settings.wrapS || gl.CLAMP_TO_EDGE; + this.glWrapT = settings.wrapT || gl.CLAMP_TO_EDGE; + this.glDataType = settings.dataType || gl.UNSIGNED_BYTE; + + const support = checkWebGLCapabilities(renderer); + if (this.glFormat === gl.HALF_FLOAT && !support.halfFloat) { + console.log('This device does not support dataType HALF_FLOAT. Falling back to FLOAT.'); + this.glDataType = gl.FLOAT; + } + if ( + this.glFormat === gl.HALF_FLOAT && + (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) && + !support.halfFloatLinear + ) { + console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.'); + if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST; + if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST; + } + if (this.glFormat === gl.FLOAT && !support.float) { + console.log('This device does not support dataType FLOAT. Falling back to UNSIGNED_BYTE.'); + this.glDataType = gl.UNSIGNED_BYTE; + } + if ( + this.glFormat === gl.FLOAT && + (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) && + !support.floatLinear + ) { + console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.'); + if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST; + if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST; + } + + // used to determine if this texture might need constant updating + // because it is a video or gif. + this.isSrcMediaElement = false; + typeof MediaElement !== 'undefined' && obj instanceof MediaElement; + this._videoPrevUpdateTime = 0; + this.isSrcHTMLElement = + typeof Element !== 'undefined' && + obj instanceof Element && + !(obj instanceof Graphics) && + !(obj instanceof Renderer); + this.isSrcP5Image = obj instanceof Image; + this.isSrcP5Graphics = obj instanceof Graphics; + this.isSrcP5Renderer = obj instanceof Renderer; + this.isImageData = + typeof ImageData !== 'undefined' && obj instanceof ImageData; + this.isFramebufferTexture = obj instanceof FramebufferTexture; + + const textureData = this._getTextureDataFromSource(); + this.width = textureData.width; + this.height = textureData.height; + + this.init(textureData); + return this; + } + + _getTextureDataFromSource () { + let textureData; + if (this.isFramebufferTexture) { + textureData = this.src.rawTexture(); + } else if (this.isSrcP5Image) { + // param is a p5.Image + textureData = this.src.canvas; + } else if ( + this.isSrcMediaElement || + this.isSrcP5Graphics || + this.isSrcHTMLElement + ) { + // if param is a video HTML element + textureData = this.src.elt; + } else if (this.isSrcP5Renderer) { + textureData = this.src.canvas; + } else if (this.isImageData) { + textureData = this.src; + } + return textureData; + } -function texture(p5, fn){ /** - * Texture class for WEBGL Mode + * Initializes common texture parameters, creates a gl texture, + * tries to upload the texture for the first time if data is + * already available. * @private - * @class p5.Texture - * @param {p5.RendererGL} renderer an instance of p5.RendererGL that - * will provide the GL context for this new p5.Texture - * @param {p5.Image|p5.Graphics|p5.Element|p5.MediaElement|ImageData|p5.Framebuffer|p5.FramebufferTexture|ImageData} [obj] the - * object containing the image data to store in the texture. - * @param {Object} [settings] optional A javascript object containing texture - * settings. - * @param {Number} [settings.format] optional The internal color component - * format for the texture. Possible values for format include gl.RGBA, - * gl.RGB, gl.ALPHA, gl.LUMINANCE, gl.LUMINANCE_ALPHA. Defaults to gl.RBGA - * @param {Number} [settings.minFilter] optional The texture minification - * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults - * to gl.LINEAR. Note, Mipmaps are not implemented in p5. - * @param {Number} [settings.magFilter] optional The texture magnification - * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults - * to gl.LINEAR. Note, Mipmaps are not implemented in p5. - * @param {Number} [settings.wrapS] optional The texture wrap settings for - * the s coordinate, or x axis. Possible values are gl.CLAMP_TO_EDGE, - * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available - * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE - * @param {Number} [settings.wrapT] optional The texture wrap settings for - * the t coordinate, or y axis. Possible values are gl.CLAMP_TO_EDGE, - * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available - * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE - * @param {Number} [settings.dataType] optional The data type of the texel - * data. Possible values are gl.UNSIGNED_BYTE or gl.FLOAT. There are more - * formats that are not implemented in p5. Defaults to gl.UNSIGNED_BYTE. + * @method init */ - p5.Texture = class Texture { - constructor (renderer, obj, settings) { - this._renderer = renderer; - - const gl = this._renderer.GL; - - settings = settings || {}; - - this.src = obj; - this.glTex = undefined; - this.glTarget = gl.TEXTURE_2D; - this.glFormat = settings.format || gl.RGBA; - this.mipmaps = false; - this.glMinFilter = settings.minFilter || gl.LINEAR; - this.glMagFilter = settings.magFilter || gl.LINEAR; - this.glWrapS = settings.wrapS || gl.CLAMP_TO_EDGE; - this.glWrapT = settings.wrapT || gl.CLAMP_TO_EDGE; - this.glDataType = settings.dataType || gl.UNSIGNED_BYTE; - - const support = checkWebGLCapabilities(renderer); - if (this.glFormat === gl.HALF_FLOAT && !support.halfFloat) { - console.log('This device does not support dataType HALF_FLOAT. Falling back to FLOAT.'); - this.glDataType = gl.FLOAT; - } - if ( - this.glFormat === gl.HALF_FLOAT && - (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) && - !support.halfFloatLinear - ) { - console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.'); - if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST; - if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST; - } - if (this.glFormat === gl.FLOAT && !support.float) { - console.log('This device does not support dataType FLOAT. Falling back to UNSIGNED_BYTE.'); - this.glDataType = gl.UNSIGNED_BYTE; - } - if ( - this.glFormat === gl.FLOAT && - (this.glMinFilter === gl.LINEAR || this.glMagFilter === gl.LINEAR) && - !support.floatLinear - ) { - console.log('This device does not support linear filtering for dataType FLOAT. Falling back to NEAREST.'); - if (this.glMinFilter === gl.LINEAR) this.glMinFilter = gl.NEAREST; - if (this.glMagFilter === gl.LINEAR) this.glMagFilter = gl.NEAREST; - } - - // used to determine if this texture might need constant updating - // because it is a video or gif. - this.isSrcMediaElement = - typeof p5.MediaElement !== 'undefined' && obj instanceof p5.MediaElement; - this._videoPrevUpdateTime = 0; - this.isSrcHTMLElement = - typeof p5.Element !== 'undefined' && - obj instanceof p5.Element && - !(obj instanceof p5.Graphics) && - !(obj instanceof p5.Renderer); - this.isSrcP5Image = obj instanceof p5.Image; - this.isSrcP5Graphics = obj instanceof p5.Graphics; - this.isSrcP5Renderer = obj instanceof p5.Renderer; - this.isImageData = - typeof ImageData !== 'undefined' && obj instanceof ImageData; - this.isFramebufferTexture = obj instanceof p5.FramebufferTexture; - - const textureData = this._getTextureDataFromSource(); - this.width = textureData.width; - this.height = textureData.height; - - this.init(textureData); - return this; + init (data) { + const gl = this._renderer.GL; + if (!this.isFramebufferTexture) { + this.glTex = gl.createTexture(); } - _getTextureDataFromSource () { - let textureData; - if (this.isFramebufferTexture) { - textureData = this.src.rawTexture(); - } else if (this.isSrcP5Image) { - // param is a p5.Image - textureData = this.src.canvas; - } else if ( - this.isSrcMediaElement || - this.isSrcP5Graphics || - this.isSrcHTMLElement - ) { - // if param is a video HTML element - textureData = this.src.elt; - } else if (this.isSrcP5Renderer) { - textureData = this.src.canvas; - } else if (this.isImageData) { - textureData = this.src; - } - return textureData; + this.glWrapS = this._renderer.textureWrapX; + this.glWrapT = this._renderer.textureWrapY; + + this.setWrapMode(this.glWrapS, this.glWrapT); + this.bindTexture(); + + //gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.glMagFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.glMinFilter); + + if (this.isFramebufferTexture) { + // Do nothing, the framebuffer manages its own content + } else if ( + this.width === 0 || + this.height === 0 || + (this.isSrcMediaElement && !this.src.loadedmetadata) + ) { + // assign a 1×1 empty texture initially, because data is not yet ready, + // so that no errors occur in gl console! + const tmpdata = new Uint8Array([1, 1, 1, 1]); + gl.texImage2D( + this.glTarget, + 0, + gl.RGBA, + 1, + 1, + 0, + this.glFormat, + this.glDataType, + tmpdata + ); + } else { + // data is ready: just push the texture! + gl.texImage2D( + this.glTarget, + 0, + this.glFormat, + this.glFormat, + this.glDataType, + data + ); } + } - /** - * Initializes common texture parameters, creates a gl texture, - * tries to upload the texture for the first time if data is - * already available. - * @private - * @method init - */ - init (data) { - const gl = this._renderer.GL; - if (!this.isFramebufferTexture) { - this.glTex = gl.createTexture(); - } - - this.glWrapS = this._renderer.textureWrapX; - this.glWrapT = this._renderer.textureWrapY; - - this.setWrapMode(this.glWrapS, this.glWrapT); - this.bindTexture(); - - //gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.glMagFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.glMinFilter); + /** + * Checks if the source data for this texture has changed (if it's + * easy to do so) and reuploads the texture if necessary. If it's not + * possible or to expensive to do a calculation to determine wheter or + * not the data has occurred, this method simply re-uploads the texture. + * @method update + */ + update () { + const data = this.src; + if (data.width === 0 || data.height === 0) { + return false; // nothing to do! + } - if (this.isFramebufferTexture) { - // Do nothing, the framebuffer manages its own content - } else if ( - this.width === 0 || - this.height === 0 || - (this.isSrcMediaElement && !this.src.loadedmetadata) - ) { - // assign a 1×1 empty texture initially, because data is not yet ready, - // so that no errors occur in gl console! - const tmpdata = new Uint8Array([1, 1, 1, 1]); - gl.texImage2D( - this.glTarget, - 0, - gl.RGBA, - 1, - 1, - 0, - this.glFormat, - this.glDataType, - tmpdata - ); - } else { - // data is ready: just push the texture! - gl.texImage2D( - this.glTarget, - 0, - this.glFormat, - this.glFormat, - this.glDataType, - data - ); - } + // FramebufferTexture instances wrap raw WebGL textures already, which + // don't need any extra updating, as they already live on the GPU + if (this.isFramebufferTexture) { + return false; } - /** - * Checks if the source data for this texture has changed (if it's - * easy to do so) and reuploads the texture if necessary. If it's not - * possible or to expensive to do a calculation to determine wheter or - * not the data has occurred, this method simply re-uploads the texture. - * @method update - */ - update () { - const data = this.src; - if (data.width === 0 || data.height === 0) { - return false; // nothing to do! + const textureData = this._getTextureDataFromSource(); + let updated = false; + + const gl = this._renderer.GL; + // pull texture from data, make sure width & height are appropriate + if ( + textureData.width !== this.width || + textureData.height !== this.height + ) { + updated = true; + + // make sure that if the width and height of this.src have changed + // for some reason, we update our metadata and upload the texture again + this.width = textureData.width || data.width; + this.height = textureData.height || data.height; + + if (this.isSrcP5Image) { + data.setModified(false); + } else if (this.isSrcMediaElement || this.isSrcHTMLElement) { + // on the first frame the metadata comes in, the size will be changed + // from 0 to actual size, but pixels may not be available. + // flag for update in a future frame. + // if we don't do this, a paused video, for example, may not + // send the first frame to texture memory. + data.setModified(true); } - - // FramebufferTexture instances wrap raw WebGL textures already, which - // don't need any extra updating, as they already live on the GPU - if (this.isFramebufferTexture) { - return false; + } else if (this.isSrcP5Image) { + // for an image, we only update if the modified field has been set, + // for example, by a call to p5.Image.set + if (data.isModified()) { + updated = true; + data.setModified(false); } - - const textureData = this._getTextureDataFromSource(); - let updated = false; - - const gl = this._renderer.GL; - // pull texture from data, make sure width & height are appropriate - if ( - textureData.width !== this.width || - textureData.height !== this.height - ) { + } else if (this.isSrcMediaElement) { + // for a media element (video), we'll check if the current time in + // the video frame matches the last time. if it doesn't match, the + // video has advanced or otherwise been taken to a new frame, + // and we need to upload it. + if (data.isModified()) { + // p5.MediaElement may have also had set/updatePixels, etc. called + // on it and should be updated, or may have been set for the first + // time! updated = true; - - // make sure that if the width and height of this.src have changed - // for some reason, we update our metadata and upload the texture again - this.width = textureData.width || data.width; - this.height = textureData.height || data.height; - - if (this.isSrcP5Image) { - data.setModified(false); - } else if (this.isSrcMediaElement || this.isSrcHTMLElement) { - // on the first frame the metadata comes in, the size will be changed - // from 0 to actual size, but pixels may not be available. - // flag for update in a future frame. - // if we don't do this, a paused video, for example, may not - // send the first frame to texture memory. - data.setModified(true); - } - } else if (this.isSrcP5Image) { - // for an image, we only update if the modified field has been set, - // for example, by a call to p5.Image.set - if (data.isModified()) { - updated = true; - data.setModified(false); - } - } else if (this.isSrcMediaElement) { - // for a media element (video), we'll check if the current time in - // the video frame matches the last time. if it doesn't match, the - // video has advanced or otherwise been taken to a new frame, - // and we need to upload it. - if (data.isModified()) { - // p5.MediaElement may have also had set/updatePixels, etc. called - // on it and should be updated, or may have been set for the first - // time! + data.setModified(false); + } else if (data.loadedmetadata) { + // if the meta data has been loaded, we can ask the video + // what it's current position (in time) is. + if (this._videoPrevUpdateTime !== data.time()) { + // update the texture in gpu mem only if the current + // video timestamp does not match the timestamp of the last + // time we uploaded this texture (and update the time we + // last uploaded, too) + this._videoPrevUpdateTime = data.time(); updated = true; - data.setModified(false); - } else if (data.loadedmetadata) { - // if the meta data has been loaded, we can ask the video - // what it's current position (in time) is. - if (this._videoPrevUpdateTime !== data.time()) { - // update the texture in gpu mem only if the current - // video timestamp does not match the timestamp of the last - // time we uploaded this texture (and update the time we - // last uploaded, too) - this._videoPrevUpdateTime = data.time(); - updated = true; - } } - } else if (this.isImageData) { - if (data._dirty) { - data._dirty = false; - updated = true; - } - } else { - /* data instanceof p5.Graphics, probably */ - // there is not enough information to tell if the texture can be - // conditionally updated; so to be safe, we just go ahead and upload it. - updated = true; } - - if (updated) { - this.bindTexture(); - gl.texImage2D( - this.glTarget, - 0, - this.glFormat, - this.glFormat, - this.glDataType, - textureData - ); + } else if (this.isImageData) { + if (data._dirty) { + data._dirty = false; + updated = true; } - - return updated; - } - - /** - * Binds the texture to the appropriate GL target. - * @method bindTexture - */ - bindTexture () { - // bind texture using gl context + glTarget and - // generated gl texture object - const gl = this._renderer.GL; - gl.bindTexture(this.glTarget, this.getTexture()); - - return this; + } else { + /* data instanceof p5.Graphics, probably */ + // there is not enough information to tell if the texture can be + // conditionally updated; so to be safe, we just go ahead and upload it. + updated = true; } - /** - * Unbinds the texture from the appropriate GL target. - * @method unbindTexture - */ - unbindTexture () { - // unbind per above, disable texturing on glTarget - const gl = this._renderer.GL; - gl.bindTexture(this.glTarget, null); + if (updated) { + this.bindTexture(); + gl.texImage2D( + this.glTarget, + 0, + this.glFormat, + this.glFormat, + this.glDataType, + textureData + ); } - getTexture() { - if (this.isFramebufferTexture) { - return this.src.rawTexture(); - } else { - return this.glTex; - } - } + return updated; + } - /** - * Sets how a texture is be interpolated when upscaled or downscaled. - * Nearest filtering uses nearest neighbor scaling when interpolating - * Linear filtering uses WebGL's linear scaling when interpolating - * @method setInterpolation - * @param {String} downScale Specifies the texture filtering when - * textures are shrunk. Options are LINEAR or NEAREST - * @param {String} upScale Specifies the texture filtering when - * textures are magnified. Options are LINEAR or NEAREST - * @todo implement mipmapping filters - */ - setInterpolation (downScale, upScale) { - const gl = this._renderer.GL; + /** + * Binds the texture to the appropriate GL target. + * @method bindTexture + */ + bindTexture () { + // bind texture using gl context + glTarget and + // generated gl texture object + const gl = this._renderer.GL; + gl.bindTexture(this.glTarget, this.getTexture()); - this.glMinFilter = this.glFilter(downScale); - this.glMagFilter = this.glFilter(upScale); + return this; + } - this.bindTexture(); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.glMinFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.glMagFilter); - this.unbindTexture(); + /** + * Unbinds the texture from the appropriate GL target. + * @method unbindTexture + */ + unbindTexture () { + // unbind per above, disable texturing on glTarget + const gl = this._renderer.GL; + gl.bindTexture(this.glTarget, null); + } + + getTexture() { + if (this.isFramebufferTexture) { + return this.src.rawTexture(); + } else { + return this.glTex; } + } - glFilter(filter) { - const gl = this._renderer.GL; - if (filter === constants.NEAREST) { - return gl.NEAREST; - } else { - return gl.LINEAR; - } + /** + * Sets how a texture is be interpolated when upscaled or downscaled. + * Nearest filtering uses nearest neighbor scaling when interpolating + * Linear filtering uses WebGL's linear scaling when interpolating + * @method setInterpolation + * @param {String} downScale Specifies the texture filtering when + * textures are shrunk. Options are LINEAR or NEAREST + * @param {String} upScale Specifies the texture filtering when + * textures are magnified. Options are LINEAR or NEAREST + * @todo implement mipmapping filters + */ + setInterpolation (downScale, upScale) { + const gl = this._renderer.GL; + + this.glMinFilter = this.glFilter(downScale); + this.glMagFilter = this.glFilter(upScale); + + this.bindTexture(); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.glMinFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.glMagFilter); + this.unbindTexture(); + } + + glFilter(filter) { + const gl = this._renderer.GL; + if (filter === constants.NEAREST) { + return gl.NEAREST; + } else { + return gl.LINEAR; } + } - /** - * Sets the texture wrapping mode. This controls how textures behave - * when their uv's go outside of the 0 - 1 range. There are three options: - * CLAMP, REPEAT, and MIRROR. REPEAT & MIRROR are only available if the texture - * is a power of two size (128, 256, 512, 1024, etc.). - * @method setWrapMode - * @param {String} wrapX Controls the horizontal texture wrapping behavior - * @param {String} wrapY Controls the vertical texture wrapping behavior - */ - setWrapMode (wrapX, wrapY) { - const gl = this._renderer.GL; - - // for webgl 1 we need to check if the texture is power of two - // if it isn't we will set the wrap mode to CLAMP - // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet - const isPowerOfTwo = x => (x & (x - 1)) === 0; - const textureData = this._getTextureDataFromSource(); + /** + * Sets the texture wrapping mode. This controls how textures behave + * when their uv's go outside of the 0 - 1 range. There are three options: + * CLAMP, REPEAT, and MIRROR. REPEAT & MIRROR are only available if the texture + * is a power of two size (128, 256, 512, 1024, etc.). + * @method setWrapMode + * @param {String} wrapX Controls the horizontal texture wrapping behavior + * @param {String} wrapY Controls the vertical texture wrapping behavior + */ + setWrapMode (wrapX, wrapY) { + const gl = this._renderer.GL; + + // for webgl 1 we need to check if the texture is power of two + // if it isn't we will set the wrap mode to CLAMP + // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet + const isPowerOfTwo = x => (x & (x - 1)) === 0; + const textureData = this._getTextureDataFromSource(); + + let wrapWidth; + let wrapHeight; + + if (textureData.naturalWidth && textureData.naturalHeight) { + wrapWidth = textureData.naturalWidth; + wrapHeight = textureData.naturalHeight; + } else { + wrapWidth = this.width; + wrapHeight = this.height; + } - let wrapWidth; - let wrapHeight; + const widthPowerOfTwo = isPowerOfTwo(wrapWidth); + const heightPowerOfTwo = isPowerOfTwo(wrapHeight); - if (textureData.naturalWidth && textureData.naturalHeight) { - wrapWidth = textureData.naturalWidth; - wrapHeight = textureData.naturalHeight; + if (wrapX === constants.REPEAT) { + if ( + this._renderer.webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + this.glWrapS = gl.REPEAT; } else { - wrapWidth = this.width; - wrapHeight = this.height; + console.warn( + 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' + ); + this.glWrapS = gl.CLAMP_TO_EDGE; } - - const widthPowerOfTwo = isPowerOfTwo(wrapWidth); - const heightPowerOfTwo = isPowerOfTwo(wrapHeight); - - if (wrapX === constants.REPEAT) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - this.glWrapS = gl.REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' - ); - this.glWrapS = gl.CLAMP_TO_EDGE; - } - } else if (wrapX === constants.MIRROR) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - this.glWrapS = gl.MIRRORED_REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' - ); - this.glWrapS = gl.CLAMP_TO_EDGE; - } + } else if (wrapX === constants.MIRROR) { + if ( + this._renderer.webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + this.glWrapS = gl.MIRRORED_REPEAT; } else { - // falling back to default if didn't get a proper mode + console.warn( + 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' + ); this.glWrapS = gl.CLAMP_TO_EDGE; } + } else { + // falling back to default if didn't get a proper mode + this.glWrapS = gl.CLAMP_TO_EDGE; + } - if (wrapY === constants.REPEAT) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - this.glWrapT = gl.REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' - ); - this.glWrapT = gl.CLAMP_TO_EDGE; - } - } else if (wrapY === constants.MIRROR) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - this.glWrapT = gl.MIRRORED_REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' - ); - this.glWrapT = gl.CLAMP_TO_EDGE; - } + if (wrapY === constants.REPEAT) { + if ( + this._renderer.webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + this.glWrapT = gl.REPEAT; } else { - // falling back to default if didn't get a proper mode + console.warn( + 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' + ); this.glWrapT = gl.CLAMP_TO_EDGE; } - - this.bindTexture(); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this.glWrapS); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this.glWrapT); - this.unbindTexture(); + } else if (wrapY === constants.MIRROR) { + if ( + this._renderer.webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + this.glWrapT = gl.MIRRORED_REPEAT; + } else { + console.warn( + 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' + ); + this.glWrapT = gl.CLAMP_TO_EDGE; + } + } else { + // falling back to default if didn't get a proper mode + this.glWrapT = gl.CLAMP_TO_EDGE; } - }; + + this.bindTexture(); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, this.glWrapS); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this.glWrapT); + this.unbindTexture(); + } +}; + +function texture(p5, fn){ + /** + * Texture class for WEBGL Mode + * @private + * @class p5.Texture + * @param {p5.RendererGL} renderer an instance of p5.RendererGL that + * will provide the GL context for this new p5.Texture + * @param {p5.Image|p5.Graphics|p5.Element|p5.MediaElement|ImageData|p5.Framebuffer|p5.FramebufferTexture|ImageData} [obj] the + * object containing the image data to store in the texture. + * @param {Object} [settings] optional A javascript object containing texture + * settings. + * @param {Number} [settings.format] optional The internal color component + * format for the texture. Possible values for format include gl.RGBA, + * gl.RGB, gl.ALPHA, gl.LUMINANCE, gl.LUMINANCE_ALPHA. Defaults to gl.RBGA + * @param {Number} [settings.minFilter] optional The texture minification + * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults + * to gl.LINEAR. Note, Mipmaps are not implemented in p5. + * @param {Number} [settings.magFilter] optional The texture magnification + * filter setting. Possible values are gl.NEAREST or gl.LINEAR. Defaults + * to gl.LINEAR. Note, Mipmaps are not implemented in p5. + * @param {Number} [settings.wrapS] optional The texture wrap settings for + * the s coordinate, or x axis. Possible values are gl.CLAMP_TO_EDGE, + * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available + * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE + * @param {Number} [settings.wrapT] optional The texture wrap settings for + * the t coordinate, or y axis. Possible values are gl.CLAMP_TO_EDGE, + * gl.REPEAT, and gl.MIRRORED_REPEAT. The mirror settings are only available + * when using a power of two sized texture. Defaults to gl.CLAMP_TO_EDGE + * @param {Number} [settings.dataType] optional The data type of the texel + * data. Possible values are gl.UNSIGNED_BYTE or gl.FLOAT. There are more + * formats that are not implemented in p5. Defaults to gl.UNSIGNED_BYTE. + */ + p5.Texture = Texture; p5.MipmapTexture = class MipmapTexture extends p5.Texture { constructor(renderer, levels, settings) { @@ -522,7 +530,8 @@ export function checkWebGLCapabilities({ GL, webglVersion }) { } export default texture; +export { Texture }; if(typeof p5 !== 'undefined'){ texture(p5, p5.prototype); -} \ No newline at end of file +} From cfa67aa2614577cd801e73e048a298c2c33f9551 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 15 Oct 2024 21:23:26 +0100 Subject: [PATCH 23/55] Use class imports wherever possible --- src/core/p5.Graphics.js | 2 +- src/webgl/3d_primitives.js | 60 +- src/webgl/GeometryBuilder.js | 9 +- src/webgl/interaction.js | 3 +- src/webgl/light.js | 133 +- src/webgl/loading.js | 15 +- src/webgl/material.js | 14 +- src/webgl/p5.Camera.js | 4429 +++++++++++++------------- src/webgl/p5.Framebuffer.js | 3 +- src/webgl/p5.Geometry.js | 631 ++-- src/webgl/p5.Matrix.js | 4 +- src/webgl/p5.RendererGL.Immediate.js | 39 +- src/webgl/p5.RendererGL.Retained.js | 26 +- src/webgl/p5.RendererGL.js | 1476 +-------- src/webgl/p5.Shader.js | 4 +- src/webgl/p5.Texture.js | 94 +- src/webgl/text.js | 50 +- 17 files changed, 2928 insertions(+), 4064 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index fc20604ef2..51c694c353 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -44,7 +44,7 @@ class Graphics { // Attach renderer properties for (const p in this._renderer) { - if(p[0] === '_') continue; + if(p[0] === '_' || typeof this._renderer[p] === 'function') continue; Object.defineProperty(this, p, { get(){ return this._renderer?.[p]; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 5de854256b..b3a0463350 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -8,6 +8,9 @@ import * as constants from '../core/constants'; import { RendererGL } from './p5.RendererGL'; +import { Vector } from '../math/p5.Vector'; +import { Geometry } from './p5.Geometry'; +import { Matrix } from './p5.Matrix'; function primitives3D(p5, fn){ /** @@ -983,13 +986,13 @@ function primitives3D(p5, fn){ v = i / this.detailY; for (let j = 0; j <= this.detailX; j++) { u = j / this.detailX; - p = new p5.Vector(u - 0.5, v - 0.5, 0); + p = new Vector(u - 0.5, v - 0.5, 0); this.vertices.push(p); this.uvs.push(u, v); } } }; - const planeGeom = new p5.Geometry(detailX, detailY, _plane); + const planeGeom = new Geometry(detailX, detailY, _plane); planeGeom.computeFaces().computeNormals(); if (detailX <= 1 && detailY <= 1) { planeGeom._makeTriangleEdges()._edgesToVertices(); @@ -1192,7 +1195,7 @@ function primitives3D(p5, fn){ //inspired by lightgl: //https://github.com/evanw/lightgl.js //octants:https://en.wikipedia.org/wiki/Octant_(solid_geometry) - const octant = new p5.Vector( + const octant = new Vector( ((d & 1) * 2 - 1) / 2, ((d & 2) - 1) / 2, ((d & 4) / 2 - 1) / 2 @@ -1204,7 +1207,7 @@ function primitives3D(p5, fn){ this.faces.push([v + 2, v + 1, v + 3]); }); }; - const boxGeom = new p5.Geometry(detailX, detailY, _box); + const boxGeom = new Geometry(detailX, detailY, _box); boxGeom.computeNormals(); if (detailX <= 4 && detailY <= 4) { boxGeom._edgesToVertices(); @@ -1417,16 +1420,16 @@ function primitives3D(p5, fn){ const cur = Math.cos(ur); //VERTICES - this.vertices.push(new p5.Vector(sur * ringRadius, y, cur * ringRadius)); + this.vertices.push(new Vector(sur * ringRadius, y, cur * ringRadius)); //VERTEX NORMALS let vertexNormal; if (yy < 0) { - vertexNormal = new p5.Vector(0, -1, 0); + vertexNormal = new Vector(0, -1, 0); } else if (yy > detailY && topRadius) { - vertexNormal = new p5.Vector(0, 1, 0); + vertexNormal = new Vector(0, 1, 0); } else { - vertexNormal = new p5.Vector(sur * cosSlant, sinSlant, cur * cosSlant); + vertexNormal = new Vector(sur * cosSlant, sinSlant, cur * cosSlant); } this.vertexNormals.push(vertexNormal); //UVs @@ -1944,7 +1947,7 @@ function primitives3D(p5, fn){ const gId = `cone|${detailX}|${detailY}|${cap}`; if (!this._renderer.geometryInHash(gId)) { - const coneGeom = new p5.Geometry(detailX, detailY); + const coneGeom = new Geometry(detailX, detailY); _truncatedCone.call(coneGeom, 1, 0, 1, detailX, detailY, cap, false); if (detailX <= 24 && detailY <= 16) { coneGeom._makeTriangleEdges()._edgesToVertices(); @@ -2162,7 +2165,7 @@ function primitives3D(p5, fn){ } } }; - const ellipsoidGeom = new p5.Geometry(detailX, detailY, _ellipsoid); + const ellipsoidGeom = new Geometry(detailX, detailY, _ellipsoid); ellipsoidGeom.computeFaces(); if (detailX <= 24 && detailY <= 24) { ellipsoidGeom._makeTriangleEdges()._edgesToVertices(); @@ -2370,13 +2373,13 @@ function primitives3D(p5, fn){ const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); - const p = new p5.Vector( + const p = new Vector( r * cosTheta, r * sinTheta, tubeRatio * sinPhi ); - const n = new p5.Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); + const n = new Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); this.vertices.push(p); this.vertexNormals.push(n); @@ -2384,7 +2387,7 @@ function primitives3D(p5, fn){ } } }; - const torusGeom = new p5.Geometry(detailX, detailY, _torus); + const torusGeom = new Geometry(detailX, detailY, _torus); torusGeom.computeFaces(); if (detailX <= 24 && detailY <= 16) { torusGeom._makeTriangleEdges()._edgesToVertices(); @@ -2443,7 +2446,7 @@ function primitives3D(p5, fn){ RendererGL.prototype.point = function(x, y, z = 0) { const _vertex = []; - _vertex.push(new p5.Vector(x, y, z)); + _vertex.push(new Vector(x, y, z)); this._drawPoints(_vertex, this.immediateMode.buffers.point); return this; @@ -2461,15 +2464,15 @@ function primitives3D(p5, fn){ if (!this.geometryInHash(gId)) { const _triangle = function() { const vertices = []; - vertices.push(new p5.Vector(0, 0, 0)); - vertices.push(new p5.Vector(1, 0, 0)); - vertices.push(new p5.Vector(0, 1, 0)); + vertices.push(new Vector(0, 0, 0)); + vertices.push(new Vector(1, 0, 0)); + vertices.push(new Vector(0, 1, 0)); this.edges = [[0, 1], [1, 2], [2, 0]]; this.vertices = vertices; this.faces = [[0, 1, 2]]; this.uvs = [0, 0, 1, 0, 1, 1]; }; - const triGeom = new p5.Geometry(1, 1, _triangle); + const triGeom = new Geometry(1, 1, _triangle); triGeom._edgesToVertices(); triGeom.computeNormals(); this.createBuffers(gId, triGeom); @@ -2485,7 +2488,7 @@ function primitives3D(p5, fn){ try { // triangle orientation. const orientation = Math.sign(x1*y2-x2*y1 + x2*y3-x3*y2 + x3*y1-x1*y3); - const mult = new p5.Matrix([ + const mult = new Matrix([ x2 - x1, y2 - y1, 0, 0, // the resulting unit X-axis x3 - x1, y3 - y1, 0, 0, // the resulting unit Y-axis 0, 0, orientation, 0, // the resulting unit Z-axis (Reflect the specified order of vertices) @@ -2544,7 +2547,7 @@ function primitives3D(p5, fn){ if (start.toFixed(10) !== stop.toFixed(10)) { // if the mode specified is PIE or null, push the mid point of the arc in vertices if (mode === constants.PIE || typeof mode === 'undefined') { - this.vertices.push(new p5.Vector(0.5, 0.5, 0)); + this.vertices.push(new Vector(0.5, 0.5, 0)); this.uvs.push([0.5, 0.5]); } @@ -2556,7 +2559,7 @@ function primitives3D(p5, fn){ const _x = 0.5 + Math.cos(theta) / 2; const _y = 0.5 + Math.sin(theta) / 2; - this.vertices.push(new p5.Vector(_x, _y, 0)); + this.vertices.push(new Vector(_x, _y, 0)); this.uvs.push([_x, _y]); if (i < detail - 1) { @@ -2604,7 +2607,7 @@ function primitives3D(p5, fn){ } }; - const arcGeom = new p5.Geometry(detail, 1, _arc); + const arcGeom = new Geometry(detail, 1, _arc); arcGeom.computeNormals(); if (detail <= 50) { @@ -2651,7 +2654,7 @@ function primitives3D(p5, fn){ const v = i / this.detailY; for (let j = 0; j <= this.detailX; j++) { const u = j / this.detailX; - const p = new p5.Vector(u, v, 0); + const p = new Vector(u, v, 0); this.vertices.push(p); this.uvs.push(u, v); } @@ -2666,7 +2669,7 @@ function primitives3D(p5, fn){ ]; } }; - const rectGeom = new p5.Geometry(detailX, detailY, _rect); + const rectGeom = new Geometry(detailX, detailY, _rect); rectGeom .computeFaces() .computeNormals() @@ -2772,7 +2775,7 @@ function primitives3D(p5, fn){ `quad|${x1}|${y1}|${z1}|${x2}|${y2}|${z2}|${x3}|${y3}|${z3}|${x4}|${y4}|${z4}|${detailX}|${detailY}`; if (!this.geometryInHash(gId)) { - const quadGeom = new p5.Geometry(detailX, detailY, function() { + const quadGeom = new Geometry(detailX, detailY, function() { //algorithm adapted from c++ to js //https://stackoverflow.com/questions/16989181/whats-the-correct-way-to-draw-a-distorted-plane-in-opengl/16993202#16993202 let xRes = 1.0 / (this.detailX - 1); @@ -2793,7 +2796,7 @@ function primitives3D(p5, fn){ let pty = (1 - pctx) * linePt0y + pctx * linePt1y; let ptz = (1 - pctx) * linePt0z + pctx * linePt1z; - this.vertices.push(new p5.Vector(ptx, pty, ptz)); + this.vertices.push(new Vector(ptx, pty, ptz)); this.uvs.push([pctx, pcty]); } } @@ -3533,11 +3536,10 @@ function primitives3D(p5, fn){ } this._pInst.push(); - - this._pInst.noLights(); + this.noLights(); this._pInst.noStroke(); - this._pInst.texture(img); + this.texture(img); this._pInst.textureMode(constants.NORMAL); let u0 = 0; diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 57cfc582e5..a6889ba457 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -1,5 +1,6 @@ -import p5 from '../core/main'; import * as constants from '../core/constants'; +import { Matrix } from './p5.Matrix'; +import { Geometry } from './p5.Geometry'; /** * @private @@ -10,9 +11,9 @@ class GeometryBuilder { constructor(renderer) { this.renderer = renderer; renderer._pInst.push(); - this.identityMatrix = new p5.Matrix(); - renderer.states.uModelMatrix = new p5.Matrix(); - this.geometry = new p5.Geometry(); + this.identityMatrix = new Matrix(); + renderer.states.uModelMatrix = new Matrix(); + this.geometry = new Geometry(); this.geometry.gid = `_p5_GeometryBuilder_${GeometryBuilder.nextGeometryId}`; GeometryBuilder.nextGeometryId++; this.hasTransform = false; diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index 8ff849fa2f..a127149a94 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -6,6 +6,7 @@ */ import * as constants from '../core/constants'; +import { Vector } from '../math/p5.Vector'; function interaction(p5, fn){ /** @@ -428,7 +429,7 @@ function interaction(p5, fn){ const viewZ = Math.sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ); // position vector of the center. - let cv = new p5.Vector(cam.centerX, cam.centerY, cam.centerZ); + let cv = new Vector(cam.centerX, cam.centerY, cam.centerZ); // Calculate the normalized device coordinates of the center. cv = cam.cameraMatrix.multiplyPoint(cv); diff --git a/src/webgl/light.js b/src/webgl/light.js index 4b6dda9057..ed70d92b00 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -5,6 +5,9 @@ * @requires core */ +import { RendererGL } from './p5.RendererGL'; +import { Vector } from '../math/p5.Vector'; + function light(p5, fn){ /** * Creates a light that shines from all directions. @@ -1492,69 +1495,69 @@ function light(p5, fn){ case 11: case 10: color = this.color(v1, v2, v3); - position = new p5.Vector(x, y, z); - direction = new p5.Vector(nx, ny, nz); + position = new Vector(x, y, z); + direction = new Vector(nx, ny, nz); break; case 9: if (v1 instanceof p5.Color) { color = v1; - position = new p5.Vector(v2, v3, x); - direction = new p5.Vector(y, z, nx); + position = new Vector(v2, v3, x); + direction = new Vector(y, z, nx); angle = ny; concentration = nz; - } else if (x instanceof p5.Vector) { + } else if (x instanceof Vector) { color = this.color(v1, v2, v3); position = x; - direction = new p5.Vector(y, z, nx); + direction = new Vector(y, z, nx); angle = ny; concentration = nz; - } else if (nx instanceof p5.Vector) { + } else if (nx instanceof Vector) { color = this.color(v1, v2, v3); - position = new p5.Vector(x, y, z); + position = new Vector(x, y, z); direction = nx; angle = ny; concentration = nz; } else { color = this.color(v1, v2, v3); - position = new p5.Vector(x, y, z); - direction = new p5.Vector(nx, ny, nz); + position = new Vector(x, y, z); + direction = new Vector(nx, ny, nz); } break; case 8: if (v1 instanceof p5.Color) { color = v1; - position = new p5.Vector(v2, v3, x); - direction = new p5.Vector(y, z, nx); + position = new Vector(v2, v3, x); + direction = new Vector(y, z, nx); angle = ny; - } else if (x instanceof p5.Vector) { + } else if (x instanceof Vector) { color = this.color(v1, v2, v3); position = x; - direction = new p5.Vector(y, z, nx); + direction = new Vector(y, z, nx); angle = ny; } else { color = this.color(v1, v2, v3); - position = new p5.Vector(x, y, z); + position = new Vector(x, y, z); direction = nx; angle = ny; } break; case 7: - if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { + if (v1 instanceof p5.Color && v2 instanceof Vector) { color = v1; position = v2; - direction = new p5.Vector(v3, x, y); + direction = new Vector(v3, x, y); angle = z; concentration = nx; - } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { + } else if (v1 instanceof p5.Color && y instanceof Vector) { color = v1; - position = new p5.Vector(v2, v3, x); + position = new Vector(v2, v3, x); direction = y; angle = z; concentration = nx; - } else if (x instanceof p5.Vector && y instanceof p5.Vector) { + } else if (x instanceof Vector && y instanceof Vector) { color = this.color(v1, v2, v3); position = x; direction = y; @@ -1562,34 +1565,34 @@ function light(p5, fn){ concentration = nx; } else if (v1 instanceof p5.Color) { color = v1; - position = new p5.Vector(v2, v3, x); - direction = new p5.Vector(y, z, nx); - } else if (x instanceof p5.Vector) { + position = new Vector(v2, v3, x); + direction = new Vector(y, z, nx); + } else if (x instanceof Vector) { color = this.color(v1, v2, v3); position = x; - direction = new p5.Vector(y, z, nx); + direction = new Vector(y, z, nx); } else { color = this.color(v1, v2, v3); - position = new p5.Vector(x, y, z); + position = new Vector(x, y, z); direction = nx; } break; case 6: - if (x instanceof p5.Vector && y instanceof p5.Vector) { + if (x instanceof Vector && y instanceof Vector) { color = this.color(v1, v2, v3); position = x; direction = y; angle = z; - } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { + } else if (v1 instanceof p5.Color && y instanceof Vector) { color = v1; - position = new p5.Vector(v2, v3, x); + position = new Vector(v2, v3, x); direction = y; angle = z; - } else if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { + } else if (v1 instanceof p5.Color && v2 instanceof Vector) { color = v1; position = v2; - direction = new p5.Vector(v3, x, y); + direction = new Vector(v3, x, y); angle = z; } break; @@ -1597,26 +1600,26 @@ function light(p5, fn){ case 5: if ( v1 instanceof p5.Color && - v2 instanceof p5.Vector && - v3 instanceof p5.Vector + v2 instanceof Vector && + v3 instanceof Vector ) { color = v1; position = v2; direction = v3; angle = x; concentration = y; - } else if (x instanceof p5.Vector && y instanceof p5.Vector) { + } else if (x instanceof Vector && y instanceof Vector) { color = this.color(v1, v2, v3); position = x; direction = y; - } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { + } else if (v1 instanceof p5.Color && y instanceof Vector) { color = v1; - position = new p5.Vector(v2, v3, x); + position = new Vector(v2, v3, x); direction = y; - } else if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { + } else if (v1 instanceof p5.Color && v2 instanceof Vector) { color = v1; position = v2; - direction = new p5.Vector(v3, x, y); + direction = new Vector(v3, x, y); } break; @@ -1743,37 +1746,41 @@ function light(p5, fn){ this._assert3d('noLights'); p5._validateParameters('noLights', args); - this._renderer.states.activeImageLight = null; - this._renderer.states._enableLighting = false; - - this._renderer.states.ambientLightColors.length = 0; - this._renderer.states.specularColors = [1, 1, 1]; - - this._renderer.states.directionalLightDirections.length = 0; - this._renderer.states.directionalLightDiffuseColors.length = 0; - this._renderer.states.directionalLightSpecularColors.length = 0; - - this._renderer.states.pointLightPositions.length = 0; - this._renderer.states.pointLightDiffuseColors.length = 0; - this._renderer.states.pointLightSpecularColors.length = 0; - - this._renderer.states.spotLightPositions.length = 0; - this._renderer.states.spotLightDirections.length = 0; - this._renderer.states.spotLightDiffuseColors.length = 0; - this._renderer.states.spotLightSpecularColors.length = 0; - this._renderer.states.spotLightAngle.length = 0; - this._renderer.states.spotLightConc.length = 0; - - this._renderer.states.constantAttenuation = 1; - this._renderer.states.linearAttenuation = 0; - this._renderer.states.quadraticAttenuation = 0; - this._renderer.states._useShininess = 1; - this._renderer.states._useMetalness = 0; + this._renderer.noLights(); return this; }; } +RendererGL.prototype.noLights = function(){ + this.states.activeImageLight = null; + this.states._enableLighting = false; + + this.states.ambientLightColors.length = 0; + this.states.specularColors = [1, 1, 1]; + + this.states.directionalLightDirections.length = 0; + this.states.directionalLightDiffuseColors.length = 0; + this.states.directionalLightSpecularColors.length = 0; + + this.states.pointLightPositions.length = 0; + this.states.pointLightDiffuseColors.length = 0; + this.states.pointLightSpecularColors.length = 0; + + this.states.spotLightPositions.length = 0; + this.states.spotLightDirections.length = 0; + this.states.spotLightDiffuseColors.length = 0; + this.states.spotLightSpecularColors.length = 0; + this.states.spotLightAngle.length = 0; + this.states.spotLightConc.length = 0; + + this.states.constantAttenuation = 1; + this.states.linearAttenuation = 0; + this.states.quadraticAttenuation = 0; + this.states._useShininess = 1; + this.states._useMetalness = 0; +} + export default light; if(typeof p5 !== 'undefined'){ diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 9eae577b57..3fadd77c5d 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -6,6 +6,9 @@ * @requires p5.Geometry */ +import { Geometry } from "./p5.Geometry"; +import { Vector } from "../math/p5.Vector"; + function loading(p5, fn){ /** * Loads a 3D model to create a @@ -363,7 +366,7 @@ function loading(p5, fn){ } } - const model = new p5.Geometry(); + const model = new Geometry(); model.gid = `${path}|${normalize}`; const self = this; @@ -810,12 +813,12 @@ function loading(p5, fn){ b = defaultB; } } - const newNormal = new p5.Vector(normalX, normalY, normalZ); + const newNormal = new Vector(normalX, normalY, normalZ); for (let i = 1; i <= 3; i++) { const vertexstart = start + i * 12; - const newVertex = new p5.Vector( + const newVertex = new Vector( reader.getFloat32(vertexstart, true), reader.getFloat32(vertexstart + 4, true), reader.getFloat32(vertexstart + 8, true) @@ -891,7 +894,7 @@ function loading(p5, fn){ return; } else { // Push normal for first face - newNormal = new p5.Vector( + newNormal = new Vector( parseFloat(parts[2]), parseFloat(parts[3]), parseFloat(parts[4]) @@ -916,7 +919,7 @@ function loading(p5, fn){ case 'vertex': if (parts[0] === 'vertex') { //Vertex of triangle - newVertex = new p5.Vector( + newVertex = new Vector( parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]) @@ -955,7 +958,7 @@ function loading(p5, fn){ // End of solid } else if (parts[0] === 'facet' && parts[1] === 'normal') { // Next face - newNormal = new p5.Vector( + newNormal = new Vector( parseFloat(parts[2]), parseFloat(parts[3]), parseFloat(parts[4]) diff --git a/src/webgl/material.js b/src/webgl/material.js index f50f7a2582..d70b88db43 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -7,6 +7,7 @@ import * as constants from '../core/constants'; import { RendererGL } from './p5.RendererGL'; +import { Shader } from './p5.Shader'; function material(p5, fn){ /** @@ -130,7 +131,7 @@ function material(p5, fn){ failureCallback = console.error; } - const loadedShader = new p5.Shader(); + const loadedShader = new Shader(); const self = this; let loadedFrag = false; @@ -534,7 +535,7 @@ function material(p5, fn){ */ fn.createShader = function (vertSrc, fragSrc, options) { p5._validateParameters('createShader', arguments); - return new p5.Shader(this._renderer, vertSrc, fragSrc, options); + return new Shader(this._renderer, vertSrc, fragSrc, options); }; /** @@ -670,7 +671,7 @@ function material(p5, fn){ } `; let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; - const shader = new p5.Shader(this._renderer, vertSrc, fragSrc); + const shader = new Shader(this._renderer, vertSrc, fragSrc); if (this._renderer.GL) { shader.ensureCompiledOnContext(this); } else { @@ -3306,6 +3307,13 @@ function material(p5, fn){ this._cachedBlendMode = this.states.curBlendMode; } }; + + RendererGL.prototype.texture = function(tex) { + this.states.drawMode = constants.TEXTURE; + this.states._useNormalMaterial = false; + this.states._tex = tex; + this.states.doFill = true; + }; } export default material; diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index ee3971a464..b19788e7e4 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -5,6 +5,9 @@ */ import { Matrix } from './p5.Matrix'; +import { Vector } from '../math/p5.Vector'; +import { Quat } from './p5.Quat'; +import { RendererGL } from './p5.RendererGL'; class Camera { constructor(renderer) { @@ -17,1182 +20,1182 @@ class Camera { this.yScale = 1; } /** - * The camera’s y-coordinate. - * - * By default, the camera’s y-coordinate is set to 0 in "world" space. - * - * @property {Number} eyeX - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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, 55); - * } - * - *
- */ + * The camera’s y-coordinate. + * + * By default, the camera’s y-coordinate is set to 0 in "world" space. + * + * @property {Number} eyeX + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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, 55); + * } + * + *
+ */ /** - * The camera’s y-coordinate. - * - * By default, the camera’s y-coordinate is set to 0 in "world" space. - * - * @property {Number} eyeY - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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(`eyeX: ${round(cam.eyeY)}`, 0, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ + * The camera’s y-coordinate. + * + * By default, the camera’s y-coordinate is set to 0 in "world" space. + * + * @property {Number} eyeY + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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(`eyeX: ${round(cam.eyeY)}`, 0, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ /** - * The camera’s z-coordinate. - * - * By default, the camera’s z-coordinate is set to 800 in "world" space. - * - * @property {Number} eyeZ - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ + * The camera’s z-coordinate. + * + * By default, the camera’s z-coordinate is set to 800 in "world" space. + * + * @property {Number} eyeZ + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 (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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 (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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 (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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 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, 55); - * } - * - *
- */ + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 (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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 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, 55); + * } + * + *
+ */ /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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 (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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * 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, 55); - * } - * - *
- */ + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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 (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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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, 55); + * } + * + *
+ */ /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- */ + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ */ /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- */ + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ */ /** - * 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 - * @readonly - * - * @example - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- * - *
- * - * let cam; - * let font; - * - * // Load a font and create a p5.Font object. - * function preload() { - * font = loadFont('assets/inconsolata.otf'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // 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, 55); - * } - * - *
- */ + * 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 + * @readonly + * + * @example + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ * + *
+ * + * let cam; + * let font; + * + * // Load a font and create a p5.Font object. + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // 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, 55); + * } + * + *
+ */ //////////////////////////////////////////////////////////////////////////////// // 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; - * } - * } - * - *
- * - *
- * - * // 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; - * } - * } - * - *
- */ + * 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; + * } + * } + * + *
+ * + *
+ * + * // 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) { this.cameraType = arguments.length > 0 ? 'custom' : 'default'; if (typeof fovy === 'undefined') { @@ -1251,161 +1254,161 @@ class Camera { } /** - * 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; - * } - * } - * - *
- * - *
- * - * // 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; - * } - * } - * - *
- */ + * 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; + * } + * } + * + *
+ * + *
+ * + * // 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; @@ -1425,7 +1428,7 @@ class Camera { const tx = -(right + left) / w; const ty = -(top + bottom) / h; const tz = -(far + near) / d; - this.projMatrix = p5.Matrix.identity(); + this.projMatrix = Matrix.identity(); /* eslint-disable indent */ this.projMatrix.set(x, 0, 0, 0, 0, -y, 0, 0, @@ -1438,106 +1441,106 @@ class Camera { 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; - * } - * } - * - *
- */ + * 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; @@ -1561,7 +1564,7 @@ class Camera { const ty = (top + bottom) / h; const tz = -(far + near) / d; - this.projMatrix = p5.Matrix.identity(); + this.projMatrix = Matrix.identity(); /* eslint-disable indent */ this.projMatrix.set(x, 0, 0, 0, @@ -1582,10 +1585,10 @@ class Camera { //////////////////////////////////////////////////////////////////////////////// /** - * Rotate camera view about arbitrary axis defined by x,y,z - * based on http://learnwebgl.brown37.net/07_cameras/camera_rotating_motion.html - * @private - */ + * 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; @@ -1596,7 +1599,7 @@ class Camera { centerY -= this.eyeY; centerZ -= this.eyeZ; - const rotation = p5.Matrix.identity(this._renderer._pInst); + const rotation = Matrix.identity(this._renderer._pInst); rotation.rotate(this._renderer._pInst._toRadians(a), x, y, z); /* eslint-disable max-len */ @@ -1626,70 +1629,70 @@ class Camera { } /** - * 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(); - * } - * - * 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. - */ + * 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(); + * } + * + * 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 = p5.Quat.fromAxisAngle( + 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 p5.Vector(this.upX, this.upY, this.upZ)); + new Vector(this.upX, this.upY, this.upZ)); this.camera( this.eyeX, this.eyeY, @@ -1704,207 +1707,207 @@ class Camera { } /** - * 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(); - * - * // 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(); - * } - * - *
- */ + * 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(); + * + * // 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(); - * - * // 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(); - * } - * - *
- */ + * 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(); + * + * // 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(); - * - * // 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; - * } - * } - * - *
- */ + * 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(); + * + * // 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, @@ -1924,169 +1927,169 @@ class Camera { //////////////////////////////////////////////////////////////////////////////// /** - * 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; - * } - * } - * - *
- * - *
- * - * // 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; - * } - * } - * - *
- */ + * 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; + * } + * } + * + *
+ * + *
+ * + * // 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, @@ -2151,79 +2154,79 @@ class Camera { } /** - * 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); - * - * 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. - * if (keyIsPressed === true) { - * - * // Move horizontally. - * if (keyCode === LEFT_ARROW) { - * cam.move(-1, 0, 0); - * } - * if (keyCode === RIGHT_ARROW) { - * cam.move(1, 0, 0); - * } - * - * // Move vertically. - * if (keyCode === UP_ARROW) { - * cam.move(0, -1, 0); - * } - * if (keyCode === DOWN_ARROW) { - * cam.move(0, 1, 0); - * } - * - * // Move in/out of the screen. - * if (key === 'i') { - * cam.move(0, 0, -1); - * } - * if (key === 'o') { - * cam.move(0, 0, 1); - * } - * } - * - * // Draw the box. - * box(); - * } - * - *
- */ + * 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); + * + * 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. + * if (keyIsPressed === true) { + * + * // Move horizontally. + * if (keyCode === LEFT_ARROW) { + * cam.move(-1, 0, 0); + * } + * if (keyCode === RIGHT_ARROW) { + * cam.move(1, 0, 0); + * } + * + * // Move vertically. + * if (keyCode === UP_ARROW) { + * cam.move(0, -1, 0); + * } + * if (keyCode === DOWN_ARROW) { + * cam.move(0, 1, 0); + * } + * + * // Move in/out of the screen. + * if (key === 'i') { + * cam.move(0, 0, -1); + * } + * if (key === 'o') { + * cam.move(0, 0, 1); + * } + * } + * + * // Draw the box. + * box(); + * } + * + *
+ */ move(x, y, z) { const local = this._getLocalAxes(); @@ -2247,140 +2250,140 @@ class Camera { } /** - * 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; - * } - * } - * - *
- * - *
- * - * // 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; - * } - * } - * - *
- */ + * 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; + * } + * } + * + *
+ * + *
+ * + * // 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; @@ -2400,60 +2403,60 @@ class Camera { } /** - * 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); - * - * 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); - * } - */ + * 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); + * + * 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', @@ -2476,79 +2479,79 @@ class Camera { } } /** - * 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(); - * } - * - *
- */ + * 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) { @@ -2576,15 +2579,15 @@ class Camera { } // prepare eye vector and center vector of argument cameras. - const eye0 = new p5.Vector(cam0.eyeX, cam0.eyeY, cam0.eyeZ); - const eye1 = new p5.Vector(cam1.eyeX, cam1.eyeY, cam1.eyeZ); - const center0 = new p5.Vector(cam0.centerX, cam0.centerY, cam0.centerZ); - const center1 = new p5.Vector(cam1.centerX, cam1.centerY, cam1.centerZ); + 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 = p5.Vector.dist(eye0, center0); - const dist1 = p5.Vector.dist(eye1, center1); + 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 @@ -2595,7 +2598,7 @@ class Camera { // 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 = p5.Vector.sub(eye0, eye1); + 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 @@ -2605,15 +2608,15 @@ class Camera { const divider = diffDiff.magSq(); let ratio = 1; // default. if (divider > 0.000001) { - ratio = p5.Vector.dot(eyeDiff, diffDiff) / divider; + 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 = p5.Vector.lerp( - p5.Vector.lerp(eye0, center0, ratio), - p5.Vector.lerp(eye1, center1, ratio), + const lerpedMedium = Vector.lerp( + Vector.lerp(eye0, center0, ratio), + Vector.lerp(eye1, center1, ratio), amt ); @@ -2628,10 +2631,10 @@ class Camera { const up1 = rotMat1.row(1); // prepare new vectors. - const newFront = new p5.Vector(); - const newUp = new p5.Vector(); - const newEye = new p5.Vector(); - const newCenter = new p5.Vector(); + 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. @@ -2653,12 +2656,12 @@ class Camera { // Obtain the front vector and up vector by linear interpolation // and normalize them. // calculate newEye, newCenter with newFront vector. - newFront.set(p5.Vector.lerp(front0, front1, amt)).normalize(); + 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(p5.Vector.lerp(up0, up1, amt)).normalize(); + newUp.set(Vector.lerp(up0, up1, amt)).normalize(); // set the camera this.camera( @@ -2713,7 +2716,7 @@ class Camera { const ab = a * b; const bc = b * c; const ca = c * a; - const lerpedRotMat = new p5.Matrix('mat3', [ + const lerpedRotMat = new Matrix('mat3', [ cosAngle + oneMinusCosAngle * a * a, oneMinusCosAngle * ab + sinAngle * c, oneMinusCosAngle * ca - sinAngle * b, @@ -2797,9 +2800,9 @@ class Camera { } /** - * Returns a copy of a camera. - * @private - */ + * Returns a copy of a camera. + * @private + */ copy() { const _cam = new Camera(this._renderer); _cam.cameraFOV = this.cameraFOV; @@ -2830,10 +2833,10 @@ class Camera { } /** - * Returns a camera's local axes: left-right, up-down, and forward-backward, - * as defined by vectors in world-space. - * @private - */ + * 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; @@ -2887,12 +2890,12 @@ class Camera { } /** - * 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 - */ + * 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; @@ -2900,13 +2903,13 @@ class Camera { const diffZ = this.eyeZ - this.centerZ; let camRadius = Math.hypot(diffX, diffY, diffZ); // front vector. unit vector from center to eye. - const front = new p5.Vector(diffX, diffY, diffZ).normalize(); + const front = new Vector(diffX, diffY, diffZ).normalize(); // up vector. normalized camera's up vector. - const up = new p5.Vector(this.upX, this.upY, this.upZ).normalize(); // y-axis + const up = new Vector(this.upX, this.upY, this.upZ).normalize(); // y-axis // side vector. Right when viewed from the front - const side = p5.Vector.cross(up, front).normalize(); // x-axis + const side = Vector.cross(up, front).normalize(); // x-axis // vertical vector. normalized vector of projection of front vector. - const vertical = p5.Vector.cross(side, up); // z-axis + const vertical = Vector.cross(side, up); // z-axis // update camRadius camRadius *= Math.pow(10, dRadius); @@ -2924,7 +2927,7 @@ class Camera { // 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, p5.Vector.dot(front, up)))) + dPhi; + 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; @@ -2955,13 +2958,13 @@ class Camera { } /** - * 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 - */ + * 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; @@ -2969,13 +2972,13 @@ class Camera { const diffZ = this.eyeZ - this.centerZ; let camRadius = Math.hypot(diffX, diffY, diffZ); // front vector. unit vector from center to eye. - const front = new p5.Vector(diffX, diffY, diffZ).normalize(); + const front = new Vector(diffX, diffY, diffZ).normalize(); // up vector. camera's up vector. - const up = new p5.Vector(this.upX, this.upY, this.upZ); + const up = new Vector(this.upX, this.upY, this.upZ); // side vector. Right when viewed from the front. (like x-axis) - const side = p5.Vector.cross(up, front).normalize(); + const side = Vector.cross(up, front).normalize(); // down vector. Bottom when viewed from the front. (like y-axis) - const down = p5.Vector.cross(front, side); + 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 @@ -2988,7 +2991,7 @@ class Camera { 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 = p5.Vector.cross(front, side); + const axis = Vector.cross(front, side); // update camRadius camRadius *= Math.pow(10, dRadius); @@ -3032,9 +3035,9 @@ class Camera { } /** - * Returns true if camera is currently attached to renderer. - * @private - */ + * Returns true if camera is currently attached to renderer. + * @private + */ _isActive() { return this === this._renderer.states.curCamera; } @@ -3430,7 +3433,7 @@ function camera(p5, fn){ fn.linePerspective = function (enable) { p5._validateParameters('linePerspective', arguments); - if (!(this._renderer instanceof p5.RendererGL)) { + if (!(this._renderer instanceof RendererGL)) { throw new Error('linePerspective() must be called in WebGL mode.'); } if (enable !== undefined) { @@ -3744,7 +3747,7 @@ function camera(p5, fn){ this._assert3d('createCamera'); // compute default camera settings, then set a default camera - const _cam = new p5.Camera(this._renderer); + const _cam = new Camera(this._renderer); _cam._computeCameraDefaultSettings(); _cam._setDefaultCamera(); diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 856d5d02ae..ce5299d00a 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -8,6 +8,7 @@ import { checkWebGLCapabilities } from './p5.Texture'; import { readPixelsWebGL, readPixelWebGL } from './p5.RendererGL'; import { Camera } from './p5.Camera'; import { Texture } from './p5.Texture'; +import { Image } from '../image/p5.Image'; class FramebufferCamera extends Camera { constructor(framebuffer) { @@ -1446,7 +1447,7 @@ class Framebuffer { } // Create an image from the data - const region = new p5.Image(w * this.density, h * this.density); + const region = new Image(w * this.density, h * this.density); region.imageData = region.canvas.getContext('2d').createImageData( region.width, region.height diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 0809fd8248..c0e1c7d0ac 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -10,6 +10,7 @@ import * as constants from '../core/constants'; import { DataArray } from './p5.DataArray'; +import { Vector } from '../math/p5.Vector'; class Geometry { constructor(detailX, detailY, callback) { @@ -168,9 +169,9 @@ class Geometry { return this.boundingBoxCache; // Return cached result if available } - let minVertex = new p5.Vector( + let minVertex = new Vector( Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE); - let maxVertex = new p5.Vector( + let maxVertex = new Vector( Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE); for (let i = 0; i < this.vertices.length; i++) { @@ -184,9 +185,9 @@ class Geometry { maxVertex.z = Math.max(maxVertex.z, vertex.z); } // Calculate size and offset properties - let size = new p5.Vector(maxVertex.x - minVertex.x, + let size = new Vector(maxVertex.x - minVertex.x, maxVertex.y - minVertex.y, maxVertex.z - minVertex.z); - let offset = new p5.Vector((minVertex.x + maxVertex.x) / 2, + 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 @@ -478,12 +479,12 @@ class Geometry { let name = fileName.substring(0, fileName.lastIndexOf('.')); let faceNormals = []; for (let f of this.faces) { - const U = p5.Vector.sub(this.vertices[f[1]], this.vertices[f[0]]); - const V = p5.Vector.sub(this.vertices[f[2]], this.vertices[f[0]]); + 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 p5.Vector(nx, ny, nz).normalize()); + faceNormals.push(new Vector(nx, ny, nz).normalize()); } if (binary) { let offset = 80; @@ -536,89 +537,89 @@ class Geometry { } /** - * 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; - * - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } - * - * function setup() { - * 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); - * } - * - *
- */ + * 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; + * + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * 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) { @@ -630,89 +631,89 @@ class Geometry { } /** - * 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] - * ``` - * - * @for p5.Geometry - * - * @example - *
- * - * let img; - * - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } - * - * function setup() { - * 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); - * } - * - *
- */ + * 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] + * ``` + * + * @for p5.Geometry + * + * @example + *
+ * + * let img; + * + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * 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) { @@ -724,134 +725,134 @@ class Geometry { } /** - * 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); - * } - * - *
- * - *
- * - * // 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(); - * } - * - *
- */ + * 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); + * } + * + *
+ * + *
+ * + * // 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; @@ -875,11 +876,11 @@ class Geometry { const vA = this.vertices[face[0]]; const vB = this.vertices[face[1]]; const vC = this.vertices[face[2]]; - const ab = p5.Vector.sub(vB, vA); - const ac = p5.Vector.sub(vC, vA); - const n = p5.Vector.cross(ab, ac); - const ln = p5.Vector.mag(n); - let sinAlpha = ln / (p5.Vector.mag(ab) * p5.Vector.mag(ac)); + 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:', @@ -1249,7 +1250,7 @@ class Geometry { // initialize the vertexNormals array with empty vectors vertexNormals.length = 0; for (iv = 0; iv < vertices.length; ++iv) { - vertexNormals.push(new p5.Vector()); + vertexNormals.push(new Vector()); } // loop through all the faces adding its normal to the normal @@ -1281,12 +1282,12 @@ class Geometry { averageNormals() { for (let i = 0; i <= this.detailY; i++) { const offset = this.detailX + 1; - let temp = p5.Vector.add( + let temp = Vector.add( this.vertexNormals[i * offset], this.vertexNormals[i * offset + this.detailX] ); - temp = p5.Vector.div(temp, 2); + temp = Vector.div(temp, 2); this.vertexNormals[i * offset] = temp; this.vertexNormals[i * offset + this.detailX] = temp; } @@ -1300,18 +1301,18 @@ class Geometry { */ averagePoleNormals() { //average the north pole - let sum = new p5.Vector(0, 0, 0); + let sum = new Vector(0, 0, 0); for (let i = 0; i < this.detailX; i++) { sum.add(this.vertexNormals[i]); } - sum = p5.Vector.div(sum, this.detailX); + sum = Vector.div(sum, this.detailX); for (let i = 0; i < this.detailX; i++) { this.vertexNormals[i] = sum; } //average the south pole - sum = new p5.Vector(0, 0, 0); + sum = new Vector(0, 0, 0); for ( let i = this.vertices.length - 1; i > this.vertices.length - 1 - this.detailX; @@ -1319,7 +1320,7 @@ class Geometry { ) { sum.add(this.vertexNormals[i]); } - sum = p5.Vector.div(sum, this.detailX); + sum = Vector.div(sum, this.detailX); for ( let i = this.vertices.length - 1; @@ -1670,8 +1671,8 @@ class Geometry { minPosition.z = Math.min(minPosition.z, this.vertices[i].z); } - const center = p5.Vector.lerp(maxPosition, minPosition, 0.5); - const dist = p5.Vector.sub(maxPosition, minPosition); + 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; diff --git a/src/webgl/p5.Matrix.js b/src/webgl/p5.Matrix.js index 9355bebac0..b358dd3098 100644 --- a/src/webgl/p5.Matrix.js +++ b/src/webgl/p5.Matrix.js @@ -153,7 +153,7 @@ class Matrix { */ transpose(a) { let a01, a02, a03, a12, a13, a23; - if (a instanceof p5.Matrix) { + if (a instanceof Matrix) { a01 = a.mat4[1]; a02 = a.mat4[2]; a03 = a.mat4[3]; @@ -214,7 +214,7 @@ class Matrix { invert(a) { let a00, a01, a02, a03, a10, a11, a12, a13; let a20, a21, a22, a23, a30, a31, a32, a33; - if (a instanceof p5.Matrix) { + if (a instanceof Matrix) { a00 = a.mat4[0]; a01 = a.mat4[1]; a02 = a.mat4[2]; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 19013aa255..f254881fef 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -12,6 +12,9 @@ * geometric ideas. */ import * as constants from '../core/constants'; +import { RendererGL } from './p5.RendererGL'; +import { Vector } from '../math/p5.Vector'; +import { RenderBuffer } from './p5.RenderBuffer'; function rendererGLImmediate(p5, fn){ /** @@ -29,7 +32,7 @@ function rendererGLImmediate(p5, fn){ * and TESS(WEBGL only) * @chainable */ - p5.RendererGL.prototype.beginShape = function(mode) { + RendererGL.prototype.beginShape = function(mode) { this.immediateMode.shapeMode = mode !== undefined ? mode : constants.TESS; if (this._useUserVertexProperties === true){ @@ -40,7 +43,7 @@ function rendererGLImmediate(p5, fn){ return this; }; - p5.RendererGL.prototype.immediateBufferStrides = { + RendererGL.prototype.immediateBufferStrides = { vertices: 1, vertexNormals: 1, vertexColors: 4, @@ -48,7 +51,7 @@ function rendererGLImmediate(p5, fn){ uvs: 2 }; - p5.RendererGL.prototype.beginContour = function() { + RendererGL.prototype.beginContour = function() { if (this.immediateMode.shapeMode !== constants.TESS) { throw new Error('WebGL mode can only use contours with beginShape(TESS).'); } @@ -67,7 +70,7 @@ function rendererGLImmediate(p5, fn){ * @chainable * @TODO implement handling of p5.Vector args */ - p5.RendererGL.prototype.vertex = function(x, y) { + RendererGL.prototype.vertex = function(x, y) { // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra // work to convert QUAD_STRIP here, since the only difference is in how edges @@ -112,7 +115,7 @@ function rendererGLImmediate(p5, fn){ u = arguments[3]; v = arguments[4]; } - const vert = new p5.Vector(x, y, z); + const vert = new Vector(x, y, z); this.immediateMode.geometry.vertices.push(vert); this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); @@ -180,7 +183,7 @@ function rendererGLImmediate(p5, fn){ return this; }; - p5.RendererGL.prototype.vertexProperty = function(propertyName, data){ + RendererGL.prototype.vertexProperty = function(propertyName, data){ if(!this._useUserVertexProperties){ this._useUserVertexProperties = true; this.immediateMode.geometry.userVertexProperties = {}; @@ -195,13 +198,13 @@ function rendererGLImmediate(p5, fn){ this.tessyVertexSize += prop.getDataSize(); this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); this.immediateMode.buffers.user.push( - new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) ); } prop.setCurrentData(data); }; - p5.RendererGL.prototype._resetUserVertexProperties = function(){ + RendererGL.prototype._resetUserVertexProperties = function(){ const properties = this.immediateMode.geometry.userVertexProperties; for (const propName in properties){ const prop = properties[propName]; @@ -227,11 +230,11 @@ function rendererGLImmediate(p5, fn){ * @param {Vector} v * @chainable */ - p5.RendererGL.prototype.normal = function(xorv, y, z) { - if (xorv instanceof p5.Vector) { + RendererGL.prototype.normal = function(xorv, y, z) { + if (xorv instanceof Vector) { this.states._currentNormal = xorv; } else { - this.states._currentNormal = new p5.Vector(xorv, y, z); + this.states._currentNormal = new Vector(xorv, y, z); } return this; @@ -241,7 +244,7 @@ function rendererGLImmediate(p5, fn){ * End shape drawing and render vertices to screen. * @chainable */ - p5.RendererGL.prototype.endShape = function( + RendererGL.prototype.endShape = function( mode, isCurve, isBezier, @@ -330,7 +333,7 @@ function rendererGLImmediate(p5, fn){ * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) */ - p5.RendererGL.prototype._processVertices = function(mode) { + RendererGL.prototype._processVertices = function(mode) { if (this.immediateMode.geometry.vertices.length === 0) return; const calculateStroke = this.states.doStroke; @@ -373,7 +376,7 @@ function rendererGLImmediate(p5, fn){ * @private * @returns {Number[]} indices for custom shape vertices indicating edges. */ - p5.RendererGL.prototype._calculateEdges = function( + RendererGL.prototype._calculateEdges = function( shapeMode, verts, shouldClose @@ -459,7 +462,7 @@ function rendererGLImmediate(p5, fn){ * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. * @private */ - p5.RendererGL.prototype._tesselateShape = function() { + RendererGL.prototype._tesselateShape = function() { // TODO: handle non-TESS shape modes that have contours this.immediateMode.shapeMode = constants.TRIANGLES; const contours = [[]]; @@ -579,7 +582,7 @@ function rendererGLImmediate(p5, fn){ * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. * @private */ - p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { + RendererGL.prototype._drawImmediateFill = function(count = 1) { const gl = this.GL; this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); @@ -629,7 +632,7 @@ function rendererGLImmediate(p5, fn){ * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. * @private */ - p5.RendererGL.prototype._drawImmediateStroke = function() { + RendererGL.prototype._drawImmediateStroke = function() { const gl = this.GL; this._useLineColor = @@ -662,4 +665,4 @@ export default rendererGLImmediate; if(typeof p5 !== 'undefined'){ rendererGLImmediate(p5, p5.prototype); -} \ No newline at end of file +} diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 10b5726336..a867f36a25 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -1,12 +1,14 @@ //Retained Mode. The default mode for rendering 3D primitives //in WEBGL. import * as constants from '../core/constants'; +import { RendererGL } from './p5.RendererGL'; +import { RenderBuffer } from './p5.RenderBuffer'; function rendererGLRetained(p5, fn){ /** * @param {p5.Geometry} geometry The model whose resources will be freed */ - p5.RendererGL.prototype.freeGeometry = function(geometry) { + RendererGL.prototype.freeGeometry = function(geometry) { if (!geometry.gid) { console.warn('The model you passed to freeGeometry does not have an id!'); return; @@ -22,7 +24,7 @@ function rendererGLRetained(p5, fn){ * @param {String} gId key of the geometry object * @returns {Object} a new buffer object */ - p5.RendererGL.prototype._initBufferDefaults = function(gId) { + RendererGL.prototype._initBufferDefaults = function(gId) { this._freeBuffers(gId); //@TODO remove this limit on hashes in retainedMode.geometry @@ -35,7 +37,7 @@ function rendererGLRetained(p5, fn){ return (this.retainedMode.geometry[gId] = {}); }; - p5.RendererGL.prototype._freeBuffers = function(gId) { + RendererGL.prototype._freeBuffers = function(gId) { const buffers = this.retainedMode.geometry[gId]; if (!buffers) { return; @@ -71,7 +73,7 @@ function rendererGLRetained(p5, fn){ * @param {String} gId key of the geometry object * @param {p5.Geometry} model contains geometry data */ - p5.RendererGL.prototype.createBuffers = function(gId, model) { + RendererGL.prototype.createBuffers = function(gId, model) { const gl = this.GL; //initialize the gl buffers for our geom groups const buffers = this._initBufferDefaults(gId); @@ -82,7 +84,7 @@ function rendererGLRetained(p5, fn){ if (model.faces.length) { // allocate space for faces if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); - const vals = p5.RendererGL.prototype._flatten(model.faces); + const vals = RendererGL.prototype._flatten(model.faces); // If any face references a vertex with an index greater than the maximum // un-singed 16 bit integer, then we need to use a Uint32Array instead of a @@ -117,7 +119,7 @@ function rendererGLRetained(p5, fn){ for (const propName in model.userVertexProperties){ const prop = model.userVertexProperties[propName]; this.retainedMode.buffers.user.push( - new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) ); } return buffers; @@ -129,7 +131,7 @@ function rendererGLRetained(p5, fn){ * @param {String} gId ID in our geom hash * @chainable */ - p5.RendererGL.prototype.drawBuffers = function(gId) { + RendererGL.prototype.drawBuffers = function(gId) { const gl = this.GL; const geometry = this.retainedMode.geometry[gId]; @@ -215,7 +217,7 @@ function rendererGLRetained(p5, fn){ * @param {Number} scaleY the amount to scale in the Y direction * @param {Number} scaleZ the amount to scale in the Z direction */ - p5.RendererGL.prototype.drawBuffersScaled = function( + RendererGL.prototype.drawBuffersScaled = function( gId, scaleX, scaleY, @@ -231,7 +233,7 @@ function rendererGLRetained(p5, fn){ this.states.uModelMatrix = originalModelMatrix; } }; - p5.RendererGL.prototype._drawArrays = function(drawMode, gId) { + RendererGL.prototype._drawArrays = function(drawMode, gId) { this.GL.drawArrays( drawMode, 0, @@ -240,7 +242,7 @@ function rendererGLRetained(p5, fn){ return this; }; - p5.RendererGL.prototype._drawElements = function(drawMode, gId) { + RendererGL.prototype._drawElements = function(drawMode, gId) { const buffers = this.retainedMode.geometry[gId]; const gl = this.GL; // render the fill @@ -270,7 +272,7 @@ function rendererGLRetained(p5, fn){ } }; - p5.RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { + RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { const gl = this.GL; const pointShader = this._getImmediatePointShader(); this._setPointUniforms(pointShader); @@ -297,4 +299,4 @@ export default rendererGLRetained; if(typeof p5 !== 'undefined'){ rendererGLRetained(p5, p5.prototype); -} \ No newline at end of file +} diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 45deab33b1..9680459b66 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -10,7 +10,8 @@ import { Geometry } from './p5.Geometry'; import { DataArray } from './p5.DataArray'; import { Shader } from './p5.Shader'; import { Image } from '../image/p5.Image'; -import { Texture } from './p5.Texture'; +import { Texture, MipmapTexture } from './p5.Texture'; +import { Framebuffer } from './p5.Framebuffer'; import lightingShader from './shaders/lighting.glsl'; import webgl2CompatibilityShader from './shaders/webgl2Compatibility.glsl'; @@ -649,8 +650,8 @@ class RendererGL extends Renderer { } /** - * [background description] - */ + * [background description] + */ background(...args) { const _col = this._pInst.color(...args); const _r = _col.levels[0] / 255; @@ -664,35 +665,35 @@ class RendererGL extends Renderer { // COLOR ////////////////////////////////////////////// /** - * Basic fill material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } - * - * function draw() { - * background(0); - * noStroke(); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
- * - * @alt - * black canvas with purple cube spinning - */ + * Basic fill material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @chainable + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * noStroke(); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube spinning + */ fill(v1, v2, v3, a) { //see material.js for more info on color blending in webgl const color = fn.color.apply(this._pInst, arguments); @@ -703,34 +704,34 @@ class RendererGL extends Renderer { } /** - * Basic stroke material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } - * - * function draw() { - * background(0); - * stroke(240, 150, 150); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
- * - * @alt - * black canvas with purple cube with pink outline spinning - */ + * Basic stroke material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * stroke(240, 150, 150); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube with pink outline spinning + */ stroke(r, g, b, a) { const color = fn.color.apply(this._pInst, arguments); this.states.curStrokeColor = color._array; @@ -1017,43 +1018,43 @@ class RendererGL extends Renderer { } /** - * Change weight of stroke - * @param {Number} stroke weight to be used for drawing - * @example - *
- * - * function setup() { - * createCanvas(200, 400, WEBGL); - * setAttributes('antialias', true); - * } - * - * function draw() { - * background(0); - * noStroke(); - * translate(0, -100, 0); - * stroke(240, 150, 150); - * fill(100, 100, 240); - * push(); - * strokeWeight(8); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * sphere(75); - * pop(); - * push(); - * translate(0, 200, 0); - * strokeWeight(1); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * sphere(75); - * pop(); - * } - * - *
- * - * @alt - * black canvas with two purple rotating spheres with pink - * outlines the sphere on top has much heavier outlines, - */ + * Change weight of stroke + * @param {Number} stroke weight to be used for drawing + * @example + *
+ * + * function setup() { + * createCanvas(200, 400, WEBGL); + * setAttributes('antialias', true); + * } + * + * function draw() { + * background(0); + * noStroke(); + * translate(0, -100, 0); + * stroke(240, 150, 150); + * fill(100, 100, 240); + * push(); + * strokeWeight(8); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * sphere(75); + * pop(); + * push(); + * translate(0, 200, 0); + * strokeWeight(1); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * sphere(75); + * pop(); + * } + * + *
+ * + * @alt + * black canvas with two purple rotating spheres with pink + * outlines the sphere on top has much heavier outlines, + */ strokeWeight(w) { if (this.curStrokeWeight !== w) { this.pointSize = w; @@ -1076,13 +1077,12 @@ class RendererGL extends Renderer { } /** - * Loads the pixels data for this canvas into the pixels[] attribute. - * Note that updatePixels() and set() do not work. - * Any pixel manipulation must be done directly to the pixels[] array. - * - * @private - */ - + * Loads the pixels data for this canvas into the pixels[] attribute. + * Note that updatePixels() and set() do not work. + * Any pixel manipulation must be done directly to the pixels[] array. + * + * @private + */ loadPixels() { const pixelsState = this._pixelsState; @@ -1127,11 +1127,11 @@ class RendererGL extends Renderer { } /** - * @private - * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings - * of the renderer's canvas. It will be created if it does not yet exist, and - * reused if it does. - */ + * @private + * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings + * of the renderer's canvas. It will be created if it does not yet exist, and + * reused if it does. + */ _getTempFramebuffer() { if (!this._tempFramebuffer) { this._tempFramebuffer = this._pInst.createFramebuffer({ @@ -1160,11 +1160,11 @@ class RendererGL extends Renderer { } /** - * [resize description] - * @private - * @param {Number} w [description] - * @param {Number} h [description] - */ + * [resize description] + * @private + * @param {Number} w [description] + * @param {Number} h [description] + */ resize(w, h) { super.resize(w, h); @@ -1225,14 +1225,14 @@ class RendererGL extends Renderer { } /** - * clears color and depth buffers - * with r,g,b,a - * @private - * @param {Number} r normalized red val. - * @param {Number} g normalized green val. - * @param {Number} b normalized blue val. - * @param {Number} a normalized alpha val. - */ + * clears color and depth buffers + * with r,g,b,a + * @private + * @param {Number} r normalized red val. + * @param {Number} g normalized green val. + * @param {Number} b normalized blue val. + * @param {Number} a normalized alpha val. + */ clear(...args) { const _r = args[0] || 0; const _g = args[1] || 0; @@ -1282,14 +1282,14 @@ class RendererGL extends Renderer { } /** - * [translate description] - * @private - * @param {Number} x [description] - * @param {Number} y [description] - * @param {Number} z [description] - * @chainable - * @todo implement handle for components or vector as args - */ + * [translate description] + * @private + * @param {Number} x [description] + * @param {Number} y [description] + * @param {Number} z [description] + * @chainable + * @todo implement handle for components or vector as args + */ translate(x, y, z) { if (x instanceof Vector) { z = x.z; @@ -1301,13 +1301,13 @@ class RendererGL extends Renderer { } /** - * Scales the Model View Matrix by a vector - * @private - * @param {Number | p5.Vector | Array} x [description] - * @param {Number} [y] y-axis scalar - * @param {Number} [z] z-axis scalar - * @chainable - */ + * Scales the Model View Matrix by a vector + * @private + * @param {Number | p5.Vector | Array} x [description] + * @param {Number} [y] y-axis scalar + * @param {Number} [z] z-axis scalar + * @chainable + */ scale(x, y, z) { this.states.uModelMatrix.scale(x, y, z); return this; @@ -1369,10 +1369,10 @@ class RendererGL extends Renderer { ////////////////////////////////////////////// /* - * shaders are created and cached on a per-renderer basis, - * on the grounds that each renderer will have its own gl context - * and the shader must be valid in that context. - */ + * shaders are created and cached on a per-renderer basis, + * on the grounds that each renderer will have its own gl context + * and the shader must be valid in that context. + */ _getImmediateStrokeShader() { // select the stroke shader to use @@ -1749,7 +1749,7 @@ class RendererGL extends Renderer { getTexture(input) { let src = input; - if (src instanceof p5.Framebuffer) { + if (src instanceof Framebuffer) { src = src.color; } @@ -1763,11 +1763,11 @@ class RendererGL extends Renderer { return tex; } /* - * used in imageLight, - * To create a blurry image from the input non blurry img, if it doesn't already exist - * Add it to the diffusedTexture map, - * Returns the blurry image - * maps a Image used by imageLight() to a p5.Framebuffer + * used in imageLight, + * To create a blurry image from the input non blurry img, if it doesn't already exist + * Add it to the diffusedTexture map, + * Returns the blurry image + * maps a Image used by imageLight() to a p5.Framebuffer */ getDiffusedTexture(input) { // if one already exists for a given input image @@ -1854,7 +1854,7 @@ class RendererGL extends Renderer { } // Free the Framebuffer framebuffer.remove(); - tex = new p5.MipmapTexture(this, levels, {}); + tex = new MipmapTexture(this, levels, {}); this.specularTextures.set(input, tex); return tex; } @@ -2004,9 +2004,9 @@ class RendererGL extends Renderer { } /* Binds a buffer to the drawing context - * when passed more than two arguments it also updates or initializes - * the data associated with the buffer - */ + * when passed more than two arguments it also updates or initializes + * the data associated with the buffer + */ _bindBuffer( buffer, target, @@ -2418,1180 +2418,6 @@ function rendererGL(p5, fn){ p5.renderers[constants.WEBGL] = p5.RendererGL; p5.renderers[constants.WEBGL2] = p5.RendererGL; - RendererGL = p5.RendererGL; - - /////////////////////// - /// 2D primitives - ///////////////////////// - // - // Note: Documentation is not generated on the p5.js website for functions on - // the p5.RendererGL prototype. - - /** - * Draws a point, a coordinate in space at the dimension of one pixel, - * given x, y and z coordinates. The color of the point is determined - * by the current stroke, while the point size is determined by current - * stroke weight. - * @private - * @param {Number} x x-coordinate of point - * @param {Number} y y-coordinate of point - * @param {Number} z z-coordinate of point - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * } - * - * function draw() { - * background(50); - * stroke(255); - * strokeWeight(4); - * point(25, 0); - * strokeWeight(3); - * point(-25, 0); - * strokeWeight(2); - * point(0, 25); - * strokeWeight(1); - * point(0, -25); - * } - * - *
- */ - p5.RendererGL.prototype.point = function(x, y, z = 0) { - - const _vertex = []; - _vertex.push(new Vector(x, y, z)); - this._drawPoints(_vertex, this.immediateMode.buffers.point); - - return this; - }; - - p5.RendererGL.prototype.triangle = function(args) { - const x1 = args[0], - y1 = args[1]; - const x2 = args[2], - y2 = args[3]; - const x3 = args[4], - y3 = args[5]; - - const gId = 'tri'; - if (!this.geometryInHash(gId)) { - const _triangle = function() { - const vertices = []; - vertices.push(new Vector(0, 0, 0)); - vertices.push(new Vector(1, 0, 0)); - vertices.push(new Vector(0, 1, 0)); - this.edges = [[0, 1], [1, 2], [2, 0]]; - this.vertices = vertices; - this.faces = [[0, 1, 2]]; - this.uvs = [0, 0, 1, 0, 1, 1]; - }; - const triGeom = new Geometry(1, 1, _triangle); - triGeom._edgesToVertices(); - triGeom.computeNormals(); - this.createBuffers(gId, triGeom); - } - - // only one triangle is cached, one point is at the origin, and the - // two adjacent sides are tne unit vectors along the X & Y axes. - // - // this matrix multiplication transforms those two unit vectors - // onto the required vector prior to rendering, and moves the - // origin appropriately. - const uModelMatrix = this.states.uModelMatrix.copy(); - try { - // triangle orientation. - const orientation = Math.sign(x1*y2-x2*y1 + x2*y3-x3*y2 + x3*y1-x1*y3); - const mult = new Matrix([ - x2 - x1, y2 - y1, 0, 0, // the resulting unit X-axis - x3 - x1, y3 - y1, 0, 0, // the resulting unit Y-axis - 0, 0, orientation, 0, // the resulting unit Z-axis (Reflect the specified order of vertices) - x1, y1, 0, 1 // the resulting origin - ]).mult(this.states.uModelMatrix); - - this.states.uModelMatrix = mult; - - this.drawBuffers(gId); - } finally { - this.states.uModelMatrix = uModelMatrix; - } - - return this; - }; - - p5.RendererGL.prototype.ellipse = function(args) { - this.arc( - args[0], - args[1], - args[2], - args[3], - 0, - constants.TWO_PI, - constants.OPEN, - args[4] - ); - }; - - p5.RendererGL.prototype.arc = function(...args) { - const x = args[0]; - const y = args[1]; - const width = args[2]; - const height = args[3]; - const start = args[4]; - const stop = args[5]; - const mode = args[6]; - const detail = args[7] || 25; - - let shape; - let gId; - - // check if it is an ellipse or an arc - if (Math.abs(stop - start) >= constants.TWO_PI) { - shape = 'ellipse'; - gId = `${shape}|${detail}|`; - } else { - shape = 'arc'; - gId = `${shape}|${start}|${stop}|${mode}|${detail}|`; - } - - if (!this.geometryInHash(gId)) { - const _arc = function() { - - // if the start and stop angles are not the same, push vertices to the array - if (start.toFixed(10) !== stop.toFixed(10)) { - // if the mode specified is PIE or null, push the mid point of the arc in vertices - if (mode === constants.PIE || typeof mode === 'undefined') { - this.vertices.push(new Vector(0.5, 0.5, 0)); - this.uvs.push([0.5, 0.5]); - } - - // vertices for the perimeter of the circle - for (let i = 0; i <= detail; i++) { - const u = i / detail; - const theta = (stop - start) * u + start; - - const _x = 0.5 + Math.cos(theta) / 2; - const _y = 0.5 + Math.sin(theta) / 2; - - this.vertices.push(new Vector(_x, _y, 0)); - this.uvs.push([_x, _y]); - - if (i < detail - 1) { - this.faces.push([0, i + 1, i + 2]); - this.edges.push([i + 1, i + 2]); - } - } - - // check the mode specified in order to push vertices and faces, different for each mode - switch (mode) { - case constants.PIE: - this.faces.push([ - 0, - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([0, 1]); - this.edges.push([ - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([0, this.vertices.length - 1]); - break; - - case constants.CHORD: - this.edges.push([0, 1]); - this.edges.push([0, this.vertices.length - 1]); - break; - - case constants.OPEN: - this.edges.push([0, 1]); - break; - - default: - this.faces.push([ - 0, - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([ - this.vertices.length - 2, - this.vertices.length - 1 - ]); - } - } - }; - - const arcGeom = new Geometry(detail, 1, _arc); - arcGeom.computeNormals(); - - if (detail <= 50) { - arcGeom._edgesToVertices(arcGeom); - } else if (this.states.doStroke) { - console.log( - `Cannot apply a stroke to an ${shape} with more than 50 detail` - ); - } - - this.createBuffers(gId, arcGeom); - } - - const uModelMatrix = this.states.uModelMatrix.copy(); - - try { - this.states.uModelMatrix.translate([x, y, 0]); - this.states.uModelMatrix.scale(width, height, 1); - - this.drawBuffers(gId); - } finally { - this.states.uModelMatrix = uModelMatrix; - } - - return this; - }; - - p5.RendererGL.prototype.rect = function(args) { - const x = args[0]; - const y = args[1]; - const width = args[2]; - const height = args[3]; - - if (typeof args[4] === 'undefined') { - // Use the retained mode for drawing rectangle, - // if args for rounding rectangle is not provided by user. - const perPixelLighting = this._pInst._glAttributes.perPixelLighting; - const detailX = args[4] || (perPixelLighting ? 1 : 24); - const detailY = args[5] || (perPixelLighting ? 1 : 16); - const gId = `rect|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { - const _rect = function() { - for (let i = 0; i <= this.detailY; i++) { - const v = i / this.detailY; - for (let j = 0; j <= this.detailX; j++) { - const u = j / this.detailX; - const p = new Vector(u, v, 0); - this.vertices.push(p); - this.uvs.push(u, v); - } - } - // using stroke indices to avoid stroke over face(s) of rectangle - if (detailX > 0 && detailY > 0) { - this.edges = [ - [0, detailX], - [detailX, (detailX + 1) * (detailY + 1) - 1], - [(detailX + 1) * (detailY + 1) - 1, (detailX + 1) * detailY], - [(detailX + 1) * detailY, 0] - ]; - } - }; - const rectGeom = new Geometry(detailX, detailY, _rect); - rectGeom - .computeFaces() - .computeNormals() - ._edgesToVertices(); - this.createBuffers(gId, rectGeom); - } - - // only a single rectangle (of a given detail) is cached: a square with - // opposite corners at (0,0) & (1,1). - // - // before rendering, this square is scaled & moved to the required location. - const uModelMatrix = this.states.uModelMatrix.copy(); - try { - this.states.uModelMatrix.translate([x, y, 0]); - this.states.uModelMatrix.scale(width, height, 1); - - this.drawBuffers(gId); - } finally { - this.states.uModelMatrix = uModelMatrix; - } - } else { - // Use Immediate mode to round the rectangle corner, - // if args for rounding corners is provided by user - let tl = args[4]; - let tr = typeof args[5] === 'undefined' ? tl : args[5]; - let br = typeof args[6] === 'undefined' ? tr : args[6]; - let bl = typeof args[7] === 'undefined' ? br : args[7]; - - let a = x; - let b = y; - let c = width; - let d = height; - - c += a; - d += b; - - if (a > c) { - const temp = a; - a = c; - c = temp; - } - - if (b > d) { - const temp = b; - b = d; - d = temp; - } - - const maxRounding = Math.min((c - a) / 2, (d - b) / 2); - if (tl > maxRounding) tl = maxRounding; - if (tr > maxRounding) tr = maxRounding; - if (br > maxRounding) br = maxRounding; - if (bl > maxRounding) bl = maxRounding; - - let x1 = a; - let y1 = b; - let x2 = c; - let y2 = d; - - this.beginShape(); - if (tr !== 0) { - this.vertex(x2 - tr, y1); - this.quadraticVertex(x2, y1, x2, y1 + tr); - } else { - this.vertex(x2, y1); - } - if (br !== 0) { - this.vertex(x2, y2 - br); - this.quadraticVertex(x2, y2, x2 - br, y2); - } else { - this.vertex(x2, y2); - } - if (bl !== 0) { - this.vertex(x1 + bl, y2); - this.quadraticVertex(x1, y2, x1, y2 - bl); - } else { - this.vertex(x1, y2); - } - if (tl !== 0) { - this.vertex(x1, y1 + tl); - this.quadraticVertex(x1, y1, x1 + tl, y1); - } else { - this.vertex(x1, y1); - } - - this.immediateMode.geometry.uvs.length = 0; - for (const vert of this.immediateMode.geometry.vertices) { - const u = (vert.x - x1) / width; - const v = (vert.y - y1) / height; - this.immediateMode.geometry.uvs.push(u, v); - } - - this.endShape(constants.CLOSE); - } - return this; - }; - - /* eslint-disable max-len */ - p5.RendererGL.prototype.quad = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, detailX=2, detailY=2) { - /* eslint-enable max-len */ - - const gId = - `quad|${x1}|${y1}|${z1}|${x2}|${y2}|${z2}|${x3}|${y3}|${z3}|${x4}|${y4}|${z4}|${detailX}|${detailY}`; - - if (!this.geometryInHash(gId)) { - const quadGeom = new Geometry(detailX, detailY, function() { - //algorithm adapted from c++ to js - //https://stackoverflow.com/questions/16989181/whats-the-correct-way-to-draw-a-distorted-plane-in-opengl/16993202#16993202 - let xRes = 1.0 / (this.detailX - 1); - let yRes = 1.0 / (this.detailY - 1); - for (let y = 0; y < this.detailY; y++) { - for (let x = 0; x < this.detailX; x++) { - let pctx = x * xRes; - let pcty = y * yRes; - - let linePt0x = (1 - pcty) * x1 + pcty * x4; - let linePt0y = (1 - pcty) * y1 + pcty * y4; - let linePt0z = (1 - pcty) * z1 + pcty * z4; - let linePt1x = (1 - pcty) * x2 + pcty * x3; - let linePt1y = (1 - pcty) * y2 + pcty * y3; - let linePt1z = (1 - pcty) * z2 + pcty * z3; - - let ptx = (1 - pctx) * linePt0x + pctx * linePt1x; - let pty = (1 - pctx) * linePt0y + pctx * linePt1y; - let ptz = (1 - pctx) * linePt0z + pctx * linePt1z; - - this.vertices.push(new Vector(ptx, pty, ptz)); - this.uvs.push([pctx, pcty]); - } - } - }); - - quadGeom.faces = []; - for(let y = 0; y < detailY-1; y++){ - for(let x = 0; x < detailX-1; x++){ - let pt0 = x + y * detailX; - let pt1 = (x + 1) + y * detailX; - let pt2 = (x + 1) + (y + 1) * detailX; - let pt3 = x + (y + 1) * detailX; - quadGeom.faces.push([pt0, pt1, pt2]); - quadGeom.faces.push([pt0, pt2, pt3]); - } - } - quadGeom.computeNormals(); - quadGeom.edges.length = 0; - const vertexOrder = [0, 2, 3, 1]; - for (let i = 0; i < vertexOrder.length; i++) { - const startVertex = vertexOrder[i]; - const endVertex = vertexOrder[(i + 1) % vertexOrder.length]; - quadGeom.edges.push([startVertex, endVertex]); - } - quadGeom._edgesToVertices(); - this.createBuffers(gId, quadGeom); - } - this.drawBuffers(gId); - return this; - }; - - //this implementation of bezier curve - //is based on Bernstein polynomial - // pretier-ignore - p5.RendererGL.prototype.bezier = function( - x1, - y1, - z1, // x2 - x2, // y2 - y2, // x3 - z2, // y3 - x3, // x4 - y3, // y4 - z3, - x4, - y4, - z4 - ) { - if (arguments.length === 8) { - y4 = y3; - x4 = x3; - y3 = z2; - x3 = y2; - y2 = x2; - x2 = z1; - z1 = z2 = z3 = z4 = 0; - } - const bezierDetail = this._pInst._bezierDetail || 20; //value of Bezier detail - this.beginShape(); - for (let i = 0; i <= bezierDetail; i++) { - const c1 = Math.pow(1 - i / bezierDetail, 3); - const c2 = 3 * (i / bezierDetail) * Math.pow(1 - i / bezierDetail, 2); - const c3 = 3 * Math.pow(i / bezierDetail, 2) * (1 - i / bezierDetail); - const c4 = Math.pow(i / bezierDetail, 3); - this.vertex( - x1 * c1 + x2 * c2 + x3 * c3 + x4 * c4, - y1 * c1 + y2 * c2 + y3 * c3 + y4 * c4, - z1 * c1 + z2 * c2 + z3 * c3 + z4 * c4 - ); - } - this.endShape(); - return this; - }; - - // pretier-ignore - p5.RendererGL.prototype.curve = function( - x1, - y1, - z1, // x2 - x2, // y2 - y2, // x3 - z2, // y3 - x3, // x4 - y3, // y4 - z3, - x4, - y4, - z4 - ) { - if (arguments.length === 8) { - x4 = x3; - y4 = y3; - x3 = y2; - y3 = x2; - x2 = z1; - y2 = x2; - z1 = z2 = z3 = z4 = 0; - } - const curveDetail = this._pInst._curveDetail; - this.beginShape(); - for (let i = 0; i <= curveDetail; i++) { - const c1 = Math.pow(i / curveDetail, 3) * 0.5; - const c2 = Math.pow(i / curveDetail, 2) * 0.5; - const c3 = i / curveDetail * 0.5; - const c4 = 0.5; - const vx = - c1 * (-x1 + 3 * x2 - 3 * x3 + x4) + - c2 * (2 * x1 - 5 * x2 + 4 * x3 - x4) + - c3 * (-x1 + x3) + - c4 * (2 * x2); - const vy = - c1 * (-y1 + 3 * y2 - 3 * y3 + y4) + - c2 * (2 * y1 - 5 * y2 + 4 * y3 - y4) + - c3 * (-y1 + y3) + - c4 * (2 * y2); - const vz = - c1 * (-z1 + 3 * z2 - 3 * z3 + z4) + - c2 * (2 * z1 - 5 * z2 + 4 * z3 - z4) + - c3 * (-z1 + z3) + - c4 * (2 * z2); - this.vertex(vx, vy, vz); - } - this.endShape(); - return this; - }; - - /** - * Draw a line given two points - * @private - * @param {Number} x0 x-coordinate of first vertex - * @param {Number} y0 y-coordinate of first vertex - * @param {Number} z0 z-coordinate of first vertex - * @param {Number} x1 x-coordinate of second vertex - * @param {Number} y1 y-coordinate of second vertex - * @param {Number} z1 z-coordinate of second vertex - * @chainable - * @example - *
- * - * //draw a line - * function setup() { - * createCanvas(100, 100, WEBGL); - * } - * - * function draw() { - * background(200); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * // Use fill instead of stroke to change the color of shape. - * fill(255, 0, 0); - * line(10, 10, 0, 60, 60, 20); - * } - * - *
- */ - p5.RendererGL.prototype.line = function(...args) { - if (args.length === 6) { - this.beginShape(constants.LINES); - this.vertex(args[0], args[1], args[2]); - this.vertex(args[3], args[4], args[5]); - this.endShape(); - } else if (args.length === 4) { - this.beginShape(constants.LINES); - this.vertex(args[0], args[1], 0); - this.vertex(args[2], args[3], 0); - this.endShape(); - } - return this; - }; - - p5.RendererGL.prototype.bezierVertex = function(...args) { - if (this.immediateMode._bezierVertex.length === 0) { - throw Error('vertex() must be used once before calling bezierVertex()'); - } else { - let w_x = []; - let w_y = []; - let w_z = []; - let t, _x, _y, _z, i, k, m; - // variable i for bezierPoints, k for components, and m for anchor points. - const argLength = args.length; - - t = 0; - - if ( - this._lookUpTableBezier.length === 0 || - this._lutBezierDetail !== this._pInst._curveDetail - ) { - this._lookUpTableBezier = []; - this._lutBezierDetail = this._pInst._curveDetail; - const step = 1 / this._lutBezierDetail; - let start = 0; - let end = 1; - let j = 0; - while (start < 1) { - t = parseFloat(start.toFixed(6)); - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - if (end.toFixed(6) === step.toFixed(6)) { - t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); - ++j; - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - break; - } - start += step; - end -= step; - ++j; - } - } - - const LUTLength = this._lookUpTableBezier.length; - const immediateGeometry = this.immediateMode.geometry; - - // fillColors[0]: start point color - // fillColors[1],[2]: control point color - // fillColors[3]: end point color - const fillColors = []; - for (m = 0; m < 4; m++) fillColors.push([]); - fillColors[0] = immediateGeometry.vertexColors.slice(-4); - fillColors[3] = this.states.curFillColor.slice(); - - // Do the same for strokeColor. - const strokeColors = []; - for (m = 0; m < 4; m++) strokeColors.push([]); - strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); - strokeColors[3] = this.states.curStrokeColor.slice(); - - // Do the same for custom vertex properties - const userVertexProperties = {}; - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - userVertexProperties[propName] = []; - for (m = 0; m < 4; m++) userVertexProperties[propName].push([]); - userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); - userVertexProperties[propName][3] = prop.getCurrentData(); - } - - if (argLength === 6) { - this.isBezier = true; - - w_x = [this.immediateMode._bezierVertex[0], args[0], args[2], args[4]]; - w_y = [this.immediateMode._bezierVertex[1], args[1], args[3], args[5]]; - // The ratio of the distance between the start point, the two control- - // points, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); - let d2 = Math.hypot(w_x[2]-w_x[3], w_y[2]-w_y[3]); - const totalLength = d0 + d1 + d2; - d0 /= totalLength; - d2 /= totalLength; - for (k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 - ); - fillColors[2].push( - fillColors[0][k] * d2 + fillColors[3][k] * (1-d2) - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[3][k] * d0 - ); - strokeColors[2].push( - strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) - ); - } - for (const propName in immediateGeometry.userVertexProperties){ - const size = immediateGeometry.userVertexProperties[propName].getDataSize(); - for (k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 - ); - userVertexProperties[propName][2].push( - userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 - ); - } - } - - for (let i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = 0; - for (let m = 0; m < 4; m++) { - for (let k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableBezier[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableBezier[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableBezier[i][m]; - _y += w_y[m] * this._lookUpTableBezier[i][m]; - } - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 4; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y); - } - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[3]; - this.states.curStrokeColor = strokeColors[3]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.immediateMode._bezierVertex[0] = args[4]; - this.immediateMode._bezierVertex[1] = args[5]; - } else if (argLength === 9) { - this.isBezier = true; - - w_x = [this.immediateMode._bezierVertex[0], args[0], args[3], args[6]]; - w_y = [this.immediateMode._bezierVertex[1], args[1], args[4], args[7]]; - w_z = [this.immediateMode._bezierVertex[2], args[2], args[5], args[8]]; - // The ratio of the distance between the start point, the two control- - // points, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1], w_z[0]-w_z[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2], w_z[1]-w_z[2]); - let d2 = Math.hypot(w_x[2]-w_x[3], w_y[2]-w_y[3], w_z[2]-w_z[3]); - const totalLength = d0 + d1 + d2; - d0 /= totalLength; - d2 /= totalLength; - for (let k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 - ); - fillColors[2].push( - fillColors[0][k] * d2 + fillColors[3][k] * (1-d2) - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[3][k] * d0 - ); - strokeColors[2].push( - strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) - ); - } - for (const propName in immediateGeometry.userVertexProperties){ - const size = immediateGeometry.userVertexProperties[propName].getDataSize(); - for (k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 - ); - userVertexProperties[propName][2].push( - userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 - ); - } - } - for (let i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = _z = 0; - for (m = 0; m < 4; m++) { - for (k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableBezier[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableBezier[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableBezier[i][m]; - _y += w_y[m] * this._lookUpTableBezier[i][m]; - _z += w_z[m] * this._lookUpTableBezier[i][m]; - } - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 4; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y, _z); - } - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[3]; - this.states.curStrokeColor = strokeColors[3]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.immediateMode._bezierVertex[0] = args[6]; - this.immediateMode._bezierVertex[1] = args[7]; - this.immediateMode._bezierVertex[2] = args[8]; - } - } - }; - - p5.RendererGL.prototype.quadraticVertex = function(...args) { - if (this.immediateMode._quadraticVertex.length === 0) { - throw Error('vertex() must be used once before calling quadraticVertex()'); - } else { - let w_x = []; - let w_y = []; - let w_z = []; - let t, _x, _y, _z, i, k, m; - // variable i for bezierPoints, k for components, and m for anchor points. - const argLength = args.length; - - t = 0; - - if ( - this._lookUpTableQuadratic.length === 0 || - this._lutQuadraticDetail !== this._pInst._curveDetail - ) { - this._lookUpTableQuadratic = []; - this._lutQuadraticDetail = this._pInst._curveDetail; - const step = 1 / this._lutQuadraticDetail; - let start = 0; - let end = 1; - let j = 0; - while (start < 1) { - t = parseFloat(start.toFixed(6)); - this._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); - if (end.toFixed(6) === step.toFixed(6)) { - t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); - ++j; - this._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); - break; - } - start += step; - end -= step; - ++j; - } - } - - const LUTLength = this._lookUpTableQuadratic.length; - const immediateGeometry = this.immediateMode.geometry; - - // fillColors[0]: start point color - // fillColors[1]: control point color - // fillColors[2]: end point color - const fillColors = []; - for (m = 0; m < 3; m++) fillColors.push([]); - fillColors[0] = immediateGeometry.vertexColors.slice(-4); - fillColors[2] = this.states.curFillColor.slice(); - - // Do the same for strokeColor. - const strokeColors = []; - for (m = 0; m < 3; m++) strokeColors.push([]); - strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); - strokeColors[2] = this.states.curStrokeColor.slice(); - - // Do the same for user defined vertex properties - const userVertexProperties = {}; - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - userVertexProperties[propName] = []; - for (m = 0; m < 3; m++) userVertexProperties[propName].push([]); - userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); - userVertexProperties[propName][2] = prop.getCurrentData(); - } - - if (argLength === 4) { - this.isQuadratic = true; - - w_x = [this.immediateMode._quadraticVertex[0], args[0], args[2]]; - w_y = [this.immediateMode._quadraticVertex[1], args[1], args[3]]; - - // The ratio of the distance between the start point, the control- - // point, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); - const totalLength = d0 + d1; - d0 /= totalLength; - for (let k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 - ); - } - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - for (let k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 - ); - } - } - - for (let i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = 0; - for (let m = 0; m < 3; m++) { - for (let k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableQuadratic[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableQuadratic[i][m]; - _y += w_y[m] * this._lookUpTableQuadratic[i][m]; - } - - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 3; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y); - } - - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[2]; - this.states.curStrokeColor = strokeColors[2]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.immediateMode._quadraticVertex[0] = args[2]; - this.immediateMode._quadraticVertex[1] = args[3]; - } else if (argLength === 6) { - this.isQuadratic = true; - - w_x = [this.immediateMode._quadraticVertex[0], args[0], args[3]]; - w_y = [this.immediateMode._quadraticVertex[1], args[1], args[4]]; - w_z = [this.immediateMode._quadraticVertex[2], args[2], args[5]]; - - // The ratio of the distance between the start point, the control- - // point, and the end point determines the intermediate color. - let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1], w_z[0]-w_z[1]); - let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2], w_z[1]-w_z[2]); - const totalLength = d0 + d1; - d0 /= totalLength; - for (k = 0; k < 4; k++) { - fillColors[1].push( - fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 - ); - strokeColors[1].push( - strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 - ); - } - - for (const propName in immediateGeometry.userVertexProperties){ - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - for (let k = 0; k < size; k++){ - userVertexProperties[propName][1].push( - userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 - ); - } - } - - for (i = 0; i < LUTLength; i++) { - // Interpolate colors using control points - this.states.curFillColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 0]; - _x = _y = _z = 0; - for (m = 0; m < 3; m++) { - for (k = 0; k < 4; k++) { - this.states.curFillColor[k] += - this._lookUpTableQuadratic[i][m] * fillColors[m][k]; - this.states.curStrokeColor[k] += - this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; - } - _x += w_x[m] * this._lookUpTableQuadratic[i][m]; - _y += w_y[m] * this._lookUpTableQuadratic[i][m]; - _z += w_z[m] * this._lookUpTableQuadratic[i][m]; - } - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - let newValues = Array(size).fill(0); - for (let m = 0; m < 3; m++){ - for (let k = 0; k < size; k++){ - newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; - } - } - prop.setCurrentData(newValues); - } - this.vertex(_x, _y, _z); - } - - // so that we leave currentColor with the last value the user set it to - this.states.curFillColor = fillColors[2]; - this.states.curStrokeColor = strokeColors[2]; - for (const propName in immediateGeometry.userVertexProperties) { - const prop = immediateGeometry.userVertexProperties[propName]; - prop.setCurrentData(userVertexProperties[propName][2]); - } - this.immediateMode._quadraticVertex[0] = args[3]; - this.immediateMode._quadraticVertex[1] = args[4]; - this.immediateMode._quadraticVertex[2] = args[5]; - } - } - }; - - p5.RendererGL.prototype.curveVertex = function(...args) { - let w_x = []; - let w_y = []; - let w_z = []; - let t, _x, _y, _z, i; - t = 0; - const argLength = args.length; - - if ( - this._lookUpTableBezier.length === 0 || - this._lutBezierDetail !== this._pInst._curveDetail - ) { - this._lookUpTableBezier = []; - this._lutBezierDetail = this._pInst._curveDetail; - const step = 1 / this._lutBezierDetail; - let start = 0; - let end = 1; - let j = 0; - while (start < 1) { - t = parseFloat(start.toFixed(6)); - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - if (end.toFixed(6) === step.toFixed(6)) { - t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); - ++j; - this._lookUpTableBezier[j] = this._bezierCoefficients(t); - break; - } - start += step; - end -= step; - ++j; - } - } - - const LUTLength = this._lookUpTableBezier.length; - - if (argLength === 2) { - this.immediateMode._curveVertex.push(args[0]); - this.immediateMode._curveVertex.push(args[1]); - if (this.immediateMode._curveVertex.length === 8) { - this.isCurve = true; - w_x = this._bezierToCatmull([ - this.immediateMode._curveVertex[0], - this.immediateMode._curveVertex[2], - this.immediateMode._curveVertex[4], - this.immediateMode._curveVertex[6] - ]); - w_y = this._bezierToCatmull([ - this.immediateMode._curveVertex[1], - this.immediateMode._curveVertex[3], - this.immediateMode._curveVertex[5], - this.immediateMode._curveVertex[7] - ]); - for (i = 0; i < LUTLength; i++) { - _x = - w_x[0] * this._lookUpTableBezier[i][0] + - w_x[1] * this._lookUpTableBezier[i][1] + - w_x[2] * this._lookUpTableBezier[i][2] + - w_x[3] * this._lookUpTableBezier[i][3]; - _y = - w_y[0] * this._lookUpTableBezier[i][0] + - w_y[1] * this._lookUpTableBezier[i][1] + - w_y[2] * this._lookUpTableBezier[i][2] + - w_y[3] * this._lookUpTableBezier[i][3]; - this.vertex(_x, _y); - } - for (i = 0; i < argLength; i++) { - this.immediateMode._curveVertex.shift(); - } - } - } else if (argLength === 3) { - this.immediateMode._curveVertex.push(args[0]); - this.immediateMode._curveVertex.push(args[1]); - this.immediateMode._curveVertex.push(args[2]); - if (this.immediateMode._curveVertex.length === 12) { - this.isCurve = true; - w_x = this._bezierToCatmull([ - this.immediateMode._curveVertex[0], - this.immediateMode._curveVertex[3], - this.immediateMode._curveVertex[6], - this.immediateMode._curveVertex[9] - ]); - w_y = this._bezierToCatmull([ - this.immediateMode._curveVertex[1], - this.immediateMode._curveVertex[4], - this.immediateMode._curveVertex[7], - this.immediateMode._curveVertex[10] - ]); - w_z = this._bezierToCatmull([ - this.immediateMode._curveVertex[2], - this.immediateMode._curveVertex[5], - this.immediateMode._curveVertex[8], - this.immediateMode._curveVertex[11] - ]); - for (i = 0; i < LUTLength; i++) { - _x = - w_x[0] * this._lookUpTableBezier[i][0] + - w_x[1] * this._lookUpTableBezier[i][1] + - w_x[2] * this._lookUpTableBezier[i][2] + - w_x[3] * this._lookUpTableBezier[i][3]; - _y = - w_y[0] * this._lookUpTableBezier[i][0] + - w_y[1] * this._lookUpTableBezier[i][1] + - w_y[2] * this._lookUpTableBezier[i][2] + - w_y[3] * this._lookUpTableBezier[i][3]; - _z = - w_z[0] * this._lookUpTableBezier[i][0] + - w_z[1] * this._lookUpTableBezier[i][1] + - w_z[2] * this._lookUpTableBezier[i][2] + - w_z[3] * this._lookUpTableBezier[i][3]; - this.vertex(_x, _y, _z); - } - for (i = 0; i < argLength; i++) { - this.immediateMode._curveVertex.shift(); - } - } - } - }; - - p5.RendererGL.prototype.image = function( - img, - sx, - sy, - sWidth, - sHeight, - dx, - dy, - dWidth, - dHeight - ) { - if (this._isErasing) { - this.blendMode(this._cachedBlendMode); - } - - this._pInst.push(); - - this._pInst.noLights(); - this._pInst.noStroke(); - - this._pInst.texture(img); - this._pInst.textureMode(constants.NORMAL); - - let u0 = 0; - if (sx <= img.width) { - u0 = sx / img.width; - } - - let u1 = 1; - if (sx + sWidth <= img.width) { - u1 = (sx + sWidth) / img.width; - } - - let v0 = 0; - if (sy <= img.height) { - v0 = sy / img.height; - } - - let v1 = 1; - if (sy + sHeight <= img.height) { - v1 = (sy + sHeight) / img.height; - } - - this.beginShape(); - this.vertex(dx, dy, 0, u0, v0); - this.vertex(dx + dWidth, dy, 0, u1, v0); - this.vertex(dx + dWidth, dy + dHeight, 0, u1, v1); - this.vertex(dx, dy + dHeight, 0, u0, v1); - this.endShape(constants.CLOSE); - - this._pInst.pop(); - - if (this._isErasing) { - this.blendMode(constants.REMOVE); - } - }; } /** diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index ff3934fd6a..8072fc00be 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -321,7 +321,7 @@ class Shader { modifiedFragment[key] = true; } - return new p5.Shader(this._renderer, this._vertSrc, this._fragSrc, { + return new Shader(this._renderer, this._vertSrc, this._fragSrc, { declarations: (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), @@ -607,7 +607,7 @@ class Shader { *
*/ copyToContext(context) { - const shader = new p5.Shader( + const shader = new Shader( context._renderer, this._vertSrc, this._fragSrc diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 6bebe1662f..0aad977323 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -426,7 +426,52 @@ class Texture { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, this.glWrapT); this.unbindTexture(); } -}; +} + +class MipmapTexture extends Texture { + constructor(renderer, levels, settings) { + super(renderer, levels, settings); + const gl = this._renderer.GL; + if (this.glMinFilter === gl.LINEAR) { + this.glMinFilter = gl.LINEAR_MIPMAP_LINEAR; + } + } + + glFilter(_filter) { + const gl = this._renderer.GL; + // TODO: support others + return gl.LINEAR_MIPMAP_LINEAR; + } + + _getTextureDataFromSource() { + return this.src; + } + + init(levels) { + const gl = this._renderer.GL; + this.glTex = gl.createTexture(); + + this.bindTexture(); + for (let level = 0; level < levels.length; level++) { + gl.texImage2D( + this.glTarget, + level, + this.glFormat, + this.glFormat, + this.glDataType, + levels[level] + ); + } + + this.glMinFilter = gl.LINEAR_MIPMAP_LINEAR; + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.glMagFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.glMinFilter); + + this.unbindTexture(); + } + + update() {} +} function texture(p5, fn){ /** @@ -462,50 +507,7 @@ function texture(p5, fn){ */ p5.Texture = Texture; - p5.MipmapTexture = class MipmapTexture extends p5.Texture { - constructor(renderer, levels, settings) { - super(renderer, levels, settings); - const gl = this._renderer.GL; - if (this.glMinFilter === gl.LINEAR) { - this.glMinFilter = gl.LINEAR_MIPMAP_LINEAR; - } - } - - glFilter(_filter) { - const gl = this._renderer.GL; - // TODO: support others - return gl.LINEAR_MIPMAP_LINEAR; - } - - _getTextureDataFromSource() { - return this.src; - } - - init(levels) { - const gl = this._renderer.GL; - this.glTex = gl.createTexture(); - - this.bindTexture(); - for (let level = 0; level < levels.length; level++) { - gl.texImage2D( - this.glTarget, - level, - this.glFormat, - this.glFormat, - this.glDataType, - levels[level] - ); - } - - this.glMinFilter = gl.LINEAR_MIPMAP_LINEAR; - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, this.glMagFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, this.glMinFilter); - - this.unbindTexture(); - } - - update() {} - }; + p5.MipmapTexture = MipmapTexture; } export function checkWebGLCapabilities({ GL, webglVersion }) { @@ -530,7 +532,7 @@ export function checkWebGLCapabilities({ GL, webglVersion }) { } export default texture; -export { Texture }; +export { Texture, MipmapTexture }; if(typeof p5 !== 'undefined'){ texture(p5, p5.prototype); diff --git a/src/webgl/text.js b/src/webgl/text.js index f86d14f620..ebc474478e 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -1,14 +1,18 @@ import * as constants from '../core/constants'; +import { RendererGL } from './p5.RendererGL'; +import { Vector } from '../math/p5.Vector'; +import { Geometry } from './p5.Geometry'; + function text(p5, fn){ // Text/Typography // @TODO: - p5.RendererGL.prototype._applyTextProperties = function() { + RendererGL.prototype._applyTextProperties = function() { //@TODO finish implementation //console.error('text commands not yet implemented in webgl'); }; - p5.RendererGL.prototype.textWidth = function(s) { + RendererGL.prototype.textWidth = function(s) { if (this._isOpenType()) { return this._textFont._textWidth(s, this._textSize); } @@ -312,9 +316,9 @@ function text(p5, fn){ */ quadError () { return ( - p5.Vector.sub( - p5.Vector.sub(this.p1, this.p0), - p5.Vector.mult(p5.Vector.sub(this.c1, this.c0), 3) + Vector.sub( + Vector.sub(this.p1, this.p0), + Vector.mult(Vector.sub(this.c1, this.c0), 3) ).mag() / 2 ); } @@ -328,13 +332,13 @@ function text(p5, fn){ * point at 't'. the 'end half is returned. */ split (t) { - const m1 = p5.Vector.lerp(this.p0, this.c0, t); - const m2 = p5.Vector.lerp(this.c0, this.c1, t); - const mm1 = p5.Vector.lerp(m1, m2, t); + const m1 = Vector.lerp(this.p0, this.c0, t); + const m2 = Vector.lerp(this.c0, this.c1, t); + const mm1 = Vector.lerp(m1, m2, t); - this.c1 = p5.Vector.lerp(this.c1, this.p1, t); - this.c0 = p5.Vector.lerp(m2, this.c1, t); - const pt = p5.Vector.lerp(mm1, this.c0, t); + this.c1 = Vector.lerp(this.c1, this.p1, t); + this.c0 = Vector.lerp(m2, this.c1, t); + const pt = Vector.lerp(mm1, this.c0, t); const part1 = new Cubic(this.p0, m1, mm1, pt); this.p0 = pt; return part1; @@ -348,11 +352,11 @@ function text(p5, fn){ * this cubic is (potentially) altered and returned in the list. */ splitInflections () { - const a = p5.Vector.sub(this.c0, this.p0); - const b = p5.Vector.sub(p5.Vector.sub(this.c1, this.c0), a); - const c = p5.Vector.sub( - p5.Vector.sub(p5.Vector.sub(this.p1, this.c1), a), - p5.Vector.mult(b, 2) + const a = Vector.sub(this.c0, this.p0); + const b = Vector.sub(Vector.sub(this.c1, this.c0), a); + const c = Vector.sub( + Vector.sub(Vector.sub(this.p1, this.c1), a), + Vector.mult(b, 2) ); const cubics = []; @@ -413,10 +417,10 @@ function text(p5, fn){ function cubicToQuadratics(x0, y0, cx0, cy0, cx1, cy1, x1, y1) { // create the Cubic object and split it at its inflections const cubics = new Cubic( - new p5.Vector(x0, y0), - new p5.Vector(cx0, cy0), - new p5.Vector(cx1, cy1), - new p5.Vector(x1, y1) + new Vector(x0, y0), + new Vector(cx0, cy0), + new Vector(cx1, cy1), + new Vector(x1, y1) ).splitInflections(); const qs = []; // the final list of quadratics @@ -631,7 +635,7 @@ function text(p5, fn){ } } - p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { + RendererGL.prototype._renderText = function(p, line, x, y, maxY) { if (!this._textFont || typeof this._textFont === 'string') { console.log( 'WEBGL: you must load and set a font before drawing text. See `loadFont` and `textFont` for more details.' @@ -691,10 +695,10 @@ function text(p5, fn){ let g = this.retainedMode.geometry['glyph']; if (!g) { // create the geometry for rendering a quad - const geom = (this._textGeom = new p5.Geometry(1, 1, function() { + const geom = (this._textGeom = new Geometry(1, 1, function() { for (let i = 0; i <= 1; i++) { for (let j = 0; j <= 1; j++) { - this.vertices.push(new p5.Vector(j, i, 0)); + this.vertices.push(new Vector(j, i, 0)); this.uvs.push(j, i); } } From 5cc155531235840f2b915736dd2a2af9a6312304 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 15 Oct 2024 22:20:49 +0100 Subject: [PATCH 24/55] Fix enableLighting state name inconsistency --- src/core/p5.Renderer2D.js | 2 -- src/webgl/3d_primitives.js | 8 ++++---- src/webgl/light.js | 12 ++++++------ src/webgl/material.js | 6 +++--- src/webgl/p5.RendererGL.js | 8 ++++---- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 60cdacf0a2..1b7433f5ec 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1242,8 +1242,6 @@ class Renderer2D extends Renderer { _setFill(fillStyle) { if (fillStyle !== this._cachedFillStyle) { this.drawingContext.fillStyle = fillStyle; - // console.log('here', this.drawingContext.fillStyle); - // console.trace(); this._cachedFillStyle = fillStyle; } } diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index b3a0463350..4a9361a694 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3535,12 +3535,12 @@ function primitives3D(p5, fn){ this.blendMode(this._cachedBlendMode); } - this._pInst.push(); + this.push(); this.noLights(); - this._pInst.noStroke(); + this.states.doStroke = false;; this.texture(img); - this._pInst.textureMode(constants.NORMAL); + this.textureMode = constants.NORMAL; let u0 = 0; if (sx <= img.width) { @@ -3569,7 +3569,7 @@ function primitives3D(p5, fn){ this.vertex(dx, dy + dHeight, 0, u0, v1); this.endShape(constants.CLOSE); - this._pInst.pop(); + this.pop(); if (this._isErasing) { this.blendMode(constants.REMOVE); diff --git a/src/webgl/light.js b/src/webgl/light.js index ed70d92b00..1dcbc0d42f 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -199,7 +199,7 @@ function light(p5, fn){ color._array[2] ); - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; return this; }; @@ -676,7 +676,7 @@ function light(p5, fn){ this._renderer.states.specularColors ); - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; return this; }; @@ -949,7 +949,7 @@ function light(p5, fn){ this._renderer.states.specularColors ); - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; return this; }; @@ -1016,7 +1016,7 @@ function light(p5, fn){ // activeImageLight property is checked by _setFillUniforms // for sending uniforms to the fillshader this._renderer.states.activeImageLight = img; - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; }; /** @@ -1679,7 +1679,7 @@ function light(p5, fn){ this._renderer.states.spotLightAngle = [Math.cos(angle)]; this._renderer.states.spotLightConc = [concentration]; - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; return this; }; @@ -1754,7 +1754,7 @@ function light(p5, fn){ RendererGL.prototype.noLights = function(){ this.states.activeImageLight = null; - this.states._enableLighting = false; + this.states.enableLighting = false; this.states.ambientLightColors.length = 0; this.states.specularColors = [1, 1, 1]; diff --git a/src/webgl/material.js b/src/webgl/material.js index d70b88db43..4497a7e7a6 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2640,7 +2640,7 @@ function material(p5, fn){ this._renderer.states._hasSetAmbient = true; this._renderer.states.curAmbientColor = color._array; this._renderer.states._useNormalMaterial = false; - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; this._renderer.states.doFill = true; return this; }; @@ -2736,7 +2736,7 @@ function material(p5, fn){ this._renderer.states.curEmissiveColor = color._array; this._renderer.states._useEmissiveMaterial = true; this._renderer.states._useNormalMaterial = false; - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; return this; }; @@ -2991,7 +2991,7 @@ function material(p5, fn){ this._renderer.states.curSpecularColor = color._array; this._renderer.states._useSpecularMaterial = true; this._renderer.states._useNormalMaterial = false; - this._renderer.states._enableLighting = true; + this._renderer.states.enableLighting = true; return this; }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 9680459b66..f658ce9f93 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -638,7 +638,7 @@ class RendererGL extends Renderer { this.states.spotLightAngle.length = 0; this.states.spotLightConc.length = 0; - this.states._enableLighting = false; + this.states.enableLighting = false; //reset tint value for new frame this.states.tint = [255, 255, 255, 255]; @@ -1414,7 +1414,7 @@ class RendererGL extends Renderer { return this._getNormalShader(); } } - if (this.states._enableLighting) { + if (this.states.enableLighting) { if (!fill || !fill.isLightShader()) { return this._getLightShader(); } @@ -1438,7 +1438,7 @@ class RendererGL extends Renderer { } const fill = this.states.userFillShader; - if (this.states._enableLighting) { + if (this.states.enableLighting) { if (!fill || !fill.isLightShader()) { return this._getLightShader(); } @@ -1916,7 +1916,7 @@ class RendererGL extends Renderer { this._setImageLightUniforms(fillShader); - fillShader.setUniform('uUseLighting', this.states._enableLighting); + fillShader.setUniform('uUseLighting', this.states.enableLighting); const pointLightCount = this.states.pointLightDiffuseColors.length / 3; fillShader.setUniform('uPointLightCount', pointLightCount); From 7b59c3127bbd91059241d2afdbf829917021d3f2 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 15 Oct 2024 22:44:41 +0100 Subject: [PATCH 25/55] Inline webgl nostroke call --- src/webgl/p5.Framebuffer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index ce5299d00a..0378ba42ec 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -1555,7 +1555,7 @@ class Framebuffer { this.target._renderer.push(); this.target._renderer.imageMode(this.target.CENTER); this.target._renderer.resetMatrix(); - this.target._renderer.noStroke(); + this.target._renderer.states.doStroke = false; this.target._renderer.clear(); this.target._renderer.image(this, 0, 0); this.target._renderer.pop(); From beb947a0daadb3abaa34c0feeaa7e6814b537360 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 18 Oct 2024 15:18:24 +0100 Subject: [PATCH 26/55] Convert a few more class usage, fix a few more tests --- src/core/p5.Element.js | 34 +++++++++++++-------------- src/core/p5.Renderer.js | 30 ++++++++++++------------ src/core/rendering.js | 3 ++- src/math/p5.Vector.js | 48 +++++++++++++++++++------------------- src/webgl/p5.RendererGL.js | 8 ++++--- src/webgl/p5.Shader.js | 2 +- 6 files changed, 64 insertions(+), 61 deletions(-) diff --git a/src/core/p5.Element.js b/src/core/p5.Element.js index 6397cf67c0..fdfa402a65 100644 --- a/src/core/p5.Element.js +++ b/src/core/p5.Element.js @@ -146,7 +146,7 @@ class Element { p = p.substring(1); } p = document.getElementById(p); - } else if (p instanceof p5.Element) { + } else if (p instanceof Element) { p = p.elt; } p.appendChild(this.elt); @@ -295,7 +295,7 @@ class Element { return fxn.call(this, event); }; // Pass along the event-prepended form of the callback. - p5.Element._adjustListener('mousedown', eventPrependedFxn, this); + Element._adjustListener('mousedown', eventPrependedFxn, this); return this; } @@ -336,7 +336,7 @@ class Element { *
*/ doubleClicked(fxn) { - p5.Element._adjustListener('dblclick', fxn, this); + Element._adjustListener('dblclick', fxn, this); return this; } @@ -421,7 +421,7 @@ class Element { *
*/ mouseWheel(fxn) { - p5.Element._adjustListener('wheel', fxn, this); + Element._adjustListener('wheel', fxn, this); return this; } @@ -465,7 +465,7 @@ class Element { *
*/ mouseReleased(fxn) { - p5.Element._adjustListener('mouseup', fxn, this); + Element._adjustListener('mouseup', fxn, this); return this; } @@ -509,7 +509,7 @@ class Element { *
*/ mouseClicked(fxn) { - p5.Element._adjustListener('click', fxn, this); + Element._adjustListener('click', fxn, this); return this; } @@ -550,7 +550,7 @@ class Element { *
*/ mouseMoved(fxn) { - p5.Element._adjustListener('mousemove', fxn, this); + Element._adjustListener('mousemove', fxn, this); return this; } @@ -591,7 +591,7 @@ class Element { *
*/ mouseOver(fxn) { - p5.Element._adjustListener('mouseover', fxn, this); + Element._adjustListener('mouseover', fxn, this); return this; } @@ -632,7 +632,7 @@ class Element { * */ mouseOut(fxn) { - p5.Element._adjustListener('mouseout', fxn, this); + Element._adjustListener('mouseout', fxn, this); return this; } @@ -675,7 +675,7 @@ class Element { * */ touchStarted(fxn) { - p5.Element._adjustListener('touchstart', fxn, this); + Element._adjustListener('touchstart', fxn, this); return this; } @@ -718,7 +718,7 @@ class Element { * */ touchMoved(fxn) { - p5.Element._adjustListener('touchmove', fxn, this); + Element._adjustListener('touchmove', fxn, this); return this; } @@ -761,7 +761,7 @@ class Element { * */ touchEnded(fxn) { - p5.Element._adjustListener('touchend', fxn, this); + Element._adjustListener('touchend', fxn, this); return this; } @@ -802,7 +802,7 @@ class Element { * */ dragOver(fxn) { - p5.Element._adjustListener('dragover', fxn, this); + Element._adjustListener('dragover', fxn, this); return this; } @@ -843,7 +843,7 @@ class Element { * */ dragLeave(fxn) { - p5.Element._adjustListener('dragleave', fxn, this); + Element._adjustListener('dragleave', fxn, this); return this; } @@ -861,9 +861,9 @@ class Element { */ static _adjustListener(ev, fxn, ctx) { if (fxn === false) { - p5.Element._detachListener(ev, ctx); + Element._detachListener(ev, ctx); } else { - p5.Element._attachListener(ev, fxn, ctx); + Element._attachListener(ev, fxn, ctx); } return this; } @@ -878,7 +878,7 @@ class Element { static _attachListener(ev, fxn, ctx) { // detach the old listener if there was one if (ctx._events[ev]) { - p5.Element._detachListener(ev, ctx); + Element._detachListener(ev, ctx); } const f = fxn.bind(ctx); ctx.elt.addEventListener(ev, f, false); diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 4c6f470943..a44b07f1fe 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -529,25 +529,25 @@ function renderer(p5, fn){ * @param {Boolean} [isMainCanvas] whether we're using it as main canvas */ p5.Renderer = Renderer; +} - /** - * Helper fxn to measure ascent and descent. - * Adapted from http://stackoverflow.com/a/25355178 - */ - function calculateOffset(object) { - let currentLeft = 0, - currentTop = 0; - if (object.offsetParent) { - do { - currentLeft += object.offsetLeft; - currentTop += object.offsetTop; - } while ((object = object.offsetParent)); - } else { +/** + * Helper fxn to measure ascent and descent. + * Adapted from http://stackoverflow.com/a/25355178 + */ +function calculateOffset(object) { + let currentLeft = 0, + currentTop = 0; + if (object.offsetParent) { + do { currentLeft += object.offsetLeft; currentTop += object.offsetTop; - } - return [currentLeft, currentTop]; + } while ((object = object.offsetParent)); + } else { + currentLeft += object.offsetLeft; + currentTop += object.offsetTop; } + return [currentLeft, currentTop]; } export default renderer; diff --git a/src/core/rendering.js b/src/core/rendering.js index 51d0494d45..96d7bc57de 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -5,6 +5,7 @@ */ import * as constants from './constants'; +import { Framebuffer } from '../webgl/p5.Framebuffer'; let renderers; function rendering(p5, fn){ @@ -533,7 +534,7 @@ function rendering(p5, fn){ * */ p5.prototype.createFramebuffer = function (options) { - return new p5.Framebuffer(this, options); + return new Framebuffer(this, options); }; /** diff --git a/src/math/p5.Vector.js b/src/math/p5.Vector.js index c4f14e8c2e..738b966f8d 100644 --- a/src/math/p5.Vector.js +++ b/src/math/p5.Vector.js @@ -6,6 +6,30 @@ import * as constants from '../core/constants'; +/// HELPERS FOR REMAINDER METHOD +const calculateRemainder2D = function (xComponent, yComponent) { + if (xComponent !== 0) { + this.x = this.x % xComponent; + } + if (yComponent !== 0) { + this.y = this.y % yComponent; + } + return this; +}; + +const calculateRemainder3D = function (xComponent, yComponent, zComponent) { + if (xComponent !== 0) { + this.x = this.x % xComponent; + } + if (yComponent !== 0) { + this.y = this.y % yComponent; + } + if (zComponent !== 0) { + this.z = this.z % zComponent; + } + return this; +}; + class Vector { // This is how it comes in with createVector() // This check if the first argument is a function @@ -3693,30 +3717,6 @@ class Vector { }; function vector(p5, fn) { - /// HELPERS FOR REMAINDER METHOD - const calculateRemainder2D = function (xComponent, yComponent) { - if (xComponent !== 0) { - this.x = this.x % xComponent; - } - if (yComponent !== 0) { - this.y = this.y % yComponent; - } - return this; - }; - - const calculateRemainder3D = function (xComponent, yComponent, zComponent) { - if (xComponent !== 0) { - this.x = this.x % xComponent; - } - if (yComponent !== 0) { - this.y = this.y % yComponent; - } - if (zComponent !== 0) { - this.z = this.z % zComponent; - } - return this; - }; - /** * A class to describe a two or three-dimensional vector. * diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index f658ce9f93..8486e05039 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -12,6 +12,8 @@ import { Shader } from './p5.Shader'; import { Image } from '../image/p5.Image'; import { Texture, MipmapTexture } from './p5.Texture'; import { Framebuffer } from './p5.Framebuffer'; +import { Graphics } from '../core/p5.Graphics'; +import { Element } from '../core/p5.Element'; import lightingShader from './shaders/lighting.glsl'; import webgl2CompatibilityShader from './shaders/webgl2Compatibility.glsl'; @@ -564,7 +566,7 @@ class RendererGL extends Renderer { const w = this.width; const h = this.height; const defaultId = this.canvas.id; - const isPGraphics = this._pInst instanceof p5.Graphics; + const isPGraphics = this._pInst instanceof Graphics; if (isPGraphics) { const pg = this._pInst; @@ -572,7 +574,7 @@ class RendererGL extends Renderer { pg.canvas = document.createElement('canvas'); const node = pg._pInst._userNode || document.body; node.appendChild(pg.canvas); - p5.Element.call(pg, pg.canvas, pg._pInst); + Element.call(pg, pg.canvas, pg._pInst); pg.width = w; pg.height = h; } else { @@ -591,7 +593,7 @@ class RendererGL extends Renderer { this.canvas = c; } - const renderer = new p5.RendererGL( + const renderer = new RendererGL( this._pInst, w, h, diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 8072fc00be..f0a93539b9 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -287,7 +287,7 @@ class Shader { * */ modify(hooks) { - p5._validateParameters('p5.Shader.modify', arguments); + // p5._validateParameters('p5.Shader.modify', arguments); const newHooks = { vertex: {}, fragment: {}, From 1ea4e25aba24d32290a6e749ba452b049d9f417a Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Mon, 21 Oct 2024 17:16:35 +0100 Subject: [PATCH 27/55] Fix for before refactors --- src/core/p5.Graphics.js | 2 +- src/core/rendering.js | 2 +- src/webgl/p5.Framebuffer.js | 142 ++++++++++++++++++------------------ src/webgl/p5.RendererGL.js | 38 +++++++--- 4 files changed, 101 insertions(+), 83 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 51c694c353..b86e094f90 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -581,7 +581,7 @@ class Graphics { * */ createFramebuffer(options) { - return new p5.Framebuffer(this, options); + return new p5.Framebuffer(this._renderer, options); } _assert3d(name) { diff --git a/src/core/rendering.js b/src/core/rendering.js index 96d7bc57de..de6387ca6b 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -534,7 +534,7 @@ function rendering(p5, fn){ * */ p5.prototype.createFramebuffer = function (options) { - return new Framebuffer(this, options); + return new Framebuffer(this._renderer, options); }; /** diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 0378ba42ec..80552128cd 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -10,9 +10,11 @@ import { Camera } from './p5.Camera'; import { Texture } from './p5.Texture'; import { Image } from '../image/p5.Image'; +const constrain = (n, low, high) => Math.max(Math.min(n, high), low); + class FramebufferCamera extends Camera { constructor(framebuffer) { - super(framebuffer.target._renderer); + super(framebuffer.renderer); this.fbo = framebuffer; // WebGL textures are upside-down compared to textures that come from @@ -50,9 +52,9 @@ class FramebufferTexture { } class Framebuffer { - constructor(target, settings = {}) { - this.target = target; - this.target._renderer.framebuffers.add(this); + constructor(renderer, settings = {}) { + this.renderer = renderer; + this.renderer.framebuffers.add(this); this._isClipApplied = false; @@ -60,7 +62,7 @@ class Framebuffer { this.format = settings.format || constants.UNSIGNED_BYTE; this.channels = settings.channels || ( - target._renderer._pInst._glAttributes.alpha + this.renderer._pInst._glAttributes.alpha ? constants.RGBA : constants.RGB ); @@ -68,7 +70,7 @@ class Framebuffer { this.depthFormat = settings.depthFormat || constants.FLOAT; this.textureFiltering = settings.textureFiltering || constants.LINEAR; if (settings.antialias === undefined) { - this.antialiasSamples = target._renderer._pInst._glAttributes.antialias + this.antialiasSamples = this.renderer._pInst._glAttributes.antialias ? 2 : 0; } else if (typeof settings.antialias === 'number') { @@ -77,16 +79,16 @@ class Framebuffer { this.antialiasSamples = settings.antialias ? 2 : 0; } this.antialias = this.antialiasSamples > 0; - if (this.antialias && target.webglVersion !== constants.WEBGL2) { + if (this.antialias && this.renderer.webglVersion !== constants.WEBGL2) { console.warn('Antialiasing is unsupported in a WebGL 1 context'); this.antialias = false; } - this.density = settings.density || target.pixelDensity(); - const gl = target._renderer.GL; + this.density = settings.density || this.renderer._pixelDensity; + const gl = this.renderer.GL; this.gl = gl; if (settings.width && settings.height) { const dimensions = - target._renderer._adjustDimensions(settings.width, settings.height); + this.renderer._adjustDimensions(settings.width, settings.height); this.width = dimensions.adjustedWidth; this.height = dimensions.adjustedHeight; this._autoSized = false; @@ -98,8 +100,8 @@ class Framebuffer { 'of its canvas.' ); } - this.width = target.width; - this.height = target.height; + this.width = this.renderer.width; + this.height = this.renderer.height; this._autoSized = true; } this._checkIfFormatsAvailable(); @@ -123,12 +125,12 @@ class Framebuffer { this._recreateTextures(); - const prevCam = this.target._renderer.states.curCamera; + const prevCam = this.renderer.states.curCamera; this.defaultCamera = this.createCamera(); this.filterCamera = this.createCamera(); - this.target._renderer.states.curCamera = prevCam; + this.renderer.states.curCamera = prevCam; - this.draw(() => this.target._renderer.clear()); + this.draw(() => this.renderer.clear()); } /** @@ -181,7 +183,7 @@ class Framebuffer { resize(width, height) { this._autoSized = false; const dimensions = - this.target._renderer._adjustDimensions(width, height); + this.renderer._adjustDimensions(width, height); width = dimensions.adjustedWidth; height = dimensions.adjustedHeight; this.width = width; @@ -374,7 +376,7 @@ class Framebuffer { if ( this.useDepth && - this.target.webglVersion === constants.WEBGL && + this.renderer.webglVersion === constants.WEBGL && !gl.getExtension('WEBGL_depth_texture') ) { console.warn( @@ -386,7 +388,7 @@ class Framebuffer { if ( this.useDepth && - this.target.webglVersion === constants.WEBGL && + this.renderer.webglVersion === constants.WEBGL && this.depthFormat === constants.FLOAT ) { console.warn( @@ -419,7 +421,7 @@ class Framebuffer { this.depthFormat = constants.FLOAT; } - const support = checkWebGLCapabilities(this.target._renderer); + const support = checkWebGLCapabilities(this.renderer); if (!support.float && this.format === constants.FLOAT) { console.warn( 'This environment does not support FLOAT textures. ' + @@ -581,14 +583,14 @@ class Framebuffer { this.depth = new FramebufferTexture(this, 'depthTexture'); const depthFilter = gl.NEAREST; this.depthP5Texture = new Texture( - this.target._renderer, + this.renderer, this.depth, { minFilter: depthFilter, magFilter: depthFilter } ); - this.target._renderer.textures.set(this.depth, this.depthP5Texture); + this.renderer.textures.set(this.depth, this.depthP5Texture); } this.color = new FramebufferTexture(this, 'colorTexture'); @@ -596,14 +598,14 @@ class Framebuffer { ? gl.LINEAR : gl.NEAREST; this.colorP5Texture = new Texture( - this.target._renderer, + this.renderer, this.color, { minFilter: filter, magFilter: filter } ); - this.target._renderer.textures.set(this.color, this.colorP5Texture); + this.renderer.textures.set(this.color, this.colorP5Texture); gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture); gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer); @@ -631,7 +633,7 @@ class Framebuffer { if (this.format === constants.FLOAT) { type = gl.FLOAT; } else if (this.format === constants.HALF_FLOAT) { - type = this.target.webglVersion === constants.WEBGL2 + type = this.renderer.webglVersion === constants.WEBGL2 ? gl.HALF_FLOAT : gl.getExtension('OES_texture_half_float').HALF_FLOAT_OES; } else { @@ -644,7 +646,7 @@ class Framebuffer { format = gl.RGB; } - if (this.target.webglVersion === constants.WEBGL2) { + if (this.renderer.webglVersion === constants.WEBGL2) { // https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html const table = { [gl.FLOAT]: { @@ -691,7 +693,7 @@ class Framebuffer { if (this.useStencil) { if (this.depthFormat === constants.FLOAT) { type = gl.FLOAT_32_UNSIGNED_INT_24_8_REV; - } else if (this.target.webglVersion === constants.WEBGL2) { + } else if (this.renderer.webglVersion === constants.WEBGL2) { type = gl.UNSIGNED_INT_24_8; } else { type = gl.getExtension('WEBGL_depth_texture').UNSIGNED_INT_24_8_WEBGL; @@ -713,12 +715,12 @@ class Framebuffer { if (this.useStencil) { if (this.depthFormat === constants.FLOAT) { internalFormat = gl.DEPTH32F_STENCIL8; - } else if (this.target.webglVersion === constants.WEBGL2) { + } else if (this.renderer.webglVersion === constants.WEBGL2) { internalFormat = gl.DEPTH24_STENCIL8; } else { internalFormat = gl.DEPTH_STENCIL; } - } else if (this.target.webglVersion === constants.WEBGL2) { + } else if (this.renderer.webglVersion === constants.WEBGL2) { if (this.depthFormat === constants.FLOAT) { internalFormat = gl.DEPTH_COMPONENT32F; } else { @@ -739,9 +741,9 @@ class Framebuffer { */ _updateSize() { if (this._autoSized) { - this.width = this.target._renderer.width; - this.height = this.target._renderer.height; - this.density = this.target.pixelDensity(); + this.width = this.renderer.width; + this.height = this.renderer.height; + this.density = this.renderer._pixelDensity; } } @@ -902,7 +904,7 @@ class Framebuffer { const cam = new FramebufferCamera(this); cam._computeCameraDefaultSettings(); cam._setDefaultCamera(); - this.target._renderer.states.curCamera = cam; + this.renderer.states.curCamera = cam; return cam; } @@ -917,7 +919,7 @@ class Framebuffer { const gl = this.gl; gl.deleteTexture(texture.rawTexture()); - this.target._renderer.textures.delete(texture); + this.renderer.textures.delete(texture); } /** @@ -1002,7 +1004,7 @@ class Framebuffer { if (this.colorRenderbuffer) { gl.deleteRenderbuffer(this.colorRenderbuffer); } - this.target._renderer.framebuffers.delete(this); + this.renderer.framebuffers.delete(this); } /** @@ -1053,27 +1055,27 @@ class Framebuffer { * */ begin() { - this.prevFramebuffer = this.target._renderer.activeFramebuffer(); + this.prevFramebuffer = this.renderer.activeFramebuffer(); if (this.prevFramebuffer) { this.prevFramebuffer._beforeEnd(); } - this.target._renderer.activeFramebuffers.push(this); + this.renderer.activeFramebuffers.push(this); this._beforeBegin(); - this.target._renderer.push(); + this.renderer.push(); // Apply the framebuffer's camera. This does almost what // RendererGL.reset() does, but this does not try to clear any buffers; // it only sets the camera. - // this.target._renderer.setCamera(this.defaultCamera); - this.target._renderer.states.curCamera = this.defaultCamera; + // this.renderer.setCamera(this.defaultCamera); + this.renderer.states.curCamera = this.defaultCamera; // set the projection matrix (which is not normally updated each frame) - this.target._renderer.states.uPMatrix.set(this.defaultCamera.projMatrix); - this.target._renderer.states.uViewMatrix.set(this.defaultCamera.cameraMatrix); - - this.target._renderer.resetMatrix(); - this.target._renderer.states.uViewMatrix - .set(this.target._renderer.states.curCamera.cameraMatrix); - this.target._renderer.states.uModelMatrix.reset(); - this.target._renderer._applyStencilTestIfClipping(); + this.renderer.states.uPMatrix.set(this.defaultCamera.projMatrix); + this.renderer.states.uViewMatrix.set(this.defaultCamera.cameraMatrix); + + this.renderer.resetMatrix(); + this.renderer.states.uViewMatrix + .set(this.renderer.states.curCamera.cameraMatrix); + this.renderer.states.uModelMatrix.reset(); + this.renderer._applyStencilTestIfClipping(); } /** @@ -1104,7 +1106,7 @@ class Framebuffer { _beforeBegin() { const gl = this.gl; gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebufferToBind()); - this.target._renderer.viewport( + this.renderer.viewport( this.width * this.density, this.height * this.density ); @@ -1190,8 +1192,8 @@ class Framebuffer { */ end() { const gl = this.gl; - this.target._renderer.pop(); - const fbo = this.target._renderer.activeFramebuffers.pop(); + this.renderer.pop(); + const fbo = this.renderer.activeFramebuffers.pop(); if (fbo !== this) { throw new Error("It looks like you've called end() while another Framebuffer is active."); } @@ -1200,12 +1202,12 @@ class Framebuffer { this.prevFramebuffer._beforeBegin(); } else { gl.bindFramebuffer(gl.FRAMEBUFFER, null); - this.target._renderer.viewport( - this.target._renderer._origViewport.width, - this.target._renderer._origViewport.height + this.renderer.viewport( + this.renderer._origViewport.width, + this.renderer._origViewport.height ); } - this.target._renderer._applyStencilTestIfClipping(); + this.renderer._applyStencilTestIfClipping(); } /** @@ -1318,7 +1320,7 @@ class Framebuffer { */ loadPixels() { const gl = this.gl; - const prevFramebuffer = this.target._renderer.activeFramebuffer(); + const prevFramebuffer = this.renderer.activeFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); const colorFormat = this._glColorFormat(); this.pixels = readPixelsWebGL( @@ -1375,7 +1377,7 @@ class Framebuffer { * @return {Number[]} color of the pixel at `(x, y)` as an array of color values `[R, G, B, A]`. */ get(x, y, w, h) { - p5._validateParameters('p5.Framebuffer.get', arguments); + // p5._validateParameters('p5.Framebuffer.get', arguments); const colorFormat = this._glColorFormat(); if (x === undefined && y === undefined) { x = 0; @@ -1387,8 +1389,8 @@ class Framebuffer { console.warn( 'The x and y values passed to p5.Framebuffer.get are outside of its range and will be clamped.' ); - x = this.target.constrain(x, 0, this.width - 1); - y = this.target.constrain(y, 0, this.height - 1); + x = constrain(x, 0, this.width - 1); + y = constrain(y, 0, this.height - 1); } return readPixelWebGL( @@ -1401,10 +1403,10 @@ class Framebuffer { ); } - x = this.target.constrain(x, 0, this.width - 1); - y = this.target.constrain(y, 0, this.height - 1); - w = this.target.constrain(w, 1, this.width - x); - h = this.target.constrain(h, 1, this.height - y); + x = constrain(x, 0, this.width - 1); + y = constrain(y, 0, this.height - 1); + w = constrain(w, 1, this.width - x); + h = constrain(h, 1, this.height - y); const rawData = readPixelsWebGL( undefined, @@ -1542,7 +1544,7 @@ class Framebuffer { ); this.colorP5Texture.unbindTexture(); - const prevFramebuffer = this.target._renderer.activeFramebuffer(); + const prevFramebuffer = this.renderer.activeFramebuffer(); if (this.antialias) { // We need to make sure the antialiased framebuffer also has the updated // pixels so that if more is drawn to it, it goes on top of the updated @@ -1552,13 +1554,13 @@ class Framebuffer { // to use image() to put the framebuffer texture onto the antialiased // framebuffer. this.begin(); - this.target._renderer.push(); - this.target._renderer.imageMode(this.target.CENTER); - this.target._renderer.resetMatrix(); - this.target._renderer.states.doStroke = false; - this.target._renderer.clear(); - this.target._renderer.image(this, 0, 0); - this.target._renderer.pop(); + this.renderer.push(); + this.renderer.imageMode(constants.CENTER); + this.renderer.resetMatrix(); + this.renderer.states.doStroke = false; + this.renderer.clear(); + this.renderer.image(this, 0, 0); + this.renderer.pop(); if (this.useDepth) { gl.clearDepth(1); gl.clear(gl.DEPTH_BUFFER_BIT); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 8486e05039..0148b74295 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -696,9 +696,10 @@ class RendererGL extends Renderer { * @alt * black canvas with purple cube spinning */ - fill(v1, v2, v3, a) { + fill(...args) { //see material.js for more info on color blending in webgl - const color = fn.color.apply(this._pInst, arguments); + // const color = fn.color.apply(this._pInst, arguments); + const color = this._pInst.color(...args); this.states.curFillColor = color._array; this.states.drawMode = constants.FILL; this.states._useNormalMaterial = false; @@ -734,8 +735,9 @@ class RendererGL extends Renderer { * @alt * black canvas with purple cube with pink outline spinning */ - stroke(r, g, b, a) { - const color = fn.color.apply(this._pInst, arguments); + stroke(...args) { + // const color = fn.color.apply(this._pInst, arguments); + const color = this._pInst.color(...args); this.states.curStrokeColor = color._array; } @@ -748,13 +750,15 @@ class RendererGL extends Renderer { } getFilterLayer() { if (!this.filterLayer) { - this.filterLayer = this._pInst.createFramebuffer(); + // this.filterLayer = this._pInst.createFramebuffer(); + this.filterLayer = new Framebuffer(this); } return this.filterLayer; } getFilterLayerTemp() { if (!this.filterLayerTemp) { - this.filterLayerTemp = this._pInst.createFramebuffer(); + // this.filterLayerTemp = this._pInst.createFramebuffer(); + this.filterLayerTemp = new Framebuffer(this); } return this.filterLayerTemp; } @@ -1136,7 +1140,13 @@ class RendererGL extends Renderer { */ _getTempFramebuffer() { if (!this._tempFramebuffer) { - this._tempFramebuffer = this._pInst.createFramebuffer({ + // this._tempFramebuffer = this._pInst.createFramebuffer({ + // format: constants.UNSIGNED_BYTE, + // useDepth: this._pInst._glAttributes.depth, + // depthFormat: constants.UNSIGNED_INT, + // antialias: this._pInst._glAttributes.antialias + // }); + this._tempFramebuffer = new Framebuffer(this, { format: constants.UNSIGNED_BYTE, useDepth: this._pInst._glAttributes.depth, depthFormat: constants.UNSIGNED_INT, @@ -1782,9 +1792,12 @@ class RendererGL extends Renderer { let smallWidth = 200; let width = smallWidth; let height = Math.floor(smallWidth * (input.height / input.width)); - newFramebuffer = this._pInst.createFramebuffer({ + // newFramebuffer = this._pInst.createFramebuffer({ + // width, height, density: 1 + // }); + newFramebuffer = new Framebuffer(this, { width, height, density: 1 - }); + }) // create framebuffer is like making a new sketch, all functions on main // sketch it would be available on framebuffer if (!this.states.diffusedShader) { @@ -1824,7 +1837,10 @@ class RendererGL extends Renderer { const size = 512; let tex; const levels = []; - const framebuffer = this._pInst.createFramebuffer({ + // const framebuffer = this._pInst.createFramebuffer({ + // width: size, height: size, density: 1 + // }); + const framebuffer = new Framebuffer(this, { width: size, height: size, density: 1 }); let count = Math.log(size) / Math.log(2); @@ -1871,7 +1887,7 @@ class RendererGL extends Renderer { } createFramebuffer(options) { - return new p5.Framebuffer(this, options); + return new Framebuffer(this, options); } _setStrokeUniforms(baseStrokeShader) { From 90bcbc0624035cb36167898f5e733951b0598ab0 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Mon, 21 Oct 2024 20:15:13 +0100 Subject: [PATCH 28/55] Duplicate pixels implementation in p5.Image from p5.Renderer2D This is necessary to avoid cyclic imports causing ReferenceError. Extrating implementation into own exported properties can help reduce code duplicated in the future. --- src/core/p5.Renderer.js | 3 +- src/core/p5.Renderer2D.js | 12 +- src/image/p5.Image.js | 256 ++++++++++++++++++++++++++++++-------- 3 files changed, 209 insertions(+), 62 deletions(-) diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index a44b07f1fe..4002ddcf6e 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -5,6 +5,7 @@ */ import * as constants from '../core/constants'; +import { Image } from '../image/p5.Image'; class Renderer { constructor(pInst, w, h, isMainCanvas) { @@ -140,7 +141,7 @@ class Renderer { // get(x,y,w,h) } - const region = new p5.Image(w*pd, h*pd); + const region = new Image(w*pd, h*pd); region.pixelDensity(pd); region.canvas .getContext('2d') diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 1b7433f5ec..4c67168500 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -646,12 +646,12 @@ class Renderer2D extends Renderer { } /* - * This function requires that: - * - * 0 <= start < TWO_PI - * - * start <= stop < start + TWO_PI - */ + * This function requires that: + * + * 0 <= start < TWO_PI + * + * start <= stop < start + TWO_PI + */ arc(x, y, w, h, start, stop, mode) { const ctx = this.drawingContext; const rx = w / 2.0; diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 3649bfe0c5..3cb428313c 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -11,7 +11,7 @@ * drawing images to the main display canvas. */ import Filters from './filters'; -import { Renderer2D } from '../core/p5.Renderer2D'; +import { Renderer } from '../core/p5.Renderer'; class Image { constructor(width, height) { @@ -31,18 +31,18 @@ class Image { } /** - * Gets or sets the pixel density for high pixel density displays. - * - * By default, the density will be set to 1. - * - * Call this method with no arguments to get the default density, or pass - * in a number to set the density. If a non-positive number is provided, - * it defaults to 1. - * - * @param {Number} [density] A scaling factor for the number of pixels per - * side - * @returns {Number} The current density if called without arguments, or the instance for chaining if setting density. - */ + * Gets or sets the pixel density for high pixel density displays. + * + * By default, the density will be set to 1. + * + * Call this method with no arguments to get the default density, or pass + * in a number to set the density. If a non-positive number is provided, + * it defaults to 1. + * + * @param {Number} [density] A scaling factor for the number of pixels per + * side + * @returns {Number} The current density if called without arguments, or the instance for chaining if setting density. + */ pixelDensity(density) { if (typeof density !== 'undefined') { // Setter: set the density and handle resize @@ -53,7 +53,7 @@ class Image { position: 1 }; - p5._friendlyParamError(errorObj, 'pixelDensity'); + // p5._friendlyParamError(errorObj, 'pixelDensity'); // Default to 1 in case of an invalid value density = 1; @@ -143,42 +143,51 @@ class Image { * * *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Image object. - * let img = createImage(66, 66); - * - * // Load the image's pixels. - * img.loadPixels(); - * - * for (let i = 0; i < img.pixels.length; i += 4) { - * // Red. - * img.pixels[i] = 0; - * // Green. - * img.pixels[i + 1] = 0; - * // Blue. - * img.pixels[i + 2] = 0; - * // Alpha. - * img.pixels[i + 3] = 255; - * } - * - * // Update the image. - * img.updatePixels(); - * - * // Display the image. - * image(img, 17, 17); - * - * describe('A black square drawn in the middle of a gray square.'); - * } - * - *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Image object. + * let img = createImage(66, 66); + * + * // Load the image's pixels. + * img.loadPixels(); + * + * for (let i = 0; i < img.pixels.length; i += 4) { + * // Red. + * img.pixels[i] = 0; + * // Green. + * img.pixels[i + 1] = 0; + * // Blue. + * img.pixels[i + 2] = 0; + * // Alpha. + * img.pixels[i + 3] = 255; + * } + * + * // Update the image. + * img.updatePixels(); + * + * // Display the image. + * image(img, 17, 17); + * + * describe('A black square drawn in the middle of a gray square.'); + * } + * + * */ loadPixels() { - Renderer2D.prototype.loadPixels.call(this); + // Renderer2D.prototype.loadPixels.call(this); + const pixelsState = this._pixelsState; + const pd = this._pixelDensity; + const w = this.width * pd; + const h = this.height * pd; + const imageData = this.drawingContext.getImageData(0, 0, w, h); + // @todo this should actually set pixels per object, so diff buffers can + // have diff pixel arrays. + pixelsState.imageData = imageData; + this.pixels = pixelsState.pixels = imageData.data; this.setModified(true); } @@ -277,7 +286,31 @@ class Image { /** */ updatePixels(x, y, w, h) { - Renderer2D.prototype.updatePixels.call(this, x, y, w, h); + // Renderer2D.prototype.updatePixels.call(this, x, y, w, h); + const pixelsState = this._pixelsState; + const pd = this._pixelDensity; + if ( + x === undefined && + y === undefined && + w === undefined && + h === undefined + ) { + x = 0; + y = 0; + w = this.width; + h = this.height; + } + x *= pd; + y *= pd; + w *= pd; + h *= pd; + + if (this.gifProperties) { + this.gifProperties.frames[this.gifProperties.displayIndex].image = + pixelsState.imageData; + } + + this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); this.setModified(true); } @@ -404,12 +437,52 @@ class Image { * @return {Number[]} color of the pixel at (x, y) in array format `[R, G, B, A]`. */ get(x, y, w, h) { - p5._validateParameters('p5.Image.get', arguments); - return Renderer2D.prototype.get.apply(this, arguments); + // p5._validateParameters('p5.Image.get', arguments); + // return Renderer2D.prototype.get.apply(this, arguments); + const pixelsState = this._pixelsState; + const pd = this._pixelDensity; + const canvas = this.canvas; + + if (typeof x === 'undefined' && typeof y === 'undefined') { + // get() + x = y = 0; + w = pixelsState.width; + h = pixelsState.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; } _getPixel(...args) { - return Renderer2D.prototype._getPixel.apply(this, args); + let imageData, index; + imageData = this.drawingContext.getImageData(x, y, 1, 1).data; + index = 0; + return [ + imageData[index + 0], + imageData[index + 1], + imageData[index + 2], + imageData[index + 3] + ]; + // return Renderer2D.prototype._getPixel.apply(this, args); } /** @@ -547,7 +620,80 @@ class Image { * */ set(x, y, imgOrCol) { - Renderer2D.prototype.set.call(this, x, y, imgOrCol); + // Renderer2D.prototype.set.call(this, x, y, imgOrCol); + // round down to get integer numbers + x = Math.floor(x); + y = Math.floor(y); + const pixelsState = this._pixelsState; + if (imgOrCol instanceof Image) { + this.drawingContext.save(); + this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); + this.drawingContext.scale( + this._pixelDensity, + this._pixelDensity + ); + this.drawingContext.clearRect(x, y, imgOrCol.width, imgOrCol.height); + this.drawingContext.drawImage(imgOrCol.canvas, x, y); + this.drawingContext.restore(); + } else { + let r = 0, + g = 0, + b = 0, + a = 0; + let idx = + 4 * + (y * + this._pixelDensity * + (this.width * this._pixelDensity) + + x * this._pixelDensity); + if (!pixelsState.imageData) { + pixelsState.loadPixels(); + } + if (typeof imgOrCol === 'number') { + if (idx < pixelsState.pixels.length) { + r = imgOrCol; + g = imgOrCol; + b = imgOrCol; + a = 255; + //this.updatePixels.call(this); + } + } else if (Array.isArray(imgOrCol)) { + if (imgOrCol.length < 4) { + throw new Error('pixel array must be of the form [R, G, B, A]'); + } + if (idx < pixelsState.pixels.length) { + r = imgOrCol[0]; + g = imgOrCol[1]; + b = imgOrCol[2]; + a = imgOrCol[3]; + //this.updatePixels.call(this); + } + } else if (imgOrCol instanceof p5.Color) { + if (idx < pixelsState.pixels.length) { + r = imgOrCol.levels[0]; + g = imgOrCol.levels[1]; + b = imgOrCol.levels[2]; + a = imgOrCol.levels[3]; + //this.updatePixels.call(this); + } + } + // loop over pixelDensity * pixelDensity + for (let i = 0; i < this._pixelDensity; i++) { + for (let j = 0; j < this._pixelDensity; j++) { + // loop over + idx = + 4 * + ((y * this._pixelDensity + j) * + this.width * + this._pixelDensity + + (x * this._pixelDensity + i)); + pixelsState.pixels[idx] = r; + pixelsState.pixels[idx + 1] = g; + pixelsState.pixels[idx + 2] = b; + pixelsState.pixels[idx + 3] = a; + } + } + } this.setModified(true); } @@ -865,7 +1011,7 @@ class Image { let imgScaleFactor = this._pixelDensity; let maskScaleFactor = 1; - if (p5Image instanceof p5.Renderer) { + if (p5Image instanceof Renderer) { maskScaleFactor = p5Image._pInst._renderer._pixelDensity; } @@ -1276,7 +1422,7 @@ class Image { * @param {(BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|NORMAL)} blendMode */ blend(...args) { - p5._validateParameters('p5.Image.blend', arguments); + // p5._validateParameters('p5.Image.blend', arguments); fn.blend.apply(this, args); this.setModified(true); } From 59e5cb1ed0c6f00c1ed2f0f9131e067f76f78119 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 22 Oct 2024 16:17:24 -0400 Subject: [PATCH 29/55] Unindent to try to reduce merge conflicts --- src/webgl/p5.RendererGL.Immediate.js | 1186 ++++++------ src/webgl/p5.RendererGL.Retained.js | 542 +++--- src/webgl/p5.Shader.js | 2674 +++++++++++++------------- 3 files changed, 2201 insertions(+), 2201 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index f254881fef..612152652e 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -17,648 +17,648 @@ import { Vector } from '../math/p5.Vector'; import { RenderBuffer } from './p5.RenderBuffer'; function rendererGLImmediate(p5, fn){ - /** - * Begin shape drawing. This is a helpful way of generating - * custom shapes quickly. However in WEBGL mode, application - * performance will likely drop as a result of too many calls to - * beginShape() / endShape(). As a high performance alternative, - * please use p5.js geometry primitives. - * @private - * @method beginShape - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, - * and TESS(WEBGL only) - * @chainable - */ - RendererGL.prototype.beginShape = function(mode) { - this.immediateMode.shapeMode = - mode !== undefined ? mode : constants.TESS; - if (this._useUserVertexProperties === true){ - this._resetUserVertexProperties(); - } - this.immediateMode.geometry.reset(); - this.immediateMode.contourIndices = []; - return this; - }; - - RendererGL.prototype.immediateBufferStrides = { - vertices: 1, - vertexNormals: 1, - vertexColors: 4, - vertexStrokeColors: 4, - uvs: 2 - }; - - RendererGL.prototype.beginContour = function() { - if (this.immediateMode.shapeMode !== constants.TESS) { - throw new Error('WebGL mode can only use contours with beginShape(TESS).'); - } - this.immediateMode.contourIndices.push( - this.immediateMode.geometry.vertices.length - ); - }; - - /** - * adds a vertex to be drawn in a custom Shape. - * @private - * @method vertex - * @param {Number} x x-coordinate of vertex - * @param {Number} y y-coordinate of vertex - * @param {Number} z z-coordinate of vertex - * @chainable - * @TODO implement handling of p5.Vector args - */ - RendererGL.prototype.vertex = function(x, y) { - // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn - // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra - // work to convert QUAD_STRIP here, since the only difference is in how edges - // are rendered.) - if (this.immediateMode.shapeMode === constants.QUADS) { - // A finished quad turned into triangles should leave 6 vertices in the - // buffer: - // 0--3 0 3--5 - // | | --> | \ \ | - // 1--2 1--2 4 - // When vertex index 3 is being added, add the necessary duplicates. - if (this.immediateMode.geometry.vertices.length % 6 === 3) { - for (const key in this.immediateBufferStrides) { - const stride = this.immediateBufferStrides[key]; - const buffer = this.immediateMode.geometry[key]; - buffer.push( - ...buffer.slice( - buffer.length - 3 * stride, - buffer.length - 2 * stride - ), - ...buffer.slice(buffer.length - stride, buffer.length) - ); - } - } - } - - 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]; - } - const vert = new Vector(x, y, z); - this.immediateMode.geometry.vertices.push(vert); - this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); +/** + * Begin shape drawing. This is a helpful way of generating + * custom shapes quickly. However in WEBGL mode, application + * performance will likely drop as a result of too many calls to + * beginShape() / endShape(). As a high performance alternative, + * please use p5.js geometry primitives. + * @private + * @method beginShape + * @param {Number} mode webgl primitives mode. beginShape supports the + * following modes: + * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, + * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, + * and TESS(WEBGL only) + * @chainable + */ +RendererGL.prototype.beginShape = function(mode) { + this.immediateMode.shapeMode = + mode !== undefined ? mode : constants.TESS; + if (this._useUserVertexProperties === true){ + this._resetUserVertexProperties(); + } + this.immediateMode.geometry.reset(); + this.immediateMode.contourIndices = []; + return this; +}; + +RendererGL.prototype.immediateBufferStrides = { + vertices: 1, + vertexNormals: 1, + vertexColors: 4, + vertexStrokeColors: 4, + uvs: 2 +}; + +RendererGL.prototype.beginContour = function() { + if (this.immediateMode.shapeMode !== constants.TESS) { + throw new Error('WebGL mode can only use contours with beginShape(TESS).'); + } + this.immediateMode.contourIndices.push( + this.immediateMode.geometry.vertices.length + ); +}; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const geom = this.immediateMode.geometry; - const prop = geom.userVertexProperties[propName]; - const verts = geom.vertices; - if (prop.getSrcArray().length === 0 && verts.length > 1) { - const numMissingValues = prop.getDataSize() * (verts.length - 1); - const missingValues = Array(numMissingValues).fill(0); - prop.pushDirect(missingValues); +/** + * adds a vertex to be drawn in a custom Shape. + * @private + * @method vertex + * @param {Number} x x-coordinate of vertex + * @param {Number} y y-coordinate of vertex + * @param {Number} z z-coordinate of vertex + * @chainable + * @TODO implement handling of p5.Vector args + */ +RendererGL.prototype.vertex = function(x, y) { + // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn + // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra + // work to convert QUAD_STRIP here, since the only difference is in how edges + // are rendered.) + if (this.immediateMode.shapeMode === constants.QUADS) { + // A finished quad turned into triangles should leave 6 vertices in the + // buffer: + // 0--3 0 3--5 + // | | --> | \ \ | + // 1--2 1--2 4 + // When vertex index 3 is being added, add the necessary duplicates. + if (this.immediateMode.geometry.vertices.length % 6 === 3) { + for (const key in this.immediateBufferStrides) { + const stride = this.immediateBufferStrides[key]; + const buffer = this.immediateMode.geometry[key]; + buffer.push( + ...buffer.slice( + buffer.length - 3 * stride, + buffer.length - 2 * stride + ), + ...buffer.slice(buffer.length - stride, buffer.length) + ); } - prop.pushCurrentData(); } - - const vertexColor = this.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; - this.immediateMode.geometry.vertexColors.push( - vertexColor[0], - vertexColor[1], - vertexColor[2], - vertexColor[3] - ); - const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; - this.immediateMode.geometry.vertexStrokeColors.push( - lineVertexColor[0], - lineVertexColor[1], - lineVertexColor[2], - lineVertexColor[3] - ); - - if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { - if (this.states._tex !== null) { - if (this.states._tex.width > 0 && this.states._tex.height > 0) { - u /= this.states._tex.width; - v /= this.states._tex.height; - } - } else if ( - this.states.userFillShader !== undefined || - this.states.userStrokeShader !== undefined || - this.states.userPointShader !== undefined - ) { - // Do nothing if user-defined shaders are present - } else if ( - this.states._tex === null && - arguments.length >= 4 - ) { - // Only throw this warning if custom uv's have been provided - console.warn( - 'You must first call texture() before using' + - ' vertex() with image based u and v coordinates' - ); + } + + 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]; + } + const vert = new Vector(x, y, z); + this.immediateMode.geometry.vertices.push(vert); + this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); + + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const geom = this.immediateMode.geometry; + const prop = geom.userVertexProperties[propName]; + const verts = geom.vertices; + if (prop.getSrcArray().length === 0 && verts.length > 1) { + const numMissingValues = prop.getDataSize() * (verts.length - 1); + const missingValues = Array(numMissingValues).fill(0); + prop.pushDirect(missingValues); + } + prop.pushCurrentData(); + } + + const vertexColor = this.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; + this.immediateMode.geometry.vertexColors.push( + vertexColor[0], + vertexColor[1], + vertexColor[2], + vertexColor[3] + ); + const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; + this.immediateMode.geometry.vertexStrokeColors.push( + lineVertexColor[0], + lineVertexColor[1], + lineVertexColor[2], + lineVertexColor[3] + ); + + if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { + if (this.states._tex !== null) { + if (this.states._tex.width > 0 && this.states._tex.height > 0) { + u /= this.states._tex.width; + v /= this.states._tex.height; } + } else if ( + this.states.userFillShader !== undefined || + this.states.userStrokeShader !== undefined || + this.states.userPointShader !== undefined + ) { + // Do nothing if user-defined shaders are present + } else if ( + this.states._tex === null && + arguments.length >= 4 + ) { + // Only throw this warning if custom uv's have been provided + console.warn( + 'You must first call texture() before using' + + ' vertex() with image based u and v coordinates' + ); } + } - this.immediateMode.geometry.uvs.push(u, v); + this.immediateMode.geometry.uvs.push(u, v); - this.immediateMode._bezierVertex[0] = x; - this.immediateMode._bezierVertex[1] = y; - this.immediateMode._bezierVertex[2] = z; + this.immediateMode._bezierVertex[0] = x; + this.immediateMode._bezierVertex[1] = y; + this.immediateMode._bezierVertex[2] = z; - this.immediateMode._quadraticVertex[0] = x; - this.immediateMode._quadraticVertex[1] = y; - this.immediateMode._quadraticVertex[2] = z; + this.immediateMode._quadraticVertex[0] = x; + this.immediateMode._quadraticVertex[1] = y; + this.immediateMode._quadraticVertex[2] = z; - return this; - }; + return this; +}; - RendererGL.prototype.vertexProperty = function(propertyName, data){ - if(!this._useUserVertexProperties){ - this._useUserVertexProperties = true; - this.immediateMode.geometry.userVertexProperties = {}; - } - const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; - let prop; - if (propertyExists){ - prop = this.immediateMode.geometry.userVertexProperties[propertyName]; - } - else { - prop = this.immediateMode.geometry._userVertexPropertyHelper(propertyName, data); - this.tessyVertexSize += prop.getDataSize(); - this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); - this.immediateMode.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) - ); - } - prop.setCurrentData(data); - }; - - RendererGL.prototype._resetUserVertexProperties = function(){ - const properties = this.immediateMode.geometry.userVertexProperties; - for (const propName in properties){ - const prop = properties[propName]; - delete this.immediateBufferStrides[propName]; - prop.delete(); - } - this._useUserVertexProperties = false; - this.tessyVertexSize = 12; +RendererGL.prototype.vertexProperty = function(propertyName, data){ + if(!this._useUserVertexProperties){ + this._useUserVertexProperties = true; this.immediateMode.geometry.userVertexProperties = {}; - this.immediateMode.buffers.user = []; - }; - - /** - * Sets the normal to use for subsequent vertices. - * @private - * @method normal - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @chainable - * - * @method normal - * @param {Vector} v - * @chainable - */ - RendererGL.prototype.normal = function(xorv, y, z) { - if (xorv instanceof Vector) { - this.states._currentNormal = xorv; - } else { - this.states._currentNormal = new Vector(xorv, y, z); - } + } + const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; + let prop; + if (propertyExists){ + prop = this.immediateMode.geometry.userVertexProperties[propertyName]; + } + else { + prop = this.immediateMode.geometry._userVertexPropertyHelper(propertyName, data); + this.tessyVertexSize += prop.getDataSize(); + this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); + this.immediateMode.buffers.user.push( + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) + ); + } + prop.setCurrentData(data); +}; + +RendererGL.prototype._resetUserVertexProperties = function(){ + const properties = this.immediateMode.geometry.userVertexProperties; + for (const propName in properties){ + const prop = properties[propName]; + delete this.immediateBufferStrides[propName]; + prop.delete(); + } + this._useUserVertexProperties = false; + this.tessyVertexSize = 12; + this.immediateMode.geometry.userVertexProperties = {}; + this.immediateMode.buffers.user = []; +}; + +/** + * Sets the normal to use for subsequent vertices. + * @private + * @method normal + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @chainable + * + * @method normal + * @param {Vector} v + * @chainable + */ +RendererGL.prototype.normal = function(xorv, y, z) { + if (xorv instanceof Vector) { + this.states._currentNormal = xorv; + } else { + this.states._currentNormal = new Vector(xorv, y, z); + } + + return this; +}; +/** + * End shape drawing and render vertices to screen. + * @chainable + */ +RendererGL.prototype.endShape = function( + mode, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind, + count = 1 +) { + if (this.immediateMode.shapeMode === constants.POINTS) { + this._drawPoints( + this.immediateMode.geometry.vertices, + this.immediateMode.buffers.point + ); return this; - }; - - /** - * End shape drawing and render vertices to screen. - * @chainable - */ - RendererGL.prototype.endShape = function( - mode, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind, - count = 1 + } + // When we are drawing a shape then the shape mode is TESS, + // but in case of triangle we can skip the breaking into small triangle + // this can optimize performance by skipping the step of breaking it into triangles + if (this.immediateMode.geometry.vertices.length === 3 && + this.immediateMode.shapeMode === constants.TESS ) { - if (this.immediateMode.shapeMode === constants.POINTS) { - this._drawPoints( - this.immediateMode.geometry.vertices, - this.immediateMode.buffers.point - ); - return this; - } - // When we are drawing a shape then the shape mode is TESS, - // but in case of triangle we can skip the breaking into small triangle - // this can optimize performance by skipping the step of breaking it into triangles - if (this.immediateMode.geometry.vertices.length === 3 && - this.immediateMode.shapeMode === constants.TESS + this.immediateMode.shapeMode === constants.TRIANGLES; + } + + this.isProcessingVertices = true; + this._processVertices(...arguments); + this.isProcessingVertices = false; + + // LINE_STRIP and LINES are not used for rendering, instead + // they only indicate a way to modify vertices during the _processVertices() step + let is_line = false; + if ( + this.immediateMode.shapeMode === constants.LINE_STRIP || + this.immediateMode.shapeMode === constants.LINES + ) { + this.immediateMode.shapeMode = constants.TRIANGLE_FAN; + is_line = true; + } + + // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we + // need to convert them to a supported format. In `vertex()`, we reformat + // the input data into the formats specified below. + if (this.immediateMode.shapeMode === constants.QUADS) { + this.immediateMode.shapeMode = constants.TRIANGLES; + } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { + this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; + } + + if (this.states.doFill && !is_line) { + if ( + !this.geometryBuilder && + this.immediateMode.geometry.vertices.length >= 3 ) { - this.immediateMode.shapeMode === constants.TRIANGLES; + this._drawImmediateFill(count); } - - this.isProcessingVertices = true; - this._processVertices(...arguments); - this.isProcessingVertices = false; - - // LINE_STRIP and LINES are not used for rendering, instead - // they only indicate a way to modify vertices during the _processVertices() step - let is_line = false; + } + if (this.states.doStroke) { if ( - this.immediateMode.shapeMode === constants.LINE_STRIP || - this.immediateMode.shapeMode === constants.LINES + !this.geometryBuilder && + this.immediateMode.geometry.lineVertices.length >= 1 ) { - this.immediateMode.shapeMode = constants.TRIANGLE_FAN; - is_line = true; + this._drawImmediateStroke(); } + } - // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we - // need to convert them to a supported format. In `vertex()`, we reformat - // the input data into the formats specified below. - if (this.immediateMode.shapeMode === constants.QUADS) { - this.immediateMode.shapeMode = constants.TRIANGLES; - } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { - this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; - } + if (this.geometryBuilder) { + this.geometryBuilder.addImmediate(); + } - if (this.states.doFill && !is_line) { - if ( - !this.geometryBuilder && - this.immediateMode.geometry.vertices.length >= 3 - ) { - this._drawImmediateFill(count); - } - } - if (this.states.doStroke) { - if ( - !this.geometryBuilder && - this.immediateMode.geometry.lineVertices.length >= 1 - ) { - this._drawImmediateStroke(); - } - } + this.isBezier = false; + this.isQuadratic = false; + this.isCurve = false; + this.immediateMode._bezierVertex.length = 0; + this.immediateMode._quadraticVertex.length = 0; + this.immediateMode._curveVertex.length = 0; - if (this.geometryBuilder) { - this.geometryBuilder.addImmediate(); - } + return this; +}; - this.isBezier = false; - this.isQuadratic = false; - this.isCurve = false; - this.immediateMode._bezierVertex.length = 0; - this.immediateMode._quadraticVertex.length = 0; - this.immediateMode._curveVertex.length = 0; +/** + * Called from endShape(). This function calculates the stroke vertices for custom shapes and + * tesselates shapes when applicable. + * @private + * @param {Number} mode webgl primitives mode. beginShape supports the + * following modes: + * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, + * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) + */ +RendererGL.prototype._processVertices = function(mode) { + if (this.immediateMode.geometry.vertices.length === 0) return; + + const calculateStroke = this.states.doStroke; + const shouldClose = mode === constants.CLOSE; + if (calculateStroke) { + this.immediateMode.geometry.edges = this._calculateEdges( + this.immediateMode.shapeMode, + this.immediateMode.geometry.vertices, + shouldClose + ); + if (!this.geometryBuilder) { + this.immediateMode.geometry._edgesToVertices(); + } + } + // For hollow shapes, user must set mode to TESS + const convexShape = this.immediateMode.shapeMode === constants.TESS; + // If the shape has a contour, we have to re-triangulate to cut out the + // contour region + const hasContour = this.immediateMode.contourIndices.length > 0; + // We tesselate when drawing curves or convex shapes + const shouldTess = + this.states.doFill && + ( + this.isBezier || + this.isQuadratic || + this.isCurve || + convexShape || + hasContour + ) && + this.immediateMode.shapeMode !== constants.LINES; + + if (shouldTess) { + this._tesselateShape(); + } +}; - return this; - }; - - /** - * Called from endShape(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) - */ - RendererGL.prototype._processVertices = function(mode) { - if (this.immediateMode.geometry.vertices.length === 0) return; - - const calculateStroke = this.states.doStroke; - const shouldClose = mode === constants.CLOSE; - if (calculateStroke) { - this.immediateMode.geometry.edges = this._calculateEdges( - this.immediateMode.shapeMode, - this.immediateMode.geometry.vertices, - shouldClose - ); - if (!this.geometryBuilder) { - this.immediateMode.geometry._edgesToVertices(); +/** + * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and + * tesselates shapes when applicable. + * @private + * @returns {Number[]} indices for custom shape vertices indicating edges. + */ +RendererGL.prototype._calculateEdges = function( + shapeMode, + verts, + shouldClose +) { + const res = []; + let i = 0; + const contourIndices = this.immediateMode.contourIndices.slice(); + let contourStart = 0; + switch (shapeMode) { + case constants.TRIANGLE_STRIP: + for (i = 0; i < verts.length - 2; i++) { + res.push([i, i + 1]); + res.push([i, i + 2]); } - } - // For hollow shapes, user must set mode to TESS - const convexShape = this.immediateMode.shapeMode === constants.TESS; - // If the shape has a contour, we have to re-triangulate to cut out the - // contour region - const hasContour = this.immediateMode.contourIndices.length > 0; - // We tesselate when drawing curves or convex shapes - const shouldTess = - this.states.doFill && - ( - this.isBezier || - this.isQuadratic || - this.isCurve || - convexShape || - hasContour - ) && - this.immediateMode.shapeMode !== constants.LINES; - - if (shouldTess) { - this._tesselateShape(); - } - }; - - /** - * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @returns {Number[]} indices for custom shape vertices indicating edges. - */ - RendererGL.prototype._calculateEdges = function( - shapeMode, - verts, - shouldClose - ) { - const res = []; - let i = 0; - const contourIndices = this.immediateMode.contourIndices.slice(); - let contourStart = 0; - switch (shapeMode) { - case constants.TRIANGLE_STRIP: - for (i = 0; i < verts.length - 2; i++) { - res.push([i, i + 1]); - res.push([i, i + 2]); - } + res.push([i, i + 1]); + break; + case constants.TRIANGLE_FAN: + for (i = 1; i < verts.length - 1; i++) { + res.push([0, i]); res.push([i, i + 1]); - break; - case constants.TRIANGLE_FAN: - for (i = 1; i < verts.length - 1; i++) { - res.push([0, i]); - res.push([i, i + 1]); - } - res.push([0, verts.length - 1]); - break; - case constants.TRIANGLES: - for (i = 0; i < verts.length - 2; i = i + 3) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 2, i]); - } - break; - case constants.LINES: - for (i = 0; i < verts.length - 1; i = i + 2) { - res.push([i, i + 1]); - } - break; - case constants.QUADS: - // Quads have been broken up into two triangles by `vertex()`: - // 0 3--5 - // | \ \ | - // 1--2 4 - for (i = 0; i < verts.length - 5; i += 6) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 3, i + 5]); - res.push([i + 4, i + 5]); - } - break; - case constants.QUAD_STRIP: - // 0---2---4 - // | | | - // 1---3---5 - for (i = 0; i < verts.length - 2; i += 2) { - res.push([i, i + 1]); - res.push([i, i + 2]); - res.push([i + 1, i + 3]); - } + } + res.push([0, verts.length - 1]); + break; + case constants.TRIANGLES: + for (i = 0; i < verts.length - 2; i = i + 3) { res.push([i, i + 1]); - break; - default: - // TODO: handle contours in other modes too - for (i = 0; i < verts.length; i++) { - // Handle breaks between contours - if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { - res.push([i, i + 1]); - } else { - if (shouldClose || contourStart) { - res.push([i, contourStart]); - } - if (contourIndices.length > 0) { - contourStart = contourIndices.shift(); - } + res.push([i + 1, i + 2]); + res.push([i + 2, i]); + } + break; + case constants.LINES: + for (i = 0; i < verts.length - 1; i = i + 2) { + res.push([i, i + 1]); + } + break; + case constants.QUADS: + // Quads have been broken up into two triangles by `vertex()`: + // 0 3--5 + // | \ \ | + // 1--2 4 + for (i = 0; i < verts.length - 5; i += 6) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 3, i + 5]); + res.push([i + 4, i + 5]); + } + break; + case constants.QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + for (i = 0; i < verts.length - 2; i += 2) { + res.push([i, i + 1]); + res.push([i, i + 2]); + res.push([i + 1, i + 3]); + } + res.push([i, i + 1]); + break; + default: + // TODO: handle contours in other modes too + for (i = 0; i < verts.length; i++) { + // Handle breaks between contours + if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { + res.push([i, i + 1]); + } else { + if (shouldClose || contourStart) { + res.push([i, contourStart]); + } + if (contourIndices.length > 0) { + contourStart = contourIndices.shift(); } } - break; - } - if (shapeMode !== constants.TESS && shouldClose) { - res.push([verts.length - 1, 0]); - } - return res; - }; - - /** - * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. - * @private - */ - RendererGL.prototype._tesselateShape = function() { - // TODO: handle non-TESS shape modes that have contours - this.immediateMode.shapeMode = constants.TRIANGLES; - const contours = [[]]; - for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { - if ( - this.immediateMode.contourIndices.length > 0 && - this.immediateMode.contourIndices[0] === i - ) { - this.immediateMode.contourIndices.shift(); - contours.push([]); - } - contours[contours.length-1].push( - this.immediateMode.geometry.vertices[i].x, - this.immediateMode.geometry.vertices[i].y, - this.immediateMode.geometry.vertices[i].z, - this.immediateMode.geometry.uvs[i * 2], - this.immediateMode.geometry.uvs[i * 2 + 1], - this.immediateMode.geometry.vertexColors[i * 4], - this.immediateMode.geometry.vertexColors[i * 4 + 1], - this.immediateMode.geometry.vertexColors[i * 4 + 2], - this.immediateMode.geometry.vertexColors[i * 4 + 3], - this.immediateMode.geometry.vertexNormals[i].x, - this.immediateMode.geometry.vertexNormals[i].y, - this.immediateMode.geometry.vertexNormals[i].z - ); - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const start = i * prop.getDataSize(); - const end = start + prop.getDataSize(); - const vals = prop.getSrcArray().slice(start, end); - contours[contours.length-1].push(...vals); } - } - const polyTriangles = this._triangulate(contours); - const originalVertices = this.immediateMode.geometry.vertices; - this.immediateMode.geometry.vertices = []; - this.immediateMode.geometry.vertexNormals = []; - this.immediateMode.geometry.uvs = []; + break; + } + if (shapeMode !== constants.TESS && shouldClose) { + res.push([verts.length - 1, 0]); + } + return res; +}; + +/** + * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. + * @private + */ +RendererGL.prototype._tesselateShape = function() { + // TODO: handle non-TESS shape modes that have contours + this.immediateMode.shapeMode = constants.TRIANGLES; + const contours = [[]]; + for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { + if ( + this.immediateMode.contourIndices.length > 0 && + this.immediateMode.contourIndices[0] === i + ) { + this.immediateMode.contourIndices.shift(); + contours.push([]); + } + contours[contours.length-1].push( + this.immediateMode.geometry.vertices[i].x, + this.immediateMode.geometry.vertices[i].y, + this.immediateMode.geometry.vertices[i].z, + this.immediateMode.geometry.uvs[i * 2], + this.immediateMode.geometry.uvs[i * 2 + 1], + this.immediateMode.geometry.vertexColors[i * 4], + this.immediateMode.geometry.vertexColors[i * 4 + 1], + this.immediateMode.geometry.vertexColors[i * 4 + 2], + this.immediateMode.geometry.vertexColors[i * 4 + 3], + this.immediateMode.geometry.vertexNormals[i].x, + this.immediateMode.geometry.vertexNormals[i].y, + this.immediateMode.geometry.vertexNormals[i].z + ); for (const propName in this.immediateMode.geometry.userVertexProperties){ const prop = this.immediateMode.geometry.userVertexProperties[propName]; - prop.resetSrcArray(); - } - const colors = []; - for ( - let j = 0, polyTriLength = polyTriangles.length; - j < polyTriLength; - j = j + this.tessyVertexSize - ) { - colors.push(...polyTriangles.slice(j + 5, j + 9)); - this.normal(...polyTriangles.slice(j + 9, j + 12)); - { - let offset = 12; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - const start = j + offset; - const end = start + size; - prop.setCurrentData(polyTriangles.slice(start, end)); - offset += size; - } + const start = i * prop.getDataSize(); + const end = start + prop.getDataSize(); + const vals = prop.getSrcArray().slice(start, end); + contours[contours.length-1].push(...vals); + } + } + const polyTriangles = this._triangulate(contours); + const originalVertices = this.immediateMode.geometry.vertices; + this.immediateMode.geometry.vertices = []; + this.immediateMode.geometry.vertexNormals = []; + this.immediateMode.geometry.uvs = []; + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const prop = this.immediateMode.geometry.userVertexProperties[propName]; + prop.resetSrcArray(); + } + const colors = []; + for ( + let j = 0, polyTriLength = polyTriangles.length; + j < polyTriLength; + j = j + this.tessyVertexSize + ) { + colors.push(...polyTriangles.slice(j + 5, j + 9)); + this.normal(...polyTriangles.slice(j + 9, j + 12)); + { + let offset = 12; + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const prop = this.immediateMode.geometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + const start = j + offset; + const end = start + size; + prop.setCurrentData(polyTriangles.slice(start, end)); + offset += size; } - this.vertex(...polyTriangles.slice(j, j + 5)); } - if (this.geometryBuilder) { - // Tesselating the face causes the indices of edge vertices to stop being - // correct. When rendering, this is not a problem, since _edgesToVertices - // will have been called before this, and edge vertex indices are no longer - // needed. However, the geometry builder still needs this information, so - // when one is active, we need to update the indices. - // - // We record index mappings in a Map so that once we have found a - // corresponding vertex, we don't need to loop to find it again. - const newIndex = new Map(); - this.immediateMode.geometry.edges = - this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => { - if (!newIndex.has(origIdx)) { - const orig = originalVertices[origIdx]; - let newVertIndex = this.immediateMode.geometry.vertices.findIndex( - v => - orig.x === v.x && - orig.y === v.y && - orig.z === v.z - ); - if (newVertIndex === -1) { - // The tesselation process didn't output a vertex with the exact - // coordinate as before, potentially due to numerical issues. This - // doesn't happen often, but in this case, pick the closest point - let closestDist = Infinity; - let closestIndex = 0; - for ( - let i = 0; - i < this.immediateMode.geometry.vertices.length; - i++ - ) { - const vert = this.immediateMode.geometry.vertices[i]; - const dX = orig.x - vert.x; - const dY = orig.y - vert.y; - const dZ = orig.z - vert.z; - const dist = dX*dX + dY*dY + dZ*dZ; - if (dist < closestDist) { - closestDist = dist; - closestIndex = i; - } + this.vertex(...polyTriangles.slice(j, j + 5)); + } + if (this.geometryBuilder) { + // Tesselating the face causes the indices of edge vertices to stop being + // correct. When rendering, this is not a problem, since _edgesToVertices + // will have been called before this, and edge vertex indices are no longer + // needed. However, the geometry builder still needs this information, so + // when one is active, we need to update the indices. + // + // We record index mappings in a Map so that once we have found a + // corresponding vertex, we don't need to loop to find it again. + const newIndex = new Map(); + this.immediateMode.geometry.edges = + this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => { + if (!newIndex.has(origIdx)) { + const orig = originalVertices[origIdx]; + let newVertIndex = this.immediateMode.geometry.vertices.findIndex( + v => + orig.x === v.x && + orig.y === v.y && + orig.z === v.z + ); + if (newVertIndex === -1) { + // The tesselation process didn't output a vertex with the exact + // coordinate as before, potentially due to numerical issues. This + // doesn't happen often, but in this case, pick the closest point + let closestDist = Infinity; + let closestIndex = 0; + for ( + let i = 0; + i < this.immediateMode.geometry.vertices.length; + i++ + ) { + const vert = this.immediateMode.geometry.vertices[i]; + const dX = orig.x - vert.x; + const dY = orig.y - vert.y; + const dZ = orig.z - vert.z; + const dist = dX*dX + dY*dY + dZ*dZ; + if (dist < closestDist) { + closestDist = dist; + closestIndex = i; } - newVertIndex = closestIndex; } - newIndex.set(origIdx, newVertIndex); + newVertIndex = closestIndex; } - return newIndex.get(origIdx); - })); - } - this.immediateMode.geometry.vertexColors = colors; - }; + newIndex.set(origIdx, newVertIndex); + } + return newIndex.get(origIdx); + })); + } + this.immediateMode.geometry.vertexColors = colors; +}; - /** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. - * @private - */ - RendererGL.prototype._drawImmediateFill = function(count = 1) { - const gl = this.GL; - this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); +/** + * Called from endShape(). Responsible for calculating normals, setting shader uniforms, + * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. + * @private + */ +RendererGL.prototype._drawImmediateFill = function(count = 1) { + const gl = this.GL; + this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); - let shader; - shader = this._getImmediateFillShader(); + let shader; + shader = this._getImmediateFillShader(); - this._setFillUniforms(shader); + this._setFillUniforms(shader); - for (const buff of this.immediateMode.buffers.fill) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - shader.disableRemainingAttributes(); + for (const buff of this.immediateMode.buffers.fill) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + shader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curFillColor, - this.immediateMode.geometry.hasFillTransparency() - ); + this._applyColorBlend( + this.states.curFillColor, + this.immediateMode.geometry.hasFillTransparency() + ); - if (count === 1) { - gl.drawArrays( + if (count === 1) { + gl.drawArrays( + this.immediateMode.shapeMode, + 0, + this.immediateMode.geometry.vertices.length + ); + } + else { + try { + gl.drawArraysInstanced( this.immediateMode.shapeMode, 0, - this.immediateMode.geometry.vertices.length + this.immediateMode.geometry.vertices.length, + count ); } - else { - try { - gl.drawArraysInstanced( - this.immediateMode.shapeMode, - 0, - this.immediateMode.geometry.vertices.length, - count - ); - } - catch (e) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); - } - } - shader.unbindShader(); - }; - - /** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. - * @private - */ - RendererGL.prototype._drawImmediateStroke = function() { - const gl = this.GL; - - this._useLineColor = - (this.immediateMode.geometry.vertexStrokeColors.length > 0); - - const shader = this._getImmediateStrokeShader(); - this._setStrokeUniforms(shader); - for (const buff of this.immediateMode.buffers.stroke) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); + catch (e) { + console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); } - shader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - this.immediateMode.geometry.hasFillTransparency() - ); + } + shader.unbindShader(); +}; - gl.drawArrays( - gl.TRIANGLES, - 0, - this.immediateMode.geometry.lineVertices.length / 3 - ); - shader.unbindShader(); - }; +/** + * Called from endShape(). Responsible for calculating normals, setting shader uniforms, + * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. + * @private + */ +RendererGL.prototype._drawImmediateStroke = function() { + const gl = this.GL; + + this._useLineColor = + (this.immediateMode.geometry.vertexStrokeColors.length > 0); + + const shader = this._getImmediateStrokeShader(); + this._setStrokeUniforms(shader); + for (const buff of this.immediateMode.buffers.stroke) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + shader.disableRemainingAttributes(); + this._applyColorBlend( + this.states.curStrokeColor, + this.immediateMode.geometry.hasFillTransparency() + ); + + gl.drawArrays( + gl.TRIANGLES, + 0, + this.immediateMode.geometry.lineVertices.length / 3 + ); + shader.unbindShader(); +}; } export default rendererGLImmediate; diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index a867f36a25..3452e1d8f2 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -5,294 +5,294 @@ import { RendererGL } from './p5.RendererGL'; import { RenderBuffer } from './p5.RenderBuffer'; function rendererGLRetained(p5, fn){ - /** - * @param {p5.Geometry} geometry The model whose resources will be freed - */ - RendererGL.prototype.freeGeometry = function(geometry) { - if (!geometry.gid) { - console.warn('The model you passed to freeGeometry does not have an id!'); - return; - } - this._freeBuffers(geometry.gid); - }; - - /** - * _initBufferDefaults - * @private - * @description initializes buffer defaults. runs each time a new geometry is - * registered - * @param {String} gId key of the geometry object - * @returns {Object} a new buffer object - */ - RendererGL.prototype._initBufferDefaults = function(gId) { - this._freeBuffers(gId); - - //@TODO remove this limit on hashes in retainedMode.geometry - if (Object.keys(this.retainedMode.geometry).length > 1000) { - const key = Object.keys(this.retainedMode.geometry)[0]; - this._freeBuffers(key); - } - - //create a new entry in our retainedMode.geometry - return (this.retainedMode.geometry[gId] = {}); - }; - - RendererGL.prototype._freeBuffers = function(gId) { - const buffers = this.retainedMode.geometry[gId]; - if (!buffers) { - return; - } - - delete this.retainedMode.geometry[gId]; - - const gl = this.GL; - if (buffers.indexBuffer) { - gl.deleteBuffer(buffers.indexBuffer); - } - - function freeBuffers(defs) { - for (const def of defs) { - if (buffers[def.dst]) { - gl.deleteBuffer(buffers[def.dst]); - buffers[def.dst] = null; - } +/** + * @param {p5.Geometry} geometry The model whose resources will be freed + */ +RendererGL.prototype.freeGeometry = function(geometry) { + if (!geometry.gid) { + console.warn('The model you passed to freeGeometry does not have an id!'); + return; + } + this._freeBuffers(geometry.gid); +}; + +/** + * _initBufferDefaults + * @private + * @description initializes buffer defaults. runs each time a new geometry is + * registered + * @param {String} gId key of the geometry object + * @returns {Object} a new buffer object + */ +RendererGL.prototype._initBufferDefaults = function(gId) { + this._freeBuffers(gId); + + //@TODO remove this limit on hashes in retainedMode.geometry + if (Object.keys(this.retainedMode.geometry).length > 1000) { + const key = Object.keys(this.retainedMode.geometry)[0]; + this._freeBuffers(key); + } + + //create a new entry in our retainedMode.geometry + return (this.retainedMode.geometry[gId] = {}); +}; + +RendererGL.prototype._freeBuffers = function(gId) { + const buffers = this.retainedMode.geometry[gId]; + if (!buffers) { + return; + } + + delete this.retainedMode.geometry[gId]; + + const gl = this.GL; + if (buffers.indexBuffer) { + gl.deleteBuffer(buffers.indexBuffer); + } + + function freeBuffers(defs) { + for (const def of defs) { + if (buffers[def.dst]) { + gl.deleteBuffer(buffers[def.dst]); + buffers[def.dst] = null; } } - - // free all the buffers - freeBuffers(this.retainedMode.buffers.stroke); - freeBuffers(this.retainedMode.buffers.fill); - freeBuffers(this.retainedMode.buffers.user); - this.retainedMode.buffers.user = []; - }; - - /** - * creates a buffers object that holds the WebGL render buffers - * for a geometry. - * @private - * @param {String} gId key of the geometry object - * @param {p5.Geometry} model contains geometry data - */ - RendererGL.prototype.createBuffers = function(gId, model) { - const gl = this.GL; - //initialize the gl buffers for our geom groups - const buffers = this._initBufferDefaults(gId); - buffers.model = model; - - let indexBuffer = buffers.indexBuffer; - - if (model.faces.length) { - // allocate space for faces - if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); - const vals = RendererGL.prototype._flatten(model.faces); - - // If any face references a vertex with an index greater than the maximum - // un-singed 16 bit integer, then we need to use a Uint32Array instead of a - // Uint16Array - const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); - let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; - this._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); - - // If we're using a Uint32Array for our indexBuffer we will need to pass a - // different enum value to WebGL draw triangles. This happens in - // the _drawElements function. - buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 - ? gl.UNSIGNED_INT - : gl.UNSIGNED_SHORT; - - // the vertex count is based on the number of faces - buffers.vertexCount = model.faces.length * 3; - } else { - // the index buffer is unused, remove it - if (indexBuffer) { - gl.deleteBuffer(indexBuffer); - buffers.indexBuffer = null; - } - // the vertex count comes directly from the model - buffers.vertexCount = model.vertices ? model.vertices.length : 0; + } + + // free all the buffers + freeBuffers(this.retainedMode.buffers.stroke); + freeBuffers(this.retainedMode.buffers.fill); + freeBuffers(this.retainedMode.buffers.user); + this.retainedMode.buffers.user = []; +}; + +/** + * creates a buffers object that holds the WebGL render buffers + * for a geometry. + * @private + * @param {String} gId key of the geometry object + * @param {p5.Geometry} model contains geometry data + */ +RendererGL.prototype.createBuffers = function(gId, model) { + const gl = this.GL; + //initialize the gl buffers for our geom groups + const buffers = this._initBufferDefaults(gId); + buffers.model = model; + + let indexBuffer = buffers.indexBuffer; + + if (model.faces.length) { + // allocate space for faces + if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); + const vals = RendererGL.prototype._flatten(model.faces); + + // If any face references a vertex with an index greater than the maximum + // un-singed 16 bit integer, then we need to use a Uint32Array instead of a + // Uint16Array + const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); + let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; + this._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); + + // If we're using a Uint32Array for our indexBuffer we will need to pass a + // different enum value to WebGL draw triangles. This happens in + // the _drawElements function. + buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 + ? gl.UNSIGNED_INT + : gl.UNSIGNED_SHORT; + + // the vertex count is based on the number of faces + buffers.vertexCount = model.faces.length * 3; + } else { + // the index buffer is unused, remove it + if (indexBuffer) { + gl.deleteBuffer(indexBuffer); + buffers.indexBuffer = null; } - - buffers.lineVertexCount = model.lineVertices - ? model.lineVertices.length / 3 - : 0; - - for (const propName in model.userVertexProperties){ - const prop = model.userVertexProperties[propName]; - this.retainedMode.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) - ); + // the vertex count comes directly from the model + buffers.vertexCount = model.vertices ? model.vertices.length : 0; + } + + buffers.lineVertexCount = model.lineVertices + ? model.lineVertices.length / 3 + : 0; + + for (const propName in model.userVertexProperties){ + const prop = model.userVertexProperties[propName]; + this.retainedMode.buffers.user.push( + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) + ); + } + return buffers; +}; + +/** + * Draws buffers given a geometry key ID + * @private + * @param {String} gId ID in our geom hash + * @chainable + */ +RendererGL.prototype.drawBuffers = function(gId) { + const gl = this.GL; + const geometry = this.retainedMode.geometry[gId]; + + if ( + !this.geometryBuilder && + this.states.doFill && + geometry.vertexCount > 0 + ) { + this._useVertexColor = (geometry.model.vertexColors.length > 0); + const fillShader = this._getRetainedFillShader(); + this._setFillUniforms(fillShader); + for (const buff of this.retainedMode.buffers.fill) { + buff._prepareBuffer(geometry, fillShader); } - return buffers; - }; - - /** - * Draws buffers given a geometry key ID - * @private - * @param {String} gId ID in our geom hash - * @chainable - */ - RendererGL.prototype.drawBuffers = function(gId) { - const gl = this.GL; - const geometry = this.retainedMode.geometry[gId]; - - if ( - !this.geometryBuilder && - this.states.doFill && - geometry.vertexCount > 0 - ) { - this._useVertexColor = (geometry.model.vertexColors.length > 0); - const fillShader = this._getRetainedFillShader(); - this._setFillUniforms(fillShader); - for (const buff of this.retainedMode.buffers.fill) { - buff._prepareBuffer(geometry, fillShader); - } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } - buff._prepareBuffer(geometry, fillShader); - } - fillShader.disableRemainingAttributes(); - if (geometry.indexBuffer) { - //vertex index buffer - this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + for (const buff of this.retainedMode.buffers.user){ + const prop = geometry.model.userVertexProperties[buff.attr]; + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); } - this._applyColorBlend( - this.states.curFillColor, - geometry.model.hasFillTransparency() - ); - this._drawElements(gl.TRIANGLES, gId); - fillShader.unbindShader(); + buff._prepareBuffer(geometry, fillShader); } - - if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { - this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); - const strokeShader = this._getRetainedStrokeShader(); - this._setStrokeUniforms(strokeShader); - for (const buff of this.retainedMode.buffers.stroke) { - buff._prepareBuffer(geometry, strokeShader); - } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } - buff._prepareBuffer(geometry, strokeShader); - } - strokeShader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - geometry.model.hasStrokeTransparency() - ); - this._drawArrays(gl.TRIANGLES, gId); - strokeShader.unbindShader(); + fillShader.disableRemainingAttributes(); + if (geometry.indexBuffer) { + //vertex index buffer + this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); } - - if (this.geometryBuilder) { - this.geometryBuilder.addRetained(geometry); + this._applyColorBlend( + this.states.curFillColor, + geometry.model.hasFillTransparency() + ); + this._drawElements(gl.TRIANGLES, gId); + fillShader.unbindShader(); + } + + if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { + this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); + const strokeShader = this._getRetainedStrokeShader(); + this._setStrokeUniforms(strokeShader); + for (const buff of this.retainedMode.buffers.stroke) { + buff._prepareBuffer(geometry, strokeShader); } - - return this; - }; - - /** - * Calls drawBuffers() with a scaled model/view matrix. - * - * This is used by various 3d primitive methods (in primitives.js, eg. plane, - * box, torus, etc...) to allow caching of un-scaled geometries. Those - * geometries are generally created with unit-length dimensions, cached as - * such, and then scaled appropriately in this method prior to rendering. - * - * @private - * @method drawBuffersScaled - * @param {String} gId ID in our geom hash - * @param {Number} scaleX the amount to scale in the X direction - * @param {Number} scaleY the amount to scale in the Y direction - * @param {Number} scaleZ the amount to scale in the Z direction - */ - RendererGL.prototype.drawBuffersScaled = function( - gId, - scaleX, - scaleY, - scaleZ - ) { - let originalModelMatrix = this.states.uModelMatrix.copy(); - try { - this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); - - this.drawBuffers(gId); - } finally { - - this.states.uModelMatrix = originalModelMatrix; + for (const buff of this.retainedMode.buffers.user){ + const prop = geometry.model.userVertexProperties[buff.attr]; + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } + buff._prepareBuffer(geometry, strokeShader); } - }; - RendererGL.prototype._drawArrays = function(drawMode, gId) { - this.GL.drawArrays( - drawMode, - 0, - this.retainedMode.geometry[gId].lineVertexCount + strokeShader.disableRemainingAttributes(); + this._applyColorBlend( + this.states.curStrokeColor, + geometry.model.hasStrokeTransparency() ); - return this; - }; - - RendererGL.prototype._drawElements = function(drawMode, gId) { - const buffers = this.retainedMode.geometry[gId]; - const gl = this.GL; - // render the fill - if (buffers.indexBuffer) { - // If this model is using a Uint32Array we need to ensure the - // OES_element_index_uint WebGL extension is enabled. - if ( - this._pInst.webglVersion !== constants.WEBGL2 && - buffers.indexBufferType === gl.UNSIGNED_INT - ) { - if (!gl.getExtension('OES_element_index_uint')) { - throw new Error( - 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' - ); - } + this._drawArrays(gl.TRIANGLES, gId); + strokeShader.unbindShader(); + } + + if (this.geometryBuilder) { + this.geometryBuilder.addRetained(geometry); + } + + return this; +}; + +/** + * Calls drawBuffers() with a scaled model/view matrix. + * + * This is used by various 3d primitive methods (in primitives.js, eg. plane, + * box, torus, etc...) to allow caching of un-scaled geometries. Those + * geometries are generally created with unit-length dimensions, cached as + * such, and then scaled appropriately in this method prior to rendering. + * + * @private + * @method drawBuffersScaled + * @param {String} gId ID in our geom hash + * @param {Number} scaleX the amount to scale in the X direction + * @param {Number} scaleY the amount to scale in the Y direction + * @param {Number} scaleZ the amount to scale in the Z direction + */ +RendererGL.prototype.drawBuffersScaled = function( + gId, + scaleX, + scaleY, + scaleZ +) { + let originalModelMatrix = this.states.uModelMatrix.copy(); + try { + this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); + + this.drawBuffers(gId); + } finally { + + this.states.uModelMatrix = originalModelMatrix; + } +}; +RendererGL.prototype._drawArrays = function(drawMode, gId) { + this.GL.drawArrays( + drawMode, + 0, + this.retainedMode.geometry[gId].lineVertexCount + ); + return this; +}; + +RendererGL.prototype._drawElements = function(drawMode, gId) { + const buffers = this.retainedMode.geometry[gId]; + const gl = this.GL; + // render the fill + if (buffers.indexBuffer) { + // If this model is using a Uint32Array we need to ensure the + // OES_element_index_uint WebGL extension is enabled. + if ( + this._pInst.webglVersion !== constants.WEBGL2 && + buffers.indexBufferType === gl.UNSIGNED_INT + ) { + if (!gl.getExtension('OES_element_index_uint')) { + throw new Error( + 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' + ); } - // we're drawing faces - gl.drawElements( - gl.TRIANGLES, - buffers.vertexCount, - buffers.indexBufferType, - 0 - ); - } else { - // drawing vertices - gl.drawArrays(drawMode || gl.TRIANGLES, 0, buffers.vertexCount); } - }; - - RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { - const gl = this.GL; - const pointShader = this._getImmediatePointShader(); - this._setPointUniforms(pointShader); - - this._bindBuffer( - vertexBuffer, - gl.ARRAY_BUFFER, - this._vToNArray(vertices), - Float32Array, - gl.STATIC_DRAW + // we're drawing faces + gl.drawElements( + gl.TRIANGLES, + buffers.vertexCount, + buffers.indexBufferType, + 0 ); + } else { + // drawing vertices + gl.drawArrays(drawMode || gl.TRIANGLES, 0, buffers.vertexCount); + } +}; + +RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { + const gl = this.GL; + const pointShader = this._getImmediatePointShader(); + this._setPointUniforms(pointShader); + + this._bindBuffer( + vertexBuffer, + gl.ARRAY_BUFFER, + this._vToNArray(vertices), + Float32Array, + gl.STATIC_DRAW + ); - pointShader.enableAttrib(pointShader.attributes.aPosition, 3); + pointShader.enableAttrib(pointShader.attributes.aPosition, 3); - this._applyColorBlend(this.states.curStrokeColor); + this._applyColorBlend(this.states.curStrokeColor); - gl.drawArrays(gl.Points, 0, vertices.length); + gl.drawArrays(gl.Points, 0, vertices.length); - pointShader.unbindShader(); - }; + pointShader.unbindShader(); +}; } export default rendererGLRetained; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index f0a93539b9..54c6a55581 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -8,1436 +8,1436 @@ import { Texture } from './p5.Texture'; -class Shader { - constructor(renderer, vertSrc, fragSrc, options = {}) { - // TODO: adapt this to not take ids, but rather, - // to take the source for a vertex and fragment shader - // to enable custom shaders at some later date - this._renderer = renderer; - this._vertSrc = vertSrc; - this._fragSrc = fragSrc; - this._vertShader = -1; - this._fragShader = -1; - this._glProgram = 0; - this._loadedAttributes = false; - this.attributes = {}; - this._loadedUniforms = false; - this.uniforms = {}; - this._bound = false; - this.samplers = []; - this.hooks = { - // These should be passed in by `.modify()` instead of being manually - // passed in. - - // Stores uniforms + default values. - uniforms: options.uniforms || {}, - - // Stores custom uniform + helper declarations as a string. - declarations: options.declarations, - - // Stores helper functions to prepend to shaders. - helpers: options.helpers || {}, - - // Stores the hook implementations - vertex: options.vertex || {}, - fragment: options.fragment || {}, - - // Stores whether or not the hook implementation has been modified - // from the default. This is supplied automatically by calling - // yourShader.modify(...). - modified: { - vertex: (options.modified && options.modified.vertex) || {}, - fragment: (options.modified && options.modified.fragment) || {} - } - }; + function shader(p5, fn){ + /** + * A class to describe a shader program. + * + * Each `p5.Shader` object contains a shader program that runs on the graphics + * processing unit (GPU). Shaders can process many pixels or vertices at the + * same time, making them fast for many graphics tasks. They’re written in a + * language called + * GLSL + * and run along with the rest of the code in a sketch. + * + * A shader program consists of two files, a vertex shader and a fragment + * shader. The vertex shader affects where 3D geometry is drawn on the screen + * and the fragment shader affects color. Once the `p5.Shader` object is + * created, it can be used with the shader() + * function, as in `shader(myShader)`. + * + * A shader can optionally describe *hooks,* which are functions in GLSL that + * users may choose to provide to customize the behavior of the shader. For the + * vertex or the fragment shader, users can pass in an object where each key is + * the type and name of a hook function, and each value is a string with the + * parameter list and default implementation of the hook. For example, to let users + * optionally run code at the start of the vertex shader, the options object could + * include: + * + * ```js + * { + * vertex: { + * 'void beforeVertex': '() {}' + * } + * } + * ``` + * + * Then, in your vertex shader source, you can run a hook by calling a function + * with the same name prefixed by `HOOK_`: + * + * ```glsl + * void main() { + * HOOK_beforeVertex(); + * // Add the rest ofy our shader code here! + * } + * ``` + * + * Note: createShader(), + * createFilterShader(), and + * loadShader() are the recommended ways to + * create an instance of this class. + * + * @class p5.Shader + * @param {p5.RendererGL} renderer WebGL context for this shader. + * @param {String} vertSrc source code for the vertex shader program. + * @param {String} fragSrc source code for the fragment shader program. + * @param {Object} [options] An optional object describing how this shader can + * be augmented with hooks. It can include: + * - `vertex`: An object describing the available vertex shader hooks. + * - `fragment`: An object describing the available frament shader hooks. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision highp float; + * + * void main() { + * // Set each pixel's RGBA value to yellow. + * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); + * } + * `; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * let myShader = createShader(vertSrc, fragSrc); + * + * // Apply the p5.Shader object. + * shader(myShader); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * + * describe('A yellow square.'); + * } + * + *
+ * + *
+ * + * // Note: A "uniform" is a global variable within a shader program. + * + * let mandelbrot; + * + * // Load the shader and create a p5.Shader object. + * function preload() { + * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Use the p5.Shader object. + * shader(mandelbrot); + * + * // Set the shader uniform p to an array. + * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); + * + * describe('A fractal image zooms in and out of focus.'); + * } + * + * function draw() { + * // Set the shader uniform r to a value that oscillates between 0 and 2. + * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); + * + * // Add a quad as a display surface for the shader. + * quad(-1, -1, 1, -1, 1, 1, -1, 1); + * } + * + *
+ */ + p5.Shader = Shader; } - shaderSrc(src, shaderType) { - const main = 'void main'; - const [preMain, postMain] = src.split(main); - - let hooks = ''; - for (const key in this.hooks.uniforms) { - hooks += `uniform ${key};\n`; - } - if (this.hooks.declarations) { - hooks += this.hooks.declarations + '\n'; - } - if (this.hooks[shaderType].declarations) { - hooks += this.hooks[shaderType].declarations + '\n'; - } - for (const hookDef in this.hooks.helpers) { - hooks += `${hookDef}${this.hooks.helpers[hookDef]}\n`; - } - for (const hookDef in this.hooks[shaderType]) { - if (hookDef === 'declarations') continue; - const [hookType, hookName] = hookDef.split(' '); - - // Add a #define so that if the shader wants to use preprocessor directives to - // optimize away the extra function calls in main, it can do so - if (this.hooks.modified[shaderType][hookDef]) { - hooks += '#define AUGMENTED_HOOK_' + hookName + '\n'; - } - - hooks += - hookType + ' HOOK_' + hookName + this.hooks[shaderType][hookDef] + '\n'; + class Shader { + constructor(renderer, vertSrc, fragSrc, options = {}) { + // TODO: adapt this to not take ids, but rather, + // to take the source for a vertex and fragment shader + // to enable custom shaders at some later date + this._renderer = renderer; + this._vertSrc = vertSrc; + this._fragSrc = fragSrc; + this._vertShader = -1; + this._fragShader = -1; + this._glProgram = 0; + this._loadedAttributes = false; + this.attributes = {}; + this._loadedUniforms = false; + this.uniforms = {}; + this._bound = false; + this.samplers = []; + this.hooks = { + // These should be passed in by `.modify()` instead of being manually + // passed in. + + // Stores uniforms + default values. + uniforms: options.uniforms || {}, + + // Stores custom uniform + helper declarations as a string. + declarations: options.declarations, + + // Stores helper functions to prepend to shaders. + helpers: options.helpers || {}, + + // Stores the hook implementations + vertex: options.vertex || {}, + fragment: options.fragment || {}, + + // Stores whether or not the hook implementation has been modified + // from the default. This is supplied automatically by calling + // yourShader.modify(...). + modified: { + vertex: (options.modified && options.modified.vertex) || {}, + fragment: (options.modified && options.modified.fragment) || {} + } + }; } - return preMain + hooks + main + postMain; - } + shaderSrc(src, shaderType) { + const main = 'void main'; + const [preMain, postMain] = src.split(main); - /** - * Shaders are written in GLSL, but - * there are different versions of GLSL that it might be written in. - * - * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. - * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. - * - * @returns {String} The GLSL version used by the shader. - */ - version() { - const match = /#version (.+)$/.exec(this.vertSrc()); - if (match) { - return match[1]; - } else { - return '100 es'; - } - } - - vertSrc() { - return this.shaderSrc(this._vertSrc, 'vertex'); - } + let hooks = ''; + for (const key in this.hooks.uniforms) { + hooks += `uniform ${key};\n`; + } + if (this.hooks.declarations) { + hooks += this.hooks.declarations + '\n'; + } + if (this.hooks[shaderType].declarations) { + hooks += this.hooks[shaderType].declarations + '\n'; + } + for (const hookDef in this.hooks.helpers) { + hooks += `${hookDef}${this.hooks.helpers[hookDef]}\n`; + } + for (const hookDef in this.hooks[shaderType]) { + if (hookDef === 'declarations') continue; + const [hookType, hookName] = hookDef.split(' '); + + // Add a #define so that if the shader wants to use preprocessor directives to + // optimize away the extra function calls in main, it can do so + if (this.hooks.modified[shaderType][hookDef]) { + hooks += '#define AUGMENTED_HOOK_' + hookName + '\n'; + } - fragSrc() { - return this.shaderSrc(this._fragSrc, 'fragment'); - } + hooks += + hookType + ' HOOK_' + hookName + this.hooks[shaderType][hookDef] + '\n'; + } - /** - * Logs the hooks available in this shader, and their current implementation. - * - * Each shader may let you override bits of its behavior. Each bit is called - * a *hook.* A hook is either for the *vertex* shader, if it affects the - * position of vertices, or in the *fragment* shader, if it affects the pixel - * color. This method logs those values to the console, letting you know what - * you are able to use in a call to - * `modify()`. - * - * For example, this shader will produce the following output: - * - * ```js - * myShader = baseMaterialShader().modify({ - * declarations: 'uniform float time;', - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * myShader.inspectHooks(); - * ``` - * - * ``` - * ==== Vertex shader hooks: ==== - * void beforeVertex() {} - * vec3 getLocalPosition(vec3 position) { return position; } - * [MODIFIED] vec3 getWorldPosition(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * } - * vec3 getLocalNormal(vec3 normal) { return normal; } - * vec3 getWorldNormal(vec3 normal) { return normal; } - * vec2 getUV(vec2 uv) { return uv; } - * vec4 getVertexColor(vec4 color) { return color; } - * void afterVertex() {} - * - * ==== Fragment shader hooks: ==== - * void beforeFragment() {} - * Inputs getPixelInputs(Inputs inputs) { return inputs; } - * vec4 combineColors(ColorComponents components) { - * vec4 color = vec4(0.); - * color.rgb += components.diffuse * components.baseColor; - * color.rgb += components.ambient * components.ambientColor; - * color.rgb += components.specular * components.specularColor; - * color.rgb += components.emissive; - * color.a = components.opacity; - * return color; - * } - * vec4 getFinalColor(vec4 color) { return color; } - * void afterFragment() {} - * ``` - * - * @beta - */ - inspectHooks() { - console.log('==== Vertex shader hooks: ===='); - for (const key in this.hooks.vertex) { - console.log( - (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.vertex[key] - ); - } - console.log(''); - console.log('==== Fragment shader hooks: ===='); - for (const key in this.hooks.fragment) { - console.log( - (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.fragment[key] - ); - } - console.log(''); - console.log('==== Helper functions: ===='); - for (const key in this.hooks.helpers) { - console.log( - key + - this.hooks.helpers[key] - ); + return preMain + hooks + main + postMain; } - } - /** - * Returns a new shader, based on the original, but with custom snippets - * of shader code replacing default behaviour. - * - * Each shader may let you override bits of its behavior. Each bit is called - * a *hook.* A hook is either for the *vertex* shader, if it affects the - * position of vertices, or in the *fragment* shader, if it affects the pixel - * color. You can inspect the different hooks available by calling - * `yourShader.inspectHooks()`. You can - * also read the reference for the default material, normal material, color, line, and point shaders to - * see what hooks they have available. - * - * `modify()` takes one parameter, `hooks`, an object with the hooks you want - * to override. Each key of the `hooks` object is the name - * of a hook, and the value is a string with the GLSL code for your hook. - * - * If you supply functions that aren't existing hooks, they will get added at the start of - * the shader as helper functions so that you can use them in your hooks. - * - * To add new uniforms to your shader, you can pass in a `uniforms` object containing - * the type and name of the uniform as the key, and a default value or function returning - * a default value as its value. These will be automatically set when the shader is set - * with `shader(yourShader)`. - * - * You can also add a `declarations` key, where the value is a GLSL string declaring - * custom uniform variables, globals, and functions shared - * between hooks. To add declarations just in a vertex or fragment shader, add - * `vertexDeclarations` and `fragmentDeclarations` keys. - * - * @beta - * @param {Object} [hooks] The hooks in the shader to replace. - * @returns {p5.Shader} - * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * // Manually specifying a uniform - * declarations: 'uniform float time;', - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * myShader.setUniform('time', millis()); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- */ - modify(hooks) { - // p5._validateParameters('p5.Shader.modify', arguments); - const newHooks = { - vertex: {}, - fragment: {}, - helpers: {} - }; - for (const key in hooks) { - if (key === 'declarations') continue; - if (key === 'uniforms') continue; - if (key === 'vertexDeclarations') { - newHooks.vertex.declarations = - (newHooks.vertex.declarations || '') + '\n' + hooks[key]; - } else if (key === 'fragmentDeclarations') { - newHooks.fragment.declarations = - (newHooks.fragment.declarations || '') + '\n' + hooks[key]; - } else if (this.hooks.vertex[key]) { - newHooks.vertex[key] = hooks[key]; - } else if (this.hooks.fragment[key]) { - newHooks.fragment[key] = hooks[key]; + /** + * Shaders are written in GLSL, but + * there are different versions of GLSL that it might be written in. + * + * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. + * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. + * + * @returns {String} The GLSL version used by the shader. + */ + version() { + const match = /#version (.+)$/.exec(this.vertSrc()); + if (match) { + return match[1]; } else { - newHooks.helpers[key] = hooks[key]; + return '100 es'; } } - const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); - const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); - for (const key in newHooks.vertex || {}) { - if (key === 'declarations') continue; - modifiedVertex[key] = true; + + vertSrc() { + return this.shaderSrc(this._vertSrc, 'vertex'); } - for (const key in newHooks.fragment || {}) { - if (key === 'declarations') continue; - modifiedFragment[key] = true; + + fragSrc() { + return this.shaderSrc(this._fragSrc, 'fragment'); } - return new Shader(this._renderer, this._vertSrc, this._fragSrc, { - declarations: - (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), - uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), - fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), - vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), - helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), - modified: { - vertex: modifiedVertex, - fragment: modifiedFragment + /** + * Logs the hooks available in this shader, and their current implementation. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. This method logs those values to the console, letting you know what + * you are able to use in a call to + * `modify()`. + * + * For example, this shader will produce the following output: + * + * ```js + * myShader = baseMaterialShader().modify({ + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * myShader.inspectHooks(); + * ``` + * + * ``` + * ==== Vertex shader hooks: ==== + * void beforeVertex() {} + * vec3 getLocalPosition(vec3 position) { return position; } + * [MODIFIED] vec3 getWorldPosition(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * } + * vec3 getLocalNormal(vec3 normal) { return normal; } + * vec3 getWorldNormal(vec3 normal) { return normal; } + * vec2 getUV(vec2 uv) { return uv; } + * vec4 getVertexColor(vec4 color) { return color; } + * void afterVertex() {} + * + * ==== Fragment shader hooks: ==== + * void beforeFragment() {} + * Inputs getPixelInputs(Inputs inputs) { return inputs; } + * vec4 combineColors(ColorComponents components) { + * vec4 color = vec4(0.); + * color.rgb += components.diffuse * components.baseColor; + * color.rgb += components.ambient * components.ambientColor; + * color.rgb += components.specular * components.specularColor; + * color.rgb += components.emissive; + * color.a = components.opacity; + * return color; + * } + * vec4 getFinalColor(vec4 color) { return color; } + * void afterFragment() {} + * ``` + * + * @beta + */ + inspectHooks() { + console.log('==== Vertex shader hooks: ===='); + for (const key in this.hooks.vertex) { + console.log( + (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.vertex[key] + ); } - }); - } - - /** - * Creates, compiles, and links the shader based on its - * sources for the vertex and fragment shaders (provided - * to the constructor). Populates known attributes and - * uniforms from the shader. - * @chainable - * @private - */ - init() { - if (this._glProgram === 0 /* or context is stale? */) { - const gl = this._renderer.GL; + console.log(''); + console.log('==== Fragment shader hooks: ===='); + for (const key in this.hooks.fragment) { + console.log( + (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.fragment[key] + ); + } + console.log(''); + console.log('==== Helper functions: ===='); + for (const key in this.hooks.helpers) { + console.log( + key + + this.hooks.helpers[key] + ); + } + } - // @todo: once custom shading is allowed, - // friendly error messages should be used here to share - // compiler and linker errors. - - //set up the shader by - // 1. creating and getting a gl id for the shader program, - // 2. compliling its vertex & fragment sources, - // 3. linking the vertex and fragment shaders - this._vertShader = gl.createShader(gl.VERTEX_SHADER); - //load in our default vertex shader - gl.shaderSource(this._vertShader, this.vertSrc()); - gl.compileShader(this._vertShader); - // if our vertex shader failed compilation? - if (!gl.getShaderParameter(this._vertShader, gl.COMPILE_STATUS)) { - const glError = gl.getShaderInfoLog(this._vertShader); - if (typeof IS_MINIFIED !== 'undefined') { - console.error(glError); + /** + * Returns a new shader, based on the original, but with custom snippets + * of shader code replacing default behaviour. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. You can inspect the different hooks available by calling + * `yourShader.inspectHooks()`. You can + * also read the reference for the default material, normal material, color, line, and point shaders to + * see what hooks they have available. + * + * `modify()` takes one parameter, `hooks`, an object with the hooks you want + * to override. Each key of the `hooks` object is the name + * of a hook, and the value is a string with the GLSL code for your hook. + * + * If you supply functions that aren't existing hooks, they will get added at the start of + * the shader as helper functions so that you can use them in your hooks. + * + * To add new uniforms to your shader, you can pass in a `uniforms` object containing + * the type and name of the uniform as the key, and a default value or function returning + * a default value as its value. These will be automatically set when the shader is set + * with `shader(yourShader)`. + * + * You can also add a `declarations` key, where the value is a GLSL string declaring + * custom uniform variables, globals, and functions shared + * between hooks. To add declarations just in a vertex or fragment shader, add + * `vertexDeclarations` and `fragmentDeclarations` keys. + * + * @beta + * @param {Object} [hooks] The hooks in the shader to replace. + * @returns {p5.Shader} + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * // Manually specifying a uniform + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * myShader.setUniform('time', millis()); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ + modify(hooks) { + // p5._validateParameters('p5.Shader.modify', arguments); + const newHooks = { + vertex: {}, + fragment: {}, + helpers: {} + }; + for (const key in hooks) { + if (key === 'declarations') continue; + if (key === 'uniforms') continue; + if (key === 'vertexDeclarations') { + newHooks.vertex.declarations = + (newHooks.vertex.declarations || '') + '\n' + hooks[key]; + } else if (key === 'fragmentDeclarations') { + newHooks.fragment.declarations = + (newHooks.fragment.declarations || '') + '\n' + hooks[key]; + } else if (this.hooks.vertex[key]) { + newHooks.vertex[key] = hooks[key]; + } else if (this.hooks.fragment[key]) { + newHooks.fragment[key] = hooks[key]; } else { - p5._friendlyError( - `Yikes! An error occurred compiling the vertex shader:${glError}` - ); + newHooks.helpers[key] = hooks[key]; } - return null; + } + const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); + const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); + for (const key in newHooks.vertex || {}) { + if (key === 'declarations') continue; + modifiedVertex[key] = true; + } + for (const key in newHooks.fragment || {}) { + if (key === 'declarations') continue; + modifiedFragment[key] = true; } - this._fragShader = gl.createShader(gl.FRAGMENT_SHADER); - //load in our material frag shader - gl.shaderSource(this._fragShader, this.fragSrc()); - gl.compileShader(this._fragShader); - // if our frag shader failed compilation? - if (!gl.getShaderParameter(this._fragShader, gl.COMPILE_STATUS)) { - const glError = gl.getShaderInfoLog(this._fragShader); - if (typeof IS_MINIFIED !== 'undefined') { - console.error(glError); - } else { + return new Shader(this._renderer, this._vertSrc, this._fragSrc, { + declarations: + (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), + uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), + fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), + vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), + helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), + modified: { + vertex: modifiedVertex, + fragment: modifiedFragment + } + }); + } + + /** + * Creates, compiles, and links the shader based on its + * sources for the vertex and fragment shaders (provided + * to the constructor). Populates known attributes and + * uniforms from the shader. + * @chainable + * @private + */ + init() { + if (this._glProgram === 0 /* or context is stale? */) { + const gl = this._renderer.GL; + + // @todo: once custom shading is allowed, + // friendly error messages should be used here to share + // compiler and linker errors. + + //set up the shader by + // 1. creating and getting a gl id for the shader program, + // 2. compliling its vertex & fragment sources, + // 3. linking the vertex and fragment shaders + this._vertShader = gl.createShader(gl.VERTEX_SHADER); + //load in our default vertex shader + gl.shaderSource(this._vertShader, this.vertSrc()); + gl.compileShader(this._vertShader); + // if our vertex shader failed compilation? + if (!gl.getShaderParameter(this._vertShader, gl.COMPILE_STATUS)) { + const glError = gl.getShaderInfoLog(this._vertShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { + p5._friendlyError( + `Yikes! An error occurred compiling the vertex shader:${glError}` + ); + } + return null; + } + + this._fragShader = gl.createShader(gl.FRAGMENT_SHADER); + //load in our material frag shader + gl.shaderSource(this._fragShader, this.fragSrc()); + gl.compileShader(this._fragShader); + // if our frag shader failed compilation? + if (!gl.getShaderParameter(this._fragShader, gl.COMPILE_STATUS)) { + const glError = gl.getShaderInfoLog(this._fragShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { + p5._friendlyError( + `Darn! An error occurred compiling the fragment shader:${glError}` + ); + } + return null; + } + + this._glProgram = gl.createProgram(); + gl.attachShader(this._glProgram, this._vertShader); + gl.attachShader(this._glProgram, this._fragShader); + gl.linkProgram(this._glProgram); + if (!gl.getProgramParameter(this._glProgram, gl.LINK_STATUS)) { p5._friendlyError( - `Darn! An error occurred compiling the fragment shader:${glError}` + `Snap! Error linking shader program: ${gl.getProgramInfoLog( + this._glProgram + )}` ); } - return null; - } - this._glProgram = gl.createProgram(); - gl.attachShader(this._glProgram, this._vertShader); - gl.attachShader(this._glProgram, this._fragShader); - gl.linkProgram(this._glProgram); - if (!gl.getProgramParameter(this._glProgram, gl.LINK_STATUS)) { - p5._friendlyError( - `Snap! Error linking shader program: ${gl.getProgramInfoLog( - this._glProgram - )}` - ); + this._loadAttributes(); + this._loadUniforms(); } - - this._loadAttributes(); - this._loadUniforms(); + return this; } - return this; - } - /** - * @private - */ - setDefaultUniforms() { - for (const key in this.hooks.uniforms) { - const [, name] = key.split(' '); - const initializer = this.hooks.uniforms[key]; - let value; - if (initializer instanceof Function) { - value = initializer(); - } else { - value = initializer; - } + /** + * @private + */ + setDefaultUniforms() { + for (const key in this.hooks.uniforms) { + const [, name] = key.split(' '); + const initializer = this.hooks.uniforms[key]; + let value; + if (initializer instanceof Function) { + value = initializer(); + } else { + value = initializer; + } - if (value !== undefined && value !== null) { - this.setUniform(name, value); + if (value !== undefined && value !== null) { + this.setUniform(name, value); + } } } - } - - /** - * Copies the shader from one drawing context to another. - * - * Each `p5.Shader` object must be compiled by calling - * shader() before it can run. Compilation happens - * in a drawing context which is usually the main canvas or an instance of - * p5.Graphics. A shader can only be used in the - * context where it was compiled. The `copyToContext()` method compiles the - * shader again and copies it to another drawing context where it can be - * reused. - * - * The parameter, `context`, is the drawing context where the shader will be - * used. The shader can be copied to an instance of - * p5.Graphics, as in - * `myShader.copyToContext(pg)`. The shader can also be copied from a - * p5.Graphics object to the main canvas using - * the `window` variable, as in `myShader.copyToContext(window)`. - * - * Note: A p5.Shader object created with - * createShader(), - * createFilterShader(), or - * loadShader() - * can be used directly with a p5.Framebuffer - * object created with - * createFramebuffer(). Both objects - * have the same context as the main canvas. - * - * @param {p5|p5.Graphics} context WebGL context for the copied shader. - * @returns {p5.Shader} new shader compiled for the target context. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * varying vec2 vTexCoord; - * - * void main() { - * vec2 uv = vTexCoord; - * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); - * gl_FragColor = vec4(color, 1.0);\ - * } - * `; - * - * let pg; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Shader object. - * let original = createShader(vertSrc, fragSrc); - * - * // Compile the p5.Shader object. - * shader(original); - * - * // Create a p5.Graphics object. - * pg = createGraphics(50, 50, WEBGL); - * - * // Copy the original shader to the p5.Graphics object. - * let copied = original.copyToContext(pg); - * - * // Apply the copied shader to the p5.Graphics object. - * pg.shader(copied); - * - * // Style the display surface. - * pg.noStroke(); - * - * // Add a display surface for the shader. - * pg.plane(50, 50); - * - * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Draw the p5.Graphics object to the main canvas. - * image(pg, -25, -25); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * varying vec2 vTexCoord; - * - * void main() { - * vec2 uv = vTexCoord; - * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); - * gl_FragColor = vec4(color, 1.0); - * } - * `; - * - * let copied; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Graphics object. - * let pg = createGraphics(25, 25, WEBGL); - * - * // Create a p5.Shader object. - * let original = pg.createShader(vertSrc, fragSrc); - * - * // Compile the p5.Shader object. - * pg.shader(original); - * - * // Copy the original shader to the main canvas. - * copied = original.copyToContext(window); - * - * // Apply the copied shader to the main canvas. - * shader(copied); - * - * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Rotate around the x-, y-, and z-axes. - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * rotateZ(frameCount * 0.01); - * - * // Draw the box. - * box(50); - * } - * - *
- */ - copyToContext(context) { - const shader = new Shader( - context._renderer, - this._vertSrc, - this._fragSrc - ); - shader.ensureCompiledOnContext(context); - return shader; - } - /** - * @private - */ - ensureCompiledOnContext(context) { - if (this._glProgram !== 0 && this._renderer !== context._renderer) { - throw new Error( - 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' + /** + * Copies the shader from one drawing context to another. + * + * Each `p5.Shader` object must be compiled by calling + * shader() before it can run. Compilation happens + * in a drawing context which is usually the main canvas or an instance of + * p5.Graphics. A shader can only be used in the + * context where it was compiled. The `copyToContext()` method compiles the + * shader again and copies it to another drawing context where it can be + * reused. + * + * The parameter, `context`, is the drawing context where the shader will be + * used. The shader can be copied to an instance of + * p5.Graphics, as in + * `myShader.copyToContext(pg)`. The shader can also be copied from a + * p5.Graphics object to the main canvas using + * the `window` variable, as in `myShader.copyToContext(window)`. + * + * Note: A p5.Shader object created with + * createShader(), + * createFilterShader(), or + * loadShader() + * can be used directly with a p5.Framebuffer + * object created with + * createFramebuffer(). Both objects + * have the same context as the main canvas. + * + * @param {p5|p5.Graphics} context WebGL context for the copied shader. + * @returns {p5.Shader} new shader compiled for the target context. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * varying vec2 vTexCoord; + * + * void main() { + * vec2 uv = vTexCoord; + * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); + * gl_FragColor = vec4(color, 1.0);\ + * } + * `; + * + * let pg; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Create a p5.Shader object. + * let original = createShader(vertSrc, fragSrc); + * + * // Compile the p5.Shader object. + * shader(original); + * + * // Create a p5.Graphics object. + * pg = createGraphics(50, 50, WEBGL); + * + * // Copy the original shader to the p5.Graphics object. + * let copied = original.copyToContext(pg); + * + * // Apply the copied shader to the p5.Graphics object. + * pg.shader(copied); + * + * // Style the display surface. + * pg.noStroke(); + * + * // Add a display surface for the shader. + * pg.plane(50, 50); + * + * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Draw the p5.Graphics object to the main canvas. + * image(pg, -25, -25); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * varying vec2 vTexCoord; + * + * void main() { + * vec2 uv = vTexCoord; + * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); + * gl_FragColor = vec4(color, 1.0); + * } + * `; + * + * let copied; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Graphics object. + * let pg = createGraphics(25, 25, WEBGL); + * + * // Create a p5.Shader object. + * let original = pg.createShader(vertSrc, fragSrc); + * + * // Compile the p5.Shader object. + * pg.shader(original); + * + * // Copy the original shader to the main canvas. + * copied = original.copyToContext(window); + * + * // Apply the copied shader to the main canvas. + * shader(copied); + * + * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Rotate around the x-, y-, and z-axes. + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * rotateZ(frameCount * 0.01); + * + * // Draw the box. + * box(50); + * } + * + *
+ */ + copyToContext(context) { + const shader = new Shader( + context._renderer, + this._vertSrc, + this._fragSrc ); - } else if (this._glProgram === 0) { - this._renderer = context._renderer; - this.init(); + shader.ensureCompiledOnContext(context); + return shader; } - } - /** - * Queries the active attributes for this shader and loads - * their names and locations into the attributes array. - * @private - */ - _loadAttributes() { - if (this._loadedAttributes) { - return; + /** + * @private + */ + ensureCompiledOnContext(context) { + if (this._glProgram !== 0 && this._renderer !== context._renderer) { + throw new Error( + 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' + ); + } else if (this._glProgram === 0) { + this._renderer = context._renderer; + this.init(); + } } - this.attributes = {}; - - const gl = this._renderer.GL; - - const numAttributes = gl.getProgramParameter( - this._glProgram, - gl.ACTIVE_ATTRIBUTES - ); - for (let i = 0; i < numAttributes; ++i) { - const attributeInfo = gl.getActiveAttrib(this._glProgram, i); - const name = attributeInfo.name; - const location = gl.getAttribLocation(this._glProgram, name); - const attribute = {}; - attribute.name = name; - attribute.location = location; - attribute.index = i; - attribute.type = attributeInfo.type; - attribute.size = attributeInfo.size; - this.attributes[name] = attribute; - } + /** + * Queries the active attributes for this shader and loads + * their names and locations into the attributes array. + * @private + */ + _loadAttributes() { + if (this._loadedAttributes) { + return; + } - this._loadedAttributes = true; - } + this.attributes = {}; + + const gl = this._renderer.GL; + + const numAttributes = gl.getProgramParameter( + this._glProgram, + gl.ACTIVE_ATTRIBUTES + ); + for (let i = 0; i < numAttributes; ++i) { + const attributeInfo = gl.getActiveAttrib(this._glProgram, i); + const name = attributeInfo.name; + const location = gl.getAttribLocation(this._glProgram, name); + const attribute = {}; + attribute.name = name; + attribute.location = location; + attribute.index = i; + attribute.type = attributeInfo.type; + attribute.size = attributeInfo.size; + this.attributes[name] = attribute; + } - /** - * Queries the active uniforms for this shader and loads - * their names and locations into the uniforms array. - * @private - */ - _loadUniforms() { - if (this._loadedUniforms) { - return; + this._loadedAttributes = true; } - const gl = this._renderer.GL; + /** + * Queries the active uniforms for this shader and loads + * their names and locations into the uniforms array. + * @private + */ + _loadUniforms() { + if (this._loadedUniforms) { + return; + } - // Inspect shader and cache uniform info - const numUniforms = gl.getProgramParameter( - this._glProgram, - gl.ACTIVE_UNIFORMS - ); + const gl = this._renderer.GL; - let samplerIndex = 0; - for (let i = 0; i < numUniforms; ++i) { - const uniformInfo = gl.getActiveUniform(this._glProgram, i); - const uniform = {}; - uniform.location = gl.getUniformLocation( + // Inspect shader and cache uniform info + const numUniforms = gl.getProgramParameter( this._glProgram, - uniformInfo.name + gl.ACTIVE_UNIFORMS ); - uniform.size = uniformInfo.size; - let uniformName = uniformInfo.name; - //uniforms that are arrays have their name returned as - //someUniform[0] which is a bit silly so we trim it - //off here. The size property tells us that its an array - //so we dont lose any information by doing this - if (uniformInfo.size > 1) { - uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); - } - uniform.name = uniformName; - uniform.type = uniformInfo.type; - uniform._cachedData = undefined; - if (uniform.type === gl.SAMPLER_2D) { - uniform.samplerIndex = samplerIndex; - samplerIndex++; - this.samplers.push(uniform); - } - uniform.isArray = - uniformInfo.size > 1 || - uniform.type === gl.FLOAT_MAT3 || - uniform.type === gl.FLOAT_MAT4 || - uniform.type === gl.FLOAT_VEC2 || - uniform.type === gl.FLOAT_VEC3 || - uniform.type === gl.FLOAT_VEC4 || - uniform.type === gl.INT_VEC2 || - uniform.type === gl.INT_VEC4 || - uniform.type === gl.INT_VEC3; - - this.uniforms[uniformName] = uniform; + let samplerIndex = 0; + for (let i = 0; i < numUniforms; ++i) { + const uniformInfo = gl.getActiveUniform(this._glProgram, i); + const uniform = {}; + uniform.location = gl.getUniformLocation( + this._glProgram, + uniformInfo.name + ); + uniform.size = uniformInfo.size; + let uniformName = uniformInfo.name; + //uniforms that are arrays have their name returned as + //someUniform[0] which is a bit silly so we trim it + //off here. The size property tells us that its an array + //so we dont lose any information by doing this + if (uniformInfo.size > 1) { + uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); + } + uniform.name = uniformName; + uniform.type = uniformInfo.type; + uniform._cachedData = undefined; + if (uniform.type === gl.SAMPLER_2D) { + uniform.samplerIndex = samplerIndex; + samplerIndex++; + this.samplers.push(uniform); + } + + uniform.isArray = + uniformInfo.size > 1 || + uniform.type === gl.FLOAT_MAT3 || + uniform.type === gl.FLOAT_MAT4 || + uniform.type === gl.FLOAT_VEC2 || + uniform.type === gl.FLOAT_VEC3 || + uniform.type === gl.FLOAT_VEC4 || + uniform.type === gl.INT_VEC2 || + uniform.type === gl.INT_VEC4 || + uniform.type === gl.INT_VEC3; + + this.uniforms[uniformName] = uniform; + } + this._loadedUniforms = true; } - this._loadedUniforms = true; - } - compile() { - // TODO - } + compile() { + // TODO + } - /** - * initializes (if needed) and binds the shader program. - * @private - */ - bindShader() { - this.init(); - if (!this._bound) { - this.useProgram(); - this._bound = true; + /** + * initializes (if needed) and binds the shader program. + * @private + */ + bindShader() { + this.init(); + if (!this._bound) { + this.useProgram(); + this._bound = true; - this._setMatrixUniforms(); + this._setMatrixUniforms(); - this.setUniform('uViewport', this._renderer._viewport); + this.setUniform('uViewport', this._renderer._viewport); + } } - } - /** - * @chainable - * @private - */ - unbindShader() { - if (this._bound) { - this.unbindTextures(); - //this._renderer.GL.useProgram(0); ?? - this._bound = false; + /** + * @chainable + * @private + */ + unbindShader() { + if (this._bound) { + this.unbindTextures(); + //this._renderer.GL.useProgram(0); ?? + this._bound = false; + } + return this; } - return this; - } - bindTextures() { - const gl = this._renderer.GL; + bindTextures() { + const gl = this._renderer.GL; - for (const uniform of this.samplers) { - let tex = uniform.texture; - if (tex === undefined) { - // user hasn't yet supplied a texture for this slot. - // (or there may not be one--maybe just lighting), - // so we supply a default texture instead. - tex = this._renderer._getEmptyTexture(); + for (const uniform of this.samplers) { + let tex = uniform.texture; + if (tex === undefined) { + // user hasn't yet supplied a texture for this slot. + // (or there may not be one--maybe just lighting), + // so we supply a default texture instead. + tex = this._renderer._getEmptyTexture(); + } + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + tex.bindTexture(); + tex.update(); + gl.uniform1i(uniform.location, uniform.samplerIndex); } - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - tex.bindTexture(); - tex.update(); - gl.uniform1i(uniform.location, uniform.samplerIndex); } - } - updateTextures() { - for (const uniform of this.samplers) { - const tex = uniform.texture; - if (tex) { - tex.update(); + updateTextures() { + for (const uniform of this.samplers) { + const tex = uniform.texture; + if (tex) { + tex.update(); + } } } - } - unbindTextures() { - for (const uniform of this.samplers) { - this.setUniform(uniform.name, this._renderer._getEmptyTexture()); + unbindTextures() { + for (const uniform of this.samplers) { + this.setUniform(uniform.name, this._renderer._getEmptyTexture()); + } } - } - _setMatrixUniforms() { - const modelMatrix = this._renderer.states.uModelMatrix; - const viewMatrix = this._renderer.states.uViewMatrix; - const projectionMatrix = this._renderer.states.uPMatrix; - const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); - this._renderer.states.uMVMatrix = modelViewMatrix; + _setMatrixUniforms() { + const modelMatrix = this._renderer.states.uModelMatrix; + const viewMatrix = this._renderer.states.uViewMatrix; + const projectionMatrix = this._renderer.states.uPMatrix; + const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); + this._renderer.states.uMVMatrix = modelViewMatrix; - const modelViewProjectionMatrix = modelViewMatrix.copy(); - modelViewProjectionMatrix.mult(projectionMatrix); + const modelViewProjectionMatrix = modelViewMatrix.copy(); + modelViewProjectionMatrix.mult(projectionMatrix); - if (this.isStrokeShader()) { + if (this.isStrokeShader()) { + this.setUniform( + 'uPerspective', + this._renderer.states.curCamera.useLinePerspective ? 1 : 0 + ); + } + this.setUniform('uViewMatrix', viewMatrix.mat4); + this.setUniform('uProjectionMatrix', projectionMatrix.mat4); + this.setUniform('uModelMatrix', modelMatrix.mat4); + this.setUniform('uModelViewMatrix', modelViewMatrix.mat4); this.setUniform( - 'uPerspective', - this._renderer.states.curCamera.useLinePerspective ? 1 : 0 + 'uModelViewProjectionMatrix', + modelViewProjectionMatrix.mat4 ); + if (this.uniforms.uNormalMatrix) { + this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); + this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); + } + if (this.uniforms.uCameraRotation) { + this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); + this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); + } } - this.setUniform('uViewMatrix', viewMatrix.mat4); - this.setUniform('uProjectionMatrix', projectionMatrix.mat4); - this.setUniform('uModelMatrix', modelMatrix.mat4); - this.setUniform('uModelViewMatrix', modelViewMatrix.mat4); - this.setUniform( - 'uModelViewProjectionMatrix', - modelViewProjectionMatrix.mat4 - ); - if (this.uniforms.uNormalMatrix) { - this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); - this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); - } - if (this.uniforms.uCameraRotation) { - this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); - this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); - } - } - - /** - * @chainable - * @private - */ - useProgram() { - const gl = this._renderer.GL; - if (this._renderer._curShader !== this) { - gl.useProgram(this._glProgram); - this._renderer._curShader = this; - } - return this; - } - /** - * Sets the shader’s uniform (global) variables. - * - * Shader programs run on the computer’s graphics processing unit (GPU). - * They live in part of the computer’s memory that’s completely separate - * from the sketch that runs them. Uniforms are global variables within a - * shader program. They provide a way to pass values from a sketch running - * on the CPU to a shader program running on the GPU. - * - * The first parameter, `uniformName`, is a string with the uniform’s name. - * For the shader above, `uniformName` would be `'r'`. - * - * The second parameter, `data`, is the value that should be used to set the - * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set - * the `r` uniform in the shader above to `0.5`. data should match the - * uniform’s type. Numbers, strings, booleans, arrays, and many types of - * images can all be passed to a shader with `setUniform()`. - * - * @chainable - * @param {String} uniformName name of the uniform. Must match the name - * used in the vertex and fragment shaders. - * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} - * data value to assign to the uniform. Must match the uniform’s data type. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * uniform float r; - * - * void main() { - * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); - * } - * `; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * let myShader = createShader(vertSrc, fragSrc); - * - * // Apply the p5.Shader object. - * shader(myShader); - * - * // Set the r uniform to 0.5. - * myShader.setUniform('r', 0.5); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface for the shader. - * plane(100, 100); - * - * describe('A cyan square.'); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * uniform float r; - * - * void main() { - * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); - * } - * `; - * - * let myShader; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * myShader = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(myShader); - * - * describe('A square oscillates color between cyan and white.'); - * } - * - * function draw() { - * background(200); - * - * // Style the drawing surface. - * noStroke(); - * - * // Update the r uniform. - * let nextR = 0.5 * (sin(frameCount * 0.01) + 1); - * myShader.setUniform('r', nextR); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision highp float; - * uniform vec2 p; - * uniform float r; - * const int numIterations = 500; - * varying vec2 vTexCoord; - * - * void main() { - * vec2 c = p + gl_FragCoord.xy * r; - * vec2 z = c; - * float n = 0.0; - * - * for (int i = numIterations; i > 0; i--) { - * if (z.x * z.x + z.y * z.y > 4.0) { - * n = float(i) / float(numIterations); - * break; - * } - * - * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; - * } - * - * gl_FragColor = vec4( - * 0.5 - cos(n * 17.0) / 2.0, - * 0.5 - cos(n * 13.0) / 2.0, - * 0.5 - cos(n * 23.0) / 2.0, - * 1.0 - * ); - * } - * `; - * - * let mandelbrot; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * mandelbrot = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(mandelbrot); - * - * // Set the shader uniform p to an array. - * // p is the center point of the Mandelbrot image. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); - * - * describe('A fractal image zooms in and out of focus.'); - * } - * - * function draw() { - * // Set the shader uniform r to a value that oscillates - * // between 0 and 0.005. - * // r is the size of the image in Mandelbrot-space. - * let radius = 0.005 * (sin(frameCount * 0.01) + 1); - * mandelbrot.setUniform('r', radius); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * } - * - *
- */ - setUniform(uniformName, data) { - const uniform = this.uniforms[uniformName]; - if (!uniform) { - return; + /** + * @chainable + * @private + */ + useProgram() { + const gl = this._renderer.GL; + if (this._renderer._curShader !== this) { + gl.useProgram(this._glProgram); + this._renderer._curShader = this; + } + return this; } - const gl = this._renderer.GL; - if (uniform.isArray) { - if ( - uniform._cachedData && - this._renderer._arraysEqual(uniform._cachedData, data) - ) { + /** + * Sets the shader’s uniform (global) variables. + * + * Shader programs run on the computer’s graphics processing unit (GPU). + * They live in part of the computer’s memory that’s completely separate + * from the sketch that runs them. Uniforms are global variables within a + * shader program. They provide a way to pass values from a sketch running + * on the CPU to a shader program running on the GPU. + * + * The first parameter, `uniformName`, is a string with the uniform’s name. + * For the shader above, `uniformName` would be `'r'`. + * + * The second parameter, `data`, is the value that should be used to set the + * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set + * the `r` uniform in the shader above to `0.5`. data should match the + * uniform’s type. Numbers, strings, booleans, arrays, and many types of + * images can all be passed to a shader with `setUniform()`. + * + * @chainable + * @param {String} uniformName name of the uniform. Must match the name + * used in the vertex and fragment shaders. + * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} + * data value to assign to the uniform. Must match the uniform’s data type. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * uniform float r; + * + * void main() { + * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); + * } + * `; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * let myShader = createShader(vertSrc, fragSrc); + * + * // Apply the p5.Shader object. + * shader(myShader); + * + * // Set the r uniform to 0.5. + * myShader.setUniform('r', 0.5); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface for the shader. + * plane(100, 100); + * + * describe('A cyan square.'); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * uniform float r; + * + * void main() { + * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); + * } + * `; + * + * let myShader; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * myShader = createShader(vertSrc, fragSrc); + * + * // Compile and apply the p5.Shader object. + * shader(myShader); + * + * describe('A square oscillates color between cyan and white.'); + * } + * + * function draw() { + * background(200); + * + * // Style the drawing surface. + * noStroke(); + * + * // Update the r uniform. + * let nextR = 0.5 * (sin(frameCount * 0.01) + 1); + * myShader.setUniform('r', nextR); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision highp float; + * uniform vec2 p; + * uniform float r; + * const int numIterations = 500; + * varying vec2 vTexCoord; + * + * void main() { + * vec2 c = p + gl_FragCoord.xy * r; + * vec2 z = c; + * float n = 0.0; + * + * for (int i = numIterations; i > 0; i--) { + * if (z.x * z.x + z.y * z.y > 4.0) { + * n = float(i) / float(numIterations); + * break; + * } + * + * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; + * } + * + * gl_FragColor = vec4( + * 0.5 - cos(n * 17.0) / 2.0, + * 0.5 - cos(n * 13.0) / 2.0, + * 0.5 - cos(n * 23.0) / 2.0, + * 1.0 + * ); + * } + * `; + * + * let mandelbrot; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * mandelbrot = createShader(vertSrc, fragSrc); + * + * // Compile and apply the p5.Shader object. + * shader(mandelbrot); + * + * // Set the shader uniform p to an array. + * // p is the center point of the Mandelbrot image. + * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); + * + * describe('A fractal image zooms in and out of focus.'); + * } + * + * function draw() { + * // Set the shader uniform r to a value that oscillates + * // between 0 and 0.005. + * // r is the size of the image in Mandelbrot-space. + * let radius = 0.005 * (sin(frameCount * 0.01) + 1); + * mandelbrot.setUniform('r', radius); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * } + * + *
+ */ + setUniform(uniformName, data) { + const uniform = this.uniforms[uniformName]; + if (!uniform) { return; - } else { - uniform._cachedData = data.slice(0); - } - } else if (uniform._cachedData && uniform._cachedData === data) { - return; - } else { - if (Array.isArray(data)) { - uniform._cachedData = data.slice(0); - } else { - uniform._cachedData = data; } - } - - const location = uniform.location; - - this.useProgram(); + const gl = this._renderer.GL; - switch (uniform.type) { - case gl.BOOL: - if (data === true) { - gl.uniform1i(location, 1); - } else { - gl.uniform1i(location, 0); - } - break; - case gl.INT: - if (uniform.size > 1) { - data.length && gl.uniform1iv(location, data); - } else { - gl.uniform1i(location, data); - } - break; - case gl.FLOAT: - if (uniform.size > 1) { - data.length && gl.uniform1fv(location, data); + if (uniform.isArray) { + if ( + uniform._cachedData && + this._renderer._arraysEqual(uniform._cachedData, data) + ) { + return; } else { - gl.uniform1f(location, data); + uniform._cachedData = data.slice(0); } - break; - case gl.FLOAT_MAT3: - gl.uniformMatrix3fv(location, false, data); - break; - case gl.FLOAT_MAT4: - gl.uniformMatrix4fv(location, false, data); - break; - case gl.FLOAT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2fv(location, data); - } else { - gl.uniform2f(location, data[0], data[1]); - } - break; - case gl.FLOAT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3fv(location, data); - } else { - gl.uniform3f(location, data[0], data[1], data[2]); - } - break; - case gl.FLOAT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4fv(location, data); - } else { - gl.uniform4f(location, data[0], data[1], data[2], data[3]); - } - break; - case gl.INT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2iv(location, data); - } else { - gl.uniform2i(location, data[0], data[1]); - } - break; - case gl.INT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3iv(location, data); - } else { - gl.uniform3i(location, data[0], data[1], data[2]); - } - break; - case gl.INT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4iv(location, data); + } else if (uniform._cachedData && uniform._cachedData === data) { + return; + } else { + if (Array.isArray(data)) { + uniform._cachedData = data.slice(0); } else { - gl.uniform4i(location, data[0], data[1], data[2], data[3]); + uniform._cachedData = data; } - break; - case gl.SAMPLER_2D: - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - uniform.texture = - data instanceof Texture ? data : this._renderer.getTexture(data); - gl.uniform1i(location, uniform.samplerIndex); - if (uniform.texture.src.gifProperties) { - uniform.texture.src._animateGif(this._renderer._pInst); - } - break; - //@todo complete all types + } + + const location = uniform.location; + + this.useProgram(); + + switch (uniform.type) { + case gl.BOOL: + if (data === true) { + gl.uniform1i(location, 1); + } else { + gl.uniform1i(location, 0); + } + break; + case gl.INT: + if (uniform.size > 1) { + data.length && gl.uniform1iv(location, data); + } else { + gl.uniform1i(location, data); + } + break; + case gl.FLOAT: + if (uniform.size > 1) { + data.length && gl.uniform1fv(location, data); + } else { + gl.uniform1f(location, data); + } + break; + case gl.FLOAT_MAT3: + gl.uniformMatrix3fv(location, false, data); + break; + case gl.FLOAT_MAT4: + gl.uniformMatrix4fv(location, false, data); + break; + case gl.FLOAT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2fv(location, data); + } else { + gl.uniform2f(location, data[0], data[1]); + } + break; + case gl.FLOAT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3fv(location, data); + } else { + gl.uniform3f(location, data[0], data[1], data[2]); + } + break; + case gl.FLOAT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4fv(location, data); + } else { + gl.uniform4f(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.INT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2iv(location, data); + } else { + gl.uniform2i(location, data[0], data[1]); + } + break; + case gl.INT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3iv(location, data); + } else { + gl.uniform3i(location, data[0], data[1], data[2]); + } + break; + case gl.INT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4iv(location, data); + } else { + gl.uniform4i(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.SAMPLER_2D: + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + uniform.texture = + data instanceof Texture ? data : this._renderer.getTexture(data); + gl.uniform1i(location, uniform.samplerIndex); + if (uniform.texture.src.gifProperties) { + uniform.texture.src._animateGif(this._renderer._pInst); + } + break; + //@todo complete all types + } + return this; } - return this; - } - /* NONE OF THIS IS FAST OR EFFICIENT BUT BEAR WITH ME - * - * these shader "type" query methods are used by various - * facilities of the renderer to determine if changing - * the shader type for the required action (for example, - * do we need to load the default lighting shader if the - * current shader cannot handle lighting?) - * - **/ - - isLightShader() { - return [ - this.attributes.aNormal, - this.uniforms.uUseLighting, - this.uniforms.uAmbientLightCount, - this.uniforms.uDirectionalLightCount, - this.uniforms.uPointLightCount, - this.uniforms.uAmbientColor, - this.uniforms.uDirectionalDiffuseColors, - this.uniforms.uDirectionalSpecularColors, - this.uniforms.uPointLightLocation, - this.uniforms.uPointLightDiffuseColors, - this.uniforms.uPointLightSpecularColors, - this.uniforms.uLightingDirection, - this.uniforms.uSpecular - ].some(x => x !== undefined); - } + /* NONE OF THIS IS FAST OR EFFICIENT BUT BEAR WITH ME + * + * these shader "type" query methods are used by various + * facilities of the renderer to determine if changing + * the shader type for the required action (for example, + * do we need to load the default lighting shader if the + * current shader cannot handle lighting?) + * + **/ + + isLightShader() { + return [ + this.attributes.aNormal, + this.uniforms.uUseLighting, + this.uniforms.uAmbientLightCount, + this.uniforms.uDirectionalLightCount, + this.uniforms.uPointLightCount, + this.uniforms.uAmbientColor, + this.uniforms.uDirectionalDiffuseColors, + this.uniforms.uDirectionalSpecularColors, + this.uniforms.uPointLightLocation, + this.uniforms.uPointLightDiffuseColors, + this.uniforms.uPointLightSpecularColors, + this.uniforms.uLightingDirection, + this.uniforms.uSpecular + ].some(x => x !== undefined); + } - isNormalShader() { - return this.attributes.aNormal !== undefined; - } + isNormalShader() { + return this.attributes.aNormal !== undefined; + } - isTextureShader() { - return this.samplers.length > 0; - } + isTextureShader() { + return this.samplers.length > 0; + } - isColorShader() { - return ( - this.attributes.aVertexColor !== undefined || - this.uniforms.uMaterialColor !== undefined - ); - } + isColorShader() { + return ( + this.attributes.aVertexColor !== undefined || + this.uniforms.uMaterialColor !== undefined + ); + } - isTexLightShader() { - return this.isLightShader() && this.isTextureShader(); - } + isTexLightShader() { + return this.isLightShader() && this.isTextureShader(); + } - isStrokeShader() { - return this.uniforms.uStrokeWeight !== undefined; - } + isStrokeShader() { + return this.uniforms.uStrokeWeight !== undefined; + } - /** - * @chainable - * @private - */ - enableAttrib(attr, size, type, normalized, stride, offset) { - if (attr) { - if ( - typeof IS_MINIFIED === 'undefined' && - this.attributes[attr.name] !== attr - ) { - console.warn( - `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.` - ); - } - const loc = attr.location; - if (loc !== -1) { - const gl = this._renderer.GL; - // Enable register even if it is disabled - if (!this._renderer.registerEnabled.has(loc)) { - gl.enableVertexAttribArray(loc); - // Record register availability - this._renderer.registerEnabled.add(loc); + /** + * @chainable + * @private + */ + enableAttrib(attr, size, type, normalized, stride, offset) { + if (attr) { + if ( + typeof IS_MINIFIED === 'undefined' && + this.attributes[attr.name] !== attr + ) { + console.warn( + `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.` + ); + } + const loc = attr.location; + if (loc !== -1) { + const gl = this._renderer.GL; + // Enable register even if it is disabled + if (!this._renderer.registerEnabled.has(loc)) { + gl.enableVertexAttribArray(loc); + // Record register availability + this._renderer.registerEnabled.add(loc); + } + this._renderer.GL.vertexAttribPointer( + loc, + size, + type || gl.FLOAT, + normalized || false, + stride || 0, + offset || 0 + ); } - this._renderer.GL.vertexAttribPointer( - loc, - size, - type || gl.FLOAT, - normalized || false, - stride || 0, - offset || 0 - ); } + return this; } - return this; - } - /** - * Once all buffers have been bound, this checks to see if there are any - * remaining active attributes, likely left over from previous renders, - * and disables them so that they don't affect rendering. - * @private - */ - disableRemainingAttributes() { - for (const location of this._renderer.registerEnabled.values()) { - if ( - !Object.keys(this.attributes).some( - key => this.attributes[key].location === location - ) - ) { - this._renderer.GL.disableVertexAttribArray(location); - this._renderer.registerEnabled.delete(location); + /** + * Once all buffers have been bound, this checks to see if there are any + * remaining active attributes, likely left over from previous renders, + * and disables them so that they don't affect rendering. + * @private + */ + disableRemainingAttributes() { + for (const location of this._renderer.registerEnabled.values()) { + if ( + !Object.keys(this.attributes).some( + key => this.attributes[key].location === location + ) + ) { + this._renderer.GL.disableVertexAttribArray(location); + this._renderer.registerEnabled.delete(location); + } } } - } -}; - -function shader(p5, fn){ - /** - * A class to describe a shader program. - * - * Each `p5.Shader` object contains a shader program that runs on the graphics - * processing unit (GPU). Shaders can process many pixels or vertices at the - * same time, making them fast for many graphics tasks. They’re written in a - * language called - * GLSL - * and run along with the rest of the code in a sketch. - * - * A shader program consists of two files, a vertex shader and a fragment - * shader. The vertex shader affects where 3D geometry is drawn on the screen - * and the fragment shader affects color. Once the `p5.Shader` object is - * created, it can be used with the shader() - * function, as in `shader(myShader)`. - * - * A shader can optionally describe *hooks,* which are functions in GLSL that - * users may choose to provide to customize the behavior of the shader. For the - * vertex or the fragment shader, users can pass in an object where each key is - * the type and name of a hook function, and each value is a string with the - * parameter list and default implementation of the hook. For example, to let users - * optionally run code at the start of the vertex shader, the options object could - * include: - * - * ```js - * { - * vertex: { - * 'void beforeVertex': '() {}' - * } - * } - * ``` - * - * Then, in your vertex shader source, you can run a hook by calling a function - * with the same name prefixed by `HOOK_`: - * - * ```glsl - * void main() { - * HOOK_beforeVertex(); - * // Add the rest ofy our shader code here! - * } - * ``` - * - * Note: createShader(), - * createFilterShader(), and - * loadShader() are the recommended ways to - * create an instance of this class. - * - * @class p5.Shader - * @param {p5.RendererGL} renderer WebGL context for this shader. - * @param {String} vertSrc source code for the vertex shader program. - * @param {String} fragSrc source code for the fragment shader program. - * @param {Object} [options] An optional object describing how this shader can - * be augmented with hooks. It can include: - * - `vertex`: An object describing the available vertex shader hooks. - * - `fragment`: An object describing the available frament shader hooks. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision highp float; - * - * void main() { - * // Set each pixel's RGBA value to yellow. - * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); - * } - * `; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * let myShader = createShader(vertSrc, fragSrc); - * - * // Apply the p5.Shader object. - * shader(myShader); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * - * describe('A yellow square.'); - * } - * - *
- * - *
- * - * // Note: A "uniform" is a global variable within a shader program. - * - * let mandelbrot; - * - * // Load the shader and create a p5.Shader object. - * function preload() { - * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Use the p5.Shader object. - * shader(mandelbrot); - * - * // Set the shader uniform p to an array. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); - * - * describe('A fractal image zooms in and out of focus.'); - * } - * - * function draw() { - * // Set the shader uniform r to a value that oscillates between 0 and 2. - * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); - * - * // Add a quad as a display surface for the shader. - * quad(-1, -1, 1, -1, 1, 1, -1, 1); - * } - * - *
- */ - p5.Shader = Shader; -} + }; export default shader; export { Shader }; From a68e4c38fe01c3ae36cc1b6e93334044fa401b7b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 22 Oct 2024 16:31:30 -0400 Subject: [PATCH 30/55] Put indentation back --- src/webgl/p5.RendererGL.Immediate.js | 1188 ++++++------ src/webgl/p5.RendererGL.Retained.js | 550 +++--- src/webgl/p5.Shader.js | 2564 +++++++++++++------------- 3 files changed, 2151 insertions(+), 2151 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index fe4efc6c67..fe87d237a2 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -17,649 +17,649 @@ import { Vector } from '../math/p5.Vector'; import { RenderBuffer } from './p5.RenderBuffer'; function rendererGLImmediate(p5, fn){ -/** - * Begin shape drawing. This is a helpful way of generating - * custom shapes quickly. However in WEBGL mode, application - * performance will likely drop as a result of too many calls to - * beginShape() / endShape(). As a high performance alternative, - * please use p5.js geometry primitives. - * @private - * @method beginShape - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, - * and TESS(WEBGL only) - * @chainable - */ -RendererGL.prototype.beginShape = function(mode) { - this.immediateMode.shapeMode = - mode !== undefined ? mode : constants.TESS; - if (this._useUserVertexProperties === true){ - this._resetUserVertexProperties(); - } - this.immediateMode.geometry.reset(); - this.immediateMode.contourIndices = []; - return this; -}; - -RendererGL.prototype.immediateBufferStrides = { - vertices: 1, - vertexNormals: 1, - vertexColors: 4, - vertexStrokeColors: 4, - uvs: 2 -}; - -RendererGL.prototype.beginContour = function() { - if (this.immediateMode.shapeMode !== constants.TESS) { - throw new Error('WebGL mode can only use contours with beginShape(TESS).'); - } - this.immediateMode.contourIndices.push( - this.immediateMode.geometry.vertices.length - ); -}; - -/** - * adds a vertex to be drawn in a custom Shape. - * @private - * @method vertex - * @param {Number} x x-coordinate of vertex - * @param {Number} y y-coordinate of vertex - * @param {Number} z z-coordinate of vertex - * @chainable - * @TODO implement handling of p5.Vector args - */ -RendererGL.prototype.vertex = function(x, y) { - // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn - // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra - // work to convert QUAD_STRIP here, since the only difference is in how edges - // are rendered.) - if (this.immediateMode.shapeMode === constants.QUADS) { - // A finished quad turned into triangles should leave 6 vertices in the - // buffer: - // 0--3 0 3--5 - // | | --> | \ \ | - // 1--2 1--2 4 - // When vertex index 3 is being added, add the necessary duplicates. - if (this.immediateMode.geometry.vertices.length % 6 === 3) { - for (const key in this.immediateBufferStrides) { - const stride = this.immediateBufferStrides[key]; - const buffer = this.immediateMode.geometry[key]; - buffer.push( - ...buffer.slice( - buffer.length - 3 * stride, - buffer.length - 2 * stride - ), - ...buffer.slice(buffer.length - stride, buffer.length) - ); + /** + * Begin shape drawing. This is a helpful way of generating + * custom shapes quickly. However in WEBGL mode, application + * performance will likely drop as a result of too many calls to + * beginShape() / endShape(). As a high performance alternative, + * please use p5.js geometry primitives. + * @private + * @method beginShape + * @param {Number} mode webgl primitives mode. beginShape supports the + * following modes: + * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, + * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, + * and TESS(WEBGL only) + * @chainable + */ + RendererGL.prototype.beginShape = function(mode) { + this.immediateMode.shapeMode = + mode !== undefined ? mode : constants.TESS; + if (this._useUserVertexProperties === true){ + this._resetUserVertexProperties(); + } + this.immediateMode.geometry.reset(); + this.immediateMode.contourIndices = []; + return this; + }; + + RendererGL.prototype.immediateBufferStrides = { + vertices: 1, + vertexNormals: 1, + vertexColors: 4, + vertexStrokeColors: 4, + uvs: 2 + }; + + RendererGL.prototype.beginContour = function() { + if (this.immediateMode.shapeMode !== constants.TESS) { + throw new Error('WebGL mode can only use contours with beginShape(TESS).'); + } + this.immediateMode.contourIndices.push( + this.immediateMode.geometry.vertices.length + ); + }; + + /** + * adds a vertex to be drawn in a custom Shape. + * @private + * @method vertex + * @param {Number} x x-coordinate of vertex + * @param {Number} y y-coordinate of vertex + * @param {Number} z z-coordinate of vertex + * @chainable + * @TODO implement handling of p5.Vector args + */ + RendererGL.prototype.vertex = function(x, y) { + // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn + // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra + // work to convert QUAD_STRIP here, since the only difference is in how edges + // are rendered.) + if (this.immediateMode.shapeMode === constants.QUADS) { + // A finished quad turned into triangles should leave 6 vertices in the + // buffer: + // 0--3 0 3--5 + // | | --> | \ \ | + // 1--2 1--2 4 + // When vertex index 3 is being added, add the necessary duplicates. + if (this.immediateMode.geometry.vertices.length % 6 === 3) { + for (const key in this.immediateBufferStrides) { + const stride = this.immediateBufferStrides[key]; + const buffer = this.immediateMode.geometry[key]; + buffer.push( + ...buffer.slice( + buffer.length - 3 * stride, + buffer.length - 2 * stride + ), + ...buffer.slice(buffer.length - stride, buffer.length) + ); + } } } - } - - 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]; - } - const vert = new Vector(x, y, z); - this.immediateMode.geometry.vertices.push(vert); - this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); - - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const geom = this.immediateMode.geometry; - const prop = geom.userVertexProperties[propName]; - const verts = geom.vertices; - if (prop.getSrcArray().length === 0 && verts.length > 1) { - const numMissingValues = prop.getDataSize() * (verts.length - 1); - const missingValues = Array(numMissingValues).fill(0); - prop.pushDirect(missingValues); - } - prop.pushCurrentData(); - } - - const vertexColor = this.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; - this.immediateMode.geometry.vertexColors.push( - vertexColor[0], - vertexColor[1], - vertexColor[2], - vertexColor[3] - ); - const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; - this.immediateMode.geometry.vertexStrokeColors.push( - lineVertexColor[0], - lineVertexColor[1], - lineVertexColor[2], - lineVertexColor[3] - ); - - if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { - if (this.states._tex !== null) { - if (this.states._tex.width > 0 && this.states._tex.height > 0) { - u /= this.states._tex.width; - v /= this.states._tex.height; + + 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]; + } + const vert = new Vector(x, y, z); + this.immediateMode.geometry.vertices.push(vert); + this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); + + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const geom = this.immediateMode.geometry; + const prop = geom.userVertexProperties[propName]; + const verts = geom.vertices; + if (prop.getSrcArray().length === 0 && verts.length > 1) { + const numMissingValues = prop.getDataSize() * (verts.length - 1); + const missingValues = Array(numMissingValues).fill(0); + prop.pushDirect(missingValues); } - } else if ( - this.states.userFillShader !== undefined || - this.states.userStrokeShader !== undefined || - this.states.userPointShader !== undefined || - this.states.userImageShader !== undefined - ) { - // Do nothing if user-defined shaders are present - } else if ( - this.states._tex === null && - arguments.length >= 4 - ) { - // Only throw this warning if custom uv's have been provided - console.warn( - 'You must first call texture() before using' + - ' vertex() with image based u and v coordinates' - ); + prop.pushCurrentData(); } - } - this.immediateMode.geometry.uvs.push(u, v); + const vertexColor = this.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; + this.immediateMode.geometry.vertexColors.push( + vertexColor[0], + vertexColor[1], + vertexColor[2], + vertexColor[3] + ); + const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; + this.immediateMode.geometry.vertexStrokeColors.push( + lineVertexColor[0], + lineVertexColor[1], + lineVertexColor[2], + lineVertexColor[3] + ); - this.immediateMode._bezierVertex[0] = x; - this.immediateMode._bezierVertex[1] = y; - this.immediateMode._bezierVertex[2] = z; + if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { + if (this.states._tex !== null) { + if (this.states._tex.width > 0 && this.states._tex.height > 0) { + u /= this.states._tex.width; + v /= this.states._tex.height; + } + } else if ( + this.states.userFillShader !== undefined || + this.states.userStrokeShader !== undefined || + this.states.userPointShader !== undefined || + this.states.userImageShader !== undefined + ) { + // Do nothing if user-defined shaders are present + } else if ( + this.states._tex === null && + arguments.length >= 4 + ) { + // Only throw this warning if custom uv's have been provided + console.warn( + 'You must first call texture() before using' + + ' vertex() with image based u and v coordinates' + ); + } + } - this.immediateMode._quadraticVertex[0] = x; - this.immediateMode._quadraticVertex[1] = y; - this.immediateMode._quadraticVertex[2] = z; + this.immediateMode.geometry.uvs.push(u, v); - return this; -}; + this.immediateMode._bezierVertex[0] = x; + this.immediateMode._bezierVertex[1] = y; + this.immediateMode._bezierVertex[2] = z; -RendererGL.prototype.vertexProperty = function(propertyName, data){ - if(!this._useUserVertexProperties){ - this._useUserVertexProperties = true; - this.immediateMode.geometry.userVertexProperties = {}; - } - const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; - let prop; - if (propertyExists){ - prop = this.immediateMode.geometry.userVertexProperties[propertyName]; - } - else { - prop = this.immediateMode.geometry._userVertexPropertyHelper(propertyName, data); - this.tessyVertexSize += prop.getDataSize(); - this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); - this.immediateMode.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) - ); - } - prop.setCurrentData(data); -}; - -RendererGL.prototype._resetUserVertexProperties = function(){ - const properties = this.immediateMode.geometry.userVertexProperties; - for (const propName in properties){ - const prop = properties[propName]; - delete this.immediateBufferStrides[propName]; - prop.delete(); - } - this._useUserVertexProperties = false; - this.tessyVertexSize = 12; - this.immediateMode.geometry.userVertexProperties = {}; - this.immediateMode.buffers.user = []; -}; + this.immediateMode._quadraticVertex[0] = x; + this.immediateMode._quadraticVertex[1] = y; + this.immediateMode._quadraticVertex[2] = z; -/** - * Sets the normal to use for subsequent vertices. - * @private - * @method normal - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @chainable - * - * @method normal - * @param {Vector} v - * @chainable - */ -RendererGL.prototype.normal = function(xorv, y, z) { - if (xorv instanceof Vector) { - this.states._currentNormal = xorv; - } else { - this.states._currentNormal = new Vector(xorv, y, z); - } + return this; + }; - return this; -}; + RendererGL.prototype.vertexProperty = function(propertyName, data){ + if(!this._useUserVertexProperties){ + this._useUserVertexProperties = true; + this.immediateMode.geometry.userVertexProperties = {}; + } + const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; + let prop; + if (propertyExists){ + prop = this.immediateMode.geometry.userVertexProperties[propertyName]; + } + else { + prop = this.immediateMode.geometry._userVertexPropertyHelper(propertyName, data); + this.tessyVertexSize += prop.getDataSize(); + this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); + this.immediateMode.buffers.user.push( + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) + ); + } + prop.setCurrentData(data); + }; + + RendererGL.prototype._resetUserVertexProperties = function(){ + const properties = this.immediateMode.geometry.userVertexProperties; + for (const propName in properties){ + const prop = properties[propName]; + delete this.immediateBufferStrides[propName]; + prop.delete(); + } + this._useUserVertexProperties = false; + this.tessyVertexSize = 12; + this.immediateMode.geometry.userVertexProperties = {}; + this.immediateMode.buffers.user = []; + }; + + /** + * Sets the normal to use for subsequent vertices. + * @private + * @method normal + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @chainable + * + * @method normal + * @param {Vector} v + * @chainable + */ + RendererGL.prototype.normal = function(xorv, y, z) { + if (xorv instanceof Vector) { + this.states._currentNormal = xorv; + } else { + this.states._currentNormal = new Vector(xorv, y, z); + } -/** - * End shape drawing and render vertices to screen. - * @chainable - */ -RendererGL.prototype.endShape = function( - mode, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind, - count = 1 -) { - if (this.immediateMode.shapeMode === constants.POINTS) { - this._drawPoints( - this.immediateMode.geometry.vertices, - this.immediateMode.buffers.point - ); return this; - } - // When we are drawing a shape then the shape mode is TESS, - // but in case of triangle we can skip the breaking into small triangle - // this can optimize performance by skipping the step of breaking it into triangles - if (this.immediateMode.geometry.vertices.length === 3 && - this.immediateMode.shapeMode === constants.TESS - ) { - this.immediateMode.shapeMode === constants.TRIANGLES; - } - - this.isProcessingVertices = true; - this._processVertices(...arguments); - this.isProcessingVertices = false; - - // LINE_STRIP and LINES are not used for rendering, instead - // they only indicate a way to modify vertices during the _processVertices() step - let is_line = false; - if ( - this.immediateMode.shapeMode === constants.LINE_STRIP || - this.immediateMode.shapeMode === constants.LINES + }; + + /** + * End shape drawing and render vertices to screen. + * @chainable + */ + RendererGL.prototype.endShape = function( + mode, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind, + count = 1 ) { - this.immediateMode.shapeMode = constants.TRIANGLE_FAN; - is_line = true; - } - - // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we - // need to convert them to a supported format. In `vertex()`, we reformat - // the input data into the formats specified below. - if (this.immediateMode.shapeMode === constants.QUADS) { - this.immediateMode.shapeMode = constants.TRIANGLES; - } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { - this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; - } - - if (this.states.doFill && !is_line) { - if ( - !this.geometryBuilder && - this.immediateMode.geometry.vertices.length >= 3 + if (this.immediateMode.shapeMode === constants.POINTS) { + this._drawPoints( + this.immediateMode.geometry.vertices, + this.immediateMode.buffers.point + ); + return this; + } + // When we are drawing a shape then the shape mode is TESS, + // but in case of triangle we can skip the breaking into small triangle + // this can optimize performance by skipping the step of breaking it into triangles + if (this.immediateMode.geometry.vertices.length === 3 && + this.immediateMode.shapeMode === constants.TESS ) { - this._drawImmediateFill(count); + this.immediateMode.shapeMode === constants.TRIANGLES; } - } - if (this.states.doStroke) { + + this.isProcessingVertices = true; + this._processVertices(...arguments); + this.isProcessingVertices = false; + + // LINE_STRIP and LINES are not used for rendering, instead + // they only indicate a way to modify vertices during the _processVertices() step + let is_line = false; if ( - !this.geometryBuilder && - this.immediateMode.geometry.lineVertices.length >= 1 + this.immediateMode.shapeMode === constants.LINE_STRIP || + this.immediateMode.shapeMode === constants.LINES ) { - this._drawImmediateStroke(); + this.immediateMode.shapeMode = constants.TRIANGLE_FAN; + is_line = true; } - } - if (this.geometryBuilder) { - this.geometryBuilder.addImmediate(); - } + // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we + // need to convert them to a supported format. In `vertex()`, we reformat + // the input data into the formats specified below. + if (this.immediateMode.shapeMode === constants.QUADS) { + this.immediateMode.shapeMode = constants.TRIANGLES; + } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { + this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; + } - this.isBezier = false; - this.isQuadratic = false; - this.isCurve = false; - this.immediateMode._bezierVertex.length = 0; - this.immediateMode._quadraticVertex.length = 0; - this.immediateMode._curveVertex.length = 0; + if (this.states.doFill && !is_line) { + if ( + !this.geometryBuilder && + this.immediateMode.geometry.vertices.length >= 3 + ) { + this._drawImmediateFill(count); + } + } + if (this.states.doStroke) { + if ( + !this.geometryBuilder && + this.immediateMode.geometry.lineVertices.length >= 1 + ) { + this._drawImmediateStroke(); + } + } - return this; -}; + if (this.geometryBuilder) { + this.geometryBuilder.addImmediate(); + } -/** - * Called from endShape(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) - */ -RendererGL.prototype._processVertices = function(mode) { - if (this.immediateMode.geometry.vertices.length === 0) return; - - const calculateStroke = this.states.doStroke; - const shouldClose = mode === constants.CLOSE; - if (calculateStroke) { - this.immediateMode.geometry.edges = this._calculateEdges( - this.immediateMode.shapeMode, - this.immediateMode.geometry.vertices, - shouldClose - ); - if (!this.geometryBuilder) { - this.immediateMode.geometry._edgesToVertices(); - } - } - // For hollow shapes, user must set mode to TESS - const convexShape = this.immediateMode.shapeMode === constants.TESS; - // If the shape has a contour, we have to re-triangulate to cut out the - // contour region - const hasContour = this.immediateMode.contourIndices.length > 0; - // We tesselate when drawing curves or convex shapes - const shouldTess = - this.states.doFill && - ( - this.isBezier || - this.isQuadratic || - this.isCurve || - convexShape || - hasContour - ) && - this.immediateMode.shapeMode !== constants.LINES; - - if (shouldTess) { - this._tesselateShape(); - } -}; + this.isBezier = false; + this.isQuadratic = false; + this.isCurve = false; + this.immediateMode._bezierVertex.length = 0; + this.immediateMode._quadraticVertex.length = 0; + this.immediateMode._curveVertex.length = 0; -/** - * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @returns {Number[]} indices for custom shape vertices indicating edges. - */ -RendererGL.prototype._calculateEdges = function( - shapeMode, - verts, - shouldClose -) { - const res = []; - let i = 0; - const contourIndices = this.immediateMode.contourIndices.slice(); - let contourStart = 0; - switch (shapeMode) { - case constants.TRIANGLE_STRIP: - for (i = 0; i < verts.length - 2; i++) { - res.push([i, i + 1]); - res.push([i, i + 2]); - } - res.push([i, i + 1]); - break; - case constants.TRIANGLE_FAN: - for (i = 1; i < verts.length - 1; i++) { - res.push([0, i]); - res.push([i, i + 1]); - } - res.push([0, verts.length - 1]); - break; - case constants.TRIANGLES: - for (i = 0; i < verts.length - 2; i = i + 3) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 2, i]); - } - break; - case constants.LINES: - for (i = 0; i < verts.length - 1; i = i + 2) { - res.push([i, i + 1]); - } - break; - case constants.QUADS: - // Quads have been broken up into two triangles by `vertex()`: - // 0 3--5 - // | \ \ | - // 1--2 4 - for (i = 0; i < verts.length - 5; i += 6) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 3, i + 5]); - res.push([i + 4, i + 5]); + return this; + }; + + /** + * Called from endShape(). This function calculates the stroke vertices for custom shapes and + * tesselates shapes when applicable. + * @private + * @param {Number} mode webgl primitives mode. beginShape supports the + * following modes: + * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, + * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) + */ + RendererGL.prototype._processVertices = function(mode) { + if (this.immediateMode.geometry.vertices.length === 0) return; + + const calculateStroke = this.states.doStroke; + const shouldClose = mode === constants.CLOSE; + if (calculateStroke) { + this.immediateMode.geometry.edges = this._calculateEdges( + this.immediateMode.shapeMode, + this.immediateMode.geometry.vertices, + shouldClose + ); + if (!this.geometryBuilder) { + this.immediateMode.geometry._edgesToVertices(); } - break; - case constants.QUAD_STRIP: - // 0---2---4 - // | | | - // 1---3---5 - for (i = 0; i < verts.length - 2; i += 2) { + } + // For hollow shapes, user must set mode to TESS + const convexShape = this.immediateMode.shapeMode === constants.TESS; + // If the shape has a contour, we have to re-triangulate to cut out the + // contour region + const hasContour = this.immediateMode.contourIndices.length > 0; + // We tesselate when drawing curves or convex shapes + const shouldTess = + this.states.doFill && + ( + this.isBezier || + this.isQuadratic || + this.isCurve || + convexShape || + hasContour + ) && + this.immediateMode.shapeMode !== constants.LINES; + + if (shouldTess) { + this._tesselateShape(); + } + }; + + /** + * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and + * tesselates shapes when applicable. + * @private + * @returns {Number[]} indices for custom shape vertices indicating edges. + */ + RendererGL.prototype._calculateEdges = function( + shapeMode, + verts, + shouldClose + ) { + const res = []; + let i = 0; + const contourIndices = this.immediateMode.contourIndices.slice(); + let contourStart = 0; + switch (shapeMode) { + case constants.TRIANGLE_STRIP: + for (i = 0; i < verts.length - 2; i++) { + res.push([i, i + 1]); + res.push([i, i + 2]); + } res.push([i, i + 1]); - res.push([i, i + 2]); - res.push([i + 1, i + 3]); - } - res.push([i, i + 1]); - break; - default: - // TODO: handle contours in other modes too - for (i = 0; i < verts.length; i++) { - // Handle breaks between contours - if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { + break; + case constants.TRIANGLE_FAN: + for (i = 1; i < verts.length - 1; i++) { + res.push([0, i]); res.push([i, i + 1]); - } else { - if (shouldClose || contourStart) { - res.push([i, contourStart]); - } - if (contourIndices.length > 0) { - contourStart = contourIndices.shift(); + } + res.push([0, verts.length - 1]); + break; + case constants.TRIANGLES: + for (i = 0; i < verts.length - 2; i = i + 3) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 2, i]); + } + break; + case constants.LINES: + for (i = 0; i < verts.length - 1; i = i + 2) { + res.push([i, i + 1]); + } + break; + case constants.QUADS: + // Quads have been broken up into two triangles by `vertex()`: + // 0 3--5 + // | \ \ | + // 1--2 4 + for (i = 0; i < verts.length - 5; i += 6) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 3, i + 5]); + res.push([i + 4, i + 5]); + } + break; + case constants.QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + for (i = 0; i < verts.length - 2; i += 2) { + res.push([i, i + 1]); + res.push([i, i + 2]); + res.push([i + 1, i + 3]); + } + res.push([i, i + 1]); + break; + default: + // TODO: handle contours in other modes too + for (i = 0; i < verts.length; i++) { + // Handle breaks between contours + if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { + res.push([i, i + 1]); + } else { + if (shouldClose || contourStart) { + res.push([i, contourStart]); + } + if (contourIndices.length > 0) { + contourStart = contourIndices.shift(); + } } } + break; + } + if (shapeMode !== constants.TESS && shouldClose) { + res.push([verts.length - 1, 0]); + } + return res; + }; + + /** + * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. + * @private + */ + RendererGL.prototype._tesselateShape = function() { + // TODO: handle non-TESS shape modes that have contours + this.immediateMode.shapeMode = constants.TRIANGLES; + const contours = [[]]; + for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { + if ( + this.immediateMode.contourIndices.length > 0 && + this.immediateMode.contourIndices[0] === i + ) { + this.immediateMode.contourIndices.shift(); + contours.push([]); } - break; - } - if (shapeMode !== constants.TESS && shouldClose) { - res.push([verts.length - 1, 0]); - } - return res; -}; - -/** - * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. - * @private - */ -RendererGL.prototype._tesselateShape = function() { - // TODO: handle non-TESS shape modes that have contours - this.immediateMode.shapeMode = constants.TRIANGLES; - const contours = [[]]; - for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { - if ( - this.immediateMode.contourIndices.length > 0 && - this.immediateMode.contourIndices[0] === i - ) { - this.immediateMode.contourIndices.shift(); - contours.push([]); - } - contours[contours.length-1].push( - this.immediateMode.geometry.vertices[i].x, - this.immediateMode.geometry.vertices[i].y, - this.immediateMode.geometry.vertices[i].z, - this.immediateMode.geometry.uvs[i * 2], - this.immediateMode.geometry.uvs[i * 2 + 1], - this.immediateMode.geometry.vertexColors[i * 4], - this.immediateMode.geometry.vertexColors[i * 4 + 1], - this.immediateMode.geometry.vertexColors[i * 4 + 2], - this.immediateMode.geometry.vertexColors[i * 4 + 3], - this.immediateMode.geometry.vertexNormals[i].x, - this.immediateMode.geometry.vertexNormals[i].y, - this.immediateMode.geometry.vertexNormals[i].z - ); + contours[contours.length-1].push( + this.immediateMode.geometry.vertices[i].x, + this.immediateMode.geometry.vertices[i].y, + this.immediateMode.geometry.vertices[i].z, + this.immediateMode.geometry.uvs[i * 2], + this.immediateMode.geometry.uvs[i * 2 + 1], + this.immediateMode.geometry.vertexColors[i * 4], + this.immediateMode.geometry.vertexColors[i * 4 + 1], + this.immediateMode.geometry.vertexColors[i * 4 + 2], + this.immediateMode.geometry.vertexColors[i * 4 + 3], + this.immediateMode.geometry.vertexNormals[i].x, + this.immediateMode.geometry.vertexNormals[i].y, + this.immediateMode.geometry.vertexNormals[i].z + ); + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const prop = this.immediateMode.geometry.userVertexProperties[propName]; + const start = i * prop.getDataSize(); + const end = start + prop.getDataSize(); + const vals = prop.getSrcArray().slice(start, end); + contours[contours.length-1].push(...vals); + } + } + const polyTriangles = this._triangulate(contours); + const originalVertices = this.immediateMode.geometry.vertices; + this.immediateMode.geometry.vertices = []; + this.immediateMode.geometry.vertexNormals = []; + this.immediateMode.geometry.uvs = []; for (const propName in this.immediateMode.geometry.userVertexProperties){ const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const start = i * prop.getDataSize(); - const end = start + prop.getDataSize(); - const vals = prop.getSrcArray().slice(start, end); - contours[contours.length-1].push(...vals); - } - } - const polyTriangles = this._triangulate(contours); - const originalVertices = this.immediateMode.geometry.vertices; - this.immediateMode.geometry.vertices = []; - this.immediateMode.geometry.vertexNormals = []; - this.immediateMode.geometry.uvs = []; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - prop.resetSrcArray(); - } - const colors = []; - for ( - let j = 0, polyTriLength = polyTriangles.length; - j < polyTriLength; - j = j + this.tessyVertexSize - ) { - colors.push(...polyTriangles.slice(j + 5, j + 9)); - this.normal(...polyTriangles.slice(j + 9, j + 12)); - { - let offset = 12; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - const start = j + offset; - const end = start + size; - prop.setCurrentData(polyTriangles.slice(start, end)); - offset += size; + prop.resetSrcArray(); + } + const colors = []; + for ( + let j = 0, polyTriLength = polyTriangles.length; + j < polyTriLength; + j = j + this.tessyVertexSize + ) { + colors.push(...polyTriangles.slice(j + 5, j + 9)); + this.normal(...polyTriangles.slice(j + 9, j + 12)); + { + let offset = 12; + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const prop = this.immediateMode.geometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + const start = j + offset; + const end = start + size; + prop.setCurrentData(polyTriangles.slice(start, end)); + offset += size; + } } + this.vertex(...polyTriangles.slice(j, j + 5)); } - this.vertex(...polyTriangles.slice(j, j + 5)); - } - if (this.geometryBuilder) { - // Tesselating the face causes the indices of edge vertices to stop being - // correct. When rendering, this is not a problem, since _edgesToVertices - // will have been called before this, and edge vertex indices are no longer - // needed. However, the geometry builder still needs this information, so - // when one is active, we need to update the indices. - // - // We record index mappings in a Map so that once we have found a - // corresponding vertex, we don't need to loop to find it again. - const newIndex = new Map(); - this.immediateMode.geometry.edges = - this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => { - if (!newIndex.has(origIdx)) { - const orig = originalVertices[origIdx]; - let newVertIndex = this.immediateMode.geometry.vertices.findIndex( - v => - orig.x === v.x && - orig.y === v.y && - orig.z === v.z - ); - if (newVertIndex === -1) { - // The tesselation process didn't output a vertex with the exact - // coordinate as before, potentially due to numerical issues. This - // doesn't happen often, but in this case, pick the closest point - let closestDist = Infinity; - let closestIndex = 0; - for ( - let i = 0; - i < this.immediateMode.geometry.vertices.length; - i++ - ) { - const vert = this.immediateMode.geometry.vertices[i]; - const dX = orig.x - vert.x; - const dY = orig.y - vert.y; - const dZ = orig.z - vert.z; - const dist = dX*dX + dY*dY + dZ*dZ; - if (dist < closestDist) { - closestDist = dist; - closestIndex = i; + if (this.geometryBuilder) { + // Tesselating the face causes the indices of edge vertices to stop being + // correct. When rendering, this is not a problem, since _edgesToVertices + // will have been called before this, and edge vertex indices are no longer + // needed. However, the geometry builder still needs this information, so + // when one is active, we need to update the indices. + // + // We record index mappings in a Map so that once we have found a + // corresponding vertex, we don't need to loop to find it again. + const newIndex = new Map(); + this.immediateMode.geometry.edges = + this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => { + if (!newIndex.has(origIdx)) { + const orig = originalVertices[origIdx]; + let newVertIndex = this.immediateMode.geometry.vertices.findIndex( + v => + orig.x === v.x && + orig.y === v.y && + orig.z === v.z + ); + if (newVertIndex === -1) { + // The tesselation process didn't output a vertex with the exact + // coordinate as before, potentially due to numerical issues. This + // doesn't happen often, but in this case, pick the closest point + let closestDist = Infinity; + let closestIndex = 0; + for ( + let i = 0; + i < this.immediateMode.geometry.vertices.length; + i++ + ) { + const vert = this.immediateMode.geometry.vertices[i]; + const dX = orig.x - vert.x; + const dY = orig.y - vert.y; + const dZ = orig.z - vert.z; + const dist = dX*dX + dY*dY + dZ*dZ; + if (dist < closestDist) { + closestDist = dist; + closestIndex = i; + } } + newVertIndex = closestIndex; } - newVertIndex = closestIndex; + newIndex.set(origIdx, newVertIndex); } - newIndex.set(origIdx, newVertIndex); - } - return newIndex.get(origIdx); - })); - } - this.immediateMode.geometry.vertexColors = colors; -}; - -/** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. - * @private - */ -RendererGL.prototype._drawImmediateFill = function(count = 1) { - const gl = this.GL; - this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); + return newIndex.get(origIdx); + })); + } + this.immediateMode.geometry.vertexColors = colors; + }; - let shader; - shader = this._getFillShader(); + /** + * Called from endShape(). Responsible for calculating normals, setting shader uniforms, + * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. + * @private + */ + RendererGL.prototype._drawImmediateFill = function(count = 1) { + const gl = this.GL; + this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); - this._setFillUniforms(shader); + let shader; + shader = this._getFillShader(); - for (const buff of this.immediateMode.buffers.fill) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - shader.disableRemainingAttributes(); + this._setFillUniforms(shader); - this._applyColorBlend( - this.states.curFillColor, - this.immediateMode.geometry.hasFillTransparency() - ); + for (const buff of this.immediateMode.buffers.fill) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + shader.disableRemainingAttributes(); - if (count === 1) { - gl.drawArrays( - this.immediateMode.shapeMode, - 0, - this.immediateMode.geometry.vertices.length + this._applyColorBlend( + this.states.curFillColor, + this.immediateMode.geometry.hasFillTransparency() ); - } - else { - try { - gl.drawArraysInstanced( + + if (count === 1) { + gl.drawArrays( this.immediateMode.shapeMode, 0, - this.immediateMode.geometry.vertices.length, - count + this.immediateMode.geometry.vertices.length ); } - catch (e) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + else { + try { + gl.drawArraysInstanced( + this.immediateMode.shapeMode, + 0, + this.immediateMode.geometry.vertices.length, + count + ); + } + catch (e) { + console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + } + } + shader.unbindShader(); + }; + + /** + * Called from endShape(). Responsible for calculating normals, setting shader uniforms, + * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. + * @private + */ + RendererGL.prototype._drawImmediateStroke = function() { + const gl = this.GL; + + this._useLineColor = + (this.immediateMode.geometry.vertexStrokeColors.length > 0); + + const shader = this._getImmediateStrokeShader(); + this._setStrokeUniforms(shader); + for (const buff of this.immediateMode.buffers.stroke) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); } - } - shader.unbindShader(); -}; + shader.disableRemainingAttributes(); + this._applyColorBlend( + this.states.curStrokeColor, + this.immediateMode.geometry.hasFillTransparency() + ); -/** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. - * @private - */ -RendererGL.prototype._drawImmediateStroke = function() { - const gl = this.GL; - - this._useLineColor = - (this.immediateMode.geometry.vertexStrokeColors.length > 0); - - const shader = this._getImmediateStrokeShader(); - this._setStrokeUniforms(shader); - for (const buff of this.immediateMode.buffers.stroke) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - shader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - this.immediateMode.geometry.hasFillTransparency() - ); - - gl.drawArrays( - gl.TRIANGLES, - 0, - this.immediateMode.geometry.lineVertices.length / 3 - ); - shader.unbindShader(); -}; + gl.drawArrays( + gl.TRIANGLES, + 0, + this.immediateMode.geometry.lineVertices.length / 3 + ); + shader.unbindShader(); + }; } export default rendererGLImmediate; diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 3e065dc240..780e4183cc 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -5,301 +5,301 @@ import { RendererGL } from './p5.RendererGL'; import { RenderBuffer } from './p5.RenderBuffer'; function rendererGLRetained(p5, fn){ -/** - * @param {p5.Geometry} geometry The model whose resources will be freed - */ -RendererGL.prototype.freeGeometry = function(geometry) { - if (!geometry.gid) { - console.warn('The model you passed to freeGeometry does not have an id!'); - return; - } - this._freeBuffers(geometry.gid); -}; - -/** - * _initBufferDefaults - * @private - * @description initializes buffer defaults. runs each time a new geometry is - * registered - * @param {String} gId key of the geometry object - * @returns {Object} a new buffer object - */ -RendererGL.prototype._initBufferDefaults = function(gId) { - this._freeBuffers(gId); - - //@TODO remove this limit on hashes in retainedMode.geometry - if (Object.keys(this.retainedMode.geometry).length > 1000) { - const key = Object.keys(this.retainedMode.geometry)[0]; - this._freeBuffers(key); - } - - //create a new entry in our retainedMode.geometry - return (this.retainedMode.geometry[gId] = {}); -}; - -RendererGL.prototype._freeBuffers = function(gId) { - const buffers = this.retainedMode.geometry[gId]; - if (!buffers) { - return; - } - - delete this.retainedMode.geometry[gId]; - - const gl = this.GL; - if (buffers.indexBuffer) { - gl.deleteBuffer(buffers.indexBuffer); - } - - function freeBuffers(defs) { - for (const def of defs) { - if (buffers[def.dst]) { - gl.deleteBuffer(buffers[def.dst]); - buffers[def.dst] = null; - } + /** + * @param {p5.Geometry} geometry The model whose resources will be freed + */ + RendererGL.prototype.freeGeometry = function(geometry) { + if (!geometry.gid) { + console.warn('The model you passed to freeGeometry does not have an id!'); + return; } - } - - // free all the buffers - freeBuffers(this.retainedMode.buffers.stroke); - freeBuffers(this.retainedMode.buffers.fill); - freeBuffers(this.retainedMode.buffers.user); - this.retainedMode.buffers.user = []; -}; - -/** - * creates a buffers object that holds the WebGL render buffers - * for a geometry. - * @private - * @param {String} gId key of the geometry object - * @param {p5.Geometry} model contains geometry data - */ -RendererGL.prototype.createBuffers = function(gId, model) { - const gl = this.GL; - //initialize the gl buffers for our geom groups - const buffers = this._initBufferDefaults(gId); - buffers.model = model; - - let indexBuffer = buffers.indexBuffer; - - if (model.faces.length) { - // allocate space for faces - if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); - const vals = RendererGL.prototype._flatten(model.faces); - - // If any face references a vertex with an index greater than the maximum - // un-singed 16 bit integer, then we need to use a Uint32Array instead of a - // Uint16Array - const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); - let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; - this._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); - - // If we're using a Uint32Array for our indexBuffer we will need to pass a - // different enum value to WebGL draw triangles. This happens in - // the _drawElements function. - buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 - ? gl.UNSIGNED_INT - : gl.UNSIGNED_SHORT; - - // the vertex count is based on the number of faces - buffers.vertexCount = model.faces.length * 3; - } else { - // the index buffer is unused, remove it - if (indexBuffer) { - gl.deleteBuffer(indexBuffer); - buffers.indexBuffer = null; + this._freeBuffers(geometry.gid); + }; + + /** + * _initBufferDefaults + * @private + * @description initializes buffer defaults. runs each time a new geometry is + * registered + * @param {String} gId key of the geometry object + * @returns {Object} a new buffer object + */ + RendererGL.prototype._initBufferDefaults = function(gId) { + this._freeBuffers(gId); + + //@TODO remove this limit on hashes in retainedMode.geometry + if (Object.keys(this.retainedMode.geometry).length > 1000) { + const key = Object.keys(this.retainedMode.geometry)[0]; + this._freeBuffers(key); } - // the vertex count comes directly from the model - buffers.vertexCount = model.vertices ? model.vertices.length : 0; - } - - buffers.lineVertexCount = model.lineVertices - ? model.lineVertices.length / 3 - : 0; - - for (const propName in model.userVertexProperties){ - const prop = model.userVertexProperties[propName]; - this.retainedMode.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) - ); - } - return buffers; -}; - -/** - * Draws buffers given a geometry key ID - * @private - * @param {String} gId ID in our geom hash - * @chainable - */ -RendererGL.prototype.drawBuffers = function(gId) { - const gl = this.GL; - const geometry = this.retainedMode.geometry[gId]; - - if ( - !this.geometryBuilder && - this.states.doFill && - geometry.vertexCount > 0 - ) { - this._useVertexColor = (geometry.model.vertexColors.length > 0); - let fillShader; - if (this._drawingFilter && this.states.userFillShader) { - fillShader = this.states.userFillShader; - } else { - fillShader = this._getFillShader(); + //create a new entry in our retainedMode.geometry + return (this.retainedMode.geometry[gId] = {}); + }; + + RendererGL.prototype._freeBuffers = function(gId) { + const buffers = this.retainedMode.geometry[gId]; + if (!buffers) { + return; } - this._setFillUniforms(fillShader); - for (const buff of this.retainedMode.buffers.fill) { - buff._prepareBuffer(geometry, fillShader); + delete this.retainedMode.geometry[gId]; + + const gl = this.GL; + if (buffers.indexBuffer) { + gl.deleteBuffer(buffers.indexBuffer); } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + + function freeBuffers(defs) { + for (const def of defs) { + if (buffers[def.dst]) { + gl.deleteBuffer(buffers[def.dst]); + buffers[def.dst] = null; + } } - buff._prepareBuffer(geometry, fillShader); - } - fillShader.disableRemainingAttributes(); - if (geometry.indexBuffer) { - //vertex index buffer - this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); - } - this._applyColorBlend( - this.states.curFillColor, - geometry.model.hasFillTransparency() - ); - this._drawElements(gl.TRIANGLES, gId); - fillShader.unbindShader(); - } - - if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { - this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); - const strokeShader = this._getRetainedStrokeShader(); - this._setStrokeUniforms(strokeShader); - for (const buff of this.retainedMode.buffers.stroke) { - buff._prepareBuffer(geometry, strokeShader); } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + + // free all the buffers + freeBuffers(this.retainedMode.buffers.stroke); + freeBuffers(this.retainedMode.buffers.fill); + freeBuffers(this.retainedMode.buffers.user); + this.retainedMode.buffers.user = []; + }; + + /** + * creates a buffers object that holds the WebGL render buffers + * for a geometry. + * @private + * @param {String} gId key of the geometry object + * @param {p5.Geometry} model contains geometry data + */ + RendererGL.prototype.createBuffers = function(gId, model) { + const gl = this.GL; + //initialize the gl buffers for our geom groups + const buffers = this._initBufferDefaults(gId); + buffers.model = model; + + let indexBuffer = buffers.indexBuffer; + + if (model.faces.length) { + // allocate space for faces + if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); + const vals = RendererGL.prototype._flatten(model.faces); + + // If any face references a vertex with an index greater than the maximum + // un-singed 16 bit integer, then we need to use a Uint32Array instead of a + // Uint16Array + const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); + let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; + this._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); + + // If we're using a Uint32Array for our indexBuffer we will need to pass a + // different enum value to WebGL draw triangles. This happens in + // the _drawElements function. + buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 + ? gl.UNSIGNED_INT + : gl.UNSIGNED_SHORT; + + // the vertex count is based on the number of faces + buffers.vertexCount = model.faces.length * 3; + } else { + // the index buffer is unused, remove it + if (indexBuffer) { + gl.deleteBuffer(indexBuffer); + buffers.indexBuffer = null; } - buff._prepareBuffer(geometry, strokeShader); + // the vertex count comes directly from the model + buffers.vertexCount = model.vertices ? model.vertices.length : 0; } - strokeShader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - geometry.model.hasStrokeTransparency() - ); - this._drawArrays(gl.TRIANGLES, gId); - strokeShader.unbindShader(); - } - - if (this.geometryBuilder) { - this.geometryBuilder.addRetained(geometry); - } - - return this; -}; - -/** - * Calls drawBuffers() with a scaled model/view matrix. - * - * This is used by various 3d primitive methods (in primitives.js, eg. plane, - * box, torus, etc...) to allow caching of un-scaled geometries. Those - * geometries are generally created with unit-length dimensions, cached as - * such, and then scaled appropriately in this method prior to rendering. - * - * @private - * @method drawBuffersScaled - * @param {String} gId ID in our geom hash - * @param {Number} scaleX the amount to scale in the X direction - * @param {Number} scaleY the amount to scale in the Y direction - * @param {Number} scaleZ the amount to scale in the Z direction - */ -RendererGL.prototype.drawBuffersScaled = function( - gId, - scaleX, - scaleY, - scaleZ -) { - let originalModelMatrix = this.states.uModelMatrix.copy(); - try { - this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); - - this.drawBuffers(gId); - } finally { - - this.states.uModelMatrix = originalModelMatrix; - } -}; -RendererGL.prototype._drawArrays = function(drawMode, gId) { - this.GL.drawArrays( - drawMode, - 0, - this.retainedMode.geometry[gId].lineVertexCount - ); - return this; -}; - -RendererGL.prototype._drawElements = function(drawMode, gId) { - const buffers = this.retainedMode.geometry[gId]; - const gl = this.GL; - // render the fill - if (buffers.indexBuffer) { - // If this model is using a Uint32Array we need to ensure the - // OES_element_index_uint WebGL extension is enabled. + + buffers.lineVertexCount = model.lineVertices + ? model.lineVertices.length / 3 + : 0; + + for (const propName in model.userVertexProperties){ + const prop = model.userVertexProperties[propName]; + this.retainedMode.buffers.user.push( + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) + ); + } + return buffers; + }; + + /** + * Draws buffers given a geometry key ID + * @private + * @param {String} gId ID in our geom hash + * @chainable + */ + RendererGL.prototype.drawBuffers = function(gId) { + const gl = this.GL; + const geometry = this.retainedMode.geometry[gId]; + if ( - this._pInst.webglVersion !== constants.WEBGL2 && - buffers.indexBufferType === gl.UNSIGNED_INT + !this.geometryBuilder && + this.states.doFill && + geometry.vertexCount > 0 ) { - if (!gl.getExtension('OES_element_index_uint')) { - throw new Error( - 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' - ); + this._useVertexColor = (geometry.model.vertexColors.length > 0); + + let fillShader; + if (this._drawingFilter && this.states.userFillShader) { + fillShader = this.states.userFillShader; + } else { + fillShader = this._getFillShader(); } + this._setFillUniforms(fillShader); + + for (const buff of this.retainedMode.buffers.fill) { + buff._prepareBuffer(geometry, fillShader); + } + for (const buff of this.retainedMode.buffers.user){ + const prop = geometry.model.userVertexProperties[buff.attr]; + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } + buff._prepareBuffer(geometry, fillShader); + } + fillShader.disableRemainingAttributes(); + if (geometry.indexBuffer) { + //vertex index buffer + this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + } + this._applyColorBlend( + this.states.curFillColor, + geometry.model.hasFillTransparency() + ); + this._drawElements(gl.TRIANGLES, gId); + fillShader.unbindShader(); } - // we're drawing faces - gl.drawElements( - gl.TRIANGLES, - buffers.vertexCount, - buffers.indexBufferType, - 0 - ); - } else { - // drawing vertices - gl.drawArrays(drawMode || gl.TRIANGLES, 0, buffers.vertexCount); - } -}; -RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { - const gl = this.GL; - const pointShader = this._getImmediatePointShader(); - this._setPointUniforms(pointShader); + if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { + this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); + const strokeShader = this._getRetainedStrokeShader(); + this._setStrokeUniforms(strokeShader); + for (const buff of this.retainedMode.buffers.stroke) { + buff._prepareBuffer(geometry, strokeShader); + } + for (const buff of this.retainedMode.buffers.user){ + const prop = geometry.model.userVertexProperties[buff.attr]; + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } + buff._prepareBuffer(geometry, strokeShader); + } + strokeShader.disableRemainingAttributes(); + this._applyColorBlend( + this.states.curStrokeColor, + geometry.model.hasStrokeTransparency() + ); + this._drawArrays(gl.TRIANGLES, gId); + strokeShader.unbindShader(); + } + + if (this.geometryBuilder) { + this.geometryBuilder.addRetained(geometry); + } + + return this; + }; + + /** + * Calls drawBuffers() with a scaled model/view matrix. + * + * This is used by various 3d primitive methods (in primitives.js, eg. plane, + * box, torus, etc...) to allow caching of un-scaled geometries. Those + * geometries are generally created with unit-length dimensions, cached as + * such, and then scaled appropriately in this method prior to rendering. + * + * @private + * @method drawBuffersScaled + * @param {String} gId ID in our geom hash + * @param {Number} scaleX the amount to scale in the X direction + * @param {Number} scaleY the amount to scale in the Y direction + * @param {Number} scaleZ the amount to scale in the Z direction + */ + RendererGL.prototype.drawBuffersScaled = function( + gId, + scaleX, + scaleY, + scaleZ + ) { + let originalModelMatrix = this.states.uModelMatrix.copy(); + try { + this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); + + this.drawBuffers(gId); + } finally { - this._bindBuffer( - vertexBuffer, - gl.ARRAY_BUFFER, - this._vToNArray(vertices), - Float32Array, - gl.STATIC_DRAW - ); + this.states.uModelMatrix = originalModelMatrix; + } + }; + RendererGL.prototype._drawArrays = function(drawMode, gId) { + this.GL.drawArrays( + drawMode, + 0, + this.retainedMode.geometry[gId].lineVertexCount + ); + return this; + }; + + RendererGL.prototype._drawElements = function(drawMode, gId) { + const buffers = this.retainedMode.geometry[gId]; + const gl = this.GL; + // render the fill + if (buffers.indexBuffer) { + // If this model is using a Uint32Array we need to ensure the + // OES_element_index_uint WebGL extension is enabled. + if ( + this._pInst.webglVersion !== constants.WEBGL2 && + buffers.indexBufferType === gl.UNSIGNED_INT + ) { + if (!gl.getExtension('OES_element_index_uint')) { + throw new Error( + 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' + ); + } + } + // we're drawing faces + gl.drawElements( + gl.TRIANGLES, + buffers.vertexCount, + buffers.indexBufferType, + 0 + ); + } else { + // drawing vertices + gl.drawArrays(drawMode || gl.TRIANGLES, 0, buffers.vertexCount); + } + }; + + RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { + const gl = this.GL; + const pointShader = this._getImmediatePointShader(); + this._setPointUniforms(pointShader); + + this._bindBuffer( + vertexBuffer, + gl.ARRAY_BUFFER, + this._vToNArray(vertices), + Float32Array, + gl.STATIC_DRAW + ); - pointShader.enableAttrib(pointShader.attributes.aPosition, 3); + pointShader.enableAttrib(pointShader.attributes.aPosition, 3); - this._applyColorBlend(this.states.curStrokeColor); + this._applyColorBlend(this.states.curStrokeColor); - gl.drawArrays(gl.Points, 0, vertices.length); + gl.drawArrays(gl.Points, 0, vertices.length); - pointShader.unbindShader(); -}; + pointShader.unbindShader(); + }; } export default rendererGLRetained; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index cf0994af7d..673a2fda28 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -8,1383 +8,1383 @@ import { Texture } from './p5.Texture'; - function shader(p5, fn){ - /** - * A class to describe a shader program. - * - * Each `p5.Shader` object contains a shader program that runs on the graphics - * processing unit (GPU). Shaders can process many pixels or vertices at the - * same time, making them fast for many graphics tasks. They’re written in a - * language called - * GLSL - * and run along with the rest of the code in a sketch. - * - * A shader program consists of two files, a vertex shader and a fragment - * shader. The vertex shader affects where 3D geometry is drawn on the screen - * and the fragment shader affects color. Once the `p5.Shader` object is - * created, it can be used with the shader() - * function, as in `shader(myShader)`. - * - * A shader can optionally describe *hooks,* which are functions in GLSL that - * users may choose to provide to customize the behavior of the shader. For the - * vertex or the fragment shader, users can pass in an object where each key is - * the type and name of a hook function, and each value is a string with the - * parameter list and default implementation of the hook. For example, to let users - * optionally run code at the start of the vertex shader, the options object could - * include: - * - * ```js - * { - * vertex: { - * 'void beforeVertex': '() {}' - * } - * } - * ``` - * - * Then, in your vertex shader source, you can run a hook by calling a function - * with the same name prefixed by `HOOK_`: - * - * ```glsl - * void main() { - * HOOK_beforeVertex(); - * // Add the rest ofy our shader code here! - * } - * ``` - * - * Note: createShader(), - * createFilterShader(), and - * loadShader() are the recommended ways to - * create an instance of this class. - * - * @class p5.Shader - * @param {p5.RendererGL} renderer WebGL context for this shader. - * @param {String} vertSrc source code for the vertex shader program. - * @param {String} fragSrc source code for the fragment shader program. - * @param {Object} [options] An optional object describing how this shader can - * be augmented with hooks. It can include: - * - `vertex`: An object describing the available vertex shader hooks. - * - `fragment`: An object describing the available frament shader hooks. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision highp float; - * - * void main() { - * // Set each pixel's RGBA value to yellow. - * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); - * } - * `; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * let myShader = createShader(vertSrc, fragSrc); - * - * // Apply the p5.Shader object. - * shader(myShader); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * - * describe('A yellow square.'); - * } - * - *
- * - *
- * - * // Note: A "uniform" is a global variable within a shader program. - * - * let mandelbrot; - * - * // Load the shader and create a p5.Shader object. - * function preload() { - * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Use the p5.Shader object. - * shader(mandelbrot); - * - * // Set the shader uniform p to an array. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); - * - * describe('A fractal image zooms in and out of focus.'); - * } - * - * function draw() { - * // Set the shader uniform r to a value that oscillates between 0 and 2. - * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); - * - * // Add a quad as a display surface for the shader. - * quad(-1, -1, 1, -1, 1, 1, -1, 1); - * } - * - *
- */ - p5.Shader = Shader; - } +class Shader { + constructor(renderer, vertSrc, fragSrc, options = {}) { + // TODO: adapt this to not take ids, but rather, + // to take the source for a vertex and fragment shader + // to enable custom shaders at some later date + this._renderer = renderer; + this._vertSrc = vertSrc; + this._fragSrc = fragSrc; + this._vertShader = -1; + this._fragShader = -1; + this._glProgram = 0; + this._loadedAttributes = false; + this.attributes = {}; + this._loadedUniforms = false; + this.uniforms = {}; + this._bound = false; + this.samplers = []; + this.hooks = { + // These should be passed in by `.modify()` instead of being manually + // passed in. - class Shader { - constructor(renderer, vertSrc, fragSrc, options = {}) { - // TODO: adapt this to not take ids, but rather, - // to take the source for a vertex and fragment shader - // to enable custom shaders at some later date - this._renderer = renderer; - this._vertSrc = vertSrc; - this._fragSrc = fragSrc; - this._vertShader = -1; - this._fragShader = -1; - this._glProgram = 0; - this._loadedAttributes = false; - this.attributes = {}; - this._loadedUniforms = false; - this.uniforms = {}; - this._bound = false; - this.samplers = []; - this.hooks = { - // These should be passed in by `.modify()` instead of being manually - // passed in. + // Stores uniforms + default values. + uniforms: options.uniforms || {}, - // Stores uniforms + default values. - uniforms: options.uniforms || {}, + // Stores custom uniform + helper declarations as a string. + declarations: options.declarations, - // Stores custom uniform + helper declarations as a string. - declarations: options.declarations, + // Stores helper functions to prepend to shaders. + helpers: options.helpers || {}, - // Stores helper functions to prepend to shaders. - helpers: options.helpers || {}, + // Stores the hook implementations + vertex: options.vertex || {}, + fragment: options.fragment || {}, - // Stores the hook implementations - vertex: options.vertex || {}, - fragment: options.fragment || {}, + // Stores whether or not the hook implementation has been modified + // from the default. This is supplied automatically by calling + // yourShader.modify(...). + modified: { + vertex: (options.modified && options.modified.vertex) || {}, + fragment: (options.modified && options.modified.fragment) || {} + } + }; + } - // Stores whether or not the hook implementation has been modified - // from the default. This is supplied automatically by calling - // yourShader.modify(...). - modified: { - vertex: (options.modified && options.modified.vertex) || {}, - fragment: (options.modified && options.modified.fragment) || {} - } - }; - } + shaderSrc(src, shaderType) { + const main = 'void main'; + const [preMain, postMain] = src.split(main); - shaderSrc(src, shaderType) { - const main = 'void main'; - const [preMain, postMain] = src.split(main); + let hooks = ''; + for (const key in this.hooks.uniforms) { + hooks += `uniform ${key};\n`; + } + if (this.hooks.declarations) { + hooks += this.hooks.declarations + '\n'; + } + if (this.hooks[shaderType].declarations) { + hooks += this.hooks[shaderType].declarations + '\n'; + } + for (const hookDef in this.hooks.helpers) { + hooks += `${hookDef}${this.hooks.helpers[hookDef]}\n`; + } + for (const hookDef in this.hooks[shaderType]) { + if (hookDef === 'declarations') continue; + const [hookType, hookName] = hookDef.split(' '); - let hooks = ''; - for (const key in this.hooks.uniforms) { - hooks += `uniform ${key};\n`; - } - if (this.hooks.declarations) { - hooks += this.hooks.declarations + '\n'; + // Add a #define so that if the shader wants to use preprocessor directives to + // optimize away the extra function calls in main, it can do so + if (this.hooks.modified[shaderType][hookDef]) { + hooks += '#define AUGMENTED_HOOK_' + hookName + '\n'; } - if (this.hooks[shaderType].declarations) { - hooks += this.hooks[shaderType].declarations + '\n'; - } - for (const hookDef in this.hooks.helpers) { - hooks += `${hookDef}${this.hooks.helpers[hookDef]}\n`; - } - for (const hookDef in this.hooks[shaderType]) { - if (hookDef === 'declarations') continue; - const [hookType, hookName] = hookDef.split(' '); - // Add a #define so that if the shader wants to use preprocessor directives to - // optimize away the extra function calls in main, it can do so - if (this.hooks.modified[shaderType][hookDef]) { - hooks += '#define AUGMENTED_HOOK_' + hookName + '\n'; - } + hooks += + hookType + ' HOOK_' + hookName + this.hooks[shaderType][hookDef] + '\n'; + } - hooks += - hookType + ' HOOK_' + hookName + this.hooks[shaderType][hookDef] + '\n'; - } + return preMain + hooks + main + postMain; + } - return preMain + hooks + main + postMain; + /** + * Shaders are written in GLSL, but + * there are different versions of GLSL that it might be written in. + * + * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. + * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. + * + * @returns {String} The GLSL version used by the shader. + */ + version() { + const match = /#version (.+)$/.exec(this.vertSrc()); + if (match) { + return match[1]; + } else { + return '100 es'; } + } - /** - * Shaders are written in GLSL, but - * there are different versions of GLSL that it might be written in. - * - * Calling this method on a `p5.Shader` will return the GLSL version it uses, either `100 es` or `300 es`. - * WebGL 1 shaders will only use `100 es`, and WebGL 2 shaders may use either. - * - * @returns {String} The GLSL version used by the shader. - */ - version() { - const match = /#version (.+)$/.exec(this.vertSrc()); - if (match) { - return match[1]; - } else { - return '100 es'; - } - } + vertSrc() { + return this.shaderSrc(this._vertSrc, 'vertex'); + } - vertSrc() { - return this.shaderSrc(this._vertSrc, 'vertex'); - } + fragSrc() { + return this.shaderSrc(this._fragSrc, 'fragment'); + } - fragSrc() { - return this.shaderSrc(this._fragSrc, 'fragment'); + /** + * Logs the hooks available in this shader, and their current implementation. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. This method logs those values to the console, letting you know what + * you are able to use in a call to + * `modify()`. + * + * For example, this shader will produce the following output: + * + * ```js + * myShader = baseMaterialShader().modify({ + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * myShader.inspectHooks(); + * ``` + * + * ``` + * ==== Vertex shader hooks: ==== + * void beforeVertex() {} + * vec3 getLocalPosition(vec3 position) { return position; } + * [MODIFIED] vec3 getWorldPosition(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * } + * vec3 getLocalNormal(vec3 normal) { return normal; } + * vec3 getWorldNormal(vec3 normal) { return normal; } + * vec2 getUV(vec2 uv) { return uv; } + * vec4 getVertexColor(vec4 color) { return color; } + * void afterVertex() {} + * + * ==== Fragment shader hooks: ==== + * void beforeFragment() {} + * Inputs getPixelInputs(Inputs inputs) { return inputs; } + * vec4 combineColors(ColorComponents components) { + * vec4 color = vec4(0.); + * color.rgb += components.diffuse * components.baseColor; + * color.rgb += components.ambient * components.ambientColor; + * color.rgb += components.specular * components.specularColor; + * color.rgb += components.emissive; + * color.a = components.opacity; + * return color; + * } + * vec4 getFinalColor(vec4 color) { return color; } + * void afterFragment() {} + * ``` + * + * @beta + */ + inspectHooks() { + console.log('==== Vertex shader hooks: ===='); + for (const key in this.hooks.vertex) { + console.log( + (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + + key + + this.hooks.vertex[key] + ); } - - /** - * Logs the hooks available in this shader, and their current implementation. - * - * Each shader may let you override bits of its behavior. Each bit is called - * a *hook.* A hook is either for the *vertex* shader, if it affects the - * position of vertices, or in the *fragment* shader, if it affects the pixel - * color. This method logs those values to the console, letting you know what - * you are able to use in a call to - * `modify()`. - * - * For example, this shader will produce the following output: - * - * ```js - * myShader = baseMaterialShader().modify({ - * declarations: 'uniform float time;', - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * myShader.inspectHooks(); - * ``` - * - * ``` - * ==== Vertex shader hooks: ==== - * void beforeVertex() {} - * vec3 getLocalPosition(vec3 position) { return position; } - * [MODIFIED] vec3 getWorldPosition(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * } - * vec3 getLocalNormal(vec3 normal) { return normal; } - * vec3 getWorldNormal(vec3 normal) { return normal; } - * vec2 getUV(vec2 uv) { return uv; } - * vec4 getVertexColor(vec4 color) { return color; } - * void afterVertex() {} - * - * ==== Fragment shader hooks: ==== - * void beforeFragment() {} - * Inputs getPixelInputs(Inputs inputs) { return inputs; } - * vec4 combineColors(ColorComponents components) { - * vec4 color = vec4(0.); - * color.rgb += components.diffuse * components.baseColor; - * color.rgb += components.ambient * components.ambientColor; - * color.rgb += components.specular * components.specularColor; - * color.rgb += components.emissive; - * color.a = components.opacity; - * return color; - * } - * vec4 getFinalColor(vec4 color) { return color; } - * void afterFragment() {} - * ``` - * - * @beta - */ - inspectHooks() { - console.log('==== Vertex shader hooks: ===='); - for (const key in this.hooks.vertex) { - console.log( - (this.hooks.modified.vertex[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.vertex[key] - ); - } - console.log(''); - console.log('==== Fragment shader hooks: ===='); - for (const key in this.hooks.fragment) { - console.log( - (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + - key + - this.hooks.fragment[key] - ); - } - console.log(''); - console.log('==== Helper functions: ===='); - for (const key in this.hooks.helpers) { - console.log( + console.log(''); + console.log('==== Fragment shader hooks: ===='); + for (const key in this.hooks.fragment) { + console.log( + (this.hooks.modified.fragment[key] ? '[MODIFIED] ' : '') + key + - this.hooks.helpers[key] - ); - } + this.hooks.fragment[key] + ); } + console.log(''); + console.log('==== Helper functions: ===='); + for (const key in this.hooks.helpers) { + console.log( + key + + this.hooks.helpers[key] + ); + } + } - /** - * Returns a new shader, based on the original, but with custom snippets - * of shader code replacing default behaviour. - * - * Each shader may let you override bits of its behavior. Each bit is called - * a *hook.* A hook is either for the *vertex* shader, if it affects the - * position of vertices, or in the *fragment* shader, if it affects the pixel - * color. You can inspect the different hooks available by calling - * `yourShader.inspectHooks()`. You can - * also read the reference for the default material, normal material, color, line, and point shaders to - * see what hooks they have available. - * - * `modify()` takes one parameter, `hooks`, an object with the hooks you want - * to override. Each key of the `hooks` object is the name - * of a hook, and the value is a string with the GLSL code for your hook. - * - * If you supply functions that aren't existing hooks, they will get added at the start of - * the shader as helper functions so that you can use them in your hooks. - * - * To add new uniforms to your shader, you can pass in a `uniforms` object containing - * the type and name of the uniform as the key, and a default value or function returning - * a default value as its value. These will be automatically set when the shader is set - * with `shader(yourShader)`. - * - * You can also add a `declarations` key, where the value is a GLSL string declaring - * custom uniform variables, globals, and functions shared - * between hooks. To add declarations just in a vertex or fragment shader, add - * `vertexDeclarations` and `fragmentDeclarations` keys. - * - * @beta - * @param {Object} [hooks] The hooks in the shader to replace. - * @returns {p5.Shader} - * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * // Manually specifying a uniform - * declarations: 'uniform float time;', - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); - * return pos; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * myShader.setUniform('time', millis()); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); - * } - * - *
- */ - modify(hooks) { - // p5._validateParameters('p5.Shader.modify', arguments); - const newHooks = { - vertex: {}, - fragment: {}, - helpers: {} - }; - for (const key in hooks) { - if (key === 'declarations') continue; - if (key === 'uniforms') continue; - if (key === 'vertexDeclarations') { - newHooks.vertex.declarations = - (newHooks.vertex.declarations || '') + '\n' + hooks[key]; - } else if (key === 'fragmentDeclarations') { - newHooks.fragment.declarations = - (newHooks.fragment.declarations || '') + '\n' + hooks[key]; - } else if (this.hooks.vertex[key]) { - newHooks.vertex[key] = hooks[key]; - } else if (this.hooks.fragment[key]) { - newHooks.fragment[key] = hooks[key]; - } else { - newHooks.helpers[key] = hooks[key]; - } - } - const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); - const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); - for (const key in newHooks.vertex || {}) { - if (key === 'declarations') continue; - modifiedVertex[key] = true; - } - for (const key in newHooks.fragment || {}) { - if (key === 'declarations') continue; - modifiedFragment[key] = true; + /** + * Returns a new shader, based on the original, but with custom snippets + * of shader code replacing default behaviour. + * + * Each shader may let you override bits of its behavior. Each bit is called + * a *hook.* A hook is either for the *vertex* shader, if it affects the + * position of vertices, or in the *fragment* shader, if it affects the pixel + * color. You can inspect the different hooks available by calling + * `yourShader.inspectHooks()`. You can + * also read the reference for the default material, normal material, color, line, and point shaders to + * see what hooks they have available. + * + * `modify()` takes one parameter, `hooks`, an object with the hooks you want + * to override. Each key of the `hooks` object is the name + * of a hook, and the value is a string with the GLSL code for your hook. + * + * If you supply functions that aren't existing hooks, they will get added at the start of + * the shader as helper functions so that you can use them in your hooks. + * + * To add new uniforms to your shader, you can pass in a `uniforms` object containing + * the type and name of the uniform as the key, and a default value or function returning + * a default value as its value. These will be automatically set when the shader is set + * with `shader(yourShader)`. + * + * You can also add a `declarations` key, where the value is a GLSL string declaring + * custom uniform variables, globals, and functions shared + * between hooks. To add declarations just in a vertex or fragment shader, add + * `vertexDeclarations` and `fragmentDeclarations` keys. + * + * @beta + * @param {Object} [hooks] The hooks in the shader to replace. + * @returns {p5.Shader} + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * // Manually specifying a uniform + * declarations: 'uniform float time;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20. * sin(time * 0.001 + pos.x * 0.05); + * return pos; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * myShader.setUniform('time', millis()); + * lights(); + * noStroke(); + * fill('red'); + * sphere(50); + * } + * + *
+ */ + modify(hooks) { + // p5._validateParameters('p5.Shader.modify', arguments); + const newHooks = { + vertex: {}, + fragment: {}, + helpers: {} + }; + for (const key in hooks) { + if (key === 'declarations') continue; + if (key === 'uniforms') continue; + if (key === 'vertexDeclarations') { + newHooks.vertex.declarations = + (newHooks.vertex.declarations || '') + '\n' + hooks[key]; + } else if (key === 'fragmentDeclarations') { + newHooks.fragment.declarations = + (newHooks.fragment.declarations || '') + '\n' + hooks[key]; + } else if (this.hooks.vertex[key]) { + newHooks.vertex[key] = hooks[key]; + } else if (this.hooks.fragment[key]) { + newHooks.fragment[key] = hooks[key]; + } else { + newHooks.helpers[key] = hooks[key]; } - - return new Shader(this._renderer, this._vertSrc, this._fragSrc, { - declarations: - (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), - uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), - fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), - vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), - helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), - modified: { - vertex: modifiedVertex, - fragment: modifiedFragment - } - }); + } + const modifiedVertex = Object.assign({}, this.hooks.modified.vertex); + const modifiedFragment = Object.assign({}, this.hooks.modified.fragment); + for (const key in newHooks.vertex || {}) { + if (key === 'declarations') continue; + modifiedVertex[key] = true; + } + for (const key in newHooks.fragment || {}) { + if (key === 'declarations') continue; + modifiedFragment[key] = true; } - /** - * Creates, compiles, and links the shader based on its - * sources for the vertex and fragment shaders (provided - * to the constructor). Populates known attributes and - * uniforms from the shader. - * @chainable - * @private - */ - init() { - if (this._glProgram === 0 /* or context is stale? */) { - const gl = this._renderer.GL; + return new Shader(this._renderer, this._vertSrc, this._fragSrc, { + declarations: + (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), + uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), + fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), + vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), + helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), + modified: { + vertex: modifiedVertex, + fragment: modifiedFragment + } + }); + } - // @todo: once custom shading is allowed, - // friendly error messages should be used here to share - // compiler and linker errors. + /** + * Creates, compiles, and links the shader based on its + * sources for the vertex and fragment shaders (provided + * to the constructor). Populates known attributes and + * uniforms from the shader. + * @chainable + * @private + */ + init() { + if (this._glProgram === 0 /* or context is stale? */) { + const gl = this._renderer.GL; - //set up the shader by - // 1. creating and getting a gl id for the shader program, - // 2. compliling its vertex & fragment sources, - // 3. linking the vertex and fragment shaders - this._vertShader = gl.createShader(gl.VERTEX_SHADER); - //load in our default vertex shader - gl.shaderSource(this._vertShader, this.vertSrc()); - gl.compileShader(this._vertShader); - // if our vertex shader failed compilation? - if (!gl.getShaderParameter(this._vertShader, gl.COMPILE_STATUS)) { - const glError = gl.getShaderInfoLog(this._vertShader); - if (typeof IS_MINIFIED !== 'undefined') { - console.error(glError); - } else { - p5._friendlyError( - `Yikes! An error occurred compiling the vertex shader:${glError}` - ); - } - return null; - } + // @todo: once custom shading is allowed, + // friendly error messages should be used here to share + // compiler and linker errors. - this._fragShader = gl.createShader(gl.FRAGMENT_SHADER); - //load in our material frag shader - gl.shaderSource(this._fragShader, this.fragSrc()); - gl.compileShader(this._fragShader); - // if our frag shader failed compilation? - if (!gl.getShaderParameter(this._fragShader, gl.COMPILE_STATUS)) { - const glError = gl.getShaderInfoLog(this._fragShader); - if (typeof IS_MINIFIED !== 'undefined') { - console.error(glError); - } else { - p5._friendlyError( - `Darn! An error occurred compiling the fragment shader:${glError}` - ); - } - return null; + //set up the shader by + // 1. creating and getting a gl id for the shader program, + // 2. compliling its vertex & fragment sources, + // 3. linking the vertex and fragment shaders + this._vertShader = gl.createShader(gl.VERTEX_SHADER); + //load in our default vertex shader + gl.shaderSource(this._vertShader, this.vertSrc()); + gl.compileShader(this._vertShader); + // if our vertex shader failed compilation? + if (!gl.getShaderParameter(this._vertShader, gl.COMPILE_STATUS)) { + const glError = gl.getShaderInfoLog(this._vertShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { + p5._friendlyError( + `Yikes! An error occurred compiling the vertex shader:${glError}` + ); } + return null; + } - this._glProgram = gl.createProgram(); - gl.attachShader(this._glProgram, this._vertShader); - gl.attachShader(this._glProgram, this._fragShader); - gl.linkProgram(this._glProgram); - if (!gl.getProgramParameter(this._glProgram, gl.LINK_STATUS)) { + this._fragShader = gl.createShader(gl.FRAGMENT_SHADER); + //load in our material frag shader + gl.shaderSource(this._fragShader, this.fragSrc()); + gl.compileShader(this._fragShader); + // if our frag shader failed compilation? + if (!gl.getShaderParameter(this._fragShader, gl.COMPILE_STATUS)) { + const glError = gl.getShaderInfoLog(this._fragShader); + if (typeof IS_MINIFIED !== 'undefined') { + console.error(glError); + } else { p5._friendlyError( - `Snap! Error linking shader program: ${gl.getProgramInfoLog( - this._glProgram - )}` + `Darn! An error occurred compiling the fragment shader:${glError}` ); } + return null; + } - this._loadAttributes(); - this._loadUniforms(); + this._glProgram = gl.createProgram(); + gl.attachShader(this._glProgram, this._vertShader); + gl.attachShader(this._glProgram, this._fragShader); + gl.linkProgram(this._glProgram); + if (!gl.getProgramParameter(this._glProgram, gl.LINK_STATUS)) { + p5._friendlyError( + `Snap! Error linking shader program: ${gl.getProgramInfoLog( + this._glProgram + )}` + ); } - return this; + + this._loadAttributes(); + this._loadUniforms(); } + return this; + } - /** - * @private - */ - setDefaultUniforms() { - for (const key in this.hooks.uniforms) { - const [, name] = key.split(' '); - const initializer = this.hooks.uniforms[key]; - let value; - if (initializer instanceof Function) { - value = initializer(); - } else { - value = initializer; - } + /** + * @private + */ + setDefaultUniforms() { + for (const key in this.hooks.uniforms) { + const [, name] = key.split(' '); + const initializer = this.hooks.uniforms[key]; + let value; + if (initializer instanceof Function) { + value = initializer(); + } else { + value = initializer; + } - if (value !== undefined && value !== null) { - this.setUniform(name, value); - } + if (value !== undefined && value !== null) { + this.setUniform(name, value); } } + } + + /** + * Copies the shader from one drawing context to another. + * + * Each `p5.Shader` object must be compiled by calling + * shader() before it can run. Compilation happens + * in a drawing context which is usually the main canvas or an instance of + * p5.Graphics. A shader can only be used in the + * context where it was compiled. The `copyToContext()` method compiles the + * shader again and copies it to another drawing context where it can be + * reused. + * + * The parameter, `context`, is the drawing context where the shader will be + * used. The shader can be copied to an instance of + * p5.Graphics, as in + * `myShader.copyToContext(pg)`. The shader can also be copied from a + * p5.Graphics object to the main canvas using + * the `window` variable, as in `myShader.copyToContext(window)`. + * + * Note: A p5.Shader object created with + * createShader(), + * createFilterShader(), or + * loadShader() + * can be used directly with a p5.Framebuffer + * object created with + * createFramebuffer(). Both objects + * have the same context as the main canvas. + * + * @param {p5|p5.Graphics} context WebGL context for the copied shader. + * @returns {p5.Shader} new shader compiled for the target context. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * varying vec2 vTexCoord; + * + * void main() { + * vec2 uv = vTexCoord; + * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); + * gl_FragColor = vec4(color, 1.0);\ + * } + * `; + * + * let pg; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Create a p5.Shader object. + * let original = createShader(vertSrc, fragSrc); + * + * // Compile the p5.Shader object. + * shader(original); + * + * // Create a p5.Graphics object. + * pg = createGraphics(50, 50, WEBGL); + * + * // Copy the original shader to the p5.Graphics object. + * let copied = original.copyToContext(pg); + * + * // Apply the copied shader to the p5.Graphics object. + * pg.shader(copied); + * + * // Style the display surface. + * pg.noStroke(); + * + * // Add a display surface for the shader. + * pg.plane(50, 50); + * + * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Draw the p5.Graphics object to the main canvas. + * image(pg, -25, -25); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * varying vec2 vTexCoord; + * + * void main() { + * vec2 uv = vTexCoord; + * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); + * gl_FragColor = vec4(color, 1.0); + * } + * `; + * + * let copied; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Graphics object. + * let pg = createGraphics(25, 25, WEBGL); + * + * // Create a p5.Shader object. + * let original = pg.createShader(vertSrc, fragSrc); + * + * // Compile the p5.Shader object. + * pg.shader(original); + * + * // Copy the original shader to the main canvas. + * copied = original.copyToContext(window); + * + * // Apply the copied shader to the main canvas. + * shader(copied); + * + * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Rotate around the x-, y-, and z-axes. + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * rotateZ(frameCount * 0.01); + * + * // Draw the box. + * box(50); + * } + * + *
+ */ + copyToContext(context) { + const shader = new Shader( + context._renderer, + this._vertSrc, + this._fragSrc + ); + shader.ensureCompiledOnContext(context); + return shader; + } - /** - * Copies the shader from one drawing context to another. - * - * Each `p5.Shader` object must be compiled by calling - * shader() before it can run. Compilation happens - * in a drawing context which is usually the main canvas or an instance of - * p5.Graphics. A shader can only be used in the - * context where it was compiled. The `copyToContext()` method compiles the - * shader again and copies it to another drawing context where it can be - * reused. - * - * The parameter, `context`, is the drawing context where the shader will be - * used. The shader can be copied to an instance of - * p5.Graphics, as in - * `myShader.copyToContext(pg)`. The shader can also be copied from a - * p5.Graphics object to the main canvas using - * the `window` variable, as in `myShader.copyToContext(window)`. - * - * Note: A p5.Shader object created with - * createShader(), - * createFilterShader(), or - * loadShader() - * can be used directly with a p5.Framebuffer - * object created with - * createFramebuffer(). Both objects - * have the same context as the main canvas. - * - * @param {p5|p5.Graphics} context WebGL context for the copied shader. - * @returns {p5.Shader} new shader compiled for the target context. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * varying vec2 vTexCoord; - * - * void main() { - * vec2 uv = vTexCoord; - * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); - * gl_FragColor = vec4(color, 1.0);\ - * } - * `; - * - * let pg; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Shader object. - * let original = createShader(vertSrc, fragSrc); - * - * // Compile the p5.Shader object. - * shader(original); - * - * // Create a p5.Graphics object. - * pg = createGraphics(50, 50, WEBGL); - * - * // Copy the original shader to the p5.Graphics object. - * let copied = original.copyToContext(pg); - * - * // Apply the copied shader to the p5.Graphics object. - * pg.shader(copied); - * - * // Style the display surface. - * pg.noStroke(); - * - * // Add a display surface for the shader. - * pg.plane(50, 50); - * - * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Draw the p5.Graphics object to the main canvas. - * image(pg, -25, -25); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * varying vec2 vTexCoord; - * - * void main() { - * vec2 uv = vTexCoord; - * vec3 color = vec3(uv.x, uv.y, min(uv.x + uv.y, 1.0)); - * gl_FragColor = vec4(color, 1.0); - * } - * `; - * - * let copied; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Graphics object. - * let pg = createGraphics(25, 25, WEBGL); - * - * // Create a p5.Shader object. - * let original = pg.createShader(vertSrc, fragSrc); - * - * // Compile the p5.Shader object. - * pg.shader(original); - * - * // Copy the original shader to the main canvas. - * copied = original.copyToContext(window); - * - * // Apply the copied shader to the main canvas. - * shader(copied); - * - * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Rotate around the x-, y-, and z-axes. - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * rotateZ(frameCount * 0.01); - * - * // Draw the box. - * box(50); - * } - * - *
- */ - copyToContext(context) { - const shader = new Shader( - context._renderer, - this._vertSrc, - this._fragSrc + /** + * @private + */ + ensureCompiledOnContext(context) { + if (this._glProgram !== 0 && this._renderer !== context._renderer) { + throw new Error( + 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' ); - shader.ensureCompiledOnContext(context); - return shader; + } else if (this._glProgram === 0) { + this._renderer = context._renderer; + this.init(); } + } - /** - * @private - */ - ensureCompiledOnContext(context) { - if (this._glProgram !== 0 && this._renderer !== context._renderer) { - throw new Error( - 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' - ); - } else if (this._glProgram === 0) { - this._renderer = context._renderer; - this.init(); - } + /** + * Queries the active attributes for this shader and loads + * their names and locations into the attributes array. + * @private + */ + _loadAttributes() { + if (this._loadedAttributes) { + return; } - /** - * Queries the active attributes for this shader and loads - * their names and locations into the attributes array. - * @private - */ - _loadAttributes() { - if (this._loadedAttributes) { - return; - } + this.attributes = {}; - this.attributes = {}; + const gl = this._renderer.GL; - const gl = this._renderer.GL; + const numAttributes = gl.getProgramParameter( + this._glProgram, + gl.ACTIVE_ATTRIBUTES + ); + for (let i = 0; i < numAttributes; ++i) { + const attributeInfo = gl.getActiveAttrib(this._glProgram, i); + const name = attributeInfo.name; + const location = gl.getAttribLocation(this._glProgram, name); + const attribute = {}; + attribute.name = name; + attribute.location = location; + attribute.index = i; + attribute.type = attributeInfo.type; + attribute.size = attributeInfo.size; + this.attributes[name] = attribute; + } - const numAttributes = gl.getProgramParameter( - this._glProgram, - gl.ACTIVE_ATTRIBUTES - ); - for (let i = 0; i < numAttributes; ++i) { - const attributeInfo = gl.getActiveAttrib(this._glProgram, i); - const name = attributeInfo.name; - const location = gl.getAttribLocation(this._glProgram, name); - const attribute = {}; - attribute.name = name; - attribute.location = location; - attribute.index = i; - attribute.type = attributeInfo.type; - attribute.size = attributeInfo.size; - this.attributes[name] = attribute; - } + this._loadedAttributes = true; + } - this._loadedAttributes = true; + /** + * Queries the active uniforms for this shader and loads + * their names and locations into the uniforms array. + * @private + */ + _loadUniforms() { + if (this._loadedUniforms) { + return; } - /** - * Queries the active uniforms for this shader and loads - * their names and locations into the uniforms array. - * @private - */ - _loadUniforms() { - if (this._loadedUniforms) { - return; - } + const gl = this._renderer.GL; - const gl = this._renderer.GL; + // Inspect shader and cache uniform info + const numUniforms = gl.getProgramParameter( + this._glProgram, + gl.ACTIVE_UNIFORMS + ); - // Inspect shader and cache uniform info - const numUniforms = gl.getProgramParameter( + let samplerIndex = 0; + for (let i = 0; i < numUniforms; ++i) { + const uniformInfo = gl.getActiveUniform(this._glProgram, i); + const uniform = {}; + uniform.location = gl.getUniformLocation( this._glProgram, - gl.ACTIVE_UNIFORMS + uniformInfo.name ); + uniform.size = uniformInfo.size; + let uniformName = uniformInfo.name; + //uniforms that are arrays have their name returned as + //someUniform[0] which is a bit silly so we trim it + //off here. The size property tells us that its an array + //so we dont lose any information by doing this + if (uniformInfo.size > 1) { + uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); + } + uniform.name = uniformName; + uniform.type = uniformInfo.type; + uniform._cachedData = undefined; + if (uniform.type === gl.SAMPLER_2D) { + uniform.samplerIndex = samplerIndex; + samplerIndex++; + this.samplers.push(uniform); + } - let samplerIndex = 0; - for (let i = 0; i < numUniforms; ++i) { - const uniformInfo = gl.getActiveUniform(this._glProgram, i); - const uniform = {}; - uniform.location = gl.getUniformLocation( - this._glProgram, - uniformInfo.name - ); - uniform.size = uniformInfo.size; - let uniformName = uniformInfo.name; - //uniforms that are arrays have their name returned as - //someUniform[0] which is a bit silly so we trim it - //off here. The size property tells us that its an array - //so we dont lose any information by doing this - if (uniformInfo.size > 1) { - uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); - } - uniform.name = uniformName; - uniform.type = uniformInfo.type; - uniform._cachedData = undefined; - if (uniform.type === gl.SAMPLER_2D) { - uniform.samplerIndex = samplerIndex; - samplerIndex++; - this.samplers.push(uniform); - } - - uniform.isArray = - uniformInfo.size > 1 || - uniform.type === gl.FLOAT_MAT3 || - uniform.type === gl.FLOAT_MAT4 || - uniform.type === gl.FLOAT_VEC2 || - uniform.type === gl.FLOAT_VEC3 || - uniform.type === gl.FLOAT_VEC4 || - uniform.type === gl.INT_VEC2 || - uniform.type === gl.INT_VEC4 || - uniform.type === gl.INT_VEC3; + uniform.isArray = + uniformInfo.size > 1 || + uniform.type === gl.FLOAT_MAT3 || + uniform.type === gl.FLOAT_MAT4 || + uniform.type === gl.FLOAT_VEC2 || + uniform.type === gl.FLOAT_VEC3 || + uniform.type === gl.FLOAT_VEC4 || + uniform.type === gl.INT_VEC2 || + uniform.type === gl.INT_VEC4 || + uniform.type === gl.INT_VEC3; - this.uniforms[uniformName] = uniform; - } - this._loadedUniforms = true; + this.uniforms[uniformName] = uniform; } + this._loadedUniforms = true; + } - compile() { - // TODO - } + compile() { + // TODO + } - /** - * initializes (if needed) and binds the shader program. - * @private - */ - bindShader() { - this.init(); - if (!this._bound) { - this.useProgram(); - this._bound = true; + /** + * initializes (if needed) and binds the shader program. + * @private + */ + bindShader() { + this.init(); + if (!this._bound) { + this.useProgram(); + this._bound = true; - this._setMatrixUniforms(); + this._setMatrixUniforms(); - this.setUniform('uViewport', this._renderer._viewport); - } + this.setUniform('uViewport', this._renderer._viewport); } + } - /** - * @chainable - * @private - */ - unbindShader() { - if (this._bound) { - this.unbindTextures(); - //this._renderer.GL.useProgram(0); ?? - this._bound = false; - } - return this; + /** + * @chainable + * @private + */ + unbindShader() { + if (this._bound) { + this.unbindTextures(); + //this._renderer.GL.useProgram(0); ?? + this._bound = false; } + return this; + } - bindTextures() { - const gl = this._renderer.GL; + bindTextures() { + const gl = this._renderer.GL; - for (const uniform of this.samplers) { - let tex = uniform.texture; - if (tex === undefined) { - // user hasn't yet supplied a texture for this slot. - // (or there may not be one--maybe just lighting), - // so we supply a default texture instead. - tex = this._renderer._getEmptyTexture(); - } - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - tex.bindTexture(); - tex.update(); - gl.uniform1i(uniform.location, uniform.samplerIndex); + for (const uniform of this.samplers) { + let tex = uniform.texture; + if (tex === undefined) { + // user hasn't yet supplied a texture for this slot. + // (or there may not be one--maybe just lighting), + // so we supply a default texture instead. + tex = this._renderer._getEmptyTexture(); } + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + tex.bindTexture(); + tex.update(); + gl.uniform1i(uniform.location, uniform.samplerIndex); } + } - updateTextures() { - for (const uniform of this.samplers) { - const tex = uniform.texture; - if (tex) { - tex.update(); - } + updateTextures() { + for (const uniform of this.samplers) { + const tex = uniform.texture; + if (tex) { + tex.update(); } } + } - unbindTextures() { - for (const uniform of this.samplers) { - this.setUniform(uniform.name, this._renderer._getEmptyTexture()); - } + unbindTextures() { + for (const uniform of this.samplers) { + this.setUniform(uniform.name, this._renderer._getEmptyTexture()); } + } - _setMatrixUniforms() { - const modelMatrix = this._renderer.states.uModelMatrix; - const viewMatrix = this._renderer.states.uViewMatrix; - const projectionMatrix = this._renderer.states.uPMatrix; - const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); - this._renderer.states.uMVMatrix = modelViewMatrix; + _setMatrixUniforms() { + const modelMatrix = this._renderer.states.uModelMatrix; + const viewMatrix = this._renderer.states.uViewMatrix; + const projectionMatrix = this._renderer.states.uPMatrix; + const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); + this._renderer.states.uMVMatrix = modelViewMatrix; - const modelViewProjectionMatrix = modelViewMatrix.copy(); - modelViewProjectionMatrix.mult(projectionMatrix); + const modelViewProjectionMatrix = modelViewMatrix.copy(); + modelViewProjectionMatrix.mult(projectionMatrix); - this.setUniform( - 'uPerspective', - this._renderer.states.curCamera.useLinePerspective ? 1 : 0 - ); - this.setUniform('uViewMatrix', viewMatrix.mat4); - this.setUniform('uProjectionMatrix', projectionMatrix.mat4); - this.setUniform('uModelMatrix', modelMatrix.mat4); - this.setUniform('uModelViewMatrix', modelViewMatrix.mat4); - this.setUniform( - 'uModelViewProjectionMatrix', - modelViewProjectionMatrix.mat4 - ); - if (this.uniforms.uNormalMatrix) { - this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); - this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); - } - if (this.uniforms.uCameraRotation) { - this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); - this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); - } + this.setUniform( + 'uPerspective', + this._renderer.states.curCamera.useLinePerspective ? 1 : 0 + ); + this.setUniform('uViewMatrix', viewMatrix.mat4); + this.setUniform('uProjectionMatrix', projectionMatrix.mat4); + this.setUniform('uModelMatrix', modelMatrix.mat4); + this.setUniform('uModelViewMatrix', modelViewMatrix.mat4); + this.setUniform( + 'uModelViewProjectionMatrix', + modelViewProjectionMatrix.mat4 + ); + if (this.uniforms.uNormalMatrix) { + this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); + this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); } + if (this.uniforms.uCameraRotation) { + this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); + this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); + } + } - /** - * @chainable - * @private - */ - useProgram() { - const gl = this._renderer.GL; - if (this._renderer._curShader !== this) { - gl.useProgram(this._glProgram); - this._renderer._curShader = this; - } - return this; + /** + * @chainable + * @private + */ + useProgram() { + const gl = this._renderer.GL; + if (this._renderer._curShader !== this) { + gl.useProgram(this._glProgram); + this._renderer._curShader = this; } + return this; + } - /** - * Sets the shader’s uniform (global) variables. - * - * Shader programs run on the computer’s graphics processing unit (GPU). - * They live in part of the computer’s memory that’s completely separate - * from the sketch that runs them. Uniforms are global variables within a - * shader program. They provide a way to pass values from a sketch running - * on the CPU to a shader program running on the GPU. - * - * The first parameter, `uniformName`, is a string with the uniform’s name. - * For the shader above, `uniformName` would be `'r'`. - * - * The second parameter, `data`, is the value that should be used to set the - * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set - * the `r` uniform in the shader above to `0.5`. data should match the - * uniform’s type. Numbers, strings, booleans, arrays, and many types of - * images can all be passed to a shader with `setUniform()`. - * - * @chainable - * @param {String} uniformName name of the uniform. Must match the name - * used in the vertex and fragment shaders. - * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} - * data value to assign to the uniform. Must match the uniform’s data type. - * - * @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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * uniform float r; - * - * void main() { - * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); - * } - * `; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * let myShader = createShader(vertSrc, fragSrc); - * - * // Apply the p5.Shader object. - * shader(myShader); - * - * // Set the r uniform to 0.5. - * myShader.setUniform('r', 0.5); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface for the shader. - * plane(100, 100); - * - * describe('A cyan square.'); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision mediump float; - * - * uniform float r; - * - * void main() { - * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); - * } - * `; - * - * let myShader; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * myShader = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(myShader); - * - * describe('A square oscillates color between cyan and white.'); - * } - * - * function draw() { - * background(200); - * - * // Style the drawing surface. - * noStroke(); - * - * // Update the r uniform. - * let nextR = 0.5 * (sin(frameCount * 0.01) + 1); - * myShader.setUniform('r', nextR); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * } - * - *
- * - *
- * - * // 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 = ` - * precision highp float; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision highp float; - * uniform vec2 p; - * uniform float r; - * const int numIterations = 500; - * varying vec2 vTexCoord; - * - * void main() { - * vec2 c = p + gl_FragCoord.xy * r; - * vec2 z = c; - * float n = 0.0; - * - * for (int i = numIterations; i > 0; i--) { - * if (z.x * z.x + z.y * z.y > 4.0) { - * n = float(i) / float(numIterations); - * break; - * } - * - * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; - * } - * - * gl_FragColor = vec4( - * 0.5 - cos(n * 17.0) / 2.0, - * 0.5 - cos(n * 13.0) / 2.0, - * 0.5 - cos(n * 23.0) / 2.0, - * 1.0 - * ); - * } - * `; - * - * let mandelbrot; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * mandelbrot = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(mandelbrot); - * - * // Set the shader uniform p to an array. - * // p is the center point of the Mandelbrot image. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); - * - * describe('A fractal image zooms in and out of focus.'); - * } - * - * function draw() { - * // Set the shader uniform r to a value that oscillates - * // between 0 and 0.005. - * // r is the size of the image in Mandelbrot-space. - * let radius = 0.005 * (sin(frameCount * 0.01) + 1); - * mandelbrot.setUniform('r', radius); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * } - * - *
- */ - setUniform(uniformName, data) { - const uniform = this.uniforms[uniformName]; - if (!uniform) { - return; - } - const gl = this._renderer.GL; + /** + * Sets the shader’s uniform (global) variables. + * + * Shader programs run on the computer’s graphics processing unit (GPU). + * They live in part of the computer’s memory that’s completely separate + * from the sketch that runs them. Uniforms are global variables within a + * shader program. They provide a way to pass values from a sketch running + * on the CPU to a shader program running on the GPU. + * + * The first parameter, `uniformName`, is a string with the uniform’s name. + * For the shader above, `uniformName` would be `'r'`. + * + * The second parameter, `data`, is the value that should be used to set the + * uniform. For example, calling `myShader.setUniform('r', 0.5)` would set + * the `r` uniform in the shader above to `0.5`. data should match the + * uniform’s type. Numbers, strings, booleans, arrays, and many types of + * images can all be passed to a shader with `setUniform()`. + * + * @chainable + * @param {String} uniformName name of the uniform. Must match the name + * used in the vertex and fragment shaders. + * @param {Boolean|Number|Number[]|p5.Image|p5.Graphics|p5.MediaElement|p5.Texture} + * data value to assign to the uniform. Must match the uniform’s data type. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * uniform float r; + * + * void main() { + * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); + * } + * `; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * let myShader = createShader(vertSrc, fragSrc); + * + * // Apply the p5.Shader object. + * shader(myShader); + * + * // Set the r uniform to 0.5. + * myShader.setUniform('r', 0.5); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface for the shader. + * plane(100, 100); + * + * describe('A cyan square.'); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision mediump float; + * + * uniform float r; + * + * void main() { + * gl_FragColor = vec4(r, 1.0, 1.0, 1.0); + * } + * `; + * + * let myShader; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * myShader = createShader(vertSrc, fragSrc); + * + * // Compile and apply the p5.Shader object. + * shader(myShader); + * + * describe('A square oscillates color between cyan and white.'); + * } + * + * function draw() { + * background(200); + * + * // Style the drawing surface. + * noStroke(); + * + * // Update the r uniform. + * let nextR = 0.5 * (sin(frameCount * 0.01) + 1); + * myShader.setUniform('r', nextR); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * } + * + *
+ * + *
+ * + * // 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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision highp float; + * uniform vec2 p; + * uniform float r; + * const int numIterations = 500; + * varying vec2 vTexCoord; + * + * void main() { + * vec2 c = p + gl_FragCoord.xy * r; + * vec2 z = c; + * float n = 0.0; + * + * for (int i = numIterations; i > 0; i--) { + * if (z.x * z.x + z.y * z.y > 4.0) { + * n = float(i) / float(numIterations); + * break; + * } + * + * z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; + * } + * + * gl_FragColor = vec4( + * 0.5 - cos(n * 17.0) / 2.0, + * 0.5 - cos(n * 13.0) / 2.0, + * 0.5 - cos(n * 23.0) / 2.0, + * 1.0 + * ); + * } + * `; + * + * let mandelbrot; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * mandelbrot = createShader(vertSrc, fragSrc); + * + * // Compile and apply the p5.Shader object. + * shader(mandelbrot); + * + * // Set the shader uniform p to an array. + * // p is the center point of the Mandelbrot image. + * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); + * + * describe('A fractal image zooms in and out of focus.'); + * } + * + * function draw() { + * // Set the shader uniform r to a value that oscillates + * // between 0 and 0.005. + * // r is the size of the image in Mandelbrot-space. + * let radius = 0.005 * (sin(frameCount * 0.01) + 1); + * mandelbrot.setUniform('r', radius); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * } + * + *
+ */ + setUniform(uniformName, data) { + const uniform = this.uniforms[uniformName]; + if (!uniform) { + return; + } + const gl = this._renderer.GL; - if (uniform.isArray) { - if ( - uniform._cachedData && - this._renderer._arraysEqual(uniform._cachedData, data) - ) { - return; - } else { - uniform._cachedData = data.slice(0); - } - } else if (uniform._cachedData && uniform._cachedData === data) { + if (uniform.isArray) { + if ( + uniform._cachedData && + this._renderer._arraysEqual(uniform._cachedData, data) + ) { return; } else { - if (Array.isArray(data)) { - uniform._cachedData = data.slice(0); - } else { - uniform._cachedData = data; - } + uniform._cachedData = data.slice(0); } + } else if (uniform._cachedData && uniform._cachedData === data) { + return; + } else { + if (Array.isArray(data)) { + uniform._cachedData = data.slice(0); + } else { + uniform._cachedData = data; + } + } - const location = uniform.location; + const location = uniform.location; - this.useProgram(); + this.useProgram(); - switch (uniform.type) { - case gl.BOOL: - if (data === true) { - gl.uniform1i(location, 1); - } else { - gl.uniform1i(location, 0); - } - break; - case gl.INT: - if (uniform.size > 1) { - data.length && gl.uniform1iv(location, data); - } else { - gl.uniform1i(location, data); - } - break; - case gl.FLOAT: - if (uniform.size > 1) { - data.length && gl.uniform1fv(location, data); - } else { - gl.uniform1f(location, data); - } - break; - case gl.FLOAT_MAT3: - gl.uniformMatrix3fv(location, false, data); - break; - case gl.FLOAT_MAT4: - gl.uniformMatrix4fv(location, false, data); - break; - case gl.FLOAT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2fv(location, data); - } else { - gl.uniform2f(location, data[0], data[1]); - } - break; - case gl.FLOAT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3fv(location, data); - } else { - gl.uniform3f(location, data[0], data[1], data[2]); - } - break; - case gl.FLOAT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4fv(location, data); - } else { - gl.uniform4f(location, data[0], data[1], data[2], data[3]); - } - break; - case gl.INT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2iv(location, data); - } else { - gl.uniform2i(location, data[0], data[1]); - } - break; - case gl.INT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3iv(location, data); - } else { - gl.uniform3i(location, data[0], data[1], data[2]); - } - break; - case gl.INT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4iv(location, data); - } else { - gl.uniform4i(location, data[0], data[1], data[2], data[3]); - } - break; - case gl.SAMPLER_2D: - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - uniform.texture = - data instanceof Texture ? data : this._renderer.getTexture(data); - gl.uniform1i(location, uniform.samplerIndex); - if (uniform.texture.src.gifProperties) { - uniform.texture.src._animateGif(this._renderer._pInst); - } - break; - //@todo complete all types - } - return this; + switch (uniform.type) { + case gl.BOOL: + if (data === true) { + gl.uniform1i(location, 1); + } else { + gl.uniform1i(location, 0); + } + break; + case gl.INT: + if (uniform.size > 1) { + data.length && gl.uniform1iv(location, data); + } else { + gl.uniform1i(location, data); + } + break; + case gl.FLOAT: + if (uniform.size > 1) { + data.length && gl.uniform1fv(location, data); + } else { + gl.uniform1f(location, data); + } + break; + case gl.FLOAT_MAT3: + gl.uniformMatrix3fv(location, false, data); + break; + case gl.FLOAT_MAT4: + gl.uniformMatrix4fv(location, false, data); + break; + case gl.FLOAT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2fv(location, data); + } else { + gl.uniform2f(location, data[0], data[1]); + } + break; + case gl.FLOAT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3fv(location, data); + } else { + gl.uniform3f(location, data[0], data[1], data[2]); + } + break; + case gl.FLOAT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4fv(location, data); + } else { + gl.uniform4f(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.INT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2iv(location, data); + } else { + gl.uniform2i(location, data[0], data[1]); + } + break; + case gl.INT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3iv(location, data); + } else { + gl.uniform3i(location, data[0], data[1], data[2]); + } + break; + case gl.INT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4iv(location, data); + } else { + gl.uniform4i(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.SAMPLER_2D: + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + uniform.texture = + data instanceof Texture ? data : this._renderer.getTexture(data); + gl.uniform1i(location, uniform.samplerIndex); + if (uniform.texture.src.gifProperties) { + uniform.texture.src._animateGif(this._renderer._pInst); + } + break; + //@todo complete all types } + return this; + } - /** - * @chainable - * @private - */ - enableAttrib(attr, size, type, normalized, stride, offset) { - if (attr) { - if ( - typeof IS_MINIFIED === 'undefined' && - this.attributes[attr.name] !== attr - ) { - console.warn( - `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.` - ); - } - const loc = attr.location; - if (loc !== -1) { - const gl = this._renderer.GL; - // Enable register even if it is disabled - if (!this._renderer.registerEnabled.has(loc)) { - gl.enableVertexAttribArray(loc); - // Record register availability - this._renderer.registerEnabled.add(loc); - } - this._renderer.GL.vertexAttribPointer( - loc, - size, - type || gl.FLOAT, - normalized || false, - stride || 0, - offset || 0 - ); + /** + * @chainable + * @private + */ + enableAttrib(attr, size, type, normalized, stride, offset) { + if (attr) { + if ( + typeof IS_MINIFIED === 'undefined' && + this.attributes[attr.name] !== attr + ) { + console.warn( + `The attribute "${attr.name}"passed to enableAttrib does not belong to this shader.` + ); + } + const loc = attr.location; + if (loc !== -1) { + const gl = this._renderer.GL; + // Enable register even if it is disabled + if (!this._renderer.registerEnabled.has(loc)) { + gl.enableVertexAttribArray(loc); + // Record register availability + this._renderer.registerEnabled.add(loc); } + this._renderer.GL.vertexAttribPointer( + loc, + size, + type || gl.FLOAT, + normalized || false, + stride || 0, + offset || 0 + ); } - return this; } + return this; + } - /** - * Once all buffers have been bound, this checks to see if there are any - * remaining active attributes, likely left over from previous renders, - * and disables them so that they don't affect rendering. - * @private - */ - disableRemainingAttributes() { - for (const location of this._renderer.registerEnabled.values()) { - if ( - !Object.keys(this.attributes).some( - key => this.attributes[key].location === location - ) - ) { - this._renderer.GL.disableVertexAttribArray(location); - this._renderer.registerEnabled.delete(location); - } + /** + * Once all buffers have been bound, this checks to see if there are any + * remaining active attributes, likely left over from previous renders, + * and disables them so that they don't affect rendering. + * @private + */ + disableRemainingAttributes() { + for (const location of this._renderer.registerEnabled.values()) { + if ( + !Object.keys(this.attributes).some( + key => this.attributes[key].location === location + ) + ) { + this._renderer.GL.disableVertexAttribArray(location); + this._renderer.registerEnabled.delete(location); } } - }; + } +}; + +function shader(p5, fn){ + /** + * A class to describe a shader program. + * + * Each `p5.Shader` object contains a shader program that runs on the graphics + * processing unit (GPU). Shaders can process many pixels or vertices at the + * same time, making them fast for many graphics tasks. They’re written in a + * language called + * GLSL + * and run along with the rest of the code in a sketch. + * + * A shader program consists of two files, a vertex shader and a fragment + * shader. The vertex shader affects where 3D geometry is drawn on the screen + * and the fragment shader affects color. Once the `p5.Shader` object is + * created, it can be used with the shader() + * function, as in `shader(myShader)`. + * + * A shader can optionally describe *hooks,* which are functions in GLSL that + * users may choose to provide to customize the behavior of the shader. For the + * vertex or the fragment shader, users can pass in an object where each key is + * the type and name of a hook function, and each value is a string with the + * parameter list and default implementation of the hook. For example, to let users + * optionally run code at the start of the vertex shader, the options object could + * include: + * + * ```js + * { + * vertex: { + * 'void beforeVertex': '() {}' + * } + * } + * ``` + * + * Then, in your vertex shader source, you can run a hook by calling a function + * with the same name prefixed by `HOOK_`: + * + * ```glsl + * void main() { + * HOOK_beforeVertex(); + * // Add the rest ofy our shader code here! + * } + * ``` + * + * Note: createShader(), + * createFilterShader(), and + * loadShader() are the recommended ways to + * create an instance of this class. + * + * @class p5.Shader + * @param {p5.RendererGL} renderer WebGL context for this shader. + * @param {String} vertSrc source code for the vertex shader program. + * @param {String} fragSrc source code for the fragment shader program. + * @param {Object} [options] An optional object describing how this shader can + * be augmented with hooks. It can include: + * - `vertex`: An object describing the available vertex shader hooks. + * - `fragment`: An object describing the available frament shader hooks. + * + * @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 = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision highp float; + * + * void main() { + * // Set each pixel's RGBA value to yellow. + * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); + * } + * `; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * let myShader = createShader(vertSrc, fragSrc); + * + * // Apply the p5.Shader object. + * shader(myShader); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * + * describe('A yellow square.'); + * } + * + *
+ * + *
+ * + * // Note: A "uniform" is a global variable within a shader program. + * + * let mandelbrot; + * + * // Load the shader and create a p5.Shader object. + * function preload() { + * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Use the p5.Shader object. + * shader(mandelbrot); + * + * // Set the shader uniform p to an array. + * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); + * + * describe('A fractal image zooms in and out of focus.'); + * } + * + * function draw() { + * // Set the shader uniform r to a value that oscillates between 0 and 2. + * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); + * + * // Add a quad as a display surface for the shader. + * quad(-1, -1, 1, -1, 1, 1, -1, 1); + * } + * + *
+ */ + p5.Shader = Shader; +} export default shader; export { Shader }; From ae12ade62d789e9fe1d5ffce4e7b3e0a36fc7e97 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 23 Oct 2024 20:22:48 +0100 Subject: [PATCH 31/55] Move 3D primitives implementation into RendererGL --- src/webgl/3d_primitives.js | 795 ++++++++++++++++++++----------------- 1 file changed, 423 insertions(+), 372 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 827f8eae85..0b491e8828 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -977,35 +977,7 @@ function primitives3D(p5, fn){ this._assert3d('plane'); p5._validateParameters('plane', arguments); - const gId = `plane|${detailX}|${detailY}`; - - if (!this._renderer.geometryInHash(gId)) { - const _plane = function() { - let u, v, p; - for (let i = 0; i <= this.detailY; i++) { - v = i / this.detailY; - for (let j = 0; j <= this.detailX; j++) { - u = j / this.detailX; - p = new Vector(u - 0.5, v - 0.5, 0); - this.vertices.push(p); - this.uvs.push(u, v); - } - } - }; - const planeGeom = new Geometry(detailX, detailY, _plane); - planeGeom.computeFaces().computeNormals(); - if (detailX <= 1 && detailY <= 1) { - planeGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer.states.doStroke) { - console.log( - 'Cannot draw stroke on plane objects with more' + - ' than 1 detailX or 1 detailY' - ); - } - this._renderer.createBuffers(gId, planeGeom); - } - - this._renderer.drawBuffersScaled(gId, width, height, 1); + this._renderer.plane(width, height, detailX, detailY); return this; }; @@ -1141,88 +1113,8 @@ function primitives3D(p5, fn){ fn.box = function(width, height, depth, detailX, detailY) { this._assert3d('box'); p5._validateParameters('box', arguments); - if (typeof width === 'undefined') { - width = 50; - } - if (typeof height === 'undefined') { - height = width; - } - if (typeof depth === 'undefined') { - depth = height; - } - - const perPixelLighting = - this._renderer.attributes && this._renderer.attributes.perPixelLighting; - if (typeof detailX === 'undefined') { - detailX = perPixelLighting ? 1 : 4; - } - if (typeof detailY === 'undefined') { - detailY = perPixelLighting ? 1 : 4; - } - - const gId = `box|${detailX}|${detailY}`; - if (!this._renderer.geometryInHash(gId)) { - const _box = function() { - const cubeIndices = [ - [0, 4, 2, 6], // -1, 0, 0],// -x - [1, 3, 5, 7], // +1, 0, 0],// +x - [0, 1, 4, 5], // 0, -1, 0],// -y - [2, 6, 3, 7], // 0, +1, 0],// +y - [0, 2, 1, 3], // 0, 0, -1],// -z - [4, 5, 6, 7] // 0, 0, +1] // +z - ]; - //using custom edges - //to avoid diagonal stroke lines across face of box - this.edges = [ - [0, 1], - [1, 3], - [3, 2], - [6, 7], - [8, 9], - [9, 11], - [14, 15], - [16, 17], - [17, 19], - [18, 19], - [20, 21], - [22, 23] - ]; - cubeIndices.forEach((cubeIndex, i) => { - const v = i * 4; - for (let j = 0; j < 4; j++) { - const d = cubeIndex[j]; - //inspired by lightgl: - //https://github.com/evanw/lightgl.js - //octants:https://en.wikipedia.org/wiki/Octant_(solid_geometry) - const octant = new Vector( - ((d & 1) * 2 - 1) / 2, - ((d & 2) - 1) / 2, - ((d & 4) / 2 - 1) / 2 - ); - this.vertices.push(octant); - this.uvs.push(j & 1, (j & 2) / 2); - } - this.faces.push([v, v + 1, v + 2]); - this.faces.push([v + 2, v + 1, v + 3]); - }); - }; - const boxGeom = new Geometry(detailX, detailY, _box); - boxGeom.computeNormals(); - if (detailX <= 4 && detailY <= 4) { - boxGeom._edgesToVertices(); - } else if (this._renderer.states.doStroke) { - console.log( - 'Cannot draw stroke on box objects with more' + - ' than 4 detailX or 4 detailY' - ); - } - //initialize our geometry buffer with - //the key val pair: - //geometry Id, Geom object - this._renderer.createBuffers(gId, boxGeom); - } - this._renderer.drawBuffersScaled(gId, width, height, depth); + this._renderer.box(width, height, depth, detailX, detailY); return this; }; @@ -1354,129 +1246,11 @@ function primitives3D(p5, fn){ this._assert3d('sphere'); p5._validateParameters('sphere', arguments); - this.ellipsoid(radius, radius, radius, detailX, detailY); + this._renderer.sphere(radius, detailX, detailY); return this; }; - /** - * @private - * Helper function for creating both cones and cylinders - * Will only generate well-defined geometry when bottomRadius, height > 0 - * and topRadius >= 0 - * If topRadius == 0, topCap should be false - */ - const _truncatedCone = function( - bottomRadius, - topRadius, - height, - detailX, - detailY, - bottomCap, - topCap - ) { - bottomRadius = bottomRadius <= 0 ? 1 : bottomRadius; - topRadius = topRadius < 0 ? 0 : topRadius; - height = height <= 0 ? bottomRadius : height; - detailX = detailX < 3 ? 3 : detailX; - detailY = detailY < 1 ? 1 : detailY; - bottomCap = bottomCap === undefined ? true : bottomCap; - topCap = topCap === undefined ? topRadius !== 0 : topCap; - const start = bottomCap ? -2 : 0; - const end = detailY + (topCap ? 2 : 0); - //ensure constant slant for interior vertex normals - const slant = Math.atan2(bottomRadius - topRadius, height); - const sinSlant = Math.sin(slant); - const cosSlant = Math.cos(slant); - let yy, ii, jj; - for (yy = start; yy <= end; ++yy) { - let v = yy / detailY; - let y = height * v; - let ringRadius; - if (yy < 0) { - //for the bottomCap edge - y = 0; - v = 0; - ringRadius = bottomRadius; - } else if (yy > detailY) { - //for the topCap edge - y = height; - v = 1; - ringRadius = topRadius; - } else { - //for the middle - ringRadius = bottomRadius + (topRadius - bottomRadius) * v; - } - if (yy === -2 || yy === detailY + 2) { - //center of bottom or top caps - ringRadius = 0; - } - - y -= height / 2; //shift coordiate origin to the center of object - for (ii = 0; ii < detailX; ++ii) { - const u = ii / (detailX - 1); - const ur = 2 * Math.PI * u; - const sur = Math.sin(ur); - const cur = Math.cos(ur); - - //VERTICES - this.vertices.push(new Vector(sur * ringRadius, y, cur * ringRadius)); - - //VERTEX NORMALS - let vertexNormal; - if (yy < 0) { - vertexNormal = new Vector(0, -1, 0); - } else if (yy > detailY && topRadius) { - vertexNormal = new Vector(0, 1, 0); - } else { - vertexNormal = new Vector(sur * cosSlant, sinSlant, cur * cosSlant); - } - this.vertexNormals.push(vertexNormal); - //UVs - this.uvs.push(u, v); - } - } - - let startIndex = 0; - if (bottomCap) { - for (jj = 0; jj < detailX; ++jj) { - const nextjj = (jj + 1) % detailX; - this.faces.push([ - startIndex + jj, - startIndex + detailX + nextjj, - startIndex + detailX + jj - ]); - } - startIndex += detailX * 2; - } - for (yy = 0; yy < detailY; ++yy) { - for (ii = 0; ii < detailX; ++ii) { - const nextii = (ii + 1) % detailX; - this.faces.push([ - startIndex + ii, - startIndex + nextii, - startIndex + detailX + nextii - ]); - this.faces.push([ - startIndex + ii, - startIndex + detailX + nextii, - startIndex + detailX + ii - ]); - } - startIndex += detailX; - } - if (topCap) { - startIndex += detailX; - for (ii = 0; ii < detailX; ++ii) { - this.faces.push([ - startIndex + ii, - startIndex + (ii + 1) % detailX, - startIndex + detailX - ]); - } - } - }; - /** * Draws a cylinder. * @@ -1700,32 +1474,7 @@ function primitives3D(p5, fn){ this._assert3d('cylinder'); p5._validateParameters('cylinder', arguments); - const gId = `cylinder|${detailX}|${detailY}|${bottomCap}|${topCap}`; - if (!this._renderer.geometryInHash(gId)) { - const cylinderGeom = new p5.Geometry(detailX, detailY); - _truncatedCone.call( - cylinderGeom, - 1, - 1, - 1, - detailX, - detailY, - bottomCap, - topCap - ); - // normals are computed in call to _truncatedCone - if (detailX <= 24 && detailY <= 16) { - cylinderGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer.states.doStroke) { - console.log( - 'Cannot draw stroke on cylinder objects with more' + - ' than 24 detailX or 16 detailY' - ); - } - this._renderer.createBuffers(gId, cylinderGeom); - } - - this._renderer.drawBuffersScaled(gId, radius, height, radius); + this._renderer.cylinder(radius, height, detailX, detailY, bottomCap, topCap); return this; }; @@ -1945,22 +1694,7 @@ function primitives3D(p5, fn){ this._assert3d('cone'); p5._validateParameters('cone', arguments); - const gId = `cone|${detailX}|${detailY}|${cap}`; - if (!this._renderer.geometryInHash(gId)) { - const coneGeom = new Geometry(detailX, detailY); - _truncatedCone.call(coneGeom, 1, 0, 1, detailX, detailY, cap, false); - if (detailX <= 24 && detailY <= 16) { - coneGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer.states.doStroke) { - console.log( - 'Cannot draw stroke on cone objects with more' + - ' than 24 detailX or 16 detailY' - ); - } - this._renderer.createBuffers(gId, coneGeom); - } - - this._renderer.drawBuffersScaled(gId, radius, height, radius); + this._renderer.cone(radius, height, detailX, detailY, cap); return this; }; @@ -2143,42 +1877,7 @@ function primitives3D(p5, fn){ this._assert3d('ellipsoid'); p5._validateParameters('ellipsoid', arguments); - const gId = `ellipsoid|${detailX}|${detailY}`; - - if (!this._renderer.geometryInHash(gId)) { - const _ellipsoid = function() { - for (let i = 0; i <= this.detailY; i++) { - const v = i / this.detailY; - const phi = Math.PI * v - Math.PI / 2; - const cosPhi = Math.cos(phi); - const sinPhi = Math.sin(phi); - - for (let j = 0; j <= this.detailX; j++) { - const u = j / this.detailX; - const theta = 2 * Math.PI * u; - const cosTheta = Math.cos(theta); - const sinTheta = Math.sin(theta); - const p = new p5.Vector(cosPhi * sinTheta, sinPhi, cosPhi * cosTheta); - this.vertices.push(p); - this.vertexNormals.push(p); - this.uvs.push(u, v); - } - } - }; - const ellipsoidGeom = new Geometry(detailX, detailY, _ellipsoid); - ellipsoidGeom.computeFaces(); - if (detailX <= 24 && detailY <= 24) { - ellipsoidGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer.states.doStroke) { - console.log( - 'Cannot draw stroke on ellipsoids with more' + - ' than 24 detailX or 24 detailY' - ); - } - this._renderer.createBuffers(gId, ellipsoidGeom); - } - - this._renderer.drawBuffersScaled(gId, radiusX, radiusY, radiusZ); + this._renderer.ellipsoid(radiusX, radiusY, radiusZ, detailX, detailY); return this; }; @@ -2336,77 +2035,15 @@ function primitives3D(p5, fn){ fn.torus = function(radius, tubeRadius, detailX, detailY) { this._assert3d('torus'); p5._validateParameters('torus', arguments); - if (typeof radius === 'undefined') { - radius = 50; - } else if (!radius) { - return; // nothing to draw - } - - if (typeof tubeRadius === 'undefined') { - tubeRadius = 10; - } else if (!tubeRadius) { - return; // nothing to draw - } - - if (typeof detailX === 'undefined') { - detailX = 24; - } - if (typeof detailY === 'undefined') { - detailY = 16; - } - - const tubeRatio = (tubeRadius / radius).toPrecision(4); - const gId = `torus|${tubeRatio}|${detailX}|${detailY}`; - - if (!this._renderer.geometryInHash(gId)) { - const _torus = function() { - for (let i = 0; i <= this.detailY; i++) { - const v = i / this.detailY; - const phi = 2 * Math.PI * v; - const cosPhi = Math.cos(phi); - const sinPhi = Math.sin(phi); - const r = 1 + tubeRatio * cosPhi; - - for (let j = 0; j <= this.detailX; j++) { - const u = j / this.detailX; - const theta = 2 * Math.PI * u; - const cosTheta = Math.cos(theta); - const sinTheta = Math.sin(theta); - - const p = new Vector( - r * cosTheta, - r * sinTheta, - tubeRatio * sinPhi - ); - const n = new Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); - - this.vertices.push(p); - this.vertexNormals.push(n); - this.uvs.push(u, v); - } - } - }; - const torusGeom = new Geometry(detailX, detailY, _torus); - torusGeom.computeFaces(); - if (detailX <= 24 && detailY <= 16) { - torusGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer.states.doStroke) { - console.log( - 'Cannot draw strokes on torus object with more' + - ' than 24 detailX or 16 detailY' - ); - } - this._renderer.createBuffers(gId, torusGeom); - } - this._renderer.drawBuffersScaled(gId, radius, radius, radius); + this._renderer.torus(radius, tubeRadius, detailX, detailY); return this; }; /////////////////////// - /// 2D primitives - ///////////////////////// + /// 2D primitives /// + /////////////////////// // // Note: Documentation is not generated on the p5.js website for functions on // the p5.RendererGL prototype. @@ -3577,6 +3214,420 @@ function primitives3D(p5, fn){ this.blendMode(constants.REMOVE); } }; + + /////////////////////// + /// 3D primitives /// + /////////////////////// + /** + * @private + * Helper function for creating both cones and cylinders + * Will only generate well-defined geometry when bottomRadius, height > 0 + * and topRadius >= 0 + * If topRadius == 0, topCap should be false + */ + const _truncatedCone = function( + bottomRadius, + topRadius, + height, + detailX, + detailY, + bottomCap, + topCap + ) { + bottomRadius = bottomRadius <= 0 ? 1 : bottomRadius; + topRadius = topRadius < 0 ? 0 : topRadius; + height = height <= 0 ? bottomRadius : height; + detailX = detailX < 3 ? 3 : detailX; + detailY = detailY < 1 ? 1 : detailY; + bottomCap = bottomCap === undefined ? true : bottomCap; + topCap = topCap === undefined ? topRadius !== 0 : topCap; + const start = bottomCap ? -2 : 0; + const end = detailY + (topCap ? 2 : 0); + //ensure constant slant for interior vertex normals + const slant = Math.atan2(bottomRadius - topRadius, height); + const sinSlant = Math.sin(slant); + const cosSlant = Math.cos(slant); + let yy, ii, jj; + for (yy = start; yy <= end; ++yy) { + let v = yy / detailY; + let y = height * v; + let ringRadius; + if (yy < 0) { + //for the bottomCap edge + y = 0; + v = 0; + ringRadius = bottomRadius; + } else if (yy > detailY) { + //for the topCap edge + y = height; + v = 1; + ringRadius = topRadius; + } else { + //for the middle + ringRadius = bottomRadius + (topRadius - bottomRadius) * v; + } + if (yy === -2 || yy === detailY + 2) { + //center of bottom or top caps + ringRadius = 0; + } + + y -= height / 2; //shift coordiate origin to the center of object + for (ii = 0; ii < detailX; ++ii) { + const u = ii / (detailX - 1); + const ur = 2 * Math.PI * u; + const sur = Math.sin(ur); + const cur = Math.cos(ur); + + //VERTICES + this.vertices.push(new Vector(sur * ringRadius, y, cur * ringRadius)); + + //VERTEX NORMALS + let vertexNormal; + if (yy < 0) { + vertexNormal = new Vector(0, -1, 0); + } else if (yy > detailY && topRadius) { + vertexNormal = new Vector(0, 1, 0); + } else { + vertexNormal = new Vector(sur * cosSlant, sinSlant, cur * cosSlant); + } + this.vertexNormals.push(vertexNormal); + //UVs + this.uvs.push(u, v); + } + } + + let startIndex = 0; + if (bottomCap) { + for (jj = 0; jj < detailX; ++jj) { + const nextjj = (jj + 1) % detailX; + this.faces.push([ + startIndex + jj, + startIndex + detailX + nextjj, + startIndex + detailX + jj + ]); + } + startIndex += detailX * 2; + } + for (yy = 0; yy < detailY; ++yy) { + for (ii = 0; ii < detailX; ++ii) { + const nextii = (ii + 1) % detailX; + this.faces.push([ + startIndex + ii, + startIndex + nextii, + startIndex + detailX + nextii + ]); + this.faces.push([ + startIndex + ii, + startIndex + detailX + nextii, + startIndex + detailX + ii + ]); + } + startIndex += detailX; + } + if (topCap) { + startIndex += detailX; + for (ii = 0; ii < detailX; ++ii) { + this.faces.push([ + startIndex + ii, + startIndex + (ii + 1) % detailX, + startIndex + detailX + ]); + } + } + }; + + RendererGL.prototype.plane = function( + width = 50, + height = width, + detailX = 1, + detailY = 1 + ) { + const gId = `plane|${detailX}|${detailY}`; + + if (!this.geometryInHash(gId)) { + const _plane = function() { + let u, v, p; + for (let i = 0; i <= this.detailY; i++) { + v = i / this.detailY; + for (let j = 0; j <= this.detailX; j++) { + u = j / this.detailX; + p = new Vector(u - 0.5, v - 0.5, 0); + this.vertices.push(p); + this.uvs.push(u, v); + } + } + }; + const planeGeom = new Geometry(detailX, detailY, _plane); + planeGeom.computeFaces().computeNormals(); + if (detailX <= 1 && detailY <= 1) { + planeGeom._makeTriangleEdges()._edgesToVertices(); + } else if (this.states.doStroke) { + console.log( + 'Cannot draw stroke on plane objects with more' + + ' than 1 detailX or 1 detailY' + ); + } + this.createBuffers(gId, planeGeom); + } + + this.drawBuffersScaled(gId, width, height, 1); + } + + RendererGL.prototype.box = function( + width = 50, + height = width, + depth = height, + detailX, + detailY + ){ + const perPixelLighting = + this.attributes && this.attributes.perPixelLighting; + if (typeof detailX === 'undefined') { + detailX = perPixelLighting ? 1 : 4; + } + if (typeof detailY === 'undefined') { + detailY = perPixelLighting ? 1 : 4; + } + + const gId = `box|${detailX}|${detailY}`; + if (!this.geometryInHash(gId)) { + const _box = function() { + const cubeIndices = [ + [0, 4, 2, 6], // -1, 0, 0],// -x + [1, 3, 5, 7], // +1, 0, 0],// +x + [0, 1, 4, 5], // 0, -1, 0],// -y + [2, 6, 3, 7], // 0, +1, 0],// +y + [0, 2, 1, 3], // 0, 0, -1],// -z + [4, 5, 6, 7] // 0, 0, +1] // +z + ]; + //using custom edges + //to avoid diagonal stroke lines across face of box + this.edges = [ + [0, 1], + [1, 3], + [3, 2], + [6, 7], + [8, 9], + [9, 11], + [14, 15], + [16, 17], + [17, 19], + [18, 19], + [20, 21], + [22, 23] + ]; + + cubeIndices.forEach((cubeIndex, i) => { + const v = i * 4; + for (let j = 0; j < 4; j++) { + const d = cubeIndex[j]; + //inspired by lightgl: + //https://github.com/evanw/lightgl.js + //octants:https://en.wikipedia.org/wiki/Octant_(solid_geometry) + const octant = new Vector( + ((d & 1) * 2 - 1) / 2, + ((d & 2) - 1) / 2, + ((d & 4) / 2 - 1) / 2 + ); + this.vertices.push(octant); + this.uvs.push(j & 1, (j & 2) / 2); + } + this.faces.push([v, v + 1, v + 2]); + this.faces.push([v + 2, v + 1, v + 3]); + }); + }; + const boxGeom = new Geometry(detailX, detailY, _box); + boxGeom.computeNormals(); + if (detailX <= 4 && detailY <= 4) { + boxGeom._edgesToVertices(); + } else if (this.states.doStroke) { + console.log( + 'Cannot draw stroke on box objects with more' + + ' than 4 detailX or 4 detailY' + ); + } + //initialize our geometry buffer with + //the key val pair: + //geometry Id, Geom object + this.createBuffers(gId, boxGeom); + } + this.drawBuffersScaled(gId, width, height, depth); + } + + RendererGL.prototype.sphere = function( + radius = 50, + detailX = 24, + detailY = 16 + ) { + this.ellipsoid(radius, radius, radius, detailX, detailY); + } + + RendererGL.prototype.ellipsoid = function( + radiusX = 50, + radiusY = radiusX, + radiusZ = radiusX, + detailX = 24, + detailY = 16 + ) { + const gId = `ellipsoid|${detailX}|${detailY}`; + + if (!this.geometryInHash(gId)) { + const _ellipsoid = function() { + for (let i = 0; i <= this.detailY; i++) { + const v = i / this.detailY; + const phi = Math.PI * v - Math.PI / 2; + const cosPhi = Math.cos(phi); + const sinPhi = Math.sin(phi); + + for (let j = 0; j <= this.detailX; j++) { + const u = j / this.detailX; + const theta = 2 * Math.PI * u; + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); + const p = new p5.Vector(cosPhi * sinTheta, sinPhi, cosPhi * cosTheta); + this.vertices.push(p); + this.vertexNormals.push(p); + this.uvs.push(u, v); + } + } + }; + const ellipsoidGeom = new Geometry(detailX, detailY, _ellipsoid); + ellipsoidGeom.computeFaces(); + if (detailX <= 24 && detailY <= 24) { + ellipsoidGeom._makeTriangleEdges()._edgesToVertices(); + } else if (this.states.doStroke) { + console.log( + 'Cannot draw stroke on ellipsoids with more' + + ' than 24 detailX or 24 detailY' + ); + } + this.createBuffers(gId, ellipsoidGeom); + } + + this.drawBuffersScaled(gId, radiusX, radiusY, radiusZ); + } + + RendererGL.prototype.cylinder = function( + radius = 50, + height = radius, + detailX = 24, + detailY = 1, + bottomCap = true, + topCap = true + ) { + const gId = `cylinder|${detailX}|${detailY}|${bottomCap}|${topCap}`; + if (!this.geometryInHash(gId)) { + const cylinderGeom = new p5.Geometry(detailX, detailY); + _truncatedCone.call( + cylinderGeom, + 1, + 1, + 1, + detailX, + detailY, + bottomCap, + topCap + ); + // normals are computed in call to _truncatedCone + if (detailX <= 24 && detailY <= 16) { + cylinderGeom._makeTriangleEdges()._edgesToVertices(); + } else if (this.states.doStroke) { + console.log( + 'Cannot draw stroke on cylinder objects with more' + + ' than 24 detailX or 16 detailY' + ); + } + this.createBuffers(gId, cylinderGeom); + } + + this.drawBuffersScaled(gId, radius, height, radius); + } + + RendererGL.prototype.cone = function( + radius = 50, + height = radius, + detailX = 24, + detailY = 1, + cap = true + ) { + const gId = `cone|${detailX}|${detailY}|${cap}`; + if (!this.geometryInHash(gId)) { + const coneGeom = new Geometry(detailX, detailY); + _truncatedCone.call(coneGeom, 1, 0, 1, detailX, detailY, cap, false); + if (detailX <= 24 && detailY <= 16) { + coneGeom._makeTriangleEdges()._edgesToVertices(); + } else if (this.states.doStroke) { + console.log( + 'Cannot draw stroke on cone objects with more' + + ' than 24 detailX or 16 detailY' + ); + } + this.createBuffers(gId, coneGeom); + } + + this.drawBuffersScaled(gId, radius, height, radius); + } + + RendererGL.prototype.torus = function( + radius = 50, + tubeRadius = 10, + detailX = 24, + detailY = 16 + ) { + if (radius === 0) { + return; // nothing to draw + } + + if (tubeRadius === 0) { + return; // nothing to draw + } + + const tubeRatio = (tubeRadius / radius).toPrecision(4); + const gId = `torus|${tubeRatio}|${detailX}|${detailY}`; + + if (!this.geometryInHash(gId)) { + const _torus = function() { + for (let i = 0; i <= this.detailY; i++) { + const v = i / this.detailY; + const phi = 2 * Math.PI * v; + const cosPhi = Math.cos(phi); + const sinPhi = Math.sin(phi); + const r = 1 + tubeRatio * cosPhi; + + for (let j = 0; j <= this.detailX; j++) { + const u = j / this.detailX; + const theta = 2 * Math.PI * u; + const cosTheta = Math.cos(theta); + const sinTheta = Math.sin(theta); + + const p = new Vector( + r * cosTheta, + r * sinTheta, + tubeRatio * sinPhi + ); + + const n = new Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); + + this.vertices.push(p); + this.vertexNormals.push(n); + this.uvs.push(u, v); + } + } + }; + const torusGeom = new Geometry(detailX, detailY, _torus); + torusGeom.computeFaces(); + if (detailX <= 24 && detailY <= 16) { + torusGeom._makeTriangleEdges()._edgesToVertices(); + } else if (this.states.doStroke) { + console.log( + 'Cannot draw strokes on torus object with more' + + ' than 24 detailX or 16 detailY' + ); + } + this.createBuffers(gId, torusGeom); + } + this.drawBuffersScaled(gId, radius, radius, radius); + } } export default primitives3D; From be387439bfa6922cf4968ac351dfc5b109788733 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 23 Oct 2024 20:36:17 +0100 Subject: [PATCH 32/55] Move some lights implementation to RendererGL Many of the remaining will need a better implementation of p5.Color internal API --- src/webgl/light.js | 168 +++++++++++++++++++++++++++------------------ 1 file changed, 102 insertions(+), 66 deletions(-) diff --git a/src/webgl/light.js b/src/webgl/light.js index 1dcbc0d42f..7a5041e599 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -1013,10 +1013,7 @@ function light(p5, fn){ * */ fn.imageLight = function (img) { - // activeImageLight property is checked by _setFillUniforms - // for sending uniforms to the fillshader - this._renderer.states.activeImageLight = img; - this._renderer.states.enableLighting = true; + this._renderer.imageLight(img); }; /** @@ -1232,40 +1229,11 @@ function light(p5, fn){ this._assert3d('lightFalloff'); p5._validateParameters('lightFalloff', arguments); - if (constantAttenuation < 0) { - constantAttenuation = 0; - console.warn( - 'Value of constant argument in lightFalloff() should be never be negative. Set to 0.' - ); - } - - if (linearAttenuation < 0) { - linearAttenuation = 0; - console.warn( - 'Value of linear argument in lightFalloff() should be never be negative. Set to 0.' - ); - } - - if (quadraticAttenuation < 0) { - quadraticAttenuation = 0; - console.warn( - 'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.' - ); - } - - if ( - constantAttenuation === 0 && - (linearAttenuation === 0 && quadraticAttenuation === 0) - ) { - constantAttenuation = 1; - console.warn( - 'Either one of the three arguments in lightFalloff() should be greater than zero. Set constant argument to 1.' - ); - } - - this._renderer.states.constantAttenuation = constantAttenuation; - this._renderer.states.linearAttenuation = linearAttenuation; - this._renderer.states.quadraticAttenuation = quadraticAttenuation; + this._renderer.lightFalloff( + constantAttenuation, + linearAttenuation, + quadraticAttenuation + ); return this; }; @@ -1750,35 +1718,103 @@ function light(p5, fn){ return this; }; -} -RendererGL.prototype.noLights = function(){ - this.states.activeImageLight = null; - this.states.enableLighting = false; - - this.states.ambientLightColors.length = 0; - this.states.specularColors = [1, 1, 1]; - - this.states.directionalLightDirections.length = 0; - this.states.directionalLightDiffuseColors.length = 0; - this.states.directionalLightSpecularColors.length = 0; - - this.states.pointLightPositions.length = 0; - this.states.pointLightDiffuseColors.length = 0; - this.states.pointLightSpecularColors.length = 0; - - this.states.spotLightPositions.length = 0; - this.states.spotLightDirections.length = 0; - this.states.spotLightDiffuseColors.length = 0; - this.states.spotLightSpecularColors.length = 0; - this.states.spotLightAngle.length = 0; - this.states.spotLightConc.length = 0; - - this.states.constantAttenuation = 1; - this.states.linearAttenuation = 0; - this.states.quadraticAttenuation = 0; - this.states._useShininess = 1; - this.states._useMetalness = 0; + + // RendererGL.prototype.ambientLight = function(v1, v2, v3, a) { + // } + + // RendererGL.prototype.specularColor = function(v1, v2, v3) { + // } + + // RendererGL.prototype.directionalLight = function(v1, v2, v3, x, y, z) { + // } + + // RendererGL.prototype.pointLight = function(v1, v2, v3, x, y, z) { + // } + + RendererGL.prototype.imageLight = function(img) { + // activeImageLight property is checked by _setFillUniforms + // for sending uniforms to the fillshader + this.states.activeImageLight = img; + this.states.enableLighting = true; + } + + // RendererGL.prototype.lights = function() { + // } + + RendererGL.prototype.lightFalloff = function( + constantAttenuation, + linearAttenuation, + quadraticAttenuation + ) { + if (constantAttenuation < 0) { + constantAttenuation = 0; + console.warn( + 'Value of constant argument in lightFalloff() should be never be negative. Set to 0.' + ); + } + + if (linearAttenuation < 0) { + linearAttenuation = 0; + console.warn( + 'Value of linear argument in lightFalloff() should be never be negative. Set to 0.' + ); + } + + if (quadraticAttenuation < 0) { + quadraticAttenuation = 0; + console.warn( + 'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.' + ); + } + + if ( + constantAttenuation === 0 && + (linearAttenuation === 0 && quadraticAttenuation === 0) + ) { + constantAttenuation = 1; + console.warn( + 'Either one of the three arguments in lightFalloff() should be greater than zero. Set constant argument to 1.' + ); + } + + this.states.constantAttenuation = constantAttenuation; + this.states.linearAttenuation = linearAttenuation; + this.states.quadraticAttenuation = quadraticAttenuation; + } + + // RendererGL.prototype.spotLight = function( + // ) { + // } + + RendererGL.prototype.noLights = function() { + this.states.activeImageLight = null; + this.states.enableLighting = false; + + this.states.ambientLightColors.length = 0; + this.states.specularColors = [1, 1, 1]; + + this.states.directionalLightDirections.length = 0; + this.states.directionalLightDiffuseColors.length = 0; + this.states.directionalLightSpecularColors.length = 0; + + this.states.pointLightPositions.length = 0; + this.states.pointLightDiffuseColors.length = 0; + this.states.pointLightSpecularColors.length = 0; + + this.states.spotLightPositions.length = 0; + this.states.spotLightDirections.length = 0; + this.states.spotLightDiffuseColors.length = 0; + this.states.spotLightSpecularColors.length = 0; + this.states.spotLightAngle.length = 0; + this.states.spotLightConc.length = 0; + + this.states.constantAttenuation = 1; + this.states.linearAttenuation = 0; + this.states.quadraticAttenuation = 0; + this.states._useShininess = 1; + this.states._useMetalness = 0; + } } export default light; From 194115042c4603c2a504add32760c580408c6e17 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 23 Oct 2024 21:10:51 +0100 Subject: [PATCH 33/55] Move material implementation to RendererGL More p5.Color normalization required --- src/image/pixels.js | 4 +- src/webgl/material.js | 106 +++++++++++++++++++++++++------------ src/webgl/p5.RendererGL.js | 32 +++++------ 3 files changed, 91 insertions(+), 51 deletions(-) diff --git a/src/image/pixels.js b/src/image/pixels.js index a85348bef5..6cf112ba80 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -728,7 +728,7 @@ function pixels(p5, fn){ // when passed a shader, use it directly if (this._renderer.isP3D && shader) { - p5.RendererGL.prototype.filter.call(this._renderer, shader); + this._renderer.filter(shader); return; } @@ -748,7 +748,7 @@ function pixels(p5, fn){ // when this is a webgl renderer, apply constant shader filter if (this._renderer.isP3D) { - p5.RendererGL.prototype.filter.call(this._renderer, operation, value); + this._renderer.filter(operation, value); } // when this is P2D renderer, create/use hidden webgl renderer diff --git a/src/webgl/material.js b/src/webgl/material.js index deac856d8a..3d4e0f8e7c 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -864,12 +864,10 @@ function material(p5, fn){ this._assert3d('shader'); p5._validateParameters('shader', arguments); + // NOTE: make generic or remove need for s.ensureCompiledOnContext(this); - // Always set the shader as a fill shader - this._renderer.states.userFillShader = s; - this._renderer.states._useNormalMaterial = false; - s.setDefaultUniforms(); + this._renderer.shader(s); return this; }; @@ -1042,16 +1040,14 @@ function material(p5, fn){ this._assert3d('strokeShader'); p5._validateParameters('strokeShader', arguments); + // NOTE: make generic or remove need for s.ensureCompiledOnContext(this); - this._renderer.states.userStrokeShader = s; - - s.setDefaultUniforms(); + this._renderer.strokeShader(s); return this; }; - /** * Sets the p5.Shader object to apply for images. * @@ -1200,16 +1196,14 @@ function material(p5, fn){ *
* */ - fn.imageShader = function (s) { this._assert3d('imageShader'); p5._validateParameters('imageShader', arguments); + // NOTE: make generic or remove need for s.ensureCompiledOnContext(this); - this._renderer.states.userImageShader = s; - - s.setDefaultUniforms(); + this._renderer.imageShader(s); return this; }; @@ -2042,10 +2036,7 @@ function material(p5, fn){ * */ fn.resetShader = function () { - this._renderer.states.userFillShader = null; - this._renderer.states.userStrokeShader = null; - this._renderer.states.userImageShader = null; - + this._renderer.resetShader(); return this; }; @@ -2224,14 +2215,13 @@ function material(p5, fn){ fn.texture = function (tex) { this._assert3d('texture'); p5._validateParameters('texture', arguments); + + // NOTE: make generic or remove need for if (tex.gifProperties) { tex._animateGif(this); } - this._renderer.states.drawMode = constants.TEXTURE; - this._renderer.states._useNormalMaterial = false; - this._renderer.states._tex = tex; - this._renderer.states.doFill = true; + this._renderer.texture(tex); return this; }; @@ -2739,13 +2729,9 @@ function material(p5, fn){ fn.normalMaterial = function (...args) { this._assert3d('normalMaterial'); p5._validateParameters('normalMaterial', args); - this._renderer.states.drawMode = constants.FILL; - this._renderer.states._useSpecularMaterial = false; - this._renderer.states._useEmissiveMaterial = false; - this._renderer.states._useNormalMaterial = true; - this._renderer.states.curFillColor = [1, 1, 1, 1]; - this._renderer.states.doFill = true; - this.noStroke(); + + this._renderer.normalMaterial(...args); + return this; }; @@ -3395,10 +3381,8 @@ function material(p5, fn){ this._assert3d('shininess'); p5._validateParameters('shininess', arguments); - if (shine < 1) { - shine = 1; - } - this._renderer.states._useShininess = shine; + this._renderer.shininess(shine); + return this; }; @@ -3513,11 +3497,13 @@ function material(p5, fn){ */ fn.metalness = function (metallic) { this._assert3d('metalness'); - const metalMix = 1 - Math.exp(-metallic / 100); - this._renderer.states._useMetalness = metalMix; + + this._renderer.metalness(metallic); + return this; }; + /** * @private blends colors according to color components. * If alpha value is less than 1, or non-standard blendMode @@ -3643,12 +3629,66 @@ function material(p5, fn){ } }; + RendererGL.prototype.shader = function(s) { + // Always set the shader as a fill shader + this.states.userFillShader = s; + this.states._useNormalMaterial = false; + s.setDefaultUniforms(); + } + + RendererGL.prototype.strokeShader = function(s) { + this.states.userStrokeShader = s; + s.setDefaultUniforms(); + } + + RendererGL.prototype.imageShader = function(s) { + this.states.userImageShader = s; + s.setDefaultUniforms(); + } + + RendererGL.prototype.resetShader = function() { + this.states.userFillShader = null; + this.states.userStrokeShader = null; + this.states.userImageShader = null; + } + RendererGL.prototype.texture = function(tex) { this.states.drawMode = constants.TEXTURE; this.states._useNormalMaterial = false; this.states._tex = tex; this.states.doFill = true; }; + + RendererGL.prototype.normalMaterial = function(...args) { + this.states.drawMode = constants.FILL; + this.states._useSpecularMaterial = false; + this.states._useEmissiveMaterial = false; + this.states._useNormalMaterial = true; + this.states.curFillColor = [1, 1, 1, 1]; + this.states.doFill = true; + this.states.doStroke = false; + } + + // RendererGL.prototype.ambientMaterial = function(v1, v2, v3) { + // } + + // RendererGL.prototype.emissiveMaterial = function(v1, v2, v3, a) { + // } + + // RendererGL.prototype.specularMaterial = function(v1, v2, v3, alpha) { + // } + + RendererGL.prototype.shininess = function(shine) { + if (shine < 1) { + shine = 1; + } + this.states._useShininess = shine; + } + + RendererGL.prototype.metalness = function(metallic) { + const metalMix = 1 - Math.exp(-metallic / 100); + this.states._useMetalness = metalMix; + } } export default material; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index dd41736435..4d5fb58462 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -828,12 +828,12 @@ class RendererGL extends Renderer { // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. this.matchSize(tmp, target); // setup - this._pInst.push(); + this.push(); this._pInst.noStroke(); this._pInst.blendMode(constants.BLEND); // draw main to temp buffer - this._pInst.shader(this.states.filterShader); + this.shader(this.states.filterShader); this.states.filterShader.setUniform('texelSize', texelSize); this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); this.states.filterShader.setUniform('radius', Math.max(1, filterParameter)); @@ -843,9 +843,9 @@ class RendererGL extends Renderer { this.states.filterShader.setUniform('direction', [1, 0]); this.states.filterShader.setUniform('tex0', target); this._pInst.clear(); - this._pInst.shader(this.states.filterShader); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); + this.shader(this.states.filterShader); + this.noLights(); + this.plane(target.width, target.height); }); // Vert pass: draw `tmp` to `fbo` @@ -853,35 +853,35 @@ class RendererGL extends Renderer { this.states.filterShader.setUniform('direction', [0, 1]); this.states.filterShader.setUniform('tex0', tmp); this._pInst.clear(); - this._pInst.shader(this.states.filterShader); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); + this.shader(this.states.filterShader); + this.noLights(); + this.plane(target.width, target.height); }); - this._pInst.pop(); + this.pop(); } // every other non-blur shader uses single pass else { fbo.draw(() => { this._pInst.noStroke(); this._pInst.blendMode(constants.BLEND); - this._pInst.shader(this.states.filterShader); + this.shader(this.states.filterShader); this.states.filterShader.setUniform('tex0', target); this.states.filterShader.setUniform('texelSize', texelSize); this.states.filterShader.setUniform('canvasSize', [target.width, target.height]); // filterParameter uniform only used for POSTERIZE, and THRESHOLD // but shouldn't hurt to always set this.states.filterShader.setUniform('filterParameter', filterParameter); - this._pInst.noLights(); - this._pInst.plane(target.width, target.height); + this.noLights(); + this.plane(target.width, target.height); }); } // draw fbo contents onto main renderer. - this._pInst.push(); + this.push(); this._pInst.noStroke(); this.clear(); - this._pInst.push(); + this.push(); this._pInst.imageMode(constants.CORNER); this._pInst.blendMode(constants.BLEND); target.filterCamera._resize(); @@ -892,8 +892,8 @@ class RendererGL extends Renderer { target.width, target.height); this._drawingFilter = false; this._pInst.clearDepth(); - this._pInst.pop(); - this._pInst.pop(); + this.pop(); + this.pop(); } // Pass this off to the host instance so that we can treat a renderer and a From b249cb5f3454a988e0ac6c1ffb7831fb06fd9d54 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 23 Oct 2024 21:44:20 +0100 Subject: [PATCH 34/55] Fix incorrect reference to renderer --- src/webgl/p5.RendererGL.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 4d5fb58462..521441e220 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -794,7 +794,7 @@ class RendererGL extends Renderer { // eg. filter(BLUR) then filter(GRAY) if (!(operation in this.defaultFilterShaders)) { this.defaultFilterShaders[operation] = new Shader( - fbo._renderer, + fbo.renderer, filterShaderVert, filterShaderFrags[operation] ); From 0021fa4cd4b6a7e5264af1b71d417195d35ae7b7 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 23 Oct 2024 22:04:27 +0100 Subject: [PATCH 35/55] Move camera implementation to RendererGL --- src/webgl/p5.Camera.js | 196 +++++++++++++++++++++---------------- src/webgl/p5.RendererGL.js | 2 +- 2 files changed, 111 insertions(+), 87 deletions(-) diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index b19788e7e4..7a906bc83c 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -3178,7 +3178,7 @@ function camera(p5, fn){ fn.camera = function (...args) { this._assert3d('camera'); p5._validateParameters('camera', args); - this._renderer.states.curCamera.camera(...args); + this._renderer.camera(...args); return this; }; @@ -3309,7 +3309,7 @@ function camera(p5, fn){ fn.perspective = function (...args) { this._assert3d('perspective'); p5._validateParameters('perspective', args); - this._renderer.states.curCamera.perspective(...args); + this._renderer.perspective(...args); return this; }; @@ -3436,13 +3436,7 @@ function camera(p5, fn){ if (!(this._renderer instanceof RendererGL)) { throw new Error('linePerspective() must be called in WebGL mode.'); } - if (enable !== undefined) { - // Set the line perspective if enable is provided - this._renderer.states.curCamera.useLinePerspective = enable; - } else { - // If no argument is provided, return the current value - return this._renderer.states.curCamera.useLinePerspective; - } + this._renderer.linePerspective(enable); }; @@ -3552,7 +3546,7 @@ function camera(p5, fn){ fn.ortho = function (...args) { this._assert3d('ortho'); p5._validateParameters('ortho', args); - this._renderer.states.curCamera.ortho(...args); + this._renderer.ortho(...args); return this; }; @@ -3664,14 +3658,10 @@ function camera(p5, fn){ fn.frustum = function (...args) { this._assert3d('frustum'); p5._validateParameters('frustum', args); - this._renderer.states.curCamera.frustum(...args); + this._renderer.frustum(...args); return this; }; - //////////////////////////////////////////////////////////////////////////////// - // p5.Camera - //////////////////////////////////////////////////////////////////////////////// - /** * Creates a new p5.Camera object and sets it * as the current (active) camera. @@ -3746,12 +3736,72 @@ function camera(p5, fn){ fn.createCamera = function () { this._assert3d('createCamera'); - // compute default camera settings, then set a default camera - const _cam = new Camera(this._renderer); - _cam._computeCameraDefaultSettings(); - _cam._setDefaultCamera(); + return this._renderer.createCamera(); + }; - return _cam; + /** + * 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); }; /** @@ -3873,74 +3923,48 @@ function camera(p5, fn){ */ p5.Camera = Camera; - /** - * 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.states.curCamera = cam; + RendererGL.prototype.camera = function(...args) { + this.states.curCamera.camera(...args); + } + + RendererGL.prototype.perspective = function(...args) { + this.states.curCamera.perspective(...args); + } + + RendererGL.prototype.linePerspective = function(enable) { + if (enable !== undefined) { + // 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; + } + } + + RendererGL.prototype.ortho = function(...args) { + this.states.curCamera.ortho(...args); + } + + RendererGL.prototype.frustum = function(...args) { + this.states.curCamera.frustum(...args); + } + + RendererGL.prototype.createCamera = function() { + // compute default camera settings, then set a default camera + const _cam = new Camera(this); + _cam._computeCameraDefaultSettings(); + _cam._setDefaultCamera(); + + return _cam; + } + + RendererGL.prototype.setCamera = function(cam) { + this.states.curCamera = cam; // set the projection matrix (which is not normally updated each frame) - this._renderer.states.uPMatrix.set(cam.projMatrix); - this._renderer.states.uViewMatrix.set(cam.cameraMatrix); - }; + this.states.uPMatrix.set(cam.projMatrix); + this.states.uViewMatrix.set(cam.cameraMatrix); + } } export default camera; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 521441e220..fca4a3caae 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -885,7 +885,7 @@ class RendererGL extends Renderer { this._pInst.imageMode(constants.CORNER); this._pInst.blendMode(constants.BLEND); target.filterCamera._resize(); - this._pInst.setCamera(target.filterCamera); + this.setCamera(target.filterCamera); this._pInst.resetMatrix(); this._drawingFilter = true; this._pInst.image(fbo, -target.width / 2, -target.height / 2, From f69ad65e7182509fee13cb1141f142851c60eef3 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 25 Oct 2024 11:04:51 +0100 Subject: [PATCH 36/55] Fix vertexProperty --- src/shape/vertex.js | 172 +++++++++++++++++++++++++++++++ src/webgl/p5.Geometry.js | 6 +- test/unit/webgl/p5.RendererGL.js | 2 +- 3 files changed, 176 insertions(+), 4 deletions(-) diff --git a/src/shape/vertex.js b/src/shape/vertex.js index ab60e8fe16..73afcc1653 100644 --- a/src/shape/vertex.js +++ b/src/shape/vertex.js @@ -2253,6 +2253,178 @@ function vertex(p5, fn){ return this; }; + + /** Sets the shader's vertex property or attribute variables. + * + * An 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. + * + * @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); + * } + * + *
+ * + *
+ * + * 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(x1,y1, 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(); + * } + * + *
+ * + * @method vertexProperty + * @param {String} attributeName the name of the vertex attribute. + * @param {Number|Number[]} data the data tied to the vertex attribute. + */ + fn.vertexProperty = function(attributeName, data){ + // this._assert3d('vertexProperty'); + // p5._validateParameters('vertexProperty', arguments); + this._renderer.vertexProperty(attributeName, data); + }; } export default vertex; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index c0e1c7d0ac..a5d5297b21 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1834,9 +1834,9 @@ class Geometry { //Setters setCurrentData(data) { const size = data.length ? data.length : 1; - 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()'); - } + // 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 diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 6fce6e402d..c4f2cb59f9 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2620,7 +2620,7 @@ suite('p5.RendererGL', function() { assert.equal(myp5._renderer.retainedMode.buffers.user.length, 0); } ); - test('Friendly error if different sizes used', + test.skip('Friendly error if different sizes used', function() { myp5.createCanvas(50, 50, myp5.WEBGL); const logs = []; From 9642ee52db4c2f265ebc8bb74243e1e0bef23f03 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 25 Oct 2024 11:09:26 +0100 Subject: [PATCH 37/55] Fix direct call to change renderer imagemode --- preview/index.html | 113 ++++++++++++++++++++++++++++--- src/webgl/p5.Framebuffer.js | 3 +- test/unit/webgl/p5.RendererGL.js | 3 +- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/preview/index.html b/preview/index.html index 299cf2ccb0..61ad05ced4 100644 --- a/preview/index.html +++ b/preview/index.html @@ -22,22 +22,115 @@ // p5.registerAddon(calculation); - const sketch = function (p) { - p.setup = function () { - p.createCanvas(200, 200); - }; + const sketch = function(p){ + let myBuffer, pg, torusLayer, boxLayer; + p.setup = function(){ + p.createCanvas(200, 200, p.WEBGL); + pg = p.createGraphics(100, 100, p.WEBGL); + + torusLayer = pg.createFramebuffer(); - p.draw = function () { - p.background(0, 50, 50); - p.circle(100, 100, 50); + const i = p.get(10, 10, 10, 10); + console.log(i); + }; - p.fill('white'); - p.textSize(30); - p.text('hello', 10, 30); + p.draw = function(){ + drawTorus(); + p.background(200); + pg.background(50); + pg.imageMode(p.CENTER); + pg.image(torusLayer, 0, 0); + p.imageMode(p.CENTER) + p.image(pg, 0, 0, 100, 100); }; + + function drawTorus() { + // Start drawing to the torus p5.Framebuffer. + torusLayer.begin(); + + // Clear the drawing surface. + pg.clear(); + + // Turn on the lights. + pg.lights(); + + // Rotate the coordinate system. + pg.rotateX(p.frameCount * 0.01); + pg.rotateY(p.frameCount * 0.01); + + // Style the torus. + pg.noStroke(); + + pg.box(20); + + // Start drawing to the torus p5.Framebuffer. + torusLayer.end(); + } }; new p5(sketch); + + // const sketch = function(p){ + // let myBuffer, pg, torusLayer, boxLayer; + // p.setup = function(){ + // p.createCanvas(200, 200); + // p.noLoop(); + // pg = p.createGraphics(100, 100, p.WEBGL); + + // torusLayer = pg.createFramebuffer(); + // }; + + // p.draw = function(){ + // // p.translate(-p.width/2, -p.height/2); + // // p.background(0, 50, 50); + // // p.circle(100, 100, 50); + + // drawTorus(); + // p.background(200); + // pg.background(50); + + // // pg.sphere(); + // for (let x = -50; x < 50; x += 25) { + // // Iterate from top to bottom. + // for (let y = -50; y < 50; y += 25) { + // // Draw the layer to the p5.Graphics object + // pg.image(torusLayer, x, y, 25, 25); + // } + // } + // p.image(pg, 0, 0, 100, 100); + + + // // p.fill('white'); + // // p.textSize(30); + // // p.text('hello', 10, 30); + // }; + + // function drawTorus() { + // // Start drawing to the torus p5.Framebuffer. + // torusLayer.begin(); + + // // Clear the drawing surface. + // pg.clear(); + + // // Turn on the lights. + // pg.lights(); + + // // Rotate the coordinate system. + // pg.rotateX(p.frameCount * 0.01); + // pg.rotateY(p.frameCount * 0.01); + + // // Style the torus. + // pg.noStroke(); + + // // Draw the torus. + // pg.torus(20); + + // // Start drawing to the torus p5.Framebuffer. + // torusLayer.end(); + // } + // }; + + // new p5(sketch); diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 80552128cd..a050b42b05 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -1555,7 +1555,8 @@ class Framebuffer { // framebuffer. this.begin(); this.renderer.push(); - this.renderer.imageMode(constants.CENTER); + // this.renderer.imageMode(constants.CENTER); + this.renderer.states.imageMode = constants.CENTER; this.renderer.resetMatrix(); this.renderer.states.doStroke = false; this.renderer.clear(); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index c4f2cb59f9..df09dfcc33 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -34,7 +34,8 @@ suite('p5.RendererGL', function() { assert.equal(myp5.webglVersion, myp5.WEBGL); }); - test('works on p5.Graphics', function() { + // NOTE: should graphics always create WebGL2 canvas? + test.skip('works on p5.Graphics', function() { myp5.createCanvas(10, 10, myp5.WEBGL); myp5.setAttributes({ version: 1 }); const g = myp5.createGraphics(10, 10, myp5.WEBGL); From 545621f06718cd449c3c9d119992d26217e2655e Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Fri, 25 Oct 2024 21:40:55 +0100 Subject: [PATCH 38/55] Fix incorrect reference to state --- preview/index.html | 116 ++++--------------------------------- src/webgl/p5.RendererGL.js | 2 +- 2 files changed, 11 insertions(+), 107 deletions(-) diff --git a/preview/index.html b/preview/index.html index 61ad05ced4..deb1e21e11 100644 --- a/preview/index.html +++ b/preview/index.html @@ -18,119 +18,23 @@ diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index fca4a3caae..b74caf59ec 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1439,7 +1439,7 @@ class RendererGL extends Renderer { return this._getNormalShader(); } // Use light shader if lighting or textures are enabled - else if (this.states._enableLighting || this.states._tex) { + else if (this.states.enableLighting || this.states._tex) { return this._getLightShader(); } // Default to color shader if no other conditions are met From 1f811b993d345e3429f3463500f1e4d811ccc1da Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 27 Oct 2024 10:30:15 +0000 Subject: [PATCH 39/55] Remove some references to pInst in RendererGL --- src/webgl/3d_primitives.js | 1 + src/webgl/p5.RendererGL.js | 55 +++++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 0b491e8828..3360c2b00c 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3168,6 +3168,7 @@ function primitives3D(p5, fn){ dWidth, dHeight ) { + // console.log(arguments); if (this._isErasing) { this.blendMode(this._cachedBlendMode); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b74caf59ec..5033eed404 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -773,7 +773,6 @@ class RendererGL extends Renderer { } } filter(...args) { - let fbo = this.getFilterLayer(); // use internal shader for filter constants BLUR, INVERT, etc @@ -814,7 +813,7 @@ class RendererGL extends Renderer { // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. this.matchSize(fbo, target); - fbo.draw(() => this._pInst.clear()); // prevent undesirable feedback effects accumulating secretly. + fbo.draw(() => this.clear()); // prevent undesirable feedback effects accumulating secretly. let texelSize = [ 1 / (target.width * target.pixelDensity()), @@ -829,8 +828,8 @@ class RendererGL extends Renderer { this.matchSize(tmp, target); // setup this.push(); - this._pInst.noStroke(); - this._pInst.blendMode(constants.BLEND); + this.states.doStroke = false; + this.blendMode(constants.BLEND); // draw main to temp buffer this.shader(this.states.filterShader); @@ -842,7 +841,7 @@ class RendererGL extends Renderer { tmp.draw(() => { this.states.filterShader.setUniform('direction', [1, 0]); this.states.filterShader.setUniform('tex0', target); - this._pInst.clear(); + this.clear(); this.shader(this.states.filterShader); this.noLights(); this.plane(target.width, target.height); @@ -852,7 +851,7 @@ class RendererGL extends Renderer { fbo.draw(() => { this.states.filterShader.setUniform('direction', [0, 1]); this.states.filterShader.setUniform('tex0', tmp); - this._pInst.clear(); + this.clear(); this.shader(this.states.filterShader); this.noLights(); this.plane(target.width, target.height); @@ -863,8 +862,8 @@ class RendererGL extends Renderer { // every other non-blur shader uses single pass else { fbo.draw(() => { - this._pInst.noStroke(); - this._pInst.blendMode(constants.BLEND); + this.states.doStroke = false; + this.blendMode(constants.BLEND); this.shader(this.states.filterShader); this.states.filterShader.setUniform('tex0', target); this.states.filterShader.setUniform('texelSize', texelSize); @@ -879,19 +878,24 @@ class RendererGL extends Renderer { } // draw fbo contents onto main renderer. this.push(); - this._pInst.noStroke(); + this.states.doStroke = false; this.clear(); this.push(); - this._pInst.imageMode(constants.CORNER); - this._pInst.blendMode(constants.BLEND); + this.states.imageMode = constants.CORNER; + this.blendMode(constants.BLEND); target.filterCamera._resize(); this.setCamera(target.filterCamera); - this._pInst.resetMatrix(); + this.resetMatrix(); this._drawingFilter = true; - this._pInst.image(fbo, -target.width / 2, -target.height / 2, - target.width, target.height); + this.image( + fbo, + 0, 0, + this.width, this.height, + -target.width / 2, -target.height / 2, + target.width, target.height + ); this._drawingFilter = false; - this._pInst.clearDepth(); + this.clearDepth(); this.pop(); this.pop(); } @@ -985,14 +989,14 @@ class RendererGL extends Renderer { ); gl.disable(gl.DEPTH_TEST); - this._pInst.push(); + this.push(); this._pInst.resetShader(); if (this.states.doFill) this._pInst.fill(0, 0); if (this.states.doStroke) this._pInst.stroke(0, 0); } endClip() { - this._pInst.pop(); + this.pop(); const gl = this.GL; gl.stencilOp( @@ -1122,12 +1126,13 @@ class RendererGL extends Renderer { const fbo = this._getTempFramebuffer(); fbo.pixels = this._pixelsState.pixels; fbo.updatePixels(); - this._pInst.push(); - this._pInst.resetMatrix(); - this._pInst.clear(); - this._pInst.imageMode(constants.CENTER); + this.push(); + this.resetMatrix(); + this.clear(); + this.states.imageMode = constants.CENTER; + // NOTE: call renderer image directly, need more arguments this._pInst.image(fbo, 0, 0); - this._pInst.pop(); + this.pop(); this.GL.clearDepth(1); this.GL.clear(this.GL.DEPTH_BUFFER_BIT); } @@ -1779,7 +1784,7 @@ class RendererGL extends Renderer { newFramebuffer.draw(() => { this._pInst.shader(this.states.diffusedShader); this.states.diffusedShader.setUniform('environmentMap', input); - this._pInst.noStroke(); + this.states.doStroke = false; this._pInst.rectMode(constants.CENTER); this._pInst.noLights(); this._pInst.rect(0, 0, width, height); @@ -1831,10 +1836,10 @@ class RendererGL extends Renderer { let roughness = 1 - currCount / count; framebuffer.draw(() => { this._pInst.shader(this.states.specularShader); - this._pInst.clear(); + this.clear(); this.states.specularShader.setUniform('environmentMap', input); this.states.specularShader.setUniform('roughness', roughness); - this._pInst.noStroke(); + this.states.doStroke = false; this._pInst.noLights(); this._pInst.plane(w, w); }); From da70a0e553121cb2f79d60cbe761245c76bb9ddb Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 27 Oct 2024 11:06:26 +0000 Subject: [PATCH 40/55] Fix p5.Graphics overwriting default drawing context --- src/core/p5.Renderer2D.js | 5 ++++- src/image/pixels.js | 1 - src/webgl/p5.RendererGL.js | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 4c67168500..3892f0e5b5 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -61,7 +61,9 @@ class Renderer2D extends Renderer { // Get and store drawing context this.drawingContext = this.canvas.getContext('2d'); - this._pInst.drawingContext = this.drawingContext; + if (isMainCanvas) { + this._pInst.drawingContext = this.drawingContext; + } this.scale(this._pixelDensity, this._pixelDensity); // Set and return p5.Element @@ -101,6 +103,7 @@ class Renderer2D extends Renderer { ) { this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); } + return this.filterGraphicsLayer; } diff --git a/src/image/pixels.js b/src/image/pixels.js index 6cf112ba80..a55e396055 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -754,7 +754,6 @@ function pixels(p5, fn){ // when this is P2D renderer, create/use hidden webgl renderer else { const filterGraphicsLayer = this.getFilterGraphicsLayer(); - // copy p2d canvas contents to secondary webgl renderer // dest filterGraphicsLayer.copy( diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 5033eed404..c567cc6350 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -126,7 +126,9 @@ class RendererGL extends Renderer { // This redundant property is useful in reminding you that you are // interacting with WebGLRenderingContext, still worth considering future removal this.GL = this.drawingContext; - this._pInst.drawingContext = this.drawingContext; + if (isMainCanvas) { + this._pInst.drawingContext = this.drawingContext; + } if (this._isMainCanvas) { // for pixel method sharing with pimage From 4314ec0666b561665fb77abbae60681ac1ab6e60 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 27 Oct 2024 11:14:09 +0000 Subject: [PATCH 41/55] Fix text rendering --- src/core/p5.Renderer2D.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 3892f0e5b5..99a7b9b20b 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1398,7 +1398,7 @@ class Renderer2D extends Renderer { } } - const p = p5.prototype.text.apply(this, arguments); + const p = super.text(...arguments); if (baselineHacked) { this.drawingContext.textBaseline = constants.BASELINE; From c402baaba0ca6331e127dd3987ea1b3cc26311f2 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 27 Oct 2024 13:51:12 +0000 Subject: [PATCH 42/55] Remove more references to pInst in RendererGL --- src/webgl/p5.RendererGL.js | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index c567cc6350..75e999fd10 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -750,14 +750,12 @@ class RendererGL extends Renderer { } getFilterLayer() { if (!this.filterLayer) { - // this.filterLayer = this._pInst.createFramebuffer(); this.filterLayer = new Framebuffer(this); } return this.filterLayer; } getFilterLayerTemp() { if (!this.filterLayerTemp) { - // this.filterLayerTemp = this._pInst.createFramebuffer(); this.filterLayerTemp = new Framebuffer(this); } return this.filterLayerTemp; @@ -992,9 +990,9 @@ class RendererGL extends Renderer { gl.disable(gl.DEPTH_TEST); this.push(); - this._pInst.resetShader(); - if (this.states.doFill) this._pInst.fill(0, 0); - if (this.states.doStroke) this._pInst.stroke(0, 0); + this.resetShader(); + if (this.states.doFill) this.fill(0, 0); + if (this.states.doStroke) this.stroke(0, 0); } endClip() { @@ -1132,8 +1130,7 @@ class RendererGL extends Renderer { this.resetMatrix(); this.clear(); this.states.imageMode = constants.CENTER; - // NOTE: call renderer image directly, need more arguments - this._pInst.image(fbo, 0, 0); + this.image(fbo, 0, 0, fbo.width, fbo.height, 0, 0, fbo.width, fbo.height); this.pop(); this.GL.clearDepth(1); this.GL.clear(this.GL.DEPTH_BUFFER_BIT); @@ -1147,12 +1144,6 @@ class RendererGL extends Renderer { */ _getTempFramebuffer() { if (!this._tempFramebuffer) { - // this._tempFramebuffer = this._pInst.createFramebuffer({ - // format: constants.UNSIGNED_BYTE, - // useDepth: this._pInst._glAttributes.depth, - // depthFormat: constants.UNSIGNED_INT, - // antialias: this._pInst._glAttributes.antialias - // }); this._tempFramebuffer = new Framebuffer(this, { format: constants.UNSIGNED_BYTE, useDepth: this._pInst._glAttributes.depth, @@ -1769,9 +1760,6 @@ class RendererGL extends Renderer { let smallWidth = 200; let width = smallWidth; let height = Math.floor(smallWidth * (input.height / input.width)); - // newFramebuffer = this._pInst.createFramebuffer({ - // width, height, density: 1 - // }); newFramebuffer = new Framebuffer(this, { width, height, density: 1 }) @@ -1784,12 +1772,12 @@ class RendererGL extends Renderer { ); } newFramebuffer.draw(() => { - this._pInst.shader(this.states.diffusedShader); + this.shader(this.states.diffusedShader); this.states.diffusedShader.setUniform('environmentMap', input); this.states.doStroke = false; - this._pInst.rectMode(constants.CENTER); - this._pInst.noLights(); - this._pInst.rect(0, 0, width, height); + this.rectMode(constants.CENTER); + this.noLights(); + this.rect(0, 0, width, height); }); this.diffusedTextures.set(input, newFramebuffer); return newFramebuffer; @@ -1814,9 +1802,6 @@ class RendererGL extends Renderer { const size = 512; let tex; const levels = []; - // const framebuffer = this._pInst.createFramebuffer({ - // width: size, height: size, density: 1 - // }); const framebuffer = new Framebuffer(this, { width: size, height: size, density: 1 }); @@ -1837,13 +1822,13 @@ class RendererGL extends Renderer { let currCount = Math.log(w) / Math.log(2); let roughness = 1 - currCount / count; framebuffer.draw(() => { - this._pInst.shader(this.states.specularShader); + this.shader(this.states.specularShader); this.clear(); this.states.specularShader.setUniform('environmentMap', input); this.states.specularShader.setUniform('roughness', roughness); this.states.doStroke = false; - this._pInst.noLights(); - this._pInst.plane(w, w); + this.noLights(); + this.plane(w, w); }); levels.push(framebuffer.get().drawingContext.getImageData(0, 0, w, w)); } From 8a3f364b172fc079f07323240ca55d4ce97e70ea Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Oct 2024 18:06:04 -0400 Subject: [PATCH 43/55] Make sure ensureCompiledOnContext is called for internal shader() calls --- src/webgl/material.js | 12 +++--------- src/webgl/p5.Shader.js | 6 +++--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 3d4e0f8e7c..4d384a2f66 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -864,9 +864,6 @@ function material(p5, fn){ this._assert3d('shader'); p5._validateParameters('shader', arguments); - // NOTE: make generic or remove need for - s.ensureCompiledOnContext(this); - this._renderer.shader(s); return this; @@ -1040,9 +1037,6 @@ function material(p5, fn){ this._assert3d('strokeShader'); p5._validateParameters('strokeShader', arguments); - // NOTE: make generic or remove need for - s.ensureCompiledOnContext(this); - this._renderer.strokeShader(s); return this; @@ -1200,9 +1194,6 @@ function material(p5, fn){ this._assert3d('imageShader'); p5._validateParameters('imageShader', arguments); - // NOTE: make generic or remove need for - s.ensureCompiledOnContext(this); - this._renderer.imageShader(s); return this; @@ -3633,16 +3624,19 @@ function material(p5, fn){ // Always set the shader as a fill shader this.states.userFillShader = s; this.states._useNormalMaterial = false; + s.ensureCompiledOnContext(this); s.setDefaultUniforms(); } RendererGL.prototype.strokeShader = function(s) { this.states.userStrokeShader = s; + s.ensureCompiledOnContext(this); s.setDefaultUniforms(); } RendererGL.prototype.imageShader = function(s) { this.states.userImageShader = s; + s.ensureCompiledOnContext(this); s.setDefaultUniforms(); } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 673a2fda28..6093b9590c 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -612,7 +612,7 @@ class Shader { this._vertSrc, this._fragSrc ); - shader.ensureCompiledOnContext(context); + shader.ensureCompiledOnContext(context._renderer); return shader; } @@ -620,12 +620,12 @@ class Shader { * @private */ ensureCompiledOnContext(context) { - if (this._glProgram !== 0 && this._renderer !== context._renderer) { + if (this._glProgram !== 0 && this._renderer !== context) { throw new Error( 'The shader being run is attached to a different context. Do you need to copy it to this context first with .copyToContext()?' ); } else if (this._glProgram === 0) { - this._renderer = context._renderer; + this._renderer = context; this.init(); } } From 6fc9e8da9d93b88e2df3963862dc7c843ee1be61 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Oct 2024 18:09:51 -0400 Subject: [PATCH 44/55] Fix createFilterShader context --- src/webgl/material.js | 4 ++-- test/unit/webgl/p5.RendererGL.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/webgl/material.js b/src/webgl/material.js index 4d384a2f66..038bd6ce8b 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -673,9 +673,9 @@ function material(p5, fn){ let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; const shader = new Shader(this._renderer, vertSrc, fragSrc); if (this._renderer.GL) { - shader.ensureCompiledOnContext(this); + shader.ensureCompiledOnContext(this._renderer); } else { - shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); + shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()._renderer); } return shader; }; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index df09dfcc33..7a5d1b9aec 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2506,10 +2506,10 @@ suite('p5.RendererGL', function() { }); suite('vertexProperty()', function() { - test('Immediate mode data and buffers created in beginShape', + test('Immediate mode data and buffers created in beginShape', function() { myp5.createCanvas(50, 50, myp5.WEBGL); - + myp5.beginShape(); myp5.vertexProperty('aCustom', 1); myp5.vertexProperty('aCustomVec3', [1, 2, 3]); @@ -2543,10 +2543,10 @@ suite('p5.RendererGL', function() { myp5.endShape(); } ); - test('Immediate mode data and buffers deleted after beginShape', + test('Immediate mode data and buffers deleted after beginShape', function() { myp5.createCanvas(50, 50, myp5.WEBGL); - + myp5.beginShape(); myp5.vertexProperty('aCustom', 1); myp5.vertexProperty('aCustomVec3', [1,2,3]); From fd02fe4b3f424aa7020530cd0189c77095229dc8 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Oct 2024 18:11:56 -0400 Subject: [PATCH 45/55] Load images before creating canvas --- test/unit/visual/cases/shapes.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/unit/visual/cases/shapes.js b/test/unit/visual/cases/shapes.js index c8b37c874b..3bf85b8218 100644 --- a/test/unit/visual/cases/shapes.js +++ b/test/unit/visual/cases/shapes.js @@ -207,9 +207,8 @@ visualSuite('Shape drawing', function() { }); visualTest('Texture coordinates', async function(p5, screenshot) { - setup(p5); const tex = await p5.loadImage('/unit/assets/cat.jpg'); - + setup(p5); p5.texture(tex); p5.beginShape(p5.QUAD_STRIP); p5.vertex(10, 10, 0, 0, 0); @@ -222,9 +221,8 @@ visualSuite('Shape drawing', function() { }); visualTest('Normalized texture coordinates', async function(p5, screenshot) { - setup(p5); const tex = await p5.loadImage('/unit/assets/cat.jpg'); - + setup(p5); p5.texture(tex); p5.textureMode(p5.NORMAL); p5.beginShape(p5.QUAD_STRIP); From 0f444136e36f68e0a747d519177ff4cd0890431d Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Oct 2024 18:20:57 -0400 Subject: [PATCH 46/55] Clear cached maxTextureSize for tests with stubbed value --- test/unit/webgl/p5.Framebuffer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/unit/webgl/p5.Framebuffer.js b/test/unit/webgl/p5.Framebuffer.js index 3c56a3fb65..cbf78d07f4 100644 --- a/test/unit/webgl/p5.Framebuffer.js +++ b/test/unit/webgl/p5.Framebuffer.js @@ -199,21 +199,25 @@ suite('p5.Framebuffer', function() { test('resizes the framebuffer by createFramebuffer based on max texture size', function() { myp5.createCanvas(10, 10, myp5.WEBGL); + delete myp5._renderer._maxTextureSize; glStub = vi.spyOn(myp5._renderer, '_getMaxTextureSize'); const fakeMaxTextureSize = 100; glStub.mockReturnValue(fakeMaxTextureSize); const fbo = myp5.createFramebuffer({ width: 200, height: 200 }); + delete myp5._renderer._maxTextureSize; expect(fbo.width).to.equal(100); expect(fbo.height).to.equal(100); }); test('resizes the framebuffer by resize method based on max texture size', function() { myp5.createCanvas(10, 10, myp5.WEBGL); + delete myp5._renderer._maxTextureSize; glStub = vi.spyOn(myp5._renderer, '_getMaxTextureSize'); const fakeMaxTextureSize = 100; glStub.mockReturnValue(fakeMaxTextureSize); const fbo = myp5.createFramebuffer({ width: 10, height: 10 }); fbo.resize(200, 200); + delete myp5._renderer._maxTextureSize; expect(fbo.width).to.equal(100); expect(fbo.height).to.equal(100); }); From bea4d022fef0d2f4c61a8ddbce20b70f8eb56d1f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Oct 2024 19:58:42 -0400 Subject: [PATCH 47/55] Make internal stroke and fill also enable stroke and fill --- src/color/setting.js | 4 ---- src/core/p5.Renderer.js | 10 ++++++++++ src/core/p5.Renderer2D.js | 2 ++ src/webgl/p5.RendererGL.js | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/color/setting.js b/src/color/setting.js index 6c2f5cb72d..3c7a68c003 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -1209,8 +1209,6 @@ function setting(p5, fn){ * @chainable */ fn.fill = function(...args) { - this._renderer.states.fillSet = true; - this._renderer.states.doFill = true; this._renderer.fill(...args); return this; }; @@ -1581,8 +1579,6 @@ function setting(p5, fn){ */ fn.stroke = function(...args) { - this._renderer.states.strokeSet = true; - this._renderer.states.doStroke = true; this._renderer.stroke(...args); return this; }; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 4002ddcf6e..308d8f771e 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -154,6 +154,16 @@ class Renderer { } + fill() { + this.states.fillSet = true; + this.states.doFill = true; + } + + stroke() { + this.states.strokeSet = true; + this.states.doStroke = true; + } + textSize(s) { if (typeof s === 'number') { this.states.textSize = s; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 99a7b9b20b..093266a7cb 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -200,6 +200,7 @@ class Renderer2D extends Renderer { } fill(...args) { + super.fill(...args); const color = this._pInst.color(...args); this._setFill(color.toString()); @@ -210,6 +211,7 @@ class Renderer2D extends Renderer { } stroke(...args) { + super.stroke(...args); const color = this._pInst.color(...args); this._setStroke(color.toString()); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 75e999fd10..4451f01efe 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -697,6 +697,7 @@ class RendererGL extends Renderer { * black canvas with purple cube spinning */ fill(...args) { + super.fill(...args); //see material.js for more info on color blending in webgl // const color = fn.color.apply(this._pInst, arguments); const color = this._pInst.color(...args); @@ -736,6 +737,7 @@ class RendererGL extends Renderer { * black canvas with purple cube with pink outline spinning */ stroke(...args) { + super.stroke(...args); // const color = fn.color.apply(this._pInst, arguments); const color = this._pInst.color(...args); this.states.curStrokeColor = color._array; From 9ae00f4b359a9abba684794eaab3932c74be1c17 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Oct 2024 20:08:00 -0400 Subject: [PATCH 48/55] Make sure default attributes are loaded before initializing WebGL context --- src/webgl/p5.RendererGL.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 4451f01efe..f4a82a5b77 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -122,6 +122,7 @@ class RendererGL extends Renderer { // Create new canvas this.canvas = this.elt = elt || document.createElement('canvas'); + this._setAttributeDefaults(pInst); this._initContext(); // This redundant property is useful in reminding you that you are // interacting with WebGLRenderingContext, still worth considering future removal @@ -176,7 +177,6 @@ class RendererGL extends Renderer { document.getElementsByTagName('main')[0].appendChild(this.elt); } - this._setAttributeDefaults(pInst); this.isP3D = true; //lets us know we're in 3d mode // When constructing a new Geometry, this will represent the builder From c0ec0641da467161b4c8b8af2046d3745c2022c5 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 2 Nov 2024 11:43:33 -0400 Subject: [PATCH 49/55] Make p5 also expose properties from the renderer like graphics do --- src/core/p5.Graphics.js | 1 + src/core/p5.Renderer.js | 7 +++---- src/core/p5.Renderer2D.js | 30 ++++++++++++---------------- src/core/rendering.js | 12 +++++++++++ src/dom/dom.js | 6 ------ src/image/pixels.js | 1 - src/webgl/3d_primitives.js | 2 +- src/webgl/material.js | 6 +++--- src/webgl/p5.RendererGL.Immediate.js | 2 +- src/webgl/p5.RendererGL.js | 21 ++++++++----------- src/webgl/p5.Texture.js | 4 ++-- test/unit/webgl/p5.Texture.js | 4 ++-- 12 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index b86e094f90..38dab1af30 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -45,6 +45,7 @@ class Graphics { // Attach renderer properties for (const p in this._renderer) { if(p[0] === '_' || typeof this._renderer[p] === 'function') continue; + if (Object.hasOwn(this, p)) continue; Object.defineProperty(this, p, { get(){ return this._renderer?.[p]; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 308d8f771e..09eebec1bd 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -9,7 +9,7 @@ import { Image } from '../image/p5.Image'; class Renderer { constructor(pInst, w, h, isMainCanvas) { - this._pInst = this._pixelsState = pInst; + this._pInst = pInst; this._isMainCanvas = isMainCanvas; this.pixels = []; this._pixelDensity = Math.ceil(window.devicePixelRatio) || 1; @@ -117,15 +117,14 @@ class Renderer { } get(x, y, w, h) { - const pixelsState = this._pixelsState; const pd = this._pixelDensity; const canvas = this.canvas; if (typeof x === 'undefined' && typeof y === 'undefined') { // get() x = y = 0; - w = pixelsState.width; - h = pixelsState.height; + w = this.width; + h = this.height; } else { x *= pd; y *= pd; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 093266a7cb..be490ca829 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -495,23 +495,20 @@ class Renderer2D extends Renderer { } loadPixels() { - const pixelsState = this._pixelsState; // if called by p5.Image - const pd = this._pixelDensity; const w = this.width * pd; const h = this.height * pd; const imageData = this.drawingContext.getImageData(0, 0, w, h); // @todo this should actually set pixels per object, so diff buffers can // have diff pixel arrays. - pixelsState.imageData = imageData; - this.pixels = pixelsState.pixels = imageData.data; + this.imageData = imageData; + this.pixels = imageData.data; } set(x, y, imgOrCol) { // round down to get integer numbers x = Math.floor(x); y = Math.floor(y); - const pixelsState = this._pixelsState; if (imgOrCol instanceof Image) { this.drawingContext.save(); this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); @@ -533,11 +530,11 @@ class Renderer2D extends Renderer { this._pixelDensity * (this.width * this._pixelDensity) + x * this._pixelDensity); - if (!pixelsState.imageData) { - pixelsState.loadPixels(); + if (!this.imageData) { + this.loadPixels(); } if (typeof imgOrCol === 'number') { - if (idx < pixelsState.pixels.length) { + if (idx < this.pixels.length) { r = imgOrCol; g = imgOrCol; b = imgOrCol; @@ -548,7 +545,7 @@ class Renderer2D extends Renderer { if (imgOrCol.length < 4) { throw new Error('pixel array must be of the form [R, G, B, A]'); } - if (idx < pixelsState.pixels.length) { + if (idx < this.pixels.length) { r = imgOrCol[0]; g = imgOrCol[1]; b = imgOrCol[2]; @@ -556,7 +553,7 @@ class Renderer2D extends Renderer { //this.updatePixels.call(this); } } else if (imgOrCol instanceof p5.Color) { - if (idx < pixelsState.pixels.length) { + if (idx < this.pixels.length) { r = imgOrCol.levels[0]; g = imgOrCol.levels[1]; b = imgOrCol.levels[2]; @@ -574,17 +571,16 @@ class Renderer2D extends Renderer { this.width * this._pixelDensity + (x * this._pixelDensity + i)); - pixelsState.pixels[idx] = r; - pixelsState.pixels[idx + 1] = g; - pixelsState.pixels[idx + 2] = b; - pixelsState.pixels[idx + 3] = a; + this.pixels[idx] = r; + this.pixels[idx + 1] = g; + this.pixels[idx + 2] = b; + this.pixels[idx + 3] = a; } } } } updatePixels(x, y, w, h) { - const pixelsState = this._pixelsState; const pd = this._pixelDensity; if ( x === undefined && @@ -604,10 +600,10 @@ class Renderer2D extends Renderer { if (this.gifProperties) { this.gifProperties.frames[this.gifProperties.displayIndex].image = - pixelsState.imageData; + this.imageData; } - this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); + this.drawingContext.putImageData(this.imageData, x, y, 0, 0, w, h); } ////////////////////////////////////////////// diff --git a/src/core/rendering.js b/src/core/rendering.js index de6387ca6b..a85b215f1f 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -143,6 +143,18 @@ function rendering(p5, fn){ this._defaultGraphicsCreated = true; this._elements.push(this._renderer); this._renderer._applyDefaults(); + + // Attach renderer properties + for (const p in this._renderer) { + if (p[0] === '_' || typeof this._renderer[p] === 'function') continue; + if (Object.hasOwn(this, p)) continue; + Object.defineProperty(this, p, { + get(){ + return this._renderer?.[p]; + } + }) + } + return this._renderer; }; diff --git a/src/dom/dom.js b/src/dom/dom.js index 549e61391a..64fea54981 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -2624,17 +2624,14 @@ function dom(p5, fn){ if (arg instanceof p5.Element && arg.elt instanceof HTMLSelectElement) { // If given argument is p5.Element of select type self = arg; - this.elt = arg.elt; } else if (arg instanceof HTMLSelectElement) { self = addElement(arg, this); - this.elt = arg; } else { const elt = document.createElement('select'); if (arg && typeof arg === 'boolean') { elt.setAttribute('multiple', 'true'); } self = addElement(elt, this); - this.elt = elt; } self.option = function (name, value) { let index; @@ -2884,21 +2881,18 @@ function dom(p5, fn){ ) { // If given argument is p5.Element of div/span type self = arg0; - this.elt = arg0.elt; } else if ( // If existing radio Element is provided as argument 0 arg0 instanceof HTMLDivElement || arg0 instanceof HTMLSpanElement ) { self = addElement(arg0, this); - this.elt = arg0; radioElement = arg0; if (typeof args[1] === 'string') name = args[1]; } else { if (typeof arg0 === 'string') name = arg0; radioElement = document.createElement('div'); self = addElement(radioElement, this); - this.elt = radioElement; } self._name = name || 'radioOption'; diff --git a/src/image/pixels.js b/src/image/pixels.js index a55e396055..78df6bd877 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -142,7 +142,6 @@ function pixels(p5, fn){ *
* */ - fn.pixels = []; /** * Copies a region of pixels from one image to another. diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 3360c2b00c..98b64726cf 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3178,7 +3178,7 @@ function primitives3D(p5, fn){ this.states.doStroke = false;; this.texture(img); - this.textureMode = constants.NORMAL; + this.states.textureMode = constants.NORMAL; let u0 = 0; if (sx <= img.width) { diff --git a/src/webgl/material.js b/src/webgl/material.js index 038bd6ce8b..0c0a8e035f 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -2392,7 +2392,7 @@ function material(p5, fn){ `You tried to set ${mode} textureMode only supports IMAGE & NORMAL ` ); } else { - this._renderer.textureMode = mode; + this._renderer.states.textureMode = mode; } }; @@ -2671,8 +2671,8 @@ function material(p5, fn){ * */ fn.textureWrap = function (wrapX, wrapY = wrapX) { - this._renderer.textureWrapX = wrapX; - this._renderer.textureWrapY = wrapY; + this._renderer.states.textureWrapX = wrapX; + this._renderer.states.textureWrapY = wrapY; for (const texture of this._renderer.textures.values()) { texture.setWrapMode(wrapX, wrapY); diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index fe87d237a2..c8454b6b8e 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -146,7 +146,7 @@ function rendererGLImmediate(p5, fn){ lineVertexColor[3] ); - if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { + if (this.states.textureMode === constants.IMAGE && !this.isProcessingVertices) { if (this.states._tex !== null) { if (this.states._tex.width > 0 && this.states._tex.height > 0) { u /= this.states._tex.width; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index f4a82a5b77..7d2ae95047 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -235,6 +235,9 @@ class RendererGL extends Renderer { this.states.drawMode = constants.FILL; this.states._tex = null; + this.states.textureMode = constants.IMAGE; + this.states.textureWrapX = constants.CLAMP; + this.states.textureWrapY = constants.CLAMP; // erasing this._isErasing = false; @@ -382,11 +385,6 @@ class RendererGL extends Renderer { this.filterLayerTemp = undefined; this.defaultFilterShaders = {}; - this.textureMode = constants.IMAGE; - // default wrap settings - this.textureWrapX = constants.CLAMP; - this.textureWrapY = constants.CLAMP; - this.states._tex = null; this._curveTightness = 6; // lookUpTable for coefficients needed to be calculated for bezierVertex, same are used for curveVertex @@ -1096,8 +1094,6 @@ class RendererGL extends Renderer { * @private */ loadPixels() { - const pixelsState = this._pixelsState; - //@todo_FES if (this._pInst._glAttributes.preserveDrawingBuffer !== true) { console.log( @@ -1109,9 +1105,9 @@ class RendererGL extends Renderer { const pd = this._pixelDensity; const gl = this.GL; - pixelsState.pixels = + this.pixels = readPixelsWebGL( - pixelsState.pixels, + this.pixels, gl, null, 0, @@ -1126,7 +1122,7 @@ class RendererGL extends Renderer { updatePixels() { const fbo = this._getTempFramebuffer(); - fbo.pixels = this._pixelsState.pixels; + fbo.pixels = this.pixels; fbo.updatePixels(); this.push(); this.resetMatrix(); @@ -1212,9 +1208,8 @@ class RendererGL extends Renderer { this.states.curCamera._resize(); //resize pixels buffer - const pixelsState = this._pixelsState; - if (typeof pixelsState.pixels !== 'undefined') { - pixelsState.pixels = + if (typeof this.pixels !== 'undefined') { + this.pixels = new Uint8Array( this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 ); diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 0aad977323..b722c8a1b2 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -122,8 +122,8 @@ class Texture { this.glTex = gl.createTexture(); } - this.glWrapS = this._renderer.textureWrapX; - this.glWrapT = this._renderer.textureWrapY; + this.glWrapS = this._renderer.states.textureWrapX; + this.glWrapT = this._renderer.states.textureWrapY; this.setWrapMode(this.glWrapS, this.glWrapT); this.bindTexture(); diff --git a/test/unit/webgl/p5.Texture.js b/test/unit/webgl/p5.Texture.js index 6c04249f2e..2c0ba6f36d 100644 --- a/test/unit/webgl/p5.Texture.js +++ b/test/unit/webgl/p5.Texture.js @@ -127,11 +127,11 @@ suite('p5.Texture', function() { ); test('Set textureMode to NORMAL', function() { myp5.textureMode(myp5.NORMAL); - assert.deepEqual(myp5._renderer.textureMode, myp5.NORMAL); + assert.deepEqual(myp5._renderer.states.textureMode, myp5.NORMAL); }); test('Set textureMode to IMAGE', function() { myp5.textureMode(myp5.IMAGE); - assert.deepEqual(myp5._renderer.textureMode, myp5.IMAGE); + assert.deepEqual(myp5._renderer.states.textureMode, myp5.IMAGE); }); test('Set global wrap mode to clamp', function() { myp5.textureWrap(myp5.CLAMP); From 313009056f64fb1204df93687f5579fc4af6bb13 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 2 Nov 2024 12:01:33 -0400 Subject: [PATCH 50/55] Just expose pixels on the main instance, actually --- src/core/p5.Graphics.js | 1 - src/core/rendering.js | 12 +++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/core/p5.Graphics.js b/src/core/p5.Graphics.js index 38dab1af30..b86e094f90 100644 --- a/src/core/p5.Graphics.js +++ b/src/core/p5.Graphics.js @@ -45,7 +45,6 @@ class Graphics { // Attach renderer properties for (const p in this._renderer) { if(p[0] === '_' || typeof this._renderer[p] === 'function') continue; - if (Object.hasOwn(this, p)) continue; Object.defineProperty(this, p, { get(){ return this._renderer?.[p]; diff --git a/src/core/rendering.js b/src/core/rendering.js index a85b215f1f..29a779d7ea 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -144,15 +144,13 @@ function rendering(p5, fn){ this._elements.push(this._renderer); this._renderer._applyDefaults(); - // Attach renderer properties - for (const p in this._renderer) { - if (p[0] === '_' || typeof this._renderer[p] === 'function') continue; - if (Object.hasOwn(this, p)) continue; - Object.defineProperty(this, p, { + // Make the renderer own `pixels` + if (!Object.hasOwn(this, 'pixels')) { + Object.defineProperty(this, 'pixels', { get(){ - return this._renderer?.[p]; + return this._renderer?.pixels; } - }) + }); } return this._renderer; From fe0d85a804343dd59950ad3a01f46834d33c5ab5 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 2 Nov 2024 16:38:07 +0000 Subject: [PATCH 51/55] Refactor out need for pInst in p5.Color --- src/color/creating_reading.js | 4 +- src/color/p5.Color.js | 825 +++++++++++++++++----------------- src/core/p5.Renderer2D.js | 1 + 3 files changed, 418 insertions(+), 412 deletions(-) diff --git a/src/color/creating_reading.js b/src/color/creating_reading.js index d4f5ee8785..cda1d9bfcc 100644 --- a/src/color/creating_reading.js +++ b/src/color/creating_reading.js @@ -683,7 +683,7 @@ function creatingReading(p5, fn){ } const arg = Array.isArray(args[0]) ? args[0] : args; - return new p5.Color(this, arg); + return new p5.Color(arg, this._colorMode, this._colorMaxes); }; /** @@ -1027,7 +1027,7 @@ function creatingReading(p5, fn){ space: c1.color.space.path[spaceIndex].id })(amt); - return new p5.Color(this, lerpColor); + return new p5.Color(lerpColor, this._colorMode, this._colorMaxes); }; /** diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 70245005b8..ab4b3f13e6 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -59,449 +59,454 @@ ColorSpace.register(P3); ColorSpace.register(A98RGB_Linear); ColorSpace.register(A98RGB); -function color(p5, fn){ +class Color { + color; + maxes; + mode; + + constructor(vals, colorMode='rgb', colorMaxes={rgb: [255, 255, 255, 255]}) { + // This changes with the sketch's setting + // NOTE: Maintaining separate maxes for different color space is awkward. + // Consider just one universal maxes. + // this.maxes = pInst._colorMaxes; + this.maxes = colorMaxes; + // This changes with the color object + // this.mode = pInst._colorMode; + this.mode = colorMode; + + if (typeof vals === 'object' && !Array.isArray(vals) && vals !== null){ + this.color = vals; + } else if(typeof vals[0] === 'string') { + try{ + // NOTE: this will not necessarily have the right color mode + this.color = parse(vals[0]); + }catch(err){ + // TODO: Invalid color string + console.error('Invalid color string'); + } + + }else{ + let alpha; + + if(vals.length === 4){ + alpha = vals[vals.length-1]; + }else if (vals.length === 2){ + alpha = vals[1]; + vals = [vals[0], vals[0], vals[0]]; + }else if(vals.length === 1){ + vals = [vals[0], vals[0], vals[0]]; + } + alpha = alpha !== undefined + ? alpha / this.maxes[this.mode][3] + : 1; + + // _colorMode can be 'rgb', 'hsb', or 'hsl' + // These should map to color.js color space + let space = 'srgb'; + let coords = vals; + switch(this.mode){ + case 'rgb': + space = 'srgb'; + coords = [ + vals[0] / this.maxes[this.mode][0], + vals[1] / this.maxes[this.mode][1], + vals[2] / this.maxes[this.mode][2] + ]; + break; + case 'hsb': + // TODO: need implementation + space = 'hsb'; + coords = [ + vals[0] / this.maxes[this.mode][0] * 360, + vals[1] / this.maxes[this.mode][1] * 100, + vals[2] / this.maxes[this.mode][2] * 100 + ]; + break; + case 'hsl': + space = 'hsl'; + coords = [ + vals[0] / this.maxes[this.mode][0] * 360, + vals[1] / this.maxes[this.mode][1] * 100, + vals[2] / this.maxes[this.mode][2] * 100 + ]; + break; + default: + console.error('Invalid color mode'); + } + + const color = { + space, + coords, + alpha + }; + this.color = to(color, space); + } + } + /** - * A class to describe a color. + * Returns the color formatted as a `String`. * - * Each `p5.Color` object stores the color mode - * and level maxes that were active during its construction. These values are - * used to interpret the arguments passed to the object's constructor. They - * also determine output formatting such as when - * saturation() is called. + * Calling `myColor.toString()` can be useful for debugging, as in + * `print(myColor.toString())`. It's also helpful for using p5.js with other + * libraries. * - * Color is stored internally as an array of ideal RGBA values in floating - * point form, normalized from 0 to 1. These values are used to calculate the - * closest screen colors, which are RGBA levels from 0 to 255. Screen colors - * are sent to the renderer. + * The parameter, `format`, is optional. If a format string is passed, as in + * `myColor.toString('#rrggbb')`, it will determine how the color string is + * formatted. By default, color strings are formatted as `'rgba(r, g, b, a)'`. * - * When different color representations are calculated, the results are cached - * for performance. These values are normalized, floating-point numbers. + * @param {String} [format] how the color string will be formatted. + * Leaving this empty formats the string as rgba(r, g, b, a). + * '#rgb' '#rgba' '#rrggbb' and '#rrggbbaa' format as hexadecimal color codes. + * 'rgb' 'hsb' and 'hsl' return the color formatted in the specified color mode. + * 'rgba' 'hsba' and 'hsla' are the same as above but with alpha channels. + * 'rgb%' 'hsb%' 'hsl%' 'rgba%' 'hsba%' and 'hsla%' format as percentages. + * @return {String} the formatted string. * - * Note: color() is the recommended way to create an - * instance of this class. + *
+ * + * function setup() { + * createCanvas(100, 100); * - * @class p5.Color - * @param {p5} [pInst] pointer to p5 instance. + * background(200); * - * @param {Number[]|String} vals an array containing the color values - * for red, green, blue and alpha channel - * or CSS color. + * // Create a p5.Color object. + * let myColor = color('darkorchid'); + * + * // Style the text. + * textAlign(CENTER); + * textSize(16); + * + * // Display the text. + * text(myColor.toString('#rrggbb'), 50, 50); + * + * describe('The text "#9932cc" written in purple on a gray background.'); + * } + * + *
*/ - p5.Color = class Color { - color; - maxes; - mode; - - constructor(pInst, vals) { - // This changes with the sketch's setting - // NOTE: Maintaining separate maxes for different color space is awkward. - // Consider just one universal maxes. - this.maxes = pInst._colorMaxes; - // This changes with the color object - this.mode = pInst._colorMode; - - if (typeof vals === 'object' && !Array.isArray(vals) && vals !== null){ - this.color = vals; - } else if(typeof vals[0] === 'string') { - try{ - // NOTE: this will not necessarily have the right color mode - this.color = parse(vals[0]); - }catch(err){ - // TODO: Invalid color string - console.error('Invalid color string'); - } - - }else{ - let alpha; - - if(vals.length === 4){ - alpha = vals[vals.length-1]; - }else if (vals.length === 2){ - alpha = vals[1]; - vals = [vals[0], vals[0], vals[0]]; - }else if(vals.length === 1){ - vals = [vals[0], vals[0], vals[0]]; - } - alpha = alpha !== undefined - ? alpha / pInst._colorMaxes[pInst._colorMode][3] - : 1; - - // _colorMode can be 'rgb', 'hsb', or 'hsl' - // These should map to color.js color space - let space = 'srgb'; - let coords = vals; - switch(pInst._colorMode){ - case 'rgb': - space = 'srgb'; - coords = [ - vals[0] / pInst._colorMaxes[pInst._colorMode][0], - vals[1] / pInst._colorMaxes[pInst._colorMode][1], - vals[2] / pInst._colorMaxes[pInst._colorMode][2] - ]; - break; - case 'hsb': - // TODO: need implementation - space = 'hsb'; - coords = [ - vals[0] / pInst._colorMaxes[pInst._colorMode][0] * 360, - vals[1] / pInst._colorMaxes[pInst._colorMode][1] * 100, - vals[2] / pInst._colorMaxes[pInst._colorMode][2] * 100 - ]; - break; - case 'hsl': - space = 'hsl'; - coords = [ - vals[0] / pInst._colorMaxes[pInst._colorMode][0] * 360, - vals[1] / pInst._colorMaxes[pInst._colorMode][1] * 100, - vals[2] / pInst._colorMaxes[pInst._colorMode][2] * 100 - ]; - break; - default: - console.error('Invalid color mode'); - } - - const color = { - space, - coords, - alpha - }; - this.color = to(color, space); - } - } + toString(format) { + // NOTE: memoize + return serialize(this.color, { + format + }); + } - /** - * Returns the color formatted as a `String`. - * - * Calling `myColor.toString()` can be useful for debugging, as in - * `print(myColor.toString())`. It's also helpful for using p5.js with other - * libraries. - * - * The parameter, `format`, is optional. If a format string is passed, as in - * `myColor.toString('#rrggbb')`, it will determine how the color string is - * formatted. By default, color strings are formatted as `'rgba(r, g, b, a)'`. - * - * @param {String} [format] how the color string will be formatted. - * Leaving this empty formats the string as rgba(r, g, b, a). - * '#rgb' '#rgba' '#rrggbb' and '#rrggbbaa' format as hexadecimal color codes. - * 'rgb' 'hsb' and 'hsl' return the color formatted in the specified color mode. - * 'rgba' 'hsba' and 'hsla' are the same as above but with alpha channels. - * 'rgb%' 'hsb%' 'hsl%' 'rgba%' 'hsba%' and 'hsla%' format as percentages. - * @return {String} the formatted string. - * - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Color object. - * let myColor = color('darkorchid'); - * - * // Style the text. - * textAlign(CENTER); - * textSize(16); - * - * // Display the text. - * text(myColor.toString('#rrggbb'), 50, 50); - * - * describe('The text "#9932cc" written in purple on a gray background.'); - * } - * - *
- */ - toString(format) { - // NOTE: memoize - return serialize(this.color, { - format - }); - } - - /** - * Sets the red component of a color. - * - * The range depends on the colorMode(). In the - * default RGB mode it's between 0 and 255. - * - * @param {Number} red the new red value. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Color object. - * let c = color(255, 128, 128); - * - * // Draw the left rectangle. - * noStroke(); - * fill(c); - * rect(15, 20, 35, 60); - * - * // Change the red value. - * c.setRed(64); - * - * // Draw the right rectangle. - * fill(c); - * rect(50, 20, 35, 60); - * - * describe('Two rectangles. The left one is salmon pink and the right one is teal.'); - * } - * - *
- */ - setRed(new_red) { - const red_val = new_red / this.maxes[constants.RGB][0]; - if(this.mode === constants.RGB){ - this.color.coords[0] = red_val; - }else{ - // Will do an imprecise conversion to 'srgb', not recommended - const space = this.color.space.id; - const representation = to(this.color, 'srgb'); - representation.coords[0] = red_val; - this.color = to(representation, space); - } + /** + * Sets the red component of a color. + * + * The range depends on the colorMode(). In the + * default RGB mode it's between 0 and 255. + * + * @param {Number} red the new red value. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Color object. + * let c = color(255, 128, 128); + * + * // Draw the left rectangle. + * noStroke(); + * fill(c); + * rect(15, 20, 35, 60); + * + * // Change the red value. + * c.setRed(64); + * + * // Draw the right rectangle. + * fill(c); + * rect(50, 20, 35, 60); + * + * describe('Two rectangles. The left one is salmon pink and the right one is teal.'); + * } + * + *
+ */ + setRed(new_red) { + const red_val = new_red / this.maxes[constants.RGB][0]; + if(this.mode === constants.RGB){ + this.color.coords[0] = red_val; + }else{ + // Will do an imprecise conversion to 'srgb', not recommended + const space = this.color.space.id; + const representation = to(this.color, 'srgb'); + representation.coords[0] = red_val; + this.color = to(representation, space); } + } - /** - * Sets the green component of a color. - * - * The range depends on the colorMode(). In the - * default RGB mode it's between 0 and 255. - * - * @param {Number} green the new green value. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Color object. - * let c = color(255, 128, 128); - * - * // Draw the left rectangle. - * noStroke(); - * fill(c); - * rect(15, 20, 35, 60); - * - * // Change the green value. - * c.setGreen(255); - * - * // Draw the right rectangle. - * fill(c); - * rect(50, 20, 35, 60); - * - * describe('Two rectangles. The left one is salmon pink and the right one is yellow.'); - * } - * - *
- **/ - setGreen(new_green) { - const green_val = new_green / this.maxes[constants.RGB][1]; - if(this.mode === constants.RGB){ - this.color.coords[1] = green_val; - }else{ - // Will do an imprecise conversion to 'srgb', not recommended - const space = this.color.space.id; - const representation = to(this.color, 'srgb'); - representation.coords[1] = green_val; - this.color = to(representation, space); - } + /** + * Sets the green component of a color. + * + * The range depends on the colorMode(). In the + * default RGB mode it's between 0 and 255. + * + * @param {Number} green the new green value. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Color object. + * let c = color(255, 128, 128); + * + * // Draw the left rectangle. + * noStroke(); + * fill(c); + * rect(15, 20, 35, 60); + * + * // Change the green value. + * c.setGreen(255); + * + * // Draw the right rectangle. + * fill(c); + * rect(50, 20, 35, 60); + * + * describe('Two rectangles. The left one is salmon pink and the right one is yellow.'); + * } + * + *
+ **/ + setGreen(new_green) { + const green_val = new_green / this.maxes[constants.RGB][1]; + if(this.mode === constants.RGB){ + this.color.coords[1] = green_val; + }else{ + // Will do an imprecise conversion to 'srgb', not recommended + const space = this.color.space.id; + const representation = to(this.color, 'srgb'); + representation.coords[1] = green_val; + this.color = to(representation, space); } + } - /** - * Sets the blue component of a color. - * - * The range depends on the colorMode(). In the - * default RGB mode it's between 0 and 255. - * - * @param {Number} blue the new blue value. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Color object. - * let c = color(255, 128, 128); - * - * // Draw the left rectangle. - * noStroke(); - * fill(c); - * rect(15, 20, 35, 60); - * - * // Change the blue value. - * c.setBlue(255); - * - * // Draw the right rectangle. - * fill(c); - * rect(50, 20, 35, 60); - * - * describe('Two rectangles. The left one is salmon pink and the right one is pale fuchsia.'); - * } - * - *
- **/ - setBlue(new_blue) { - const blue_val = new_blue / this.maxes[constants.RGB][2]; - if(this.mode === constants.RGB){ - this.color.coords[2] = blue_val; - }else{ - // Will do an imprecise conversion to 'srgb', not recommended - const space = this.color.space.id; - const representation = to(this.color, 'srgb'); - representation.coords[2] = blue_val; - this.color = to(representation, space); - } + /** + * Sets the blue component of a color. + * + * The range depends on the colorMode(). In the + * default RGB mode it's between 0 and 255. + * + * @param {Number} blue the new blue value. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Color object. + * let c = color(255, 128, 128); + * + * // Draw the left rectangle. + * noStroke(); + * fill(c); + * rect(15, 20, 35, 60); + * + * // Change the blue value. + * c.setBlue(255); + * + * // Draw the right rectangle. + * fill(c); + * rect(50, 20, 35, 60); + * + * describe('Two rectangles. The left one is salmon pink and the right one is pale fuchsia.'); + * } + * + *
+ **/ + setBlue(new_blue) { + const blue_val = new_blue / this.maxes[constants.RGB][2]; + if(this.mode === constants.RGB){ + this.color.coords[2] = blue_val; + }else{ + // Will do an imprecise conversion to 'srgb', not recommended + const space = this.color.space.id; + const representation = to(this.color, 'srgb'); + representation.coords[2] = blue_val; + this.color = to(representation, space); } + } - /** - * Sets the alpha (transparency) value of a color. - * - * The range depends on the - * colorMode(). In the default RGB mode it's - * between 0 and 255. - * - * @param {Number} alpha the new alpha value. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100); - * - * background(200); - * - * // Create a p5.Color object. - * let c = color(255, 128, 128); - * - * // Draw the left rectangle. - * noStroke(); - * fill(c); - * rect(15, 20, 35, 60); - * - * // Change the alpha value. - * c.setAlpha(128); - * - * // Draw the right rectangle. - * fill(c); - * rect(50, 20, 35, 60); - * - * describe('Two rectangles. The left one is salmon pink and the right one is faded pink.'); - * } - * - *
- **/ - setAlpha(new_alpha) { - this.color.alpha = new_alpha / this.maxes[this.mode][3]; + /** + * Sets the alpha (transparency) value of a color. + * + * The range depends on the + * colorMode(). In the default RGB mode it's + * between 0 and 255. + * + * @param {Number} alpha the new alpha value. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100); + * + * background(200); + * + * // Create a p5.Color object. + * let c = color(255, 128, 128); + * + * // Draw the left rectangle. + * noStroke(); + * fill(c); + * rect(15, 20, 35, 60); + * + * // Change the alpha value. + * c.setAlpha(128); + * + * // Draw the right rectangle. + * fill(c); + * rect(50, 20, 35, 60); + * + * describe('Two rectangles. The left one is salmon pink and the right one is faded pink.'); + * } + * + *
+ **/ + setAlpha(new_alpha) { + this.color.alpha = new_alpha / this.maxes[this.mode][3]; + } + + _getRed() { + if(this.mode === constants.RGB){ + return this.color.coords[0] * this.maxes[constants.RGB][0]; + }else{ + // Will do an imprecise conversion to 'srgb', not recommended + return to(this.color, 'srgb').coords[0] * this.maxes[constants.RGB][0]; } - - _getRed() { - if(this.mode === constants.RGB){ - return this.color.coords[0] * this.maxes[constants.RGB][0]; - }else{ - // Will do an imprecise conversion to 'srgb', not recommended - return to(this.color, 'srgb').coords[0] * this.maxes[constants.RGB][0]; - } + } + + _getGreen() { + if(this.mode === constants.RGB){ + return this.color.coords[1] * this.maxes[constants.RGB][1]; + }else{ + // Will do an imprecise conversion to 'srgb', not recommended + return to(this.color, 'srgb').coords[1] * this.maxes[constants.RGB][1]; } - - _getGreen() { - if(this.mode === constants.RGB){ - return this.color.coords[1] * this.maxes[constants.RGB][1]; - }else{ - // Will do an imprecise conversion to 'srgb', not recommended - return to(this.color, 'srgb').coords[1] * this.maxes[constants.RGB][1]; - } + } + + _getBlue() { + if(this.mode === constants.RGB){ + return this.color.coords[2] * this.maxes[constants.RGB][2]; + }else{ + // Will do an imprecise conversion to 'srgb', not recommended + return to(this.color, 'srgb').coords[2] * this.maxes[constants.RGB][2]; } + } - _getBlue() { - if(this.mode === constants.RGB){ - return this.color.coords[2] * this.maxes[constants.RGB][2]; - }else{ - // Will do an imprecise conversion to 'srgb', not recommended - return to(this.color, 'srgb').coords[2] * this.maxes[constants.RGB][2]; - } - } + _getAlpha() { + return this.color.alpha * this.maxes[this.mode][3]; + } - _getAlpha() { - return this.color.alpha * this.maxes[this.mode][3]; - } + _getMode() { + return this.mode; + } - _getMode() { - return this.mode; - } + _getMaxes() { + return this.maxes; + } - _getMaxes() { - return this.maxes; + /** + * Hue is the same in HSB and HSL, but the maximum value may be different. + * This function will return the HSB-normalized saturation when supplied with + * an HSB color object, but will default to the HSL-normalized saturation + * otherwise. + */ + _getHue() { + if(this.mode === constants.HSB || this.mode === constants.HSL){ + return this.color.coords[0] / 360 * this.maxes[this.mode][0]; + }else{ + // Will do an imprecise conversion to 'HSL', not recommended + return to(this.color, 'hsl').coords[0] / 360 * this.maxes[this.mode][0]; } + } - /** - * Hue is the same in HSB and HSL, but the maximum value may be different. - * This function will return the HSB-normalized saturation when supplied with - * an HSB color object, but will default to the HSL-normalized saturation - * otherwise. - */ - _getHue() { - if(this.mode === constants.HSB || this.mode === constants.HSL){ - return this.color.coords[0] / 360 * this.maxes[this.mode][0]; - }else{ - // Will do an imprecise conversion to 'HSL', not recommended - return to(this.color, 'hsl').coords[0] / 360 * this.maxes[this.mode][0]; - } + /** + * Saturation is scaled differently in HSB and HSL. This function will return + * the HSB saturation when supplied with an HSB color object, but will default + * to the HSL saturation otherwise. + */ + _getSaturation() { + if(this.mode === constants.HSB || this.mode === constants.HSL){ + return this.color.coords[1] / 100 * this.maxes[this.mode][1]; + }else{ + // Will do an imprecise conversion to 'HSL', not recommended + return to(this.color, 'hsl').coords[1] / 100 * this.maxes[this.mode][1]; } - - /** - * Saturation is scaled differently in HSB and HSL. This function will return - * the HSB saturation when supplied with an HSB color object, but will default - * to the HSL saturation otherwise. - */ - _getSaturation() { - if(this.mode === constants.HSB || this.mode === constants.HSL){ - return this.color.coords[1] / 100 * this.maxes[this.mode][1]; - }else{ - // Will do an imprecise conversion to 'HSL', not recommended - return to(this.color, 'hsl').coords[1] / 100 * this.maxes[this.mode][1]; - } + } + + _getBrightness() { + if(this.mode === constants.HSB){ + return this.color.coords[2] / 100 * this.maxes[this.mode][2]; + }else{ + // Will do an imprecise conversion to 'HSB', not recommended + return to(this.color, 'hsb').coords[2] / 100 * this.maxes[this.mode][2]; } - - _getBrightness() { - if(this.mode === constants.HSB){ - return this.color.coords[2] / 100 * this.maxes[this.mode][2]; - }else{ - // Will do an imprecise conversion to 'HSB', not recommended - return to(this.color, 'hsb').coords[2] / 100 * this.maxes[this.mode][2]; - } + } + + _getLightness() { + if(this.mode === constants.HSL){ + return this.color.coords[2] / 100 * this.maxes[this.mode][2]; + }else{ + // Will do an imprecise conversion to 'HSB', not recommended + return to(this.color, 'hsl').coords[2] / 100 * this.maxes[this.mode][2]; } + } - _getLightness() { - if(this.mode === constants.HSL){ - return this.color.coords[2] / 100 * this.maxes[this.mode][2]; - }else{ - // Will do an imprecise conversion to 'HSB', not recommended - return to(this.color, 'hsl').coords[2] / 100 * this.maxes[this.mode][2]; - } - } + get _array() { + return [...this.color.coords, this.color.alpha]; + } - get _array() { - return [...this.color.coords, this.color.alpha]; - } + get levels() { + return this._array.map(v => v * 255); + } +} - get levels() { - return this._array.map(v => v * 255); - } - }; +function color(p5, fn){ + /** + * A class to describe a color. + * + * Each `p5.Color` object stores the color mode + * and level maxes that were active during its construction. These values are + * used to interpret the arguments passed to the object's constructor. They + * also determine output formatting such as when + * saturation() is called. + * + * Color is stored internally as an array of ideal RGBA values in floating + * point form, normalized from 0 to 1. These values are used to calculate the + * closest screen colors, which are RGBA levels from 0 to 255. Screen colors + * are sent to the renderer. + * + * When different color representations are calculated, the results are cached + * for performance. These values are normalized, floating-point numbers. + * + * Note: color() is the recommended way to create an + * instance of this class. + * + * @class p5.Color + * @param {p5} [pInst] pointer to p5 instance. + * + * @param {Number[]|String} vals an array containing the color values + * for red, green, blue and alpha channel + * or CSS color. + */ + p5.Color = Color; } export default color; +export { Color } if(typeof p5 !== 'undefined'){ color(p5, p5.prototype); diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index be490ca829..6d5f35c279 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -202,6 +202,7 @@ class Renderer2D extends Renderer { fill(...args) { super.fill(...args); const color = this._pInst.color(...args); + console.log(color.toString(), color); this._setFill(color.toString()); //accessible Outputs From 24525f90b2b4e92f84481e4b82691d292b109a85 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 2 Nov 2024 16:39:31 +0000 Subject: [PATCH 52/55] Remove console.log --- src/core/p5.Renderer2D.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 6d5f35c279..be490ca829 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -202,7 +202,6 @@ class Renderer2D extends Renderer { fill(...args) { super.fill(...args); const color = this._pInst.color(...args); - console.log(color.toString(), color); this._setFill(color.toString()); //accessible Outputs From d85876c2730ba507bd023bc357138e6c4eef0e36 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sat, 2 Nov 2024 17:13:38 +0000 Subject: [PATCH 53/55] Move rest of light implementation to Renderer3D --- src/webgl/light.js | 518 +++++++++++++++++++++++---------------------- 1 file changed, 269 insertions(+), 249 deletions(-) diff --git a/src/webgl/light.js b/src/webgl/light.js index 7a5041e599..bfeca289e8 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -7,6 +7,7 @@ import { RendererGL } from './p5.RendererGL'; import { Vector } from '../math/p5.Vector'; +import { Color } from '../color/p5.Color'; function light(p5, fn){ /** @@ -191,15 +192,8 @@ function light(p5, fn){ fn.ambientLight = function (v1, v2, v3, a) { this._assert3d('ambientLight'); p5._validateParameters('ambientLight', arguments); - const color = this.color(...arguments); - this._renderer.states.ambientLightColors.push( - color._array[0], - color._array[1], - color._array[2] - ); - - this._renderer.states.enableLighting = true; + this._renderer.ambientLight(...arguments); return this; }; @@ -449,13 +443,8 @@ function light(p5, fn){ fn.specularColor = function (v1, v2, v3) { this._assert3d('specularColor'); p5._validateParameters('specularColor', arguments); - const color = this.color(...arguments); - this._renderer.states.specularColors = [ - color._array[0], - color._array[1], - color._array[2] - ]; + this._renderer.specularColor(...arguments); return this; }; @@ -643,40 +632,7 @@ function light(p5, fn){ p5._validateParameters('directionalLight', arguments); //@TODO: check parameters number - let color; - if (v1 instanceof p5.Color) { - color = v1; - } else { - color = this.color(v1, v2, v3); - } - - let _x, _y, _z; - const v = arguments[arguments.length - 1]; - if (typeof v === 'number') { - _x = arguments[arguments.length - 3]; - _y = arguments[arguments.length - 2]; - _z = arguments[arguments.length - 1]; - } else { - _x = v.x; - _y = v.y; - _z = v.z; - } - - // normalize direction - const l = Math.sqrt(_x * _x + _y * _y + _z * _z); - this._renderer.states.directionalLightDirections.push(_x / l, _y / l, _z / l); - - this._renderer.states.directionalLightDiffuseColors.push( - color._array[0], - color._array[1], - color._array[2] - ); - Array.prototype.push.apply( - this._renderer.states.directionalLightSpecularColors, - this._renderer.states.specularColors - ); - - this._renderer.states.enableLighting = true; + this._renderer.directionalLight(...arguments); return this; }; @@ -919,37 +875,7 @@ function light(p5, fn){ p5._validateParameters('pointLight', arguments); //@TODO: check parameters number - let color; - if (v1 instanceof p5.Color) { - color = v1; - } else { - color = this.color(v1, v2, v3); - } - - let _x, _y, _z; - const v = arguments[arguments.length - 1]; - if (typeof v === 'number') { - _x = arguments[arguments.length - 3]; - _y = arguments[arguments.length - 2]; - _z = arguments[arguments.length - 1]; - } else { - _x = v.x; - _y = v.y; - _z = v.z; - } - - this._renderer.states.pointLightPositions.push(_x, _y, _z); - this._renderer.states.pointLightDiffuseColors.push( - color._array[0], - color._array[1], - color._array[2] - ); - Array.prototype.push.apply( - this._renderer.states.pointLightSpecularColors, - this._renderer.states.specularColors - ); - - this._renderer.states.enableLighting = true; + this._renderer.pointLight(...arguments); return this; }; @@ -1150,9 +1076,7 @@ function light(p5, fn){ fn.lights = function () { this._assert3d('lights'); // Both specify gray by default. - const grayColor = this.color('rgb(128,128,128)'); - this.ambientLight(grayColor); - this.directionalLight(grayColor, 0, 0, -1); + this._renderer.lights(); return this; }; @@ -1456,56 +1380,289 @@ function light(p5, fn){ this._assert3d('spotLight'); p5._validateParameters('spotLight', arguments); + this._renderer.spotLight(...arguments); + + return this; + }; + + /** + * Removes all lights from the sketch. + * + * Calling `noLights()` removes any lights created with + * lights(), + * ambientLight(), + * directionalLight(), + * pointLight(), or + * spotLight(). These functions may be called + * after `noLights()` to create a new lighting scheme. + * + * @method noLights + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('Two spheres drawn against a gray background. The top sphere is white and the bottom sphere is red.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the spheres. + * noStroke(); + * + * // Draw the top sphere. + * push(); + * translate(0, -25, 0); + * sphere(20); + * pop(); + * + * // Turn off the lights. + * noLights(); + * + * // Add a red directional light that points into the screen. + * directionalLight(255, 0, 0, 0, 0, -1); + * + * // Draw the bottom sphere. + * push(); + * translate(0, 25, 0); + * sphere(20); + * pop(); + * } + * + *
+ */ + fn.noLights = function (...args) { + this._assert3d('noLights'); + p5._validateParameters('noLights', args); + + this._renderer.noLights(); + + return this; + }; + + + RendererGL.prototype.ambientLight = function(v1, v2, v3, a) { + const color = this._pInst.color(...arguments); + + this.states.ambientLightColors.push( + color._array[0], + color._array[1], + color._array[2] + ); + + this.states.enableLighting = true; + } + + RendererGL.prototype.specularColor = function(v1, v2, v3) { + const color = this._pInst.color(...arguments); + + this.states.specularColors = [ + color._array[0], + color._array[1], + color._array[2] + ]; + } + + RendererGL.prototype.directionalLight = function(v1, v2, v3, x, y, z) { + let color; + if (v1 instanceof Color) { + color = v1; + } else { + color = this._pInst.color(v1, v2, v3); + } + + let _x, _y, _z; + const v = arguments[arguments.length - 1]; + if (typeof v === 'number') { + _x = arguments[arguments.length - 3]; + _y = arguments[arguments.length - 2]; + _z = arguments[arguments.length - 1]; + } else { + _x = v.x; + _y = v.y; + _z = v.z; + } + + // normalize direction + const l = Math.sqrt(_x * _x + _y * _y + _z * _z); + this.states.directionalLightDirections.push(_x / l, _y / l, _z / l); + + this.states.directionalLightDiffuseColors.push( + color._array[0], + color._array[1], + color._array[2] + ); + Array.prototype.push.apply( + this.states.directionalLightSpecularColors, + this.states.specularColors + ); + + this.states.enableLighting = true; + } + + RendererGL.prototype.pointLight = function(v1, v2, v3, x, y, z) { + let color; + if (v1 instanceof Color) { + color = v1; + } else { + color = this._pInst.color(v1, v2, v3); + } + + let _x, _y, _z; + const v = arguments[arguments.length - 1]; + if (typeof v === 'number') { + _x = arguments[arguments.length - 3]; + _y = arguments[arguments.length - 2]; + _z = arguments[arguments.length - 1]; + } else { + _x = v.x; + _y = v.y; + _z = v.z; + } + + this.states.pointLightPositions.push(_x, _y, _z); + this.states.pointLightDiffuseColors.push( + color._array[0], + color._array[1], + color._array[2] + ); + Array.prototype.push.apply( + this.states.pointLightSpecularColors, + this.states.specularColors + ); + + this.states.enableLighting = true; + } + + RendererGL.prototype.imageLight = function(img) { + // activeImageLight property is checked by _setFillUniforms + // for sending uniforms to the fillshader + this.states.activeImageLight = img; + this.states.enableLighting = true; + } + + RendererGL.prototype.lights = function() { + const grayColor = this._pInst.color('rgb(128,128,128)'); + this.ambientLight(grayColor); + this.directionalLight(grayColor, 0, 0, -1); + } + + RendererGL.prototype.lightFalloff = function( + constantAttenuation, + linearAttenuation, + quadraticAttenuation + ) { + if (constantAttenuation < 0) { + constantAttenuation = 0; + console.warn( + 'Value of constant argument in lightFalloff() should be never be negative. Set to 0.' + ); + } + + if (linearAttenuation < 0) { + linearAttenuation = 0; + console.warn( + 'Value of linear argument in lightFalloff() should be never be negative. Set to 0.' + ); + } + + if (quadraticAttenuation < 0) { + quadraticAttenuation = 0; + console.warn( + 'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.' + ); + } + + if ( + constantAttenuation === 0 && + (linearAttenuation === 0 && quadraticAttenuation === 0) + ) { + constantAttenuation = 1; + console.warn( + 'Either one of the three arguments in lightFalloff() should be greater than zero. Set constant argument to 1.' + ); + } + + this.states.constantAttenuation = constantAttenuation; + this.states.linearAttenuation = linearAttenuation; + this.states.quadraticAttenuation = quadraticAttenuation; + } + + RendererGL.prototype.spotLight = function( + v1, + v2, + v3, + x, + y, + z, + nx, + ny, + nz, + angle, + concentration + ) { let color, position, direction; const length = arguments.length; switch (length) { case 11: case 10: - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = new Vector(nx, ny, nz); break; case 9: - if (v1 instanceof p5.Color) { + if (v1 instanceof Color) { color = v1; position = new Vector(v2, v3, x); direction = new Vector(y, z, nx); angle = ny; concentration = nz; } else if (x instanceof Vector) { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = x; direction = new Vector(y, z, nx); angle = ny; concentration = nz; } else if (nx instanceof Vector) { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = nx; angle = ny; concentration = nz; } else { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = new Vector(nx, ny, nz); } break; case 8: - if (v1 instanceof p5.Color) { + if (v1 instanceof Color) { color = v1; position = new Vector(v2, v3, x); direction = new Vector(y, z, nx); angle = ny; } else if (x instanceof Vector) { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = x; direction = new Vector(y, z, nx); angle = ny; } else { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = nx; angle = ny; @@ -1513,34 +1670,34 @@ function light(p5, fn){ break; case 7: - if (v1 instanceof p5.Color && v2 instanceof Vector) { + if (v1 instanceof Color && v2 instanceof Vector) { color = v1; position = v2; direction = new Vector(v3, x, y); angle = z; concentration = nx; - } else if (v1 instanceof p5.Color && y instanceof Vector) { + } else if (v1 instanceof Color && y instanceof Vector) { color = v1; position = new Vector(v2, v3, x); direction = y; angle = z; concentration = nx; } else if (x instanceof Vector && y instanceof Vector) { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = x; direction = y; angle = z; concentration = nx; - } else if (v1 instanceof p5.Color) { + } else if (v1 instanceof Color) { color = v1; position = new Vector(v2, v3, x); direction = new Vector(y, z, nx); } else if (x instanceof Vector) { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = x; direction = new Vector(y, z, nx); } else { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = new Vector(x, y, z); direction = nx; } @@ -1548,16 +1705,16 @@ function light(p5, fn){ case 6: if (x instanceof Vector && y instanceof Vector) { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = x; direction = y; angle = z; - } else if (v1 instanceof p5.Color && y instanceof Vector) { + } else if (v1 instanceof Color && y instanceof Vector) { color = v1; position = new Vector(v2, v3, x); direction = y; angle = z; - } else if (v1 instanceof p5.Color && v2 instanceof Vector) { + } else if (v1 instanceof Color && v2 instanceof Vector) { color = v1; position = v2; direction = new Vector(v3, x, y); @@ -1567,7 +1724,7 @@ function light(p5, fn){ case 5: if ( - v1 instanceof p5.Color && + v1 instanceof Color && v2 instanceof Vector && v3 instanceof Vector ) { @@ -1577,14 +1734,14 @@ function light(p5, fn){ angle = x; concentration = y; } else if (x instanceof Vector && y instanceof Vector) { - color = this.color(v1, v2, v3); + color = this._pInst.color(v1, v2, v3); position = x; direction = y; - } else if (v1 instanceof p5.Color && y instanceof Vector) { + } else if (v1 instanceof Color && y instanceof Vector) { color = v1; position = new Vector(v2, v3, x); direction = y; - } else if (v1 instanceof p5.Color && v2 instanceof Vector) { + } else if (v1 instanceof Color && v2 instanceof Vector) { color = v1; position = v2; direction = new Vector(v3, x, y); @@ -1610,21 +1767,21 @@ function light(p5, fn){ length < 3 ? 'few' : 'many' } arguments were provided` ); - return this; + return; } - this._renderer.states.spotLightDiffuseColors = [ + this.states.spotLightDiffuseColors = [ color._array[0], color._array[1], color._array[2] ]; - this._renderer.states.spotLightSpecularColors = [ - ...this._renderer.states.specularColors + this.states.spotLightSpecularColors = [ + ...this.states.specularColors ]; - this._renderer.states.spotLightPositions = [position.x, position.y, position.z]; + this.states.spotLightPositions = [position.x, position.y, position.z]; direction.normalize(); - this._renderer.states.spotLightDirections = [ + this.states.spotLightDirections = [ direction.x, direction.y, direction.z @@ -1643,150 +1800,13 @@ function light(p5, fn){ concentration = 100; } - angle = this._renderer._pInst._toRadians(angle); - this._renderer.states.spotLightAngle = [Math.cos(angle)]; - this._renderer.states.spotLightConc = [concentration]; - - this._renderer.states.enableLighting = true; + angle = this._pInst._toRadians(angle); + this.states.spotLightAngle = [Math.cos(angle)]; + this.states.spotLightConc = [concentration]; - return this; - }; - - /** - * Removes all lights from the sketch. - * - * Calling `noLights()` removes any lights created with - * lights(), - * ambientLight(), - * directionalLight(), - * pointLight(), or - * spotLight(). These functions may be called - * after `noLights()` to create a new lighting scheme. - * - * @method noLights - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('Two spheres drawn against a gray background. The top sphere is white and the bottom sphere is red.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the spheres. - * noStroke(); - * - * // Draw the top sphere. - * push(); - * translate(0, -25, 0); - * sphere(20); - * pop(); - * - * // Turn off the lights. - * noLights(); - * - * // Add a red directional light that points into the screen. - * directionalLight(255, 0, 0, 0, 0, -1); - * - * // Draw the bottom sphere. - * push(); - * translate(0, 25, 0); - * sphere(20); - * pop(); - * } - * - *
- */ - fn.noLights = function (...args) { - this._assert3d('noLights'); - p5._validateParameters('noLights', args); - - this._renderer.noLights(); - - return this; - }; - - - // RendererGL.prototype.ambientLight = function(v1, v2, v3, a) { - // } - - // RendererGL.prototype.specularColor = function(v1, v2, v3) { - // } - - // RendererGL.prototype.directionalLight = function(v1, v2, v3, x, y, z) { - // } - - // RendererGL.prototype.pointLight = function(v1, v2, v3, x, y, z) { - // } - - RendererGL.prototype.imageLight = function(img) { - // activeImageLight property is checked by _setFillUniforms - // for sending uniforms to the fillshader - this.states.activeImageLight = img; this.states.enableLighting = true; } - // RendererGL.prototype.lights = function() { - // } - - RendererGL.prototype.lightFalloff = function( - constantAttenuation, - linearAttenuation, - quadraticAttenuation - ) { - if (constantAttenuation < 0) { - constantAttenuation = 0; - console.warn( - 'Value of constant argument in lightFalloff() should be never be negative. Set to 0.' - ); - } - - if (linearAttenuation < 0) { - linearAttenuation = 0; - console.warn( - 'Value of linear argument in lightFalloff() should be never be negative. Set to 0.' - ); - } - - if (quadraticAttenuation < 0) { - quadraticAttenuation = 0; - console.warn( - 'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.' - ); - } - - if ( - constantAttenuation === 0 && - (linearAttenuation === 0 && quadraticAttenuation === 0) - ) { - constantAttenuation = 1; - console.warn( - 'Either one of the three arguments in lightFalloff() should be greater than zero. Set constant argument to 1.' - ); - } - - this.states.constantAttenuation = constantAttenuation; - this.states.linearAttenuation = linearAttenuation; - this.states.quadraticAttenuation = quadraticAttenuation; - } - - // RendererGL.prototype.spotLight = function( - // ) { - // } - RendererGL.prototype.noLights = function() { this.states.activeImageLight = null; this.states.enableLighting = false; From 7fd3c4f7cb186b118ce2c2164a40dfa88e637df7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 2 Nov 2024 13:45:36 -0400 Subject: [PATCH 54/55] Fix updatePixels incorrect on WebGL mode --- src/webgl/p5.Framebuffer.js | 13 +++++++++++-- src/webgl/p5.RendererGL.js | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index a050b42b05..9d78813469 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -1556,11 +1556,20 @@ class Framebuffer { this.begin(); this.renderer.push(); // this.renderer.imageMode(constants.CENTER); - this.renderer.states.imageMode = constants.CENTER; + this.renderer.states.imageMode = constants.CORNER; + this.renderer.setCamera(this.filterCamera); this.renderer.resetMatrix(); this.renderer.states.doStroke = false; this.renderer.clear(); - this.renderer.image(this, 0, 0); + this.renderer._drawingFilter = true; + this.renderer.image( + this, + 0, 0, + this.width, this.height, + -this.renderer.width / 2, -this.renderer.height / 2, + this.renderer.width, this.renderer.height + ); + this.renderer._drawingFilter = false; this.renderer.pop(); if (this.useDepth) { gl.clearDepth(1); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 7d2ae95047..6d35f828ff 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1127,8 +1127,8 @@ class RendererGL extends Renderer { this.push(); this.resetMatrix(); this.clear(); - this.states.imageMode = constants.CENTER; - this.image(fbo, 0, 0, fbo.width, fbo.height, 0, 0, fbo.width, fbo.height); + this.states.imageMode = constants.CORNER; + this.image(fbo, 0, 0, fbo.width, fbo.height, -fbo.width/2, -fbo.height/2, fbo.width, fbo.height); this.pop(); this.GL.clearDepth(1); this.GL.clear(this.GL.DEPTH_BUFFER_BIT); From 468ab53a255e120de53bf54e31ab591298911945 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 2 Nov 2024 14:03:50 -0400 Subject: [PATCH 55/55] Fix constant usage on graphic that should be on the main instance --- test/unit/webgl/p5.RendererGL.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 7a5d1b9aec..bb3c39e115 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1224,13 +1224,14 @@ suite('p5.RendererGL', function() { target.push(); target.background(0); target.blendMode(mode); - target.rectMode(target.CENTER); + target.rectMode(myp5.CENTER); target.noStroke(); target.fill(colorA); target.rect(0, 0, target.width, target.height); target.fill(colorB); target.rect(0, 0, target.width, target.height); target.pop(); + console.log(`${colorA} ${mode} ${colorB}: ` + target.canvas.toDataURL()) return target.get(0, 0); };