From 09f57c97fc2f6ee4ee322c45e26983fd9f4d5e77 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Sun, 15 Sep 2024 21:48:59 +0100 Subject: [PATCH 001/120] Set up states machine in p5.Renderer --- src/color/setting.js | 12 +- src/core/p5.Renderer.js | 250 ++++++++++++++++----------- src/core/p5.Renderer2D.js | 104 +++++------ src/core/shape/2d_primitives.js | 20 +-- src/core/shape/attributes.js | 4 +- src/core/shape/curves.js | 4 +- src/core/shape/vertex.js | 2 +- src/core/structure.js | 1 + src/image/loading_displaying.js | 8 +- src/image/p5.Image.js | 3 +- src/io/files.js | 3 +- src/typography/loading_displaying.js | 15 +- src/typography/p5.Font.js | 22 +-- src/webgl/3d_primitives.js | 14 +- src/webgl/GeometryBuilder.js | 6 +- src/webgl/material.js | 6 +- src/webgl/p5.RendererGL.Immediate.js | 8 +- src/webgl/p5.RendererGL.Retained.js | 4 +- src/webgl/p5.RendererGL.js | 10 +- src/webgl/text.js | 8 +- test/unit/accessibility/describe.js | 1 + test/unit/webgl/p5.RendererGL.js | 36 ++-- 22 files changed, 291 insertions(+), 250 deletions(-) diff --git a/src/color/setting.js b/src/color/setting.js index 36361bd759..97546501e9 100644 --- a/src/color/setting.js +++ b/src/color/setting.js @@ -1209,8 +1209,8 @@ function setting(p5, fn){ * @chainable */ fn.fill = function(...args) { - this._renderer._setProperty('_fillSet', true); - this._renderer._setProperty('_doFill', true); + this._renderer.states.fillSet = true; + this._renderer.states.doFill = true; this._renderer.fill(...args); return this; }; @@ -1271,7 +1271,7 @@ function setting(p5, fn){ * */ fn.noFill = function() { - this._renderer._setProperty('_doFill', false); + this._renderer.states.doFill = false; return this; }; @@ -1327,7 +1327,7 @@ function setting(p5, fn){ * */ fn.noStroke = function() { - this._renderer._setProperty('_doStroke', false); + this._renderer.states.doStroke = false; return this; }; @@ -1581,8 +1581,8 @@ function setting(p5, fn){ */ fn.stroke = function(...args) { - this._renderer._setProperty('_strokeSet', true); - this._renderer._setProperty('_doStroke', true); + 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 5f98a7de23..a7b55e7dd3 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -13,15 +13,20 @@ import * as constants from '../core/constants'; * Renderer2D and Renderer3D classes, respectively. * * @class p5.Renderer - * @extends p5.Element * @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 extends p5.Element { +p5.Renderer = class Renderer { constructor(elt, pInst, isMainCanvas) { - super(elt, pInst); + // this.elt = new p5.Element(elt, pInst); + this.elt = elt; + this._pInst = this._pixelsState = pInst; + this._events = {}; this.canvas = elt; + this.width = this.canvas.offsetWidth; + this.height = this.canvas.offsetHeight; + if (isMainCanvas) { this._isMainCanvas = true; // for pixel method sharing with pimage @@ -35,58 +40,95 @@ p5.Renderer = class Renderer extends p5.Element { this._styles = []; // non-main elt styles stored in p5.Renderer } + // 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 = []; + + + this._clipping = false; this._clipInvert = false; - this._textSize = 12; - this._textLeading = 15; - this._textFont = 'sans-serif'; - this._textStyle = constants.NORMAL; - this._textAscent = null; - this._textDescent = null; - this._textAlign = constants.LEFT; - this._textBaseline = constants.BASELINE; - this._textWrap = constants.WORD; - - this._rectMode = constants.CORNER; - this._ellipseMode = constants.CENTER; + // this._textSize = 12; + // this._textLeading = 15; + // this._textFont = 'sans-serif'; + // this._textStyle = constants.NORMAL; + // this._textAscent = null; + // this._textDescent = null; + // this._textAlign = constants.LEFT; + // this._textBaseline = constants.BASELINE; + // this._textWrap = constants.WORD; + + // this._rectMode = constants.CORNER; + // this._ellipseMode = constants.CENTER; this._curveTightness = 0; - this._imageMode = constants.CORNER; + // this._imageMode = constants.CORNER; - this._tint = null; - this._doStroke = true; - this._doFill = true; - this._strokeSet = false; - this._fillSet = false; - this._leadingSet = false; + // this._tint = null; + // this._doStroke = true; + // this._doFill = true; + // this._strokeSet = false; + // this._fillSet = false; + // this._leadingSet = false; this._pushPopDepth = 0; } + id(id) { + if (typeof id === 'undefined') { + return this.elt.id; + } + + this.elt.id = id; + this.width = this.elt.offsetWidth; + this.height = this.elt.offsetHeight; + return this; + } + // the renderer should return a 'style' object that it wishes to // store on the push stack. push () { this._pushPopDepth++; - return { - properties: { - _doStroke: this._doStroke, - _strokeSet: this._strokeSet, - _doFill: this._doFill, - _fillSet: this._fillSet, - _tint: this._tint, - _imageMode: this._imageMode, - _rectMode: this._rectMode, - _ellipseMode: this._ellipseMode, - _textFont: this._textFont, - _textLeading: this._textLeading, - _leadingSet: this._leadingSet, - _textSize: this._textSize, - _textAlign: this._textAlign, - _textBaseline: this._textBaseline, - _textStyle: this._textStyle, - _textWrap: this._textWrap - } - }; + const currentStates = Object.assign({}, this.states); + this.pushPopStack.push(currentStates); + return currentStates; + // return { + // properties: { + // _doStroke: this._doStroke, + // _strokeSet: this._strokeSet, + // _doFill: this._doFill, + // _fillSet: this._fillSet, + // _tint: this._tint, + // _imageMode: this._imageMode, + // _rectMode: this._rectMode, + // _ellipseMode: this._ellipseMode, + // _textFont: this._textFont, + // _textLeading: this._textLeading, + // _leadingSet: this._leadingSet, + // _textSize: this._textSize, + // _textAlign: this._textAlign, + // _textBaseline: this._textBaseline, + // _textStyle: this._textStyle, + // _textWrap: this._textWrap + // } + // }; } // a pop() operation is in progress @@ -94,10 +136,11 @@ p5.Renderer = class Renderer extends p5.Element { // from its push() method. pop (style) { this._pushPopDepth--; - if (style.properties) { - // copy the style properties back into the renderer - Object.assign(this, style.properties); - } + Object.assign(this.states, this.pushPopStack.pop()); + // if (style.properties) { + // // copy the style properties back into the renderer + // Object.assign(this, style.properties); + // } } beginClip(options = {}) { @@ -121,10 +164,10 @@ p5.Renderer = class Renderer extends p5.Element { resize (w, h) { this.width = w; this.height = h; - 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`; + this.canvas.width = w * this._pInst._pixelDensity; + this.canvas.height = h * this._pInst._pixelDensity; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; if (this._isMainCanvas) { this._pInst._setProperty('width', this.width); this._pInst._setProperty('height', this.height); @@ -165,72 +208,85 @@ p5.Renderer = class Renderer extends p5.Element { return region; } + 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; + } + textLeading (l) { if (typeof l === 'number') { - this._setProperty('_leadingSet', true); - this._setProperty('_textLeading', l); + this.states.leadingSet = true; + this.states.textLeading = l; return this._pInst; } - return this._textLeading; + return this.states.textLeading; } textStyle (s) { if (s) { if ( s === constants.NORMAL || - s === constants.ITALIC || - s === constants.BOLD || - s === constants.BOLDITALIC + s === constants.ITALIC || + s === constants.BOLD || + s === constants.BOLDITALIC ) { - this._setProperty('_textStyle', s); + this.states.textStyle = s; } return this._applyTextProperties(); } - return this._textStyle; + return this.states.textStyle; } textAscent () { - if (this._textAscent === null) { + if (this.states.textAscent === null) { this._updateTextMetrics(); } - return this._textAscent; + return this.states.textAscent; } textDescent () { - if (this._textDescent === null) { + if (this.states.textDescent === null) { this._updateTextMetrics(); } - return this._textDescent; + return this.states.textDescent; } textAlign (h, v) { if (typeof h !== 'undefined') { - this._setProperty('_textAlign', h); + this.states.textAlign = h; if (typeof v !== 'undefined') { - this._setProperty('_textBaseline', v); + this.states.textBaseline = v; } return this._applyTextProperties(); } else { return { - horizontal: this._textAlign, - vertical: this._textBaseline + horizontal: this.states.textAlign, + vertical: this.states.textBaseline }; } } textWrap (wrapStyle) { - this._setProperty('_textWrap', wrapStyle); - return this._textWrap; + this.states.textWrap = wrapStyle; + return this.states.textWrap; } text(str, x, y, maxWidth, maxHeight) { const p = this._pInst; - const textWrapStyle = this._textWrap; + const textWrapStyle = this.states.textWrap; let lines; let line; @@ -243,7 +299,7 @@ p5.Renderer = class Renderer extends p5.Element { // fix for #5785 (top of bounding box) let finalMinHeight = y; - if (!(this._doFill || this._doStroke)) { + if (!(this.states.doFill || this.states.doStroke)) { return; } @@ -259,11 +315,11 @@ p5.Renderer = class Renderer extends p5.Element { lines = str.split('\n'); if (typeof maxWidth !== 'undefined') { - if (this._rectMode === constants.CENTER) { + if (this.states.rectMode === constants.CENTER) { x -= maxWidth / 2; } - switch (this._textAlign) { + switch (this.states.textAlign) { case constants.CENTER: x += maxWidth / 2; break; @@ -273,7 +329,7 @@ p5.Renderer = class Renderer extends p5.Element { } if (typeof maxHeight !== 'undefined') { - if (this._rectMode === constants.CENTER) { + if (this.states.rectMode === constants.CENTER) { y -= maxHeight / 2; finalMinHeight -= maxHeight / 2; } @@ -281,7 +337,7 @@ p5.Renderer = class Renderer extends p5.Element { let originalY = y; let ascent = p.textAscent(); - switch (this._textBaseline) { + switch (this.states.textBaseline) { case constants.BOTTOM: shiftedY = y + maxHeight; y = Math.max(shiftedY, y); @@ -300,15 +356,15 @@ p5.Renderer = class Renderer extends p5.Element { finalMaxHeight = y + maxHeight - ascent; // fix for #5785 (bottom of bounding box) - if (this._textBaseline === constants.CENTER) { + if (this.states.textBaseline === constants.CENTER) { finalMaxHeight = originalY + maxHeight - ascent / 2; } } else { // no text-height specified, show warning for BOTTOM / CENTER - if (this._textBaseline === constants.BOTTOM || - this._textBaseline === constants.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._textLeading; + let rectHeight = p.textSize() * this.states.textLeading; finalMinHeight = y - rectHeight / 2; finalMaxHeight = y + rectHeight / 2; } @@ -336,9 +392,9 @@ p5.Renderer = class Renderer extends p5.Element { } let offset = 0; - if (this._textBaseline === constants.CENTER) { + if (this.states.textBaseline === constants.CENTER) { offset = (nlines.length - 1) * p.textLeading() / 2; - } else if (this._textBaseline === constants.BOTTOM) { + } else if (this.states.textBaseline === constants.BOTTOM) { offset = (nlines.length - 1) * p.textLeading(); } @@ -392,9 +448,9 @@ p5.Renderer = class Renderer extends p5.Element { nlines.push(line); let offset = 0; - if (this._textBaseline === constants.CENTER) { + if (this.states.textBaseline === constants.CENTER) { offset = (nlines.length - 1) * p.textLeading() / 2; - } else if (this._textBaseline === constants.BOTTOM) { + } else if (this.states.textBaseline === constants.BOTTOM) { offset = (nlines.length - 1) * p.textLeading(); } @@ -436,9 +492,9 @@ p5.Renderer = class Renderer extends p5.Element { // 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._textBaseline === constants.CENTER) { + if (this.states.textBaseline === constants.CENTER) { offset = (lines.length - 1) * p.textLeading() / 2; - } else if (this._textBaseline === constants.BOTTOM) { + } else if (this.states.textBaseline === constants.BOTTOM) { offset = (lines.length - 1) * p.textLeading(); } @@ -466,21 +522,21 @@ p5.Renderer = class Renderer extends p5.Element { /** * Helper function to check font type (system or otf) */ - _isOpenType(f = this._textFont) { + _isOpenType(f = this.states.textFont) { return typeof f === 'object' && f.font && f.font.supported; } _updateTextMetrics() { if (this._isOpenType()) { - this._setProperty('_textAscent', this._textFont._textAscent()); - this._setProperty('_textDescent', this._textFont._textDescent()); + 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._textFont; - text.style.fontSize = `${this._textSize}px`; + text.style.fontFamily = this.states.textFont; + text.style.fontSize = `${this.states.textSize}px`; text.innerHTML = 'ABCjgq|'; const block = document.createElement('div'); @@ -509,8 +565,8 @@ p5.Renderer = class Renderer extends p5.Element { document.body.removeChild(container); - this._setProperty('_textAscent', ascent); - this._setProperty('_textDescent', descent); + this.states.textAscent = ascent; + this.states.textDescent = descent; return this; } @@ -534,17 +590,5 @@ function calculateOffset(object) { } return [currentLeft, currentTop]; } -p5.Renderer.prototype.textSize = function(s) { - if (typeof s === 'number') { - this._setProperty('_textSize', s); - if (!this._leadingSet) { - // only use a default value if not previously set (#5181) - this._setProperty('_textLeading', s * constants._DEFAULT_LEADMULT); - } - return this._applyTextProperties(); - } - - return this._textSize; -}; export default p5.Renderer; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 0476224559..e70e219e83 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -253,7 +253,7 @@ class Renderer2D extends Renderer { if (p5.MediaElement && img instanceof p5.MediaElement) { img._ensureCanvas(); } - if (this._tint && img.canvas) { + if (this.states.tint && img.canvas) { cnv = this._getTintedImageCanvas(img); } if (!cnv) { @@ -314,7 +314,7 @@ class Renderer2D extends Renderer { ctx.save(); ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); - if (this._tint[0] < 255 || this._tint[1] < 255 || this._tint[2] < 255) { + 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 @@ -336,16 +336,16 @@ class Renderer2D extends Renderer { // Apply color tint ctx.globalCompositeOperation = 'multiply'; - ctx.fillStyle = `rgb(${this._tint.slice(0, 3).join(', ')})`; + 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._tint[3] / 255; + 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._tint[3] / 255; + ctx.globalAlpha = this.states.tint[3] / 255; ctx.drawImage(img.canvas, 0, 0); } @@ -595,7 +595,7 @@ class Renderer2D extends Renderer { } // Fill curves - if (this._doFill) { + if (this.states.doFill) { if (!this._clipping) ctx.beginPath(); curves.forEach((curve, index) => { if (index === 0) { @@ -615,7 +615,7 @@ class Renderer2D extends Renderer { } // Stroke curves - if (this._doStroke) { + if (this.states.doStroke) { if (!this._clipping) ctx.beginPath(); curves.forEach((curve, index) => { if (index === 0) { @@ -640,8 +640,8 @@ class Renderer2D extends Renderer { ellipse(args) { const ctx = this.drawingContext; - const doFill = this._doFill, - doStroke = this._doStroke; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; const x = parseFloat(args[0]), y = parseFloat(args[1]), w = parseFloat(args[2]), @@ -673,7 +673,7 @@ class Renderer2D extends Renderer { line(x1, y1, x2, y2) { const ctx = this.drawingContext; - if (!this._doStroke) { + if (!this.states.doStroke) { return this; } else if (this._getStroke() === styleEmpty) { return this; @@ -687,7 +687,7 @@ class Renderer2D extends Renderer { point(x, y) { const ctx = this.drawingContext; - if (!this._doStroke) { + if (!this.states.doStroke) { return this; } else if (this._getStroke() === styleEmpty) { return this; @@ -708,8 +708,8 @@ class Renderer2D extends Renderer { quad(x1, y1, x2, y2, x3, y3, x4, y4) { const ctx = this.drawingContext; - const doFill = this._doFill, - doStroke = this._doStroke; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; @@ -744,8 +744,8 @@ class Renderer2D extends Renderer { let br = args[6]; let bl = args[7]; const ctx = this.drawingContext; - const doFill = this._doFill, - doStroke = this._doStroke; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; if (doFill && !doStroke) { if (this._getFill() === styleEmpty) { return this; @@ -814,10 +814,10 @@ class Renderer2D extends Renderer { ctx.arcTo(x, y, x + w, y, tl); ctx.closePath(); } - if (!this._clipping && this._doFill) { + if (!this._clipping && this.states.doFill) { ctx.fill(); } - if (!this._clipping && this._doStroke) { + if (!this._clipping && this.states.doStroke) { ctx.stroke(); } return this; @@ -826,8 +826,8 @@ class Renderer2D extends Renderer { triangle(args) { const ctx = this.drawingContext; - const doFill = this._doFill, - doStroke = this._doStroke; + const doFill = this.states.doFill, + doStroke = this.states.doStroke; const x1 = args[0], y1 = args[1]; const x2 = args[2], @@ -868,7 +868,7 @@ class Renderer2D extends Renderer { if (vertices.length === 0) { return this; } - if (!this._doStroke && !this._doFill) { + if (!this.states.doStroke && !this.states.doFill) { return this; } const closeShape = mode === constants.CLOSE; @@ -962,7 +962,7 @@ class Renderer2D extends Renderer { if (shapeKind === constants.POINTS) { for (i = 0; i < numVerts; i++) { v = vertices[i]; - if (this._doStroke) { + if (this.states.doStroke) { this._pInst.stroke(v[6]); } this._pInst.point(v[0], v[1]); @@ -970,7 +970,7 @@ class Renderer2D extends Renderer { } else if (shapeKind === constants.LINES) { for (i = 0; i + 1 < numVerts; i += 2) { v = vertices[i]; - if (this._doStroke) { + 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]); @@ -983,11 +983,11 @@ class Renderer2D extends Renderer { 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._doFill) { + if (!this._clipping && this.states.doFill) { this._pInst.fill(vertices[i + 2][5]); this.drawingContext.fill(); } - if (!this._clipping && this._doStroke) { + if (!this._clipping && this.states.doStroke) { this._pInst.stroke(vertices[i + 2][6]); this.drawingContext.stroke(); } @@ -998,18 +998,18 @@ class Renderer2D extends Renderer { 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._doStroke) { + if (!this._clipping && this.states.doStroke) { this._pInst.stroke(vertices[i + 1][6]); } - if (!this._clipping && this._doFill) { + 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._doStroke) { + if (!this._clipping && this.states.doStroke) { this._pInst.stroke(vertices[i + 2][6]); } - if (!this._clipping && this._doFill) { + if (!this._clipping && this.states.doFill) { this._pInst.fill(vertices[i + 2][5]); } } @@ -1029,15 +1029,15 @@ class Renderer2D extends Renderer { // If the next colour is going to be different, stroke / fill now if (i < numVerts - 1) { if ( - (this._doFill && v[5] !== vertices[i + 1][5]) || - (this._doStroke && v[6] !== vertices[i + 1][6]) + (this.states.doFill && v[5] !== vertices[i + 1][5]) || + (this.states.doStroke && v[6] !== vertices[i + 1][6]) ) { - if (!this._clipping && this._doFill) { + 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._doStroke) { + if (!this._clipping && this.states.doStroke) { this._pInst.stroke(v[6]); this.drawingContext.stroke(); this._pInst.stroke(vertices[i + 1][6]); @@ -1058,10 +1058,10 @@ class Renderer2D extends Renderer { this.drawingContext.lineTo(vertices[i + j][0], vertices[i + j][1]); } this.drawingContext.lineTo(v[0], v[1]); - if (!this._clipping && this._doFill) { + if (!this._clipping && this.states.doFill) { this._pInst.fill(vertices[i + 3][5]); } - if (!this._clipping && this._doStroke) { + if (!this._clipping && this.states.doStroke) { this._pInst.stroke(vertices[i + 3][6]); } this._doFillStrokeClose(closeShape); @@ -1079,10 +1079,10 @@ class Renderer2D extends Renderer { vertices[i + 1][0], vertices[i + 1][1]); this.drawingContext.lineTo( vertices[i + 3][0], vertices[i + 3][1]); - if (!this._clipping && this._doFill) { + if (!this._clipping && this.states.doFill) { this._pInst.fill(vertices[i + 3][5]); } - if (!this._clipping && this._doStroke) { + if (!this._clipping && this.states.doStroke) { this._pInst.stroke(vertices[i + 3][6]); } } else { @@ -1213,10 +1213,10 @@ class Renderer2D extends Renderer { if (closeShape) { this.drawingContext.closePath(); } - if (!this._clipping && this._doFill) { + if (!this._clipping && this.states.doFill) { this.drawingContext.fill(); } - if (!this._clipping && this._doStroke) { + if (!this._clipping && this.states.doStroke) { this.drawingContext.stroke(); } } @@ -1275,13 +1275,13 @@ class Renderer2D extends Renderer { // a system/browser font // no stroke unless specified by user - if (this._doStroke && this._strokeSet) { + if (this.states.doStroke && this.states.strokeSet) { this.drawingContext.strokeText(line, x, y); } - if (!this._clipping && this._doFill) { + if (!this._clipping && this.states.doFill) { // if fill hasn't been set by user, use default text fill - if (!this._fillSet) { + if (!this.states.fillSet) { this._setFill(constants._DEFAULT_TEXT_FILL); } @@ -1290,7 +1290,7 @@ class Renderer2D extends Renderer { } else { // an opentype font, let it handle the rendering - this._textFont._renderPath(line, x, y, { renderer: this }); + this.states.textFont._renderPath(line, x, y, { renderer: this }); } p.pop(); @@ -1299,7 +1299,7 @@ class Renderer2D extends Renderer { textWidth(s) { if (this._isOpenType()) { - return this._textFont._textWidth(s, this._textSize); + return this.states.textFont._textWidth(s, this.states.textSize); } return this.drawingContext.measureText(s).width; @@ -1309,14 +1309,14 @@ class Renderer2D extends Renderer { let font; const p = this._pInst; - this._setProperty('_textAscent', null); - this._setProperty('_textDescent', null); + this.states.textAscent = null; + this.states.textDescent = null; - font = this._textFont; + font = this.states.textFont; if (this._isOpenType()) { - font = this._textFont.font.familyName; - this._setProperty('_textStyle', this._textFont.font.styleName); + font = this.states.textFont.font.familyName; + this.states.textStyle = this._textFont.font.styleName; } let fontNameString = font || 'sans-serif'; @@ -1324,14 +1324,14 @@ class Renderer2D extends Renderer { // If the name includes spaces, surround in quotes fontNameString = `"${fontNameString}"`; } - this.drawingContext.font = `${this._textStyle || 'normal'} ${this._textSize || + this.drawingContext.font = `${this.states.textStyle || 'normal'} ${this.states.textSize || 12}px ${fontNameString}`; - this.drawingContext.textAlign = this._textAlign; - if (this._textBaseline === constants.CENTER) { + this.drawingContext.textAlign = this.states.textAlign; + if (this.states.textBaseline === constants.CENTER) { this.drawingContext.textBaseline = constants._CTX_MIDDLE; } else { - this.drawingContext.textBaseline = this._textBaseline; + this.drawingContext.textBaseline = this.states.textBaseline; } return p; diff --git a/src/core/shape/2d_primitives.js b/src/core/shape/2d_primitives.js index 8074e40afd..bc18e7647e 100644 --- a/src/core/shape/2d_primitives.js +++ b/src/core/shape/2d_primitives.js @@ -316,7 +316,7 @@ p5.prototype.arc = function(x, y, w, h, start, stop, mode, detail) { // if the current stroke and fill settings wouldn't result in something // visible, exit immediately - if (!this._renderer._doStroke && !this._renderer._doFill) { + if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { return this; } @@ -331,7 +331,7 @@ p5.prototype.arc = function(x, y, w, h, start, stop, mode, detail) { w = Math.abs(w); h = Math.abs(h); - const vals = canvas.modeAdjust(x, y, w, h, this._renderer._ellipseMode); + 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) { @@ -543,7 +543,7 @@ p5.prototype.circle = function(...args) { 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._doStroke && !this._renderer._doFill) { + if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { return this; } @@ -559,7 +559,7 @@ p5.prototype._renderEllipse = function(x, y, w, h, detailX) { h = Math.abs(h); } - const vals = canvas.modeAdjust(x, y, w, h, this._renderer._ellipseMode); + 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 @@ -715,7 +715,7 @@ p5.prototype._renderEllipse = function(x, y, w, h, detailX) { p5.prototype.line = function(...args) { p5._validateParameters('line', args); - if (this._renderer._doStroke) { + if (this._renderer.states.doStroke) { this._renderer.line(...args); } @@ -899,7 +899,7 @@ p5.prototype.line = function(...args) { p5.prototype.point = function(...args) { p5._validateParameters('point', args); - if (this._renderer._doStroke) { + if (this._renderer.states.doStroke) { if (args.length === 1 && args[0] instanceof p5.Vector) { this._renderer.point.call( this._renderer, @@ -1060,7 +1060,7 @@ p5.prototype.point = function(...args) { p5.prototype.quad = function(...args) { p5._validateParameters('quad', args); - if (this._renderer._doStroke || this._renderer._doFill) { + 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( @@ -1337,7 +1337,7 @@ p5.prototype.square = function(x, y, s, tl, tr, br, bl) { // internal method to have renderer draw a rectangle p5.prototype._renderRect = function() { - if (this._renderer._doStroke || this._renderer._doFill) { + 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]; @@ -1347,7 +1347,7 @@ p5.prototype._renderRect = function() { arguments[1], arguments[2], arguments[3], - this._renderer._rectMode + this._renderer.states.rectMode ); const args = [vals.x, vals.y, vals.w, vals.h]; @@ -1436,7 +1436,7 @@ p5.prototype._renderRect = function() { p5.prototype.triangle = function(...args) { p5._validateParameters('triangle', args); - if (this._renderer._doStroke || this._renderer._doFill) { + if (this._renderer.states.doStroke || this._renderer.states.doFill) { this._renderer.triangle(args); } diff --git a/src/core/shape/attributes.js b/src/core/shape/attributes.js index 0db5a961c7..08f942bd0e 100644 --- a/src/core/shape/attributes.js +++ b/src/core/shape/attributes.js @@ -92,7 +92,7 @@ p5.prototype.ellipseMode = function(m) { m === constants.RADIUS || m === constants.CENTER ) { - this._renderer._ellipseMode = m; + this._renderer.states.ellipseMode = m; } return this; }; @@ -295,7 +295,7 @@ p5.prototype.rectMode = function(m) { m === constants.RADIUS || m === constants.CENTER ) { - this._renderer._rectMode = m; + this._renderer.states.rectMode = m; } return this; }; diff --git a/src/core/shape/curves.js b/src/core/shape/curves.js index 0a3695118c..47077b86a5 100644 --- a/src/core/shape/curves.js +++ b/src/core/shape/curves.js @@ -209,7 +209,7 @@ p5.prototype.bezier = function(...args) { // if the current stroke and fill settings wouldn't result in something // visible, exit immediately - if (!this._renderer._doStroke && !this._renderer._doFill) { + if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { return this; } @@ -762,7 +762,7 @@ p5.prototype.bezierTangent = function(a, b, c, d, t) { p5.prototype.curve = function(...args) { p5._validateParameters('curve', args); - if (this._renderer._doStroke) { + if (this._renderer.states.doStroke) { this._renderer.curve(...args); } diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 60c2cf1489..92459fe083 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -1528,7 +1528,7 @@ p5.prototype.endShape = function(mode, count = 1) { if (vertices.length === 0) { return this; } - if (!this._renderer._doStroke && !this._renderer._doFill) { + if (!this._renderer.states.doStroke && !this._renderer.states.doFill) { return this; } diff --git a/src/core/structure.js b/src/core/structure.js index 5539ec0c4c..b0cbf19677 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -542,6 +542,7 @@ p5.prototype.isLooping = function() { * */ p5.prototype.push = function() { + // NOTE: change how state machine is handled from here this._styles.push({ props: { _colorMode: this._colorMode diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index 584cf7604b..87a879d3aa 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -1172,7 +1172,7 @@ function loadingDisplaying(p5, fn){ _sh *= pd; _sw *= pd; - let vals = canvas.modeAdjust(_dx, _dy, _dw, _dh, this._renderer._imageMode); + let vals = canvas.modeAdjust(_dx, _dy, _dw, _dh, this._renderer.states.imageMode); vals = _imageFit( fit, xAlign, @@ -1351,7 +1351,7 @@ function loadingDisplaying(p5, fn){ fn.tint = function(...args) { p5._validateParameters('tint', args); const c = this.color(...args); - this._renderer._tint = c.levels; + this._renderer.states.tint = c.levels; }; /** @@ -1390,7 +1390,7 @@ function loadingDisplaying(p5, fn){ * */ fn.noTint = function() { - this._renderer._tint = null; + this._renderer.states.tint = null; }; /** @@ -1509,7 +1509,7 @@ function loadingDisplaying(p5, fn){ m === constants.CORNERS || m === constants.CENTER ) { - this._renderer._imageMode = m; + this._renderer.states.imageMode = m; } }; } diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index fc45772c00..959e8868c3 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -11,7 +11,6 @@ * drawing images to the main display canvas. */ import Filters from './filters'; -import Renderer from '../core/p5.Renderer'; function image(p5, fn){ /** @@ -964,7 +963,7 @@ function image(p5, fn){ let imgScaleFactor = this._pixelDensity; let maskScaleFactor = 1; - if (p5Image instanceof Renderer) { + if (p5Image instanceof p5.Renderer) { maskScaleFactor = p5Image._pInst._pixelDensity; } diff --git a/src/io/files.js b/src/io/files.js index cb5404cbe6..7f00915914 100644 --- a/src/io/files.js +++ b/src/io/files.js @@ -6,7 +6,6 @@ */ import * as fileSaver from 'file-saver'; -import Renderer from '../core/p5.Renderer'; function files(p5, fn){ /** @@ -1902,7 +1901,7 @@ function files(p5, fn){ if (args.length === 0) { fn.saveCanvas(cnv); return; - } else if (args[0] instanceof Renderer || args[0] instanceof p5.Graphics) { + } else if (args[0] instanceof p5.Renderer || args[0] instanceof p5.Graphics) { // otherwise, parse the arguments // if first param is a p5Graphics, then saveCanvas diff --git a/src/typography/loading_displaying.js b/src/typography/loading_displaying.js index 14410e66b9..47e7fc5812 100644 --- a/src/typography/loading_displaying.js +++ b/src/typography/loading_displaying.js @@ -325,7 +325,7 @@ p5.prototype.loadFont = async function(path, onSuccess, onError) { */ p5.prototype.text = function(str, x, y, maxWidth, maxHeight) { p5._validateParameters('text', arguments); - return !(this._renderer._doFill || this._renderer._doStroke) + return !(this._renderer.states.doFill || this._renderer.states.doStroke) ? this : this._renderer.text(...arguments); }; @@ -426,23 +426,20 @@ p5.prototype.textFont = function(theFont, theSize) { throw new Error('null font passed to textFont'); } - this._renderer._setProperty('_textFont', theFont); + this._renderer.states.textFont = theFont; if (theSize) { - this._renderer._setProperty('_textSize', theSize); - if (!this._renderer._leadingSet) { + this._renderer.states.textSize = theSize; + if (!this._renderer.states.leadingSet) { // only use a default value if not previously set (#5181) - this._renderer._setProperty( - '_textLeading', - theSize * constants._DEFAULT_LEADMULT - ); + this._renderer.states._textLeading = theSize * constants._DEFAULT_LEADMULT; } } return this._renderer._applyTextProperties(); } - return this._renderer._textFont; + return this._renderer.states.textFont; }; export default p5; diff --git a/src/typography/p5.Font.js b/src/typography/p5.Font.js index d228b43094..a91712e2ed 100644 --- a/src/typography/p5.Font.js +++ b/src/typography/p5.Font.js @@ -178,7 +178,7 @@ p5.Font = class Font { let result; let key; - fontSize = fontSize || p._renderer._textSize; + fontSize = fontSize || p._renderer.states.textSize; // NOTE: cache disabled for now pending further discussion of #3436 if (cacheResults) { @@ -335,7 +335,7 @@ p5.Font = class Font { const result = []; let lines = txt.split(/\r?\n|\r|\n/g); - fontSize = fontSize || this.parent._renderer._textSize; + fontSize = fontSize || this.parent._renderer.states.textSize; function isSpace(i, text, glyphsLine) { return ( @@ -373,7 +373,7 @@ p5.Font = class Font { xoff += glyphs[j].advanceWidth * this._scale(fontSize); } - y = y + this.parent._renderer._textLeading; + y = y + this.parent._renderer.states.textLeading; } return result; @@ -412,7 +412,7 @@ p5.Font = class Font { renderer = p._renderer, pos = this._handleAlignment(renderer, line, x, y); - return this.font.getPath(line, pos.x, pos.y, renderer._textSize, options); + return this.font.getPath(line, pos.x, pos.y, renderer.states.textSize, options); } /* @@ -538,13 +538,13 @@ p5.Font = class Font { } // only draw stroke if manually set by user - if (pg._doStroke && pg._strokeSet && !pg._clipping) { + if (pg.states.doStroke && pg.states.strokeSet && !pg._clipping) { ctx.stroke(); } - if (pg._doFill && !pg._clipping) { + if (pg.states.doFill && !pg._clipping) { // if fill hasn't been set by user, use default-text-fill - if (!pg._fillSet) { + if (!pg.states.fillSet) { pg._setFill(constants._DEFAULT_TEXT_FILL); } ctx.fill(); @@ -567,18 +567,18 @@ p5.Font = class Font { _scale(fontSize) { return ( - 1 / this.font.unitsPerEm * (fontSize || this.parent._renderer._textSize) + 1 / this.font.unitsPerEm * (fontSize || this.parent._renderer.states.textSize) ); } _handleAlignment(renderer, line, x, y, textWidth) { - const fontSize = renderer._textSize; + const fontSize = renderer.states.textSize; if (typeof textWidth === 'undefined') { textWidth = this._textWidth(line, fontSize); } - switch (renderer._textAlign) { + switch (renderer.states.textAlign) { case constants.CENTER: x -= textWidth / 2; break; @@ -587,7 +587,7 @@ p5.Font = class Font { break; } - switch (renderer._textBaseline) { + switch (renderer.states.textBaseline) { case constants.TOP: y += this._textAscent(fontSize); break; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 9dc5d65622..b5340cd19f 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -993,7 +993,7 @@ p5.prototype.plane = function( planeGeom.computeFaces().computeNormals(); if (detailX <= 1 && detailY <= 1) { planeGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer._doStroke) { + } else if (this._renderer.states.doStroke) { console.log( 'Cannot draw stroke on plane objects with more' + ' than 1 detailX or 1 detailY' @@ -1208,7 +1208,7 @@ p5.prototype.box = function(width, height, depth, detailX, detailY) { boxGeom.computeNormals(); if (detailX <= 4 && detailY <= 4) { boxGeom._edgesToVertices(); - } else if (this._renderer._doStroke) { + } else if (this._renderer.states.doStroke) { console.log( 'Cannot draw stroke on box objects with more' + ' than 4 detailX or 4 detailY' @@ -1713,7 +1713,7 @@ p5.prototype.cylinder = function( // normals are computed in call to _truncatedCone if (detailX <= 24 && detailY <= 16) { cylinderGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer._doStroke) { + } else if (this._renderer.states.doStroke) { console.log( 'Cannot draw stroke on cylinder objects with more' + ' than 24 detailX or 16 detailY' @@ -1948,7 +1948,7 @@ p5.prototype.cone = function( _truncatedCone.call(coneGeom, 1, 0, 1, detailX, detailY, cap, false); if (detailX <= 24 && detailY <= 16) { coneGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer._doStroke) { + } else if (this._renderer.states.doStroke) { console.log( 'Cannot draw stroke on cone objects with more' + ' than 24 detailX or 16 detailY' @@ -2166,7 +2166,7 @@ p5.prototype.ellipsoid = function( ellipsoidGeom.computeFaces(); if (detailX <= 24 && detailY <= 24) { ellipsoidGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer._doStroke) { + } else if (this._renderer.states.doStroke) { console.log( 'Cannot draw stroke on ellipsoids with more' + ' than 24 detailX or 24 detailY' @@ -2388,7 +2388,7 @@ p5.prototype.torus = function(radius, tubeRadius, detailX, detailY) { torusGeom.computeFaces(); if (detailX <= 24 && detailY <= 16) { torusGeom._makeTriangleEdges()._edgesToVertices(); - } else if (this._renderer._doStroke) { + } else if (this._renderer.states.doStroke) { console.log( 'Cannot draw strokes on torus object with more' + ' than 24 detailX or 16 detailY' @@ -2609,7 +2609,7 @@ p5.RendererGL.prototype.arc = function(...args) { if (detail <= 50) { arcGeom._edgesToVertices(arcGeom); - } else if (this._doStroke) { + } else if (this.states.doStroke) { console.log( `Cannot apply a stroke to an ${shape} with more than 50 detail` ); diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index ac78ec7e94..89f115ed76 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -60,12 +60,12 @@ class GeometryBuilder { ); this.geometry.uvs.push(...input.uvs); - if (this.renderer._doFill) { + if (this.renderer.states.doFill) { this.geometry.faces.push( ...input.faces.map(f => f.map(idx => idx + startIdx)) ); } - if (this.renderer._doStroke) { + if (this.renderer.states.doStroke) { this.geometry.edges.push( ...input.edges.map(edge => edge.map(idx => idx + startIdx)) ); @@ -86,7 +86,7 @@ class GeometryBuilder { const shapeMode = this.renderer.immediateMode.shapeMode; const faces = []; - if (this.renderer._doFill) { + if (this.renderer.states.doFill) { if ( shapeMode === constants.TRIANGLE_STRIP || shapeMode === constants.QUAD_STRIP diff --git a/src/webgl/material.js b/src/webgl/material.js index 54bebc9ba1..b1cb9ca6fb 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -1040,7 +1040,7 @@ p5.prototype.texture = function (tex) { this._renderer.drawMode = constants.TEXTURE; this._renderer._useNormalMaterial = false; this._renderer._tex = tex; - this._renderer._setProperty('_doFill', true); + this._renderer.states.doFill = true; return this; }; @@ -1553,7 +1553,7 @@ p5.prototype.normalMaterial = function (...args) { this._renderer._useEmissiveMaterial = false; this._renderer._useNormalMaterial = true; this._renderer.curFillColor = [1, 1, 1, 1]; - this._renderer._setProperty('_doFill', true); + this._renderer.states.doFill = true; this.noStroke(); return this; }; @@ -1785,7 +1785,7 @@ p5.prototype.ambientMaterial = function (v1, v2, v3) { this._renderer.curAmbientColor = color._array; this._renderer._useNormalMaterial = false; this._renderer._enableLighting = true; - this._renderer._setProperty('_doFill', true); + this._renderer.states.doFill = true; return this; }; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 47a656fa83..e216880e87 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -241,7 +241,7 @@ p5.RendererGL.prototype.endShape = function( this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; } - if (this._doFill && !is_line) { + if (this.states.doFill && !is_line) { if ( !this.geometryBuilder && this.immediateMode.geometry.vertices.length >= 3 @@ -249,7 +249,7 @@ p5.RendererGL.prototype.endShape = function( this._drawImmediateFill(count); } } - if (this._doStroke) { + if (this.states.doStroke) { if ( !this.geometryBuilder && this.immediateMode.geometry.lineVertices.length >= 1 @@ -283,7 +283,7 @@ p5.RendererGL.prototype.endShape = function( p5.RendererGL.prototype._processVertices = function(mode) { if (this.immediateMode.geometry.vertices.length === 0) return; - const calculateStroke = this._doStroke; + const calculateStroke = this.states.doStroke; const shouldClose = mode === constants.CLOSE; if (calculateStroke) { this.immediateMode.geometry.edges = this._calculateEdges( @@ -302,7 +302,7 @@ p5.RendererGL.prototype._processVertices = function(mode) { const hasContour = this.immediateMode.contourIndices.length > 0; // We tesselate when drawing curves or convex shapes const shouldTess = - this._doFill && + this.states.doFill && ( this.isBezier || this.isQuadratic || diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 49f2dd772b..dffe69bf1b 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -129,7 +129,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { if ( !this.geometryBuilder && - this._doFill && + this.states.doFill && this.retainedMode.geometry[gId].vertexCount > 0 ) { this._useVertexColor = (geometry.model.vertexColors.length > 0); @@ -151,7 +151,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { fillShader.unbindShader(); } - if (!this.geometryBuilder && this._doStroke && geometry.lineVertexCount > 0) { + if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); const strokeShader = this._getRetainedStrokeShader(); this._setStrokeUniforms(strokeShader); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 69145241a8..b6f59d4a50 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -522,7 +522,7 @@ p5.RendererGL = class RendererGL extends Renderer { this.registerEnabled = new Set(); - this._tint = [255, 255, 255, 255]; + this.states.tint = [255, 255, 255, 255]; // lightFalloff variables this.constantAttenuation = 1; @@ -914,7 +914,7 @@ p5.RendererGL = class RendererGL extends Renderer { this._enableLighting = false; //reset tint value for new frame - this._tint = [255, 255, 255, 255]; + this.states.tint = [255, 255, 255, 255]; //Clear depth every frame this.GL.clearStencil(0); @@ -1586,10 +1586,10 @@ p5.RendererGL = class RendererGL extends Renderer { push() { // get the base renderer style - const style = Renderer.prototype.push.apply(this); + const style = super.push(); // add webgl-specific style properties - const properties = style.properties; + const properties = style; properties.uModelMatrix = this.uModelMatrix.copy(); properties.uViewMatrix = this.uViewMatrix.copy(); @@ -2079,7 +2079,7 @@ p5.RendererGL = class RendererGL extends Renderer { if (this._tex) { fillShader.setUniform('uSampler', this._tex); } - fillShader.setUniform('uTint', this._tint); + fillShader.setUniform('uTint', this.states.tint); fillShader.setUniform('uHasSetAmbient', this._hasSetAmbient); fillShader.setUniform('uAmbientMatColor', this.curAmbientColor); diff --git a/src/webgl/text.js b/src/webgl/text.js index 292a7d59cf..00ac7d037a 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -640,7 +640,7 @@ p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { ); return; } - if (y >= maxY || !this._doFill) { + if (y >= maxY || !this.states.doFill) { return; // don't render lines beyond our maxY position } @@ -654,10 +654,10 @@ p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { p.push(); // fix to #803 // remember this state, so it can be restored later - const doStroke = this._doStroke; + const doStroke = this.states.doStroke; const drawMode = this.drawMode; - this._doStroke = false; + this.states.doStroke = false; this.drawMode = constants.TEXTURE; // get the cached FontInfo object @@ -750,7 +750,7 @@ p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { // clean up sh.unbindShader(); - this._doStroke = doStroke; + this.states.doStroke = doStroke; this.drawMode = drawMode; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index f16a426d54..50a1c5f31a 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -10,6 +10,7 @@ suite('describe', function() { let cnv = p.createCanvas(100, 100); cnv.id(myID); myp5 = p; + console.log("here", p.describe); }; }); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 66e9008713..86f9e13bf6 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -72,21 +72,21 @@ suite('p5.RendererGL', function() { test('check activate and deactivating fill and stroke', function() { myp5.noStroke(); assert( - !myp5._renderer._doStroke, + !myp5._renderer.states.doStroke, 'stroke shader still active after noStroke()' ); assert.isTrue( - myp5._renderer._doFill, + myp5._renderer.states.doFill, 'fill shader deactivated by noStroke()' ); myp5.stroke(0); myp5.noFill(); assert( - myp5._renderer._doStroke, + myp5._renderer.states.doStroke, 'stroke shader not active after stroke()' ); assert.isTrue( - !myp5._renderer._doFill, + !myp5._renderer.states.doFill, 'fill shader still active after noFill()' ); }); @@ -284,9 +284,9 @@ suite('p5.RendererGL', function() { test('stroke and other settings are unaffected after filter', function() { let c = myp5.createCanvas(5, 5, myp5.WEBGL); let getShapeAttributes = () => [ - c._ellipseMode, + c.states.ellipseMode, c.drawingContext.imageSmoothingEnabled, - c._rectMode, + c.states.rectMode, c.curStrokeWeight, c.curStrokeCap, c.curStrokeJoin, @@ -1344,31 +1344,31 @@ suite('p5.RendererGL', function() { suite('tint() in WEBGL mode', function() { test('default tint value is set and not null', function() { myp5.createCanvas(100, 100, myp5.WEBGL); - assert.deepEqual(myp5._renderer._tint, [255, 255, 255, 255]); + assert.deepEqual(myp5._renderer.states.tint, [255, 255, 255, 255]); }); test('tint value is modified correctly when tint() is called', function() { myp5.createCanvas(100, 100, myp5.WEBGL); myp5.tint(0, 153, 204, 126); - assert.deepEqual(myp5._renderer._tint, [0, 153, 204, 126]); + assert.deepEqual(myp5._renderer.states.tint, [0, 153, 204, 126]); myp5.tint(100, 120, 140); - assert.deepEqual(myp5._renderer._tint, [100, 120, 140, 255]); + assert.deepEqual(myp5._renderer.states.tint, [100, 120, 140, 255]); myp5.tint('violet'); - assert.deepEqual(myp5._renderer._tint, [238, 130, 238, 255]); + assert.deepEqual(myp5._renderer.states.tint, [238, 130, 238, 255]); myp5.tint(100); - assert.deepEqual(myp5._renderer._tint, [100, 100, 100, 255]); + assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 255]); myp5.tint(100, 126); - assert.deepEqual(myp5._renderer._tint, [100, 100, 100, 126]); + assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 126]); myp5.tint([100, 126, 0, 200]); - assert.deepEqual(myp5._renderer._tint, [100, 126, 0, 200]); + assert.deepEqual(myp5._renderer.states.tint, [100, 126, 0, 200]); myp5.tint([100, 126, 0]); - assert.deepEqual(myp5._renderer._tint, [100, 126, 0, 255]); + assert.deepEqual(myp5._renderer.states.tint, [100, 126, 0, 255]); myp5.tint([100]); - assert.deepEqual(myp5._renderer._tint, [100, 100, 100, 255]); + assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 255]); myp5.tint([100, 126]); - assert.deepEqual(myp5._renderer._tint, [100, 100, 100, 126]); + assert.deepEqual(myp5._renderer.states.tint, [100, 100, 100, 126]); myp5.tint(myp5.color(255, 204, 0)); - assert.deepEqual(myp5._renderer._tint, [255, 204, 0, 255]); + assert.deepEqual(myp5._renderer.states.tint, [255, 204, 0, 255]); }); test('tint should be reset after draw loop', function() { @@ -1379,7 +1379,7 @@ suite('p5.RendererGL', function() { }; p.draw = function() { if (p.frameCount === 2) { - resolve(p._renderer._tint); + resolve(p._renderer.states.tint); } p.tint(0, 153, 204, 126); }; From ab22f1df2681ef45cf01c60f9e5f36652c6de2b9 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 17 Sep 2024 19:57:12 +0100 Subject: [PATCH 002/120] Expose p5.Element methods to p5.Renderer2D directly --- src/core/p5.Renderer.js | 63 +---------------------------- src/core/p5.Renderer2D.js | 13 ++++++ test/unit/accessibility/describe.js | 1 - 3 files changed, 14 insertions(+), 63 deletions(-) diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index a7b55e7dd3..c6e30bc722 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -19,8 +19,6 @@ import * as constants from '../core/constants'; */ p5.Renderer = class Renderer { constructor(elt, pInst, isMainCanvas) { - // this.elt = new p5.Element(elt, pInst); - this.elt = elt; this._pInst = this._pixelsState = pInst; this._events = {}; this.canvas = elt; @@ -60,46 +58,11 @@ p5.Renderer = class Renderer { textWrap: constants.WORD }; this.pushPopStack = []; - - + this._pushPopDepth = 0; this._clipping = false; this._clipInvert = false; - - // this._textSize = 12; - // this._textLeading = 15; - // this._textFont = 'sans-serif'; - // this._textStyle = constants.NORMAL; - // this._textAscent = null; - // this._textDescent = null; - // this._textAlign = constants.LEFT; - // this._textBaseline = constants.BASELINE; - // this._textWrap = constants.WORD; - - // this._rectMode = constants.CORNER; - // this._ellipseMode = constants.CENTER; this._curveTightness = 0; - // this._imageMode = constants.CORNER; - - // this._tint = null; - // this._doStroke = true; - // this._doFill = true; - // this._strokeSet = false; - // this._fillSet = false; - // this._leadingSet = false; - - this._pushPopDepth = 0; - } - - id(id) { - if (typeof id === 'undefined') { - return this.elt.id; - } - - this.elt.id = id; - this.width = this.elt.offsetWidth; - this.height = this.elt.offsetHeight; - return this; } // the renderer should return a 'style' object that it wishes to @@ -109,26 +72,6 @@ p5.Renderer = class Renderer { const currentStates = Object.assign({}, this.states); this.pushPopStack.push(currentStates); return currentStates; - // return { - // properties: { - // _doStroke: this._doStroke, - // _strokeSet: this._strokeSet, - // _doFill: this._doFill, - // _fillSet: this._fillSet, - // _tint: this._tint, - // _imageMode: this._imageMode, - // _rectMode: this._rectMode, - // _ellipseMode: this._ellipseMode, - // _textFont: this._textFont, - // _textLeading: this._textLeading, - // _leadingSet: this._leadingSet, - // _textSize: this._textSize, - // _textAlign: this._textAlign, - // _textBaseline: this._textBaseline, - // _textStyle: this._textStyle, - // _textWrap: this._textWrap - // } - // }; } // a pop() operation is in progress @@ -137,10 +80,6 @@ p5.Renderer = class Renderer { pop (style) { this._pushPopDepth--; Object.assign(this.states, this.pushPopStack.pop()); - // if (style.properties) { - // // copy the style properties back into the renderer - // Object.assign(this, style.properties); - // } } beginClip(options = {}) { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index e70e219e83..e66fb0c404 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -17,6 +17,19 @@ class Renderer2D extends Renderer { super(elt, pInst, isMainCanvas); this.drawingContext = this.canvas.getContext('2d'); this._pInst._setProperty('drawingContext', this.drawingContext); + this.elt = elt; + + // 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]; + } + }) + } + } } getFilterGraphicsLayer() { diff --git a/test/unit/accessibility/describe.js b/test/unit/accessibility/describe.js index 50a1c5f31a..f16a426d54 100644 --- a/test/unit/accessibility/describe.js +++ b/test/unit/accessibility/describe.js @@ -10,7 +10,6 @@ suite('describe', function() { let cnv = p.createCanvas(100, 100); cnv.id(myID); myp5 = p; - console.log("here", p.describe); }; }); }); From c5441dc8cc74b7318826d2bf86a069916475360a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 17 Sep 2024 09:13:19 -0400 Subject: [PATCH 003/120] Move more things into state --- src/webgl/p5.RendererGL.js | 121 +++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 65 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b6f59d4a50..067d6dbf18 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -447,6 +447,60 @@ p5.RendererGL = class RendererGL extends Renderer { this.GL = this.drawingContext; this._pInst._setProperty('drawingContext', this.drawingContext); + // 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; @@ -455,53 +509,20 @@ p5.RendererGL = class RendererGL extends Renderer { this._isClipApplied = false; this._stencilTestOn = false; - // lights - this._enableLighting = false; - - this.ambientLightColors = []; this.mixedAmbientLight = []; this.mixedSpecularColor = []; - this.specularColors = [1, 1, 1]; - this.directionalLightDirections = []; - this.directionalLightDiffuseColors = []; - this.directionalLightSpecularColors = []; - - this.pointLightPositions = []; - this.pointLightDiffuseColors = []; - this.pointLightSpecularColors = []; - - this.spotLightPositions = []; - this.spotLightDirections = []; - this.spotLightDiffuseColors = []; - this.spotLightSpecularColors = []; - this.spotLightAngle = []; - this.spotLightConc = []; - - // This property contains the input image if imageLight function - // is called. - // activeImageLight is checked by _setFillUniforms - // for sending uniforms to the fillshader - this.activeImageLight = null; - // If activeImageLight property is Null, diffusedTextures, - // specularTextures are Empty. - // Else, it maps a p5.Image used by imageLight() to a p5.framebuffer. // 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.drawMode = constants.FILL; - this.curFillColor = this._cachedFillStyle = [1, 1, 1, 1]; - this.curAmbientColor = this._cachedFillStyle = [1, 1, 1, 1]; - this.curSpecularColor = this._cachedFillStyle = [0, 0, 0, 0]; - this.curEmissiveColor = this._cachedFillStyle = [0, 0, 0, 0]; - this.curStrokeColor = this._cachedStrokeStyle = [0, 0, 0, 1]; - this.curBlendMode = constants.BLEND; 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 { @@ -509,42 +530,12 @@ p5.RendererGL = class RendererGL extends Renderer { } this._isBlending = false; - - this._hasSetAmbient = false; - this._useSpecularMaterial = false; - this._useEmissiveMaterial = false; - this._useNormalMaterial = false; - this._useShininess = 1; - this._useMetalness = 0; - this._useLineColor = false; this._useVertexColor = false; this.registerEnabled = new Set(); - this.states.tint = [255, 255, 255, 255]; - - // lightFalloff variables - this.constantAttenuation = 1; - this.linearAttenuation = 0; - this.quadraticAttenuation = 0; - - /** - * model view, projection, & normal - * matrices - */ - this.uModelMatrix = new p5.Matrix(); - this.uViewMatrix = new p5.Matrix(); - this.uMVMatrix = new p5.Matrix(); - this.uPMatrix = new p5.Matrix(); - this.uNMatrix = new p5.Matrix('mat3'); - this.curMatrix = new p5.Matrix('mat3'); - - // Current vertex normal - this._currentNormal = new p5.Vector(0, 0, 1); - // Camera - this._curCamera = new p5.Camera(this); this._curCamera._computeCameraDefaultSettings(); this._curCamera._setDefaultCamera(); From e93cbd54c108cbaa0034d6543cbd5aecbe0a1f4e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 17 Sep 2024 11:27:32 -0400 Subject: [PATCH 004/120] Start converting WebGL state --- src/accessibility/outputs.js | 4 +- src/core/p5.Renderer.js | 6 + src/webgl/3d_primitives.js | 80 +++--- src/webgl/interaction.js | 20 +- src/webgl/light.js | 90 +++---- src/webgl/material.js | 54 ++--- src/webgl/p5.Camera.js | 36 +-- src/webgl/p5.Framebuffer.js | 12 +- src/webgl/p5.RendererGL.Immediate.js | 24 +- src/webgl/p5.RendererGL.Retained.js | 12 +- src/webgl/p5.RendererGL.js | 272 ++++++++------------- src/webgl/p5.Shader.js | 18 +- src/webgl/text.js | 10 +- test/unit/color/setting.js | 16 +- test/unit/webgl/light.js | 350 +++++++++++++-------------- test/unit/webgl/p5.Camera.js | 22 +- test/unit/webgl/p5.RendererGL.js | 142 +++++------ test/unit/webgl/p5.Shader.js | 4 +- 18 files changed, 553 insertions(+), 619 deletions(-) diff --git a/src/accessibility/outputs.js b/src/accessibility/outputs.js index 450679abe0..eb785b7458 100644 --- a/src/accessibility/outputs.js +++ b/src/accessibility/outputs.js @@ -543,7 +543,7 @@ function outputs(p5, fn){ fn._getPos = function (x, y) { const untransformedPosition = new DOMPointReadOnly(x, y); const currentTransform = this._renderer.isP3D ? - new DOMMatrix(this._renderer.uMVMatrix.mat4) : + new DOMMatrix(this._renderer.states.uMVMatrix.mat4) : this.drawingContext.getTransform(); const { x: transformedX, y: transformedY } = untransformedPosition .matrixTransform(currentTransform); @@ -663,7 +663,7 @@ function outputs(p5, fn){ ]; // Apply the inverse of the current transformations to the canvas corners const currentTransform = this._renderer.isP3D ? - new DOMMatrix(this._renderer.uMVMatrix.mat4) : + new DOMMatrix(this._renderer.states.uMVMatrix.mat4) : this.drawingContext.getTransform(); const invertedTransform = currentTransform.inverse(); const tc = canvasCorners.map( diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index c6e30bc722..6a3ae8d6dd 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -70,6 +70,12 @@ p5.Renderer = class Renderer { push () { this._pushPopDepth++; const currentStates = Object.assign({}, this.states); + // Clone properties that support it + for (const key in currentStates) { + if (currentStates[key] && currentStates[key].copy instanceof Function) { + currentStates[key] = currentStates[key].copy(); + } + } this.pushPopStack.push(currentStates); return currentStates; } diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index b5340cd19f..9524b6b3aa 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -2481,7 +2481,7 @@ p5.RendererGL.prototype.triangle = function(args) { // this matrix multiplication transforms those two unit vectors // onto the required vector prior to rendering, and moves the // origin appropriately. - const uModelMatrix = this.uModelMatrix.copy(); + 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); @@ -2490,13 +2490,13 @@ p5.RendererGL.prototype.triangle = function(args) { 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.uModelMatrix); + ]).mult(this.states.uModelMatrix); - this.uModelMatrix = mult; + this.states.uModelMatrix = mult; this.drawBuffers(gId); } finally { - this.uModelMatrix = uModelMatrix; + this.states.uModelMatrix = uModelMatrix; } return this; @@ -2618,15 +2618,15 @@ p5.RendererGL.prototype.arc = function(...args) { this.createBuffers(gId, arcGeom); } - const uModelMatrix = this.uModelMatrix.copy(); + const uModelMatrix = this.states.uModelMatrix.copy(); try { - this.uModelMatrix.translate([x, y, 0]); - this.uModelMatrix.scale(width, height, 1); + this.states.uModelMatrix.translate([x, y, 0]); + this.states.uModelMatrix.scale(width, height, 1); this.drawBuffers(gId); } finally { - this.uModelMatrix = uModelMatrix; + this.states.uModelMatrix = uModelMatrix; } return this; @@ -2678,14 +2678,14 @@ p5.RendererGL.prototype.rect = function(args) { // opposite corners at (0,0) & (1,1). // // before rendering, this square is scaled & moved to the required location. - const uModelMatrix = this.uModelMatrix.copy(); + const uModelMatrix = this.states.uModelMatrix.copy(); try { - this.uModelMatrix.translate([x, y, 0]); - this.uModelMatrix.scale(width, height, 1); + this.states.uModelMatrix.translate([x, y, 0]); + this.states.uModelMatrix.scale(width, height, 1); this.drawBuffers(gId); } finally { - this.uModelMatrix = uModelMatrix; + this.states.uModelMatrix = uModelMatrix; } } else { // Use Immediate mode to round the rectangle corner, @@ -3010,13 +3010,13 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { const fillColors = []; for (m = 0; m < 4; m++) fillColors.push([]); fillColors[0] = this.immediateMode.geometry.vertexColors.slice(-4); - fillColors[3] = this.curFillColor.slice(); + fillColors[3] = this.states.curFillColor.slice(); // Do the same for strokeColor. const strokeColors = []; for (m = 0; m < 4; m++) strokeColors.push([]); strokeColors[0] = this.immediateMode.geometry.vertexStrokeColors.slice(-4); - strokeColors[3] = this.curStrokeColor.slice(); + strokeColors[3] = this.states.curStrokeColor.slice(); if (argLength === 6) { this.isBezier = true; @@ -3048,14 +3048,14 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { for (i = 0; i < LUTLength; i++) { // Interpolate colors using control points - this.curFillColor = [0, 0, 0, 0]; - this.curStrokeColor = [0, 0, 0, 0]; + this.states.curFillColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 0]; _x = _y = 0; for (m = 0; m < 4; m++) { for (k = 0; k < 4; k++) { - this.curFillColor[k] += + this.states.curFillColor[k] += this._lookUpTableBezier[i][m] * fillColors[m][k]; - this.curStrokeColor[k] += + this.states.curStrokeColor[k] += this._lookUpTableBezier[i][m] * strokeColors[m][k]; } _x += w_x[m] * this._lookUpTableBezier[i][m]; @@ -3064,8 +3064,8 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { this.vertex(_x, _y); } // so that we leave currentColor with the last value the user set it to - this.curFillColor = fillColors[3]; - this.curStrokeColor = strokeColors[3]; + this.states.curFillColor = fillColors[3]; + this.states.curStrokeColor = strokeColors[3]; this.immediateMode._bezierVertex[0] = args[4]; this.immediateMode._bezierVertex[1] = args[5]; } else if (argLength === 9) { @@ -3098,14 +3098,14 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { } for (i = 0; i < LUTLength; i++) { // Interpolate colors using control points - this.curFillColor = [0, 0, 0, 0]; - this.curStrokeColor = [0, 0, 0, 0]; + 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.curFillColor[k] += + this.states.curFillColor[k] += this._lookUpTableBezier[i][m] * fillColors[m][k]; - this.curStrokeColor[k] += + this.states.curStrokeColor[k] += this._lookUpTableBezier[i][m] * strokeColors[m][k]; } _x += w_x[m] * this._lookUpTableBezier[i][m]; @@ -3115,8 +3115,8 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { this.vertex(_x, _y, _z); } // so that we leave currentColor with the last value the user set it to - this.curFillColor = fillColors[3]; - this.curStrokeColor = strokeColors[3]; + this.states.curFillColor = fillColors[3]; + this.states.curStrokeColor = strokeColors[3]; this.immediateMode._bezierVertex[0] = args[6]; this.immediateMode._bezierVertex[1] = args[7]; this.immediateMode._bezierVertex[2] = args[8]; @@ -3170,13 +3170,13 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { const fillColors = []; for (m = 0; m < 3; m++) fillColors.push([]); fillColors[0] = this.immediateMode.geometry.vertexColors.slice(-4); - fillColors[2] = this.curFillColor.slice(); + fillColors[2] = this.states.curFillColor.slice(); // Do the same for strokeColor. const strokeColors = []; for (m = 0; m < 3; m++) strokeColors.push([]); strokeColors[0] = this.immediateMode.geometry.vertexStrokeColors.slice(-4); - strokeColors[2] = this.curStrokeColor.slice(); + strokeColors[2] = this.states.curStrokeColor.slice(); if (argLength === 4) { this.isQuadratic = true; @@ -3201,14 +3201,14 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { for (i = 0; i < LUTLength; i++) { // Interpolate colors using control points - this.curFillColor = [0, 0, 0, 0]; - this.curStrokeColor = [0, 0, 0, 0]; + this.states.curFillColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 0]; _x = _y = 0; for (m = 0; m < 3; m++) { for (k = 0; k < 4; k++) { - this.curFillColor[k] += + this.states.curFillColor[k] += this._lookUpTableQuadratic[i][m] * fillColors[m][k]; - this.curStrokeColor[k] += + this.states.curStrokeColor[k] += this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; } _x += w_x[m] * this._lookUpTableQuadratic[i][m]; @@ -3218,8 +3218,8 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { } // so that we leave currentColor with the last value the user set it to - this.curFillColor = fillColors[2]; - this.curStrokeColor = strokeColors[2]; + this.states.curFillColor = fillColors[2]; + this.states.curStrokeColor = strokeColors[2]; this.immediateMode._quadraticVertex[0] = args[2]; this.immediateMode._quadraticVertex[1] = args[3]; } else if (argLength === 6) { @@ -3246,14 +3246,14 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { for (i = 0; i < LUTLength; i++) { // Interpolate colors using control points - this.curFillColor = [0, 0, 0, 0]; - this.curStrokeColor = [0, 0, 0, 0]; + 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.curFillColor[k] += + this.states.curFillColor[k] += this._lookUpTableQuadratic[i][m] * fillColors[m][k]; - this.curStrokeColor[k] += + this.states.curStrokeColor[k] += this._lookUpTableQuadratic[i][m] * strokeColors[m][k]; } _x += w_x[m] * this._lookUpTableQuadratic[i][m]; @@ -3264,8 +3264,8 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { } // so that we leave currentColor with the last value the user set it to - this.curFillColor = fillColors[2]; - this.curStrokeColor = strokeColors[2]; + this.states.curFillColor = fillColors[2]; + this.states.curStrokeColor = strokeColors[2]; this.immediateMode._quadraticVertex[0] = args[3]; this.immediateMode._quadraticVertex[1] = args[4]; this.immediateMode._quadraticVertex[2] = args[5]; diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index 8070354a59..3a53f83c98 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -170,7 +170,7 @@ p5.prototype.orbitControl = function( this._assert3d('orbitControl'); p5._validateParameters('orbitControl', arguments); - const cam = this._renderer._curCamera; + const cam = this._renderer.states.curCamera; if (typeof sensitivityX === 'undefined') { sensitivityX = 1; @@ -363,8 +363,8 @@ p5.prototype.orbitControl = function( 10, -this._renderer.zoomVelocity ); // modify uPMatrix - this._renderer.uPMatrix.mat4[0] = cam.projMatrix.mat4[0]; - this._renderer.uPMatrix.mat4[5] = cam.projMatrix.mat4[5]; + this._renderer.states.uPMatrix.mat4[0] = cam.projMatrix.mat4[0]; + this._renderer.states.uPMatrix.mat4[5] = cam.projMatrix.mat4[5]; } // damping this._renderer.zoomVelocity *= damping; @@ -432,7 +432,7 @@ p5.prototype.orbitControl = function( // Calculate the normalized device coordinates of the center. cv = cam.cameraMatrix.multiplyPoint(cv); - cv = this._renderer.uPMatrix.multiplyAndNormalizePoint(cv); + cv = this._renderer.states.uPMatrix.multiplyAndNormalizePoint(cv); // Move the center by this distance // in the normalized device coordinate system. @@ -442,7 +442,7 @@ p5.prototype.orbitControl = function( // Calculate the translation vector // in the direction perpendicular to the line of sight of center. let dx, dy; - const uP = this._renderer.uPMatrix.mat4; + const uP = this._renderer.states.uPMatrix.mat4; if (uP[15] === 0) { dx = ((uP[8] + cv.x)/uP[0]) * viewZ; @@ -809,11 +809,11 @@ p5.prototype._grid = function(size, numDivs, xOff, yOff, zOff) { return function() { this.push(); this.stroke( - this._renderer.curStrokeColor[0] * 255, - this._renderer.curStrokeColor[1] * 255, - this._renderer.curStrokeColor[2] * 255 + this._renderer.states.curStrokeColor[0] * 255, + this._renderer.states.curStrokeColor[1] * 255, + this._renderer.states.curStrokeColor[2] * 255 ); - this._renderer.uModelMatrix.reset(); + this._renderer.states.uModelMatrix.reset(); // Lines along X axis for (let q = 0; q <= numDivs; q++) { @@ -860,7 +860,7 @@ p5.prototype._axesIcon = function(size, xOff, yOff, zOff) { return function() { this.push(); - this._renderer.uModelMatrix.reset(); + this._renderer.states.uModelMatrix.reset(); // X axis this.strokeWeight(2); diff --git a/src/webgl/light.js b/src/webgl/light.js index 3cffbca5f9..8057eadb2a 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -191,7 +191,7 @@ p5.prototype.ambientLight = function (v1, v2, v3, a) { p5._validateParameters('ambientLight', arguments); const color = this.color(...arguments); - this._renderer.ambientLightColors.push( + this._renderer.states.ambientLightColors.push( color._array[0], color._array[1], color._array[2] @@ -449,7 +449,7 @@ p5.prototype.specularColor = function (v1, v2, v3) { p5._validateParameters('specularColor', arguments); const color = this.color(...arguments); - this._renderer.specularColors = [ + this._renderer.states.specularColors = [ color._array[0], color._array[1], color._array[2] @@ -662,16 +662,16 @@ p5.prototype.directionalLight = function (v1, v2, v3, x, y, z) { // normalize direction const l = Math.sqrt(_x * _x + _y * _y + _z * _z); - this._renderer.directionalLightDirections.push(_x / l, _y / l, _z / l); + this._renderer.states.directionalLightDirections.push(_x / l, _y / l, _z / l); - this._renderer.directionalLightDiffuseColors.push( + this._renderer.states.directionalLightDiffuseColors.push( color._array[0], color._array[1], color._array[2] ); Array.prototype.push.apply( - this._renderer.directionalLightSpecularColors, - this._renderer.specularColors + this._renderer.states.directionalLightSpecularColors, + this._renderer.states.specularColors ); this._renderer._enableLighting = true; @@ -936,15 +936,15 @@ p5.prototype.pointLight = function (v1, v2, v3, x, y, z) { _z = v.z; } - this._renderer.pointLightPositions.push(_x, _y, _z); - this._renderer.pointLightDiffuseColors.push( + 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.pointLightSpecularColors, - this._renderer.specularColors + this._renderer.states.pointLightSpecularColors, + this._renderer.states.specularColors ); this._renderer._enableLighting = true; @@ -1013,7 +1013,7 @@ p5.prototype.pointLight = function (v1, v2, v3, x, y, z) { p5.prototype.imageLight = function (img) { // activeImageLight property is checked by _setFillUniforms // for sending uniforms to the fillshader - this._renderer.activeImageLight = img; + this._renderer.states.activeImageLight = img; this._renderer._enableLighting = true; }; @@ -1261,9 +1261,9 @@ p5.prototype.lightFalloff = function ( ); } - this._renderer.constantAttenuation = constantAttenuation; - this._renderer.linearAttenuation = linearAttenuation; - this._renderer.quadraticAttenuation = quadraticAttenuation; + this._renderer.states.constantAttenuation = constantAttenuation; + this._renderer.states.linearAttenuation = linearAttenuation; + this._renderer.states.quadraticAttenuation = quadraticAttenuation; return this; }; @@ -1642,19 +1642,19 @@ p5.prototype.spotLight = function ( ); return this; } - this._renderer.spotLightDiffuseColors = [ + this._renderer.states.spotLightDiffuseColors = [ color._array[0], color._array[1], color._array[2] ]; - this._renderer.spotLightSpecularColors = [ - ...this._renderer.specularColors + this._renderer.states.spotLightSpecularColors = [ + ...this._renderer.states.specularColors ]; - this._renderer.spotLightPositions = [position.x, position.y, position.z]; + this._renderer.states.spotLightPositions = [position.x, position.y, position.z]; direction.normalize(); - this._renderer.spotLightDirections = [ + this._renderer.states.spotLightDirections = [ direction.x, direction.y, direction.z @@ -1674,8 +1674,8 @@ p5.prototype.spotLight = function ( } angle = this._renderer._pInst._toRadians(angle); - this._renderer.spotLightAngle = [Math.cos(angle)]; - this._renderer.spotLightConc = [concentration]; + this._renderer.states.spotLightAngle = [Math.cos(angle)]; + this._renderer.states.spotLightConc = [concentration]; this._renderer._enableLighting = true; @@ -1744,32 +1744,32 @@ p5.prototype.noLights = function (...args) { this._assert3d('noLights'); p5._validateParameters('noLights', args); - this._renderer.activeImageLight = null; + this._renderer.states.activeImageLight = null; this._renderer._enableLighting = false; - this._renderer.ambientLightColors.length = 0; - this._renderer.specularColors = [1, 1, 1]; - - this._renderer.directionalLightDirections.length = 0; - this._renderer.directionalLightDiffuseColors.length = 0; - this._renderer.directionalLightSpecularColors.length = 0; - - this._renderer.pointLightPositions.length = 0; - this._renderer.pointLightDiffuseColors.length = 0; - this._renderer.pointLightSpecularColors.length = 0; - - this._renderer.spotLightPositions.length = 0; - this._renderer.spotLightDirections.length = 0; - this._renderer.spotLightDiffuseColors.length = 0; - this._renderer.spotLightSpecularColors.length = 0; - this._renderer.spotLightAngle.length = 0; - this._renderer.spotLightConc.length = 0; - - this._renderer.constantAttenuation = 1; - this._renderer.linearAttenuation = 0; - this._renderer.quadraticAttenuation = 0; - this._renderer._useShininess = 1; - this._renderer._useMetalness = 0; + 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; return this; }; diff --git a/src/webgl/material.js b/src/webgl/material.js index b1cb9ca6fb..90f7bf308f 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -764,7 +764,7 @@ p5.prototype.shader = function (s) { this._renderer.userStrokeShader = s; } else { this._renderer.userFillShader = s; - this._renderer._useNormalMaterial = false; + this._renderer.states._useNormalMaterial = false; } return this; @@ -1037,9 +1037,9 @@ p5.prototype.texture = function (tex) { tex._animateGif(this); } - this._renderer.drawMode = constants.TEXTURE; - this._renderer._useNormalMaterial = false; - this._renderer._tex = tex; + this._renderer.states.drawMode = constants.TEXTURE; + this._renderer.states._useNormalMaterial = false; + this._renderer.states._tex = tex; this._renderer.states.doFill = true; return this; @@ -1548,11 +1548,11 @@ p5.prototype.textureWrap = function (wrapX, wrapY = wrapX) { p5.prototype.normalMaterial = function (...args) { this._assert3d('normalMaterial'); p5._validateParameters('normalMaterial', args); - this._renderer.drawMode = constants.FILL; - this._renderer._useSpecularMaterial = false; - this._renderer._useEmissiveMaterial = false; - this._renderer._useNormalMaterial = true; - this._renderer.curFillColor = [1, 1, 1, 1]; + 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(); return this; @@ -1781,9 +1781,9 @@ p5.prototype.ambientMaterial = function (v1, v2, v3) { p5._validateParameters('ambientMaterial', arguments); const color = p5.prototype.color.apply(this, arguments); - this._renderer._hasSetAmbient = true; - this._renderer.curAmbientColor = color._array; - this._renderer._useNormalMaterial = false; + this._renderer.states._hasSetAmbient = true; + this._renderer.states.curAmbientColor = color._array; + this._renderer.states._useNormalMaterial = false; this._renderer._enableLighting = true; this._renderer.states.doFill = true; return this; @@ -1877,9 +1877,9 @@ p5.prototype.emissiveMaterial = function (v1, v2, v3, a) { p5._validateParameters('emissiveMaterial', arguments); const color = p5.prototype.color.apply(this, arguments); - this._renderer.curEmissiveColor = color._array; - this._renderer._useEmissiveMaterial = true; - this._renderer._useNormalMaterial = false; + this._renderer.states.curEmissiveColor = color._array; + this._renderer.states._useEmissiveMaterial = true; + this._renderer.states._useNormalMaterial = false; this._renderer._enableLighting = true; return this; @@ -2132,9 +2132,9 @@ p5.prototype.specularMaterial = function (v1, v2, v3, alpha) { p5._validateParameters('specularMaterial', arguments); const color = p5.prototype.color.apply(this, arguments); - this._renderer.curSpecularColor = color._array; - this._renderer._useSpecularMaterial = true; - this._renderer._useNormalMaterial = false; + this._renderer.states.curSpecularColor = color._array; + this._renderer.states._useSpecularMaterial = true; + this._renderer.states._useNormalMaterial = false; this._renderer._enableLighting = true; return this; @@ -2207,7 +2207,7 @@ p5.prototype.shininess = function (shine) { if (shine < 1) { shine = 1; } - this._renderer._useShininess = shine; + this._renderer.states._useShininess = shine; return this; }; @@ -2323,7 +2323,7 @@ p5.prototype.shininess = function (shine) { p5.prototype.metalness = function (metallic) { this._assert3d('metalness'); const metalMix = 1 - Math.exp(-metallic / 100); - this._renderer._useMetalness = metalMix; + this._renderer.states._useMetalness = metalMix; return this; }; @@ -2339,22 +2339,22 @@ p5.prototype.metalness = function (metallic) { p5.RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { const gl = this.GL; - const isTexture = this.drawMode === constants.TEXTURE; + const isTexture = this.states.drawMode === constants.TEXTURE; const doBlend = hasTransparency || this.userFillShader || this.userStrokeShader || this.userPointShader || isTexture || - this.curBlendMode !== constants.BLEND || + this.states.curBlendMode !== constants.BLEND || colors[colors.length - 1] < 1.0 || this._isErasing; if (doBlend !== this._isBlending) { if ( doBlend || - (this.curBlendMode !== constants.BLEND && - this.curBlendMode !== constants.ADD) + (this.states.curBlendMode !== constants.BLEND && + this.states.curBlendMode !== constants.ADD) ) { gl.enable(gl.BLEND); } else { @@ -2373,11 +2373,11 @@ p5.RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { * @return {Number[]} Normalized numbers array */ p5.RendererGL.prototype._applyBlendMode = function () { - if (this._cachedBlendMode === this.curBlendMode) { + if (this._cachedBlendMode === this.states.curBlendMode) { return; } const gl = this.GL; - switch (this.curBlendMode) { + switch (this.states.curBlendMode) { case constants.BLEND: gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); @@ -2448,7 +2448,7 @@ p5.RendererGL.prototype._applyBlendMode = function () { break; } if (!this._isErasing) { - this._cachedBlendMode = this.curBlendMode; + this._cachedBlendMode = this.states.curBlendMode; } }; diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 2b3b7c7d00..c189b566b7 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -140,7 +140,7 @@ import p5 from '../core/main'; p5.prototype.camera = function (...args) { this._assert3d('camera'); p5._validateParameters('camera', args); - this._renderer._curCamera.camera(...args); + this._renderer.states.curCamera.camera(...args); return this; }; @@ -271,7 +271,7 @@ p5.prototype.camera = function (...args) { p5.prototype.perspective = function (...args) { this._assert3d('perspective'); p5._validateParameters('perspective', args); - this._renderer._curCamera.perspective(...args); + this._renderer.states.curCamera.perspective(...args); return this; }; @@ -400,10 +400,10 @@ p5.prototype.linePerspective = function (enable) { } if (enable !== undefined) { // Set the line perspective if enable is provided - this._renderer._curCamera.useLinePerspective = enable; + this._renderer.states.curCamera.useLinePerspective = enable; } else { // If no argument is provided, return the current value - return this._renderer._curCamera.useLinePerspective; + return this._renderer.states.curCamera.useLinePerspective; } }; @@ -514,7 +514,7 @@ p5.prototype.linePerspective = function (enable) { p5.prototype.ortho = function (...args) { this._assert3d('ortho'); p5._validateParameters('ortho', args); - this._renderer._curCamera.ortho(...args); + this._renderer.states.curCamera.ortho(...args); return this; }; @@ -626,7 +626,7 @@ p5.prototype.ortho = function (...args) { p5.prototype.frustum = function (...args) { this._assert3d('frustum'); p5._validateParameters('frustum', args); - this._renderer._curCamera.frustum(...args); + this._renderer.states.curCamera.frustum(...args); return this; }; @@ -2073,7 +2073,7 @@ p5.Camera = class Camera { /* eslint-enable indent */ if (this._isActive()) { - this._renderer.uPMatrix.set(this.projMatrix); + this._renderer.states.uPMatrix.set(this.projMatrix); } } @@ -2260,7 +2260,7 @@ p5.Camera = class Camera { tx, ty, tz, 1); /* eslint-enable indent */ if (this._isActive()) { - this._renderer.uPMatrix.set(this.projMatrix); + this._renderer.states.uPMatrix.set(this.projMatrix); } this.cameraType = 'custom'; } @@ -2398,7 +2398,7 @@ p5.Camera = class Camera { /* eslint-enable indent */ if (this._isActive()) { - this._renderer.uPMatrix.set(this.projMatrix); + this._renderer.states.uPMatrix.set(this.projMatrix); } this.cameraType = 'custom'; @@ -2972,7 +2972,7 @@ p5.Camera = class Camera { this.cameraMatrix.translate([tx, ty, tz]); if (this._isActive()) { - this._renderer.uViewMatrix.set(this.cameraMatrix); + this._renderer.states.uViewMatrix.set(this.cameraMatrix); } return this; } @@ -3297,9 +3297,9 @@ p5.Camera = class Camera { this.projMatrix = cam.projMatrix.copy(); if (this._isActive()) { - this._renderer.uModelMatrix.reset(); - this._renderer.uViewMatrix.set(this.cameraMatrix); - this._renderer.uPMatrix.set(this.projMatrix); + this._renderer.states.uModelMatrix.reset(); + this._renderer.states.uViewMatrix.set(this.cameraMatrix); + this._renderer.states.uPMatrix.set(this.projMatrix); } } /** @@ -3398,7 +3398,7 @@ p5.Camera = class Camera { 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.uPMatrix.mat4 = this.projMatrix.mat4.slice(); + this._renderer.states.uPMatrix.mat4 = this.projMatrix.mat4.slice(); } } @@ -3859,7 +3859,7 @@ p5.Camera = class Camera { * @private */ _isActive() { - return this === this._renderer._curCamera; + return this === this._renderer.states.curCamera; } }; @@ -3925,11 +3925,11 @@ p5.Camera = class Camera { * */ p5.prototype.setCamera = function (cam) { - this._renderer._curCamera = cam; + this._renderer.states.curCamera = cam; // set the projection matrix (which is not normally updated each frame) - this._renderer.uPMatrix.set(cam.projMatrix); - this._renderer.uViewMatrix.set(cam.cameraMatrix); + this._renderer.states.uPMatrix.set(cam.projMatrix); + this._renderer.states.uViewMatrix.set(cam.cameraMatrix); }; export default p5.Camera; diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 82a5ed7d4f..e5a4b5477a 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -169,10 +169,10 @@ p5.Framebuffer = class Framebuffer { this._recreateTextures(); - const prevCam = this.target._renderer._curCamera; + const prevCam = this.target._renderer.states.curCamera; this.defaultCamera = this.createCamera(); this.filterCamera = this.createCamera(); - this.target._renderer._curCamera = prevCam; + this.target._renderer.states.curCamera = prevCam; this.draw(() => this.target.clear()); } @@ -948,7 +948,7 @@ p5.Framebuffer = class Framebuffer { const cam = new p5.FramebufferCamera(this); cam._computeCameraDefaultSettings(); cam._setDefaultCamera(); - this.target._renderer._curCamera = cam; + this.target._renderer.states.curCamera = cam; return cam; } @@ -1111,9 +1111,9 @@ p5.Framebuffer = class Framebuffer { // it only sets the camera. this.target.setCamera(this.defaultCamera); this.target.resetMatrix(); - this.target._renderer.uViewMatrix - .set(this.target._renderer._curCamera.cameraMatrix); - this.target._renderer.uModelMatrix.reset(); + this.target._renderer.states.uViewMatrix + .set(this.target._renderer.states.curCamera.cameraMatrix); + this.target._renderer.states.uModelMatrix.reset(); this.target._renderer._applyStencilTestIfClipping(); } diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index e216880e87..e6283478fc 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -112,15 +112,15 @@ p5.RendererGL.prototype.vertex = function(x, y) { } const vert = new p5.Vector(x, y, z); this.immediateMode.geometry.vertices.push(vert); - this.immediateMode.geometry.vertexNormals.push(this._currentNormal); - const vertexColor = this.curFillColor || [0.5, 0.5, 0.5, 1.0]; + this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); + 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.curStrokeColor || [0.5, 0.5, 0.5, 1]; + const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; this.immediateMode.geometry.vertexStrokeColors.push( lineVertexColor[0], lineVertexColor[1], @@ -129,10 +129,10 @@ p5.RendererGL.prototype.vertex = function(x, y) { ); if (this.textureMode === constants.IMAGE && !this.isProcessingVertices) { - if (this._tex !== null) { - if (this._tex.width > 0 && this._tex.height > 0) { - u /= this._tex.width; - v /= this._tex.height; + 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.userFillShader !== undefined || @@ -141,7 +141,7 @@ p5.RendererGL.prototype.vertex = function(x, y) { ) { // Do nothing if user-defined shaders are present } else if ( - this._tex === null && + this.states._tex === null && arguments.length >= 4 ) { // Only throw this warning if custom uv's have been provided @@ -180,9 +180,9 @@ p5.RendererGL.prototype.vertex = function(x, y) { */ p5.RendererGL.prototype.normal = function(xorv, y, z) { if (xorv instanceof p5.Vector) { - this._currentNormal = xorv; + this.states._currentNormal = xorv; } else { - this._currentNormal = new p5.Vector(xorv, y, z); + this.states._currentNormal = new p5.Vector(xorv, y, z); } return this; @@ -522,7 +522,7 @@ p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { shader.disableRemainingAttributes(); this._applyColorBlend( - this.curFillColor, + this.states.curFillColor, this.immediateMode.geometry.hasFillTransparency() ); @@ -567,7 +567,7 @@ p5.RendererGL.prototype._drawImmediateStroke = function() { } shader.disableRemainingAttributes(); this._applyColorBlend( - this.curStrokeColor, + this.states.curStrokeColor, this.immediateMode.geometry.hasFillTransparency() ); diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index dffe69bf1b..12e0388fe7 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -144,7 +144,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); } this._applyColorBlend( - this.curFillColor, + this.states.curFillColor, geometry.model.hasFillTransparency() ); this._drawElements(gl.TRIANGLES, gId); @@ -160,7 +160,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { } strokeShader.disableRemainingAttributes(); this._applyColorBlend( - this.curStrokeColor, + this.states.curStrokeColor, geometry.model.hasStrokeTransparency() ); this._drawArrays(gl.TRIANGLES, gId); @@ -195,14 +195,14 @@ p5.RendererGL.prototype.drawBuffersScaled = function( scaleY, scaleZ ) { - let originalModelMatrix = this.uModelMatrix.copy(); + let originalModelMatrix = this.states.uModelMatrix.copy(); try { - this.uModelMatrix.scale(scaleX, scaleY, scaleZ); + this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); this.drawBuffers(gId); } finally { - this.uModelMatrix = originalModelMatrix; + this.states.uModelMatrix = originalModelMatrix; } }; p5.RendererGL.prototype._drawArrays = function(drawMode, gId) { @@ -259,7 +259,7 @@ p5.RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { pointShader.enableAttrib(pointShader.attributes.aPosition, 3); - this._applyColorBlend(this.curStrokeColor); + this._applyColorBlend(this.states.curStrokeColor); gl.drawArrays(gl.Points, 0, vertices.length); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 067d6dbf18..a3c48e963b 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -309,8 +309,8 @@ p5.prototype.setAttributes = function (key, value) { this._renderer._resetContext(); this.pop(); - if (this._renderer._curCamera) { - this._renderer._curCamera._renderer = this._renderer; + if (this._renderer.states.curCamera) { + this._renderer.states.curCamera._renderer = this._renderer; } }; /** @@ -536,8 +536,8 @@ p5.RendererGL = class RendererGL extends Renderer { this.registerEnabled = new Set(); // Camera - this._curCamera._computeCameraDefaultSettings(); - this._curCamera._setDefaultCamera(); + this.states.curCamera._computeCameraDefaultSettings(); + this.states.curCamera._setDefaultCamera(); // FilterCamera this.filterCamera = new p5.Camera(this); @@ -645,7 +645,7 @@ p5.RendererGL = class RendererGL extends Renderer { // default wrap settings this.textureWrapX = constants.CLAMP; this.textureWrapY = constants.CLAMP; - this._tex = null; + this.states._tex = null; this._curveTightness = 6; // lookUpTable for coefficients needed to be calculated for bezierVertex, same are used for curveVertex @@ -683,8 +683,8 @@ p5.RendererGL = class RendererGL extends Renderer { 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.curFillColor]; - this.curFillColor = [-1, -1, -1, -1]; + this.geometryBuilder.prevFillColor = [...this.states.curFillColor]; + this.states.curFillColor = [-1, -1, -1, -1]; } /** @@ -700,7 +700,7 @@ p5.RendererGL = class RendererGL extends Renderer { throw new Error('Make sure you call beginGeometry() before endGeometry()!'); } const geometry = this.geometryBuilder.finish(); - this.curFillColor = this.geometryBuilder.prevFillColor; + this.states.curFillColor = this.geometryBuilder.prevFillColor; this.geometryBuilder = undefined; return geometry; } @@ -879,28 +879,28 @@ p5.RendererGL = class RendererGL extends Renderer { _update() { // reset model view and apply initial camera transform // (containing only look at info; no projection). - this.uModelMatrix.reset(); - this.uViewMatrix.set(this._curCamera.cameraMatrix); + this.states.uModelMatrix.reset(); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); // reset light data for new frame. - this.ambientLightColors.length = 0; - this.specularColors = [1, 1, 1]; + this.states.ambientLightColors.length = 0; + this.states.specularColors = [1, 1, 1]; - this.directionalLightDirections.length = 0; - this.directionalLightDiffuseColors.length = 0; - this.directionalLightSpecularColors.length = 0; + this.states.directionalLightDirections.length = 0; + this.states.directionalLightDiffuseColors.length = 0; + this.states.directionalLightSpecularColors.length = 0; - this.pointLightPositions.length = 0; - this.pointLightDiffuseColors.length = 0; - this.pointLightSpecularColors.length = 0; + this.states.pointLightPositions.length = 0; + this.states.pointLightDiffuseColors.length = 0; + this.states.pointLightSpecularColors.length = 0; - this.spotLightPositions.length = 0; - this.spotLightDirections.length = 0; - this.spotLightDiffuseColors.length = 0; - this.spotLightSpecularColors.length = 0; - this.spotLightAngle.length = 0; - this.spotLightConc.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._enableLighting = false; @@ -961,10 +961,10 @@ p5.RendererGL = class RendererGL extends Renderer { 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.curFillColor = color._array; - this.drawMode = constants.FILL; - this._useNormalMaterial = false; - this._tex = null; + this.states.curFillColor = color._array; + this.states.drawMode = constants.FILL; + this.states._useNormalMaterial = false; + this.states._tex = null; } /** @@ -998,7 +998,7 @@ p5.RendererGL = class RendererGL extends Renderer { */ stroke(r, g, b, a) { const color = p5.prototype.color.apply(this._pInst, arguments); - this.curStrokeColor = color._array; + this.states.curStrokeColor = color._array; } strokeCap(cap) { @@ -1177,7 +1177,7 @@ p5.RendererGL = class RendererGL extends Renderer { mode === constants.MULTIPLY || mode === constants.REMOVE ) - this.curBlendMode = mode; + this.states.curBlendMode = mode; else if ( mode === constants.BURN || mode === constants.OVERLAY || @@ -1193,23 +1193,23 @@ p5.RendererGL = class RendererGL extends Renderer { erase(opacityFill, opacityStroke) { if (!this._isErasing) { - this.preEraseBlend = this.curBlendMode; + this.preEraseBlend = this.states.curBlendMode; this._isErasing = true; this.blendMode(constants.REMOVE); - this._cachedFillStyle = this.curFillColor.slice(); - this.curFillColor = [1, 1, 1, opacityFill / 255]; - this._cachedStrokeStyle = this.curStrokeColor.slice(); - this.curStrokeColor = [1, 1, 1, opacityStroke / 255]; + 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.curFillColor = this._cachedFillStyle.slice(); - this.curStrokeColor = this._cachedStrokeStyle.slice(); + this.states.curFillColor = this._cachedFillStyle.slice(); + this.states.curStrokeColor = this._cachedStrokeStyle.slice(); // Restore blend mode - this.curBlendMode = this.preEraseBlend; + this.states.curBlendMode = this.preEraseBlend; this.blendMode(this.preEraseBlend); // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode this._isErasing = false; @@ -1443,7 +1443,7 @@ p5.RendererGL = class RendererGL extends Renderer { this._origViewport.height ); - this._curCamera._resize(); + this.states.curCamera._resize(); //resize pixels buffer const pixelsState = this._pixelsState; @@ -1509,9 +1509,9 @@ p5.RendererGL = class RendererGL extends Renderer { applyMatrix(a, b, c, d, e, f) { if (arguments.length === 16) { - p5.Matrix.prototype.apply.apply(this.uModelMatrix, arguments); + p5.Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); } else { - this.uModelMatrix.apply([ + this.states.uModelMatrix.apply([ a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, @@ -1535,7 +1535,7 @@ p5.RendererGL = class RendererGL extends Renderer { y = x.y; x = x.x; } - this.uModelMatrix.translate([x, y, z]); + this.states.uModelMatrix.translate([x, y, z]); return this; } @@ -1548,7 +1548,7 @@ p5.RendererGL = class RendererGL extends Renderer { * @chainable */ scale(x, y, z) { - this.uModelMatrix.scale(x, y, z); + this.states.uModelMatrix.scale(x, y, z); return this; } @@ -1556,7 +1556,7 @@ p5.RendererGL = class RendererGL extends Renderer { if (typeof axis === 'undefined') { return this.rotateZ(rad); } - p5.Matrix.prototype.rotate.apply(this.uModelMatrix, arguments); + p5.Matrix.prototype.rotate.apply(this.states.uModelMatrix, arguments); return this; } @@ -1575,78 +1575,6 @@ p5.RendererGL = class RendererGL extends Renderer { return this; } - push() { - // get the base renderer style - const style = super.push(); - - // add webgl-specific style properties - const properties = style; - - properties.uModelMatrix = this.uModelMatrix.copy(); - properties.uViewMatrix = this.uViewMatrix.copy(); - properties.uPMatrix = this.uPMatrix.copy(); - properties._curCamera = this._curCamera; - - // make a copy of the current camera for the push state - // this preserves any references stored using 'createCamera' - this._curCamera = this._curCamera.copy(); - - properties.ambientLightColors = this.ambientLightColors.slice(); - properties.specularColors = this.specularColors.slice(); - - properties.directionalLightDirections = - this.directionalLightDirections.slice(); - properties.directionalLightDiffuseColors = - this.directionalLightDiffuseColors.slice(); - properties.directionalLightSpecularColors = - this.directionalLightSpecularColors.slice(); - - properties.pointLightPositions = this.pointLightPositions.slice(); - properties.pointLightDiffuseColors = this.pointLightDiffuseColors.slice(); - properties.pointLightSpecularColors = this.pointLightSpecularColors.slice(); - - properties.spotLightPositions = this.spotLightPositions.slice(); - properties.spotLightDirections = this.spotLightDirections.slice(); - properties.spotLightDiffuseColors = this.spotLightDiffuseColors.slice(); - properties.spotLightSpecularColors = this.spotLightSpecularColors.slice(); - properties.spotLightAngle = this.spotLightAngle.slice(); - properties.spotLightConc = this.spotLightConc.slice(); - - properties.userFillShader = this.userFillShader; - properties.userStrokeShader = this.userStrokeShader; - properties.userPointShader = this.userPointShader; - - properties.pointSize = this.pointSize; - properties.curStrokeWeight = this.curStrokeWeight; - properties.curStrokeColor = this.curStrokeColor; - properties.curFillColor = this.curFillColor; - properties.curAmbientColor = this.curAmbientColor; - properties.curSpecularColor = this.curSpecularColor; - properties.curEmissiveColor = this.curEmissiveColor; - - properties._hasSetAmbient = this._hasSetAmbient; - properties._useSpecularMaterial = this._useSpecularMaterial; - properties._useEmissiveMaterial = this._useEmissiveMaterial; - properties._useShininess = this._useShininess; - properties._useMetalness = this._useMetalness; - - properties.constantAttenuation = this.constantAttenuation; - properties.linearAttenuation = this.linearAttenuation; - properties.quadraticAttenuation = this.quadraticAttenuation; - - properties._enableLighting = this._enableLighting; - properties._useNormalMaterial = this._useNormalMaterial; - properties._tex = this._tex; - properties.drawMode = this.drawMode; - - properties._currentNormal = this._currentNormal; - properties.curBlendMode = this.curBlendMode; - - // So that the activeImageLight gets reset in push/pop - properties.activeImageLight = this.activeImageLight; - - return style; - } pop(...args) { if ( this._clipDepths.length > 0 && @@ -1670,8 +1598,8 @@ p5.RendererGL = class RendererGL extends Renderer { } } resetMatrix() { - this.uModelMatrix.reset(); - this.uViewMatrix.set(this._curCamera.cameraMatrix); + this.states.uModelMatrix.reset(); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); return this; } @@ -1705,11 +1633,11 @@ p5.RendererGL = class RendererGL extends Renderer { sphereMapping ); } - this.uNMatrix.inverseTranspose(this.uMVMatrix); - this.uNMatrix.invert3x3(this.uNMatrix); - this.sphereMapping.setUniform('uFovY', this._curCamera.cameraFOV); - this.sphereMapping.setUniform('uAspect', this._curCamera.aspectRatio); - this.sphereMapping.setUniform('uNewNormalMatrix', this.uNMatrix.mat3); + this.states.uNMatrix.inverseTranspose(this.states.uMVMatrix); + 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; } @@ -1720,7 +1648,7 @@ p5.RendererGL = class RendererGL extends Renderer { */ _getImmediateFillShader() { const fill = this.userFillShader; - if (this._useNormalMaterial) { + if (this.states._useNormalMaterial) { if (!fill || !fill.isNormalShader()) { return this._getNormalShader(); } @@ -1729,7 +1657,7 @@ p5.RendererGL = class RendererGL extends Renderer { if (!fill || !fill.isLightShader()) { return this._getLightShader(); } - } else if (this._tex) { + } else if (this.states._tex) { if (!fill || !fill.isTextureShader()) { return this._getLightShader(); } @@ -1744,7 +1672,7 @@ p5.RendererGL = class RendererGL extends Renderer { * for retained mode. */ _getRetainedFillShader() { - if (this._useNormalMaterial) { + if (this.states._useNormalMaterial) { return this._getNormalShader(); } @@ -1753,7 +1681,7 @@ p5.RendererGL = class RendererGL extends Renderer { if (!fill || !fill.isLightShader()) { return this._getLightShader(); } - } else if (this._tex) { + } else if (this.states._tex) { if (!fill || !fill.isTextureShader()) { return this._getLightShader(); } @@ -2044,7 +1972,7 @@ p5.RendererGL = class RendererGL extends Renderer { // set the uniform values strokeShader.setUniform('uUseLineColor', this._useLineColor); - strokeShader.setUniform('uMaterialColor', this.curStrokeColor); + strokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); strokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); strokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); strokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); @@ -2053,90 +1981,90 @@ p5.RendererGL = class RendererGL extends Renderer { _setFillUniforms(fillShader) { fillShader.bindShader(); - this.mixedSpecularColor = [...this.curSpecularColor]; + this.mixedSpecularColor = [...this.states.curSpecularColor]; - if (this._useMetalness > 0) { + if (this.states._useMetalness > 0) { this.mixedSpecularColor = this.mixedSpecularColor.map( (mixedSpecularColor, index) => - this.curFillColor[index] * this._useMetalness + - mixedSpecularColor * (1 - this._useMetalness) + this.states.curFillColor[index] * this.states._useMetalness + + mixedSpecularColor * (1 - this.states._useMetalness) ); } // TODO: optimize fillShader.setUniform('uUseVertexColor', this._useVertexColor); - fillShader.setUniform('uMaterialColor', this.curFillColor); - fillShader.setUniform('isTexture', !!this._tex); - if (this._tex) { - fillShader.setUniform('uSampler', this._tex); + 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._hasSetAmbient); - fillShader.setUniform('uAmbientMatColor', this.curAmbientColor); + fillShader.setUniform('uHasSetAmbient', this.states._hasSetAmbient); + fillShader.setUniform('uAmbientMatColor', this.states.curAmbientColor); fillShader.setUniform('uSpecularMatColor', this.mixedSpecularColor); - fillShader.setUniform('uEmissiveMatColor', this.curEmissiveColor); - fillShader.setUniform('uSpecular', this._useSpecularMaterial); - fillShader.setUniform('uEmissive', this._useEmissiveMaterial); - fillShader.setUniform('uShininess', this._useShininess); - fillShader.setUniform('metallic', this._useMetalness); + 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('metallic', this.states._useMetalness); this._setImageLightUniforms(fillShader); fillShader.setUniform('uUseLighting', this._enableLighting); - const pointLightCount = this.pointLightDiffuseColors.length / 3; + const pointLightCount = this.states.pointLightDiffuseColors.length / 3; fillShader.setUniform('uPointLightCount', pointLightCount); - fillShader.setUniform('uPointLightLocation', this.pointLightPositions); + fillShader.setUniform('uPointLightLocation', this.states.pointLightPositions); fillShader.setUniform( 'uPointLightDiffuseColors', - this.pointLightDiffuseColors + this.states.pointLightDiffuseColors ); fillShader.setUniform( 'uPointLightSpecularColors', - this.pointLightSpecularColors + this.states.pointLightSpecularColors ); - const directionalLightCount = this.directionalLightDiffuseColors.length / 3; + const directionalLightCount = this.states.directionalLightDiffuseColors.length / 3; fillShader.setUniform('uDirectionalLightCount', directionalLightCount); - fillShader.setUniform('uLightingDirection', this.directionalLightDirections); + fillShader.setUniform('uLightingDirection', this.states.directionalLightDirections); fillShader.setUniform( 'uDirectionalDiffuseColors', - this.directionalLightDiffuseColors + this.states.directionalLightDiffuseColors ); fillShader.setUniform( 'uDirectionalSpecularColors', - this.directionalLightSpecularColors + this.states.directionalLightSpecularColors ); // TODO: sum these here... - const ambientLightCount = this.ambientLightColors.length / 3; - this.mixedAmbientLight = [...this.ambientLightColors]; + const ambientLightCount = this.states.ambientLightColors.length / 3; + this.mixedAmbientLight = [...this.states.ambientLightColors]; - if (this._useMetalness > 0) { + if (this.states._useMetalness > 0) { this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors => { - let mixing = ambientColors - this._useMetalness; + let mixing = ambientColors - this.states._useMetalness; return Math.max(0, mixing); })); } fillShader.setUniform('uAmbientLightCount', ambientLightCount); fillShader.setUniform('uAmbientColor', this.mixedAmbientLight); - const spotLightCount = this.spotLightDiffuseColors.length / 3; + const spotLightCount = this.states.spotLightDiffuseColors.length / 3; fillShader.setUniform('uSpotLightCount', spotLightCount); - fillShader.setUniform('uSpotLightAngle', this.spotLightAngle); - fillShader.setUniform('uSpotLightConc', this.spotLightConc); - fillShader.setUniform('uSpotLightDiffuseColors', this.spotLightDiffuseColors); + fillShader.setUniform('uSpotLightAngle', this.states.spotLightAngle); + fillShader.setUniform('uSpotLightConc', this.states.spotLightConc); + fillShader.setUniform('uSpotLightDiffuseColors', this.states.spotLightDiffuseColors); fillShader.setUniform( 'uSpotLightSpecularColors', - this.spotLightSpecularColors + this.states.spotLightSpecularColors ); - fillShader.setUniform('uSpotLightLocation', this.spotLightPositions); - fillShader.setUniform('uSpotLightDirection', this.spotLightDirections); + fillShader.setUniform('uSpotLightLocation', this.states.spotLightPositions); + fillShader.setUniform('uSpotLightDirection', this.states.spotLightDirections); - fillShader.setUniform('uConstantAttenuation', this.constantAttenuation); - fillShader.setUniform('uLinearAttenuation', this.linearAttenuation); - fillShader.setUniform('uQuadraticAttenuation', this.quadraticAttenuation); + fillShader.setUniform('uConstantAttenuation', this.states.constantAttenuation); + fillShader.setUniform('uLinearAttenuation', this.states.linearAttenuation); + fillShader.setUniform('uQuadraticAttenuation', this.states.quadraticAttenuation); fillShader.bindTextures(); } @@ -2144,21 +2072,21 @@ p5.RendererGL = class RendererGL extends Renderer { // getting called from _setFillUniforms _setImageLightUniforms(shader) { //set uniform values - shader.setUniform('uUseImageLight', this.activeImageLight != null); + shader.setUniform('uUseImageLight', this.states.activeImageLight != null); // true - if (this.activeImageLight) { - // this.activeImageLight has image as a key + 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.activeImageLight); + let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); shader.setUniform('environmentMapDiffused', diffusedLight); - let specularLight = this.getSpecularTexture(this.activeImageLight); + let specularLight = this.getSpecularTexture(this.states.activeImageLight); // In p5js the range of shininess is >= 1, // Therefore roughness range will be ([0,1]*8)*20 or [0, 160] // The factor of 8 is because currently the getSpecularTexture // only calculated 8 different levels of roughness // The factor of 20 is just to spread up this range so that, // [1, max] of shininess is converted to [0,160] of roughness - let roughness = 20 / this._useShininess; + let roughness = 20 / this.states._useShininess; shader.setUniform('levelOfDetail', roughness * 8); shader.setUniform('environmentMapSpecular', specularLight); } @@ -2168,7 +2096,7 @@ p5.RendererGL = class RendererGL extends Renderer { pointShader.bindShader(); // set the uniform values - pointShader.setUniform('uMaterialColor', this.curStrokeColor); + 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( diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 8333c92ac5..369b490538 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -570,17 +570,17 @@ p5.Shader = class Shader { } _setMatrixUniforms() { - const modelMatrix = this._renderer.uModelMatrix; - const viewMatrix = this._renderer.uViewMatrix; - const projectionMatrix = this._renderer.uPMatrix; + 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.uMVMatrix = modelViewMatrix; + this._renderer.states.uMVMatrix = modelViewMatrix; const modelViewProjectionMatrix = modelViewMatrix.copy(); modelViewProjectionMatrix.mult(projectionMatrix); if (this.isStrokeShader()) { - this.setUniform('uPerspective', this._renderer._curCamera.useLinePerspective ? 1 : 0); + this.setUniform('uPerspective', this._renderer.states.curCamera.useLinePerspective ? 1 : 0); } this.setUniform('uViewMatrix', viewMatrix.mat4); this.setUniform('uProjectionMatrix', projectionMatrix.mat4); @@ -591,12 +591,12 @@ p5.Shader = class Shader { modelViewProjectionMatrix.mat4 ); if (this.uniforms.uNormalMatrix) { - this._renderer.uNMatrix.inverseTranspose(this._renderer.uMVMatrix); - this.setUniform('uNormalMatrix', this._renderer.uNMatrix.mat3); + this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); + this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); } if (this.uniforms.uCameraRotation) { - this._renderer.curMatrix.inverseTranspose(this._renderer.uViewMatrix); - this.setUniform('uCameraRotation', this._renderer.curMatrix.mat3); + this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); + this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); } } diff --git a/src/webgl/text.js b/src/webgl/text.js index 00ac7d037a..63eede213c 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -655,10 +655,10 @@ p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { // remember this state, so it can be restored later const doStroke = this.states.doStroke; - const drawMode = this.drawMode; + const drawMode = this.states.drawMode; this.states.doStroke = false; - this.drawMode = constants.TEXTURE; + this.states.drawMode = constants.TEXTURE; // get the cached FontInfo object const font = this._textFont.font; @@ -688,7 +688,7 @@ p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { sh.setUniform('uStrokeImageSize', [strokeImageWidth, strokeImageHeight]); sh.setUniform('uGridSize', [charGridWidth, charGridHeight]); } - this._applyColorBlend(this.curFillColor); + this._applyColorBlend(this.states.curFillColor); let g = this.retainedMode.geometry['glyph']; if (!g) { @@ -712,7 +712,7 @@ p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { this._bindBuffer(g.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); // this will have to do for now... - sh.setUniform('uMaterialColor', this.curFillColor); + sh.setUniform('uMaterialColor', this.states.curFillColor); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); try { @@ -751,7 +751,7 @@ p5.RendererGL.prototype._renderText = function(p, line, x, y, maxY) { sh.unbindShader(); this.states.doStroke = doStroke; - this.drawMode = drawMode; + this.states.drawMode = drawMode; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); p.pop(); diff --git a/test/unit/color/setting.js b/test/unit/color/setting.js index 08d97f9208..6bf5dbcfc6 100644 --- a/test/unit/color/setting.js +++ b/test/unit/color/setting.js @@ -86,14 +86,14 @@ suite('color/Setting', function() { test.todo('should cache renderer fill', function() { my3D.fill(255, 0, 0); - const curFillColor = my3D._renderer.curFillColor; + const curFillColor = my3D._renderer.states.curFillColor; my3D.erase(); assert.deepEqual(my3D._renderer._cachedFillStyle, curFillColor); }); test.todo('should cache renderer stroke', function() { my3D.stroke(255, 0, 0); - const strokeStyle = my3D._renderer.curStrokeColor; + const strokeStyle = my3D._renderer.states.curStrokeColor; my3D.erase(); assert.deepEqual(my3D._renderer._cachedStrokeStyle, strokeStyle); }); @@ -106,18 +106,18 @@ suite('color/Setting', function() { test('should set fill strength', function() { my3D.erase(125); - assert.deepEqual(my3D._renderer.curFillColor, [1, 1, 1, 125 / 255]); + assert.deepEqual(my3D._renderer.states.curFillColor, [1, 1, 1, 125 / 255]); }); test('should set stroke strength', function() { my3D.erase(255, 50); - assert.deepEqual(my3D._renderer.curStrokeColor, [1, 1, 1, 50 / 255]); + assert.deepEqual(my3D._renderer.states.curStrokeColor, [1, 1, 1, 50 / 255]); }); test('should set default values when no arguments', function() { my3D.erase(); - assert.deepEqual(my3D._renderer.curFillColor, [1, 1, 1, 1]); - assert.deepEqual(my3D._renderer.curStrokeColor, [1, 1, 1, 1]); + assert.deepEqual(my3D._renderer.states.curFillColor, [1, 1, 1, 1]); + assert.deepEqual(my3D._renderer.states.curStrokeColor, [1, 1, 1, 1]); }); }); @@ -158,7 +158,7 @@ suite('color/Setting', function() { test.todo('should restore cached renderer fill', function() { my3D.fill(255, 0, 0); - const fillStyle = my3D._renderer.curFillColor.slice(); + const fillStyle = my3D._renderer.states.curFillColor.slice(); my3D.erase(); my3D.noErase(); assert.deepEqual([1, 0, 0, 1], fillStyle); @@ -166,7 +166,7 @@ suite('color/Setting', function() { test.todo('should restore cached renderer stroke', function() { my3D.stroke(255, 0, 0); - const strokeStyle = my3D._renderer.curStrokeColor.slice(); + const strokeStyle = my3D._renderer.states.curStrokeColor.slice(); my3D.erase(); my3D.noErase(); assert.deepEqual([1, 0, 0, 1], strokeStyle); diff --git a/test/unit/webgl/light.js b/test/unit/webgl/light.js index dacb9a7886..3f8785a5c9 100644 --- a/test/unit/webgl/light.js +++ b/test/unit/webgl/light.js @@ -17,25 +17,25 @@ suite('light', function() { suite('Light', function() { test('lightFalloff is initialised and set properly', function() { - assert.deepEqual(myp5._renderer.constantAttenuation, 1); - assert.deepEqual(myp5._renderer.linearAttenuation, 0); - assert.deepEqual(myp5._renderer.quadraticAttenuation, 0); + assert.deepEqual(myp5._renderer.states.constantAttenuation, 1); + assert.deepEqual(myp5._renderer.states.linearAttenuation, 0); + assert.deepEqual(myp5._renderer.states.quadraticAttenuation, 0); myp5.lightFalloff(2, 3, 4); - assert.deepEqual(myp5._renderer.constantAttenuation, 2); - assert.deepEqual(myp5._renderer.linearAttenuation, 3); - assert.deepEqual(myp5._renderer.quadraticAttenuation, 4); + assert.deepEqual(myp5._renderer.states.constantAttenuation, 2); + assert.deepEqual(myp5._renderer.states.linearAttenuation, 3); + assert.deepEqual(myp5._renderer.states.quadraticAttenuation, 4); }); test('specularColor is initialised and set properly', function() { - assert.deepEqual(myp5._renderer.specularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.pointLightSpecularColors, []); - assert.deepEqual(myp5._renderer.directionalLightSpecularColors, []); + assert.deepEqual(myp5._renderer.states.specularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.pointLightSpecularColors, []); + assert.deepEqual(myp5._renderer.states.directionalLightSpecularColors, []); myp5.specularColor(255, 0, 0); - assert.deepEqual(myp5._renderer.specularColors, [1, 0, 0]); + assert.deepEqual(myp5._renderer.states.specularColors, [1, 0, 0]); myp5.pointLight(255, 0, 0, 1, 0, 0); myp5.directionalLight(255, 0, 0, 0, 0, 0); - assert.deepEqual(myp5._renderer.pointLightSpecularColors, [1, 0, 0]); - assert.deepEqual(myp5._renderer.directionalLightSpecularColors, [ + assert.deepEqual(myp5._renderer.states.pointLightSpecularColors, [1, 0, 0]); + assert.deepEqual(myp5._renderer.states.directionalLightSpecularColors, [ 1, 0, 0 @@ -50,19 +50,19 @@ suite('light', function() { myp5.shininess(50); myp5.noLights(); - assert.deepEqual([], myp5._renderer.ambientLightColors); - assert.deepEqual([], myp5._renderer.pointLightDiffuseColors); - assert.deepEqual([], myp5._renderer.pointLightSpecularColors); - assert.deepEqual([], myp5._renderer.pointLightPositions); - assert.deepEqual([], myp5._renderer.directionalLightDiffuseColors); - assert.deepEqual([], myp5._renderer.directionalLightSpecularColors); - assert.deepEqual([], myp5._renderer.directionalLightDirections); - assert.deepEqual([1, 1, 1], myp5._renderer.specularColors); - assert.deepEqual([], myp5._renderer.spotLightDiffuseColors); - assert.deepEqual([], myp5._renderer.spotLightSpecularColors); - assert.deepEqual([], myp5._renderer.spotLightPositions); - assert.deepEqual([], myp5._renderer.spotLightDirections); - assert.deepEqual(1, myp5._renderer._useShininess); + assert.deepEqual([], myp5._renderer.states.ambientLightColors); + assert.deepEqual([], myp5._renderer.states.pointLightDiffuseColors); + assert.deepEqual([], myp5._renderer.states.pointLightSpecularColors); + assert.deepEqual([], myp5._renderer.states.pointLightPositions); + assert.deepEqual([], myp5._renderer.states.directionalLightDiffuseColors); + assert.deepEqual([], myp5._renderer.states.directionalLightSpecularColors); + assert.deepEqual([], myp5._renderer.states.directionalLightDirections); + assert.deepEqual([1, 1, 1], myp5._renderer.states.specularColors); + assert.deepEqual([], myp5._renderer.states.spotLightDiffuseColors); + assert.deepEqual([], myp5._renderer.states.spotLightSpecularColors); + assert.deepEqual([], myp5._renderer.states.spotLightPositions); + assert.deepEqual([], myp5._renderer.states.spotLightDirections); + assert.deepEqual(1, myp5._renderer.states._useShininess); }); }); @@ -73,264 +73,264 @@ suite('light', function() { let conc = 7; let defaultConc = 100; test('default', function() { - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, []); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, []); - assert.deepEqual(myp5._renderer.spotLightPositions, []); - assert.deepEqual(myp5._renderer.spotLightDirections, []); - assert.deepEqual(myp5._renderer.spotLightAngle, []); - assert.deepEqual(myp5._renderer.spotLightConc, []); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, []); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, []); + assert.deepEqual(myp5._renderer.states.spotLightPositions, []); + assert.deepEqual(myp5._renderer.states.spotLightDirections, []); + assert.deepEqual(myp5._renderer.states.spotLightAngle, []); + assert.deepEqual(myp5._renderer.states.spotLightConc, []); }); test('color,positions,directions', function() { let color = myp5.color(255, 0, 255); let positions = new p5.Vector(1, 2, 3); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(color, positions, directions); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,positions,directions,angle', function() { let color = myp5.color(255, 0, 255); let positions = new p5.Vector(1, 2, 3); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(color, positions, directions, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,positions,directions,angle,conc', function() { let color = myp5.color(255, 0, 255); let positions = new p5.Vector(1, 2, 3); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(color, positions, directions, angle, conc); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [conc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [conc]); }); test('c1,c2,c3,positions,directions', function() { let positions = new p5.Vector(1, 2, 3); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(255, 0, 255, positions, directions); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,p1,p2,p3,directions', function() { let color = myp5.color(255, 0, 255); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(color, 1, 2, 3, directions); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,positions,r1,r2,r3', function() { let color = myp5.color(255, 0, 255); let positions = new p5.Vector(1, 2, 3); myp5.spotLight(color, positions, 0, 1, 0); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,positions,directions,angle', function() { let positions = new p5.Vector(1, 2, 3); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(255, 0, 255, positions, directions, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,p1,p2,p3,directions,angle', function() { let color = myp5.color(255, 0, 255); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(color, 1, 2, 3, directions, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,positions,r1,r2,r3,angle', function() { let color = myp5.color(255, 0, 255); let positions = new p5.Vector(1, 2, 3); myp5.spotLight(color, positions, 0, 1, 0, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,positions,directions,angle,conc', function() { let positions = new p5.Vector(1, 2, 3); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(255, 0, 255, positions, directions, angle, conc); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [7]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [7]); }); test('color,p1,p2,p3,directions,angle,conc', function() { let color = myp5.color(255, 0, 255); let directions = new p5.Vector(0, 1, 0); myp5.spotLight(color, 1, 2, 3, directions, angle, conc); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [7]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [7]); }); test('color,positions,r1,r2,r3,angle,conc', function() { let color = myp5.color(255, 0, 255); let positions = new p5.Vector(1, 2, 3); myp5.spotLight(color, positions, 0, 1, 0, angle, conc); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [7]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [7]); }); test('c1,c2,c3,p1,p2,p3,directions', function() { let directions = new p5.Vector(0, 1, 0); myp5.spotLight(255, 0, 255, 1, 2, 3, directions); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,positions,r1,r2,r3', function() { let positions = new p5.Vector(1, 2, 3); myp5.spotLight(255, 0, 255, positions, 0, 1, 0); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,p1,p2,p3,r1,r2,r3', function() { let color = myp5.color(255, 0, 255); myp5.spotLight(color, 1, 2, 3, 0, 1, 0); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,p1,p2,p3,directions,angle', function() { let directions = new p5.Vector(0, 1, 0); myp5.spotLight(255, 0, 255, 1, 2, 3, directions, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,positions,r1,r2,r3,angle', function() { let positions = new p5.Vector(1, 2, 3); myp5.spotLight(255, 0, 255, positions, 0, 1, 0, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('color,p1,p2,p3,r1,r2,r3,angle', function() { let color = myp5.color(255, 0, 255); myp5.spotLight(color, 1, 2, 3, 0, 1, 0, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,p1,p2,p3,directions,angle,conc', function() { let directions = new p5.Vector(0, 1, 0); myp5.spotLight(255, 0, 255, 1, 2, 3, directions, angle, conc); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [7]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [7]); }); test('c1,c2,c3,positions,r1,r2,r3,angle,conc', function() { let positions = new p5.Vector(1, 2, 3); myp5.spotLight(255, 0, 255, positions, 0, 1, 0, angle, conc); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [7]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [7]); }); test('color,p1,p2,p3,r1,r2,r3,angle,conc', function() { let color = myp5.color(255, 0, 255); myp5.spotLight(color, 1, 2, 3, 0, 1, 0, angle, conc); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [7]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [7]); }); test('c1,c2,c3,p1,p2,p3,r1,r2,r3', function() { myp5.spotLight(255, 0, 255, 1, 2, 3, 0, 1, 0); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [defaultAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [defaultAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,p1,p2,p3,r1,r2,r3,angle', function() { myp5.spotLight(255, 0, 255, 1, 2, 3, 0, 1, 0, angle); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [defaultConc]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [defaultConc]); }); test('c1,c2,c3,p1,p2,p3,r1,r2,r3,angle,conc', function() { myp5.spotLight(255, 0, 255, 1, 2, 3, 0, 1, 0, angle, 7); - assert.deepEqual(myp5._renderer.spotLightDiffuseColors, [1, 0, 1]); - assert.deepEqual(myp5._renderer.spotLightSpecularColors, [1, 1, 1]); - assert.deepEqual(myp5._renderer.spotLightPositions, [1, 2, 3]); - assert.deepEqual(myp5._renderer.spotLightDirections, [0, 1, 0]); - assert.deepEqual(myp5._renderer.spotLightAngle, [cosAngle]); - assert.deepEqual(myp5._renderer.spotLightConc, [7]); + assert.deepEqual(myp5._renderer.states.spotLightDiffuseColors, [1, 0, 1]); + assert.deepEqual(myp5._renderer.states.spotLightSpecularColors, [1, 1, 1]); + assert.deepEqual(myp5._renderer.states.spotLightPositions, [1, 2, 3]); + assert.deepEqual(myp5._renderer.states.spotLightDirections, [0, 1, 0]); + assert.deepEqual(myp5._renderer.states.spotLightAngle, [cosAngle]); + assert.deepEqual(myp5._renderer.states.spotLightConc, [7]); }); }); }); diff --git a/test/unit/webgl/p5.Camera.js b/test/unit/webgl/p5.Camera.js index 76cde18993..f3e6e24850 100644 --- a/test/unit/webgl/p5.Camera.js +++ b/test/unit/webgl/p5.Camera.js @@ -68,13 +68,13 @@ suite('p5.Camera', function() { test('createCamera does not immediately attach to renderer', function() { var myCam2 = myp5.createCamera(); - assert.notEqual(myCam2, myp5._renderer._curCamera); + assert.notEqual(myCam2, myp5._renderer.states.curCamera); }); test('setCamera() attaches a camera to renderer', function() { var myCam2 = myp5.createCamera(); myp5.setCamera(myCam2); - assert.equal(myCam2, myp5._renderer._curCamera); + assert.equal(myCam2, myp5._renderer.states.curCamera); }); }); @@ -487,11 +487,11 @@ suite('p5.Camera', function() { // the renderer's matrix will also change. assert.deepCloseTo( copyCam.cameraMatrix.mat4, - myp5._renderer.uViewMatrix.mat4 + myp5._renderer.states.uViewMatrix.mat4 ); assert.deepCloseTo( copyCam.projMatrix.mat4, - myp5._renderer.uPMatrix.mat4 + myp5._renderer.states.uPMatrix.mat4 ); }); @@ -725,7 +725,7 @@ suite('p5.Camera', function() { test('ortho() sets renderer uPMatrix', function() { myCam.ortho(-10, 10, -10, 10, 0, 100); - assert.deepCloseTo(myCam.projMatrix.mat4, myp5._renderer.uPMatrix.mat4); + assert.deepCloseTo(myCam.projMatrix.mat4, myp5._renderer.states.uPMatrix.mat4); }); test('ortho() sets projection matrix correctly', function() { @@ -765,7 +765,7 @@ suite('p5.Camera', function() { test('perspective() sets renderer uPMatrix', function() { myCam.perspective(Math.PI / 3.0, 1, 1, 100); - assert.deepCloseTo(myCam.projMatrix.mat4, myp5._renderer.uPMatrix.mat4); + assert.deepCloseTo(myCam.projMatrix.mat4, myp5._renderer.states.uPMatrix.mat4); }); test('perspective() sets projection matrix correctly', function() { var expectedMatrix = new Float32Array([ @@ -802,7 +802,7 @@ suite('p5.Camera', function() { test('frustum() sets renderer uPMatrix', function() { myCam.frustum(-10, 10, -20, 20, -100, 100); - assert.deepCloseTo(myCam.projMatrix.mat4, myp5._renderer.uPMatrix.mat4); + assert.deepCloseTo(myCam.projMatrix.mat4, myp5._renderer.states.uPMatrix.mat4); }); test('frustum() sets projection matrix correctly', function() { /* eslint-disable indent */ @@ -1111,15 +1111,15 @@ suite('p5.Camera', function() { var myCam2 = myp5.createCamera(); var myCam3 = myp5.createCamera(); myp5.setCamera(myCam2); - assert.deepCloseTo(myCam2, myp5._renderer._curCamera); + assert.deepCloseTo(myCam2, myp5._renderer.states.curCamera); myp5.setCamera(myCam3); - assert.deepCloseTo(myCam3, myp5._renderer._curCamera); + assert.deepCloseTo(myCam3, myp5._renderer.states.curCamera); myp5.setCamera(myCam); - assert.deepCloseTo(myCam, myp5._renderer._curCamera); + assert.deepCloseTo(myCam, myp5._renderer.states.curCamera); }); test("Camera's Renderer is correctly set after setAttributes", function() { var myCam2 = myp5.createCamera(); - assert.deepCloseTo(myCam2, myp5._renderer._curCamera); + assert.deepCloseTo(myCam2, myp5._renderer.states.curCamera); myp5.setAttributes('antialias', true); assert.deepCloseTo(myCam2._renderer, myp5._renderer); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 86f9e13bf6..42cf9157dd 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -605,8 +605,8 @@ suite('p5.RendererGL', function() { suite('push() and pop() work in WEBGL Mode', function() { test('push/pop and translation works as expected in WEBGL Mode', function() { myp5.createCanvas(100, 100, myp5.WEBGL); - var modelMatrixBefore = myp5._renderer.uModelMatrix.copy(); - var viewMatrixBefore = myp5._renderer.uViewMatrix.copy(); + var modelMatrixBefore = myp5._renderer.states.uModelMatrix.copy(); + var viewMatrixBefore = myp5._renderer.states.uViewMatrix.copy(); myp5.push(); // Change view @@ -616,52 +616,52 @@ suite('p5.RendererGL', function() { myp5.translate(20, 100, 5); // Check if the model matrix has changed assert.notDeepEqual(modelMatrixBefore.mat4, - myp5._renderer.uModelMatrix.mat4); + myp5._renderer.states.uModelMatrix.mat4); // Check if the view matrix has changed assert.notDeepEqual(viewMatrixBefore.mat4, - myp5._renderer.uViewMatrix.mat4); + myp5._renderer.states.uViewMatrix.mat4); myp5.pop(); // Check if both the model and view matrices are restored after popping assert.deepEqual(modelMatrixBefore.mat4, - myp5._renderer.uModelMatrix.mat4); - assert.deepEqual(viewMatrixBefore.mat4, myp5._renderer.uViewMatrix.mat4); + myp5._renderer.states.uModelMatrix.mat4); + assert.deepEqual(viewMatrixBefore.mat4, myp5._renderer.states.uViewMatrix.mat4); }); test('push/pop and directionalLight() works', function() { myp5.createCanvas(100, 100, myp5.WEBGL); myp5.directionalLight(255, 0, 0, 0, 0, 0); var dirDiffuseColors = - myp5._renderer.directionalLightDiffuseColors.slice(); + myp5._renderer.states.directionalLightDiffuseColors.slice(); var dirSpecularColors = - myp5._renderer.directionalLightSpecularColors.slice(); + myp5._renderer.states.directionalLightSpecularColors.slice(); var dirLightDirections = - myp5._renderer.directionalLightDirections.slice(); + myp5._renderer.states.directionalLightDirections.slice(); myp5.push(); myp5.directionalLight(0, 0, 255, 0, 10, 5); assert.notEqual( dirDiffuseColors, - myp5._renderer.directionalLightDiffuseColors + myp5._renderer.states.directionalLightDiffuseColors ); assert.notEqual( dirSpecularColors, - myp5._renderer.directionalLightSpecularColors + myp5._renderer.states.directionalLightSpecularColors ); assert.notEqual( dirLightDirections, - myp5._renderer.directionalLightDirections + myp5._renderer.states.directionalLightDirections ); myp5.pop(); assert.deepEqual( dirDiffuseColors, - myp5._renderer.directionalLightDiffuseColors + myp5._renderer.states.directionalLightDiffuseColors ); assert.deepEqual( dirSpecularColors, - myp5._renderer.directionalLightSpecularColors + myp5._renderer.states.directionalLightSpecularColors ); assert.deepEqual( dirLightDirections, - myp5._renderer.directionalLightDirections + myp5._renderer.states.directionalLightDirections ); }); @@ -669,120 +669,120 @@ suite('p5.RendererGL', function() { myp5.createCanvas(100, 100, myp5.WEBGL); myp5.ambientLight(100, 0, 100); myp5.ambientLight(0, 0, 200); - var ambColors = myp5._renderer.ambientLightColors.slice(); + var ambColors = myp5._renderer.states.ambientLightColors.slice(); myp5.push(); myp5.ambientLight(0, 0, 0); - assert.notEqual(ambColors, myp5._renderer.ambientLightColors); + assert.notEqual(ambColors, myp5._renderer.states.ambientLightColors); myp5.pop(); - assert.deepEqual(ambColors, myp5._renderer.ambientLightColors); + assert.deepEqual(ambColors, myp5._renderer.states.ambientLightColors); }); test('push/pop and pointLight() works', function() { myp5.createCanvas(100, 100, myp5.WEBGL); myp5.pointLight(255, 0, 0, 0, 0, 0); - var pointDiffuseColors = myp5._renderer.pointLightDiffuseColors.slice(); - var pointSpecularColors = myp5._renderer.pointLightSpecularColors.slice(); - var pointLocs = myp5._renderer.pointLightPositions.slice(); + var pointDiffuseColors = myp5._renderer.states.pointLightDiffuseColors.slice(); + var pointSpecularColors = myp5._renderer.states.pointLightSpecularColors.slice(); + var pointLocs = myp5._renderer.states.pointLightPositions.slice(); myp5.push(); myp5.pointLight(0, 0, 255, 0, 10, 5); assert.notEqual( pointDiffuseColors, - myp5._renderer.pointLightDiffuseColors + myp5._renderer.states.pointLightDiffuseColors ); assert.notEqual( pointSpecularColors, - myp5._renderer.pointLightSpecularColors + myp5._renderer.states.pointLightSpecularColors ); - assert.notEqual(pointLocs, myp5._renderer.pointLightPositions); + assert.notEqual(pointLocs, myp5._renderer.states.pointLightPositions); myp5.pop(); assert.deepEqual( pointDiffuseColors, - myp5._renderer.pointLightDiffuseColors + myp5._renderer.states.pointLightDiffuseColors ); assert.deepEqual( pointSpecularColors, - myp5._renderer.pointLightSpecularColors + myp5._renderer.states.pointLightSpecularColors ); - assert.deepEqual(pointLocs, myp5._renderer.pointLightPositions); + assert.deepEqual(pointLocs, myp5._renderer.states.pointLightPositions); }); test('push/pop and specularColor() works', function() { myp5.createCanvas(100, 100, myp5.WEBGL); myp5.specularColor(255, 0, 0); - var specularColors = myp5._renderer.specularColors.slice(); + var specularColors = myp5._renderer.states.specularColors.slice(); myp5.push(); myp5.specularColor(0, 0, 255); - assert.notEqual(specularColors, myp5._renderer.specularColors); + assert.notEqual(specularColors, myp5._renderer.states.specularColors); myp5.pop(); - assert.deepEqual(specularColors, myp5._renderer.specularColors); + assert.deepEqual(specularColors, myp5._renderer.states.specularColors); }); test('push/pop and spotLight() works', function() { myp5.createCanvas(100, 100, myp5.WEBGL); myp5.spotLight(255, 0, 255, 1, 2, 3, 0, 1, 0, Math.PI / 4, 7); let spotLightDiffuseColors = - myp5._renderer.spotLightDiffuseColors.slice(); + myp5._renderer.states.spotLightDiffuseColors.slice(); let spotLightSpecularColors = - myp5._renderer.spotLightSpecularColors.slice(); - let spotLightPositions = myp5._renderer.spotLightPositions.slice(); - let spotLightDirections = myp5._renderer.spotLightDirections.slice(); - let spotLightAngle = myp5._renderer.spotLightAngle.slice(); - let spotLightConc = myp5._renderer.spotLightConc.slice(); + myp5._renderer.states.spotLightSpecularColors.slice(); + let spotLightPositions = myp5._renderer.states.spotLightPositions.slice(); + let spotLightDirections = myp5._renderer.states.spotLightDirections.slice(); + let spotLightAngle = myp5._renderer.states.spotLightAngle.slice(); + let spotLightConc = myp5._renderer.states.spotLightConc.slice(); myp5.push(); myp5.spotLight(255, 0, 0, 2, 2, 3, 1, 0, 0, Math.PI / 3, 8); assert.notEqual( spotLightDiffuseColors, - myp5._renderer.spotLightDiffuseColors + myp5._renderer.states.spotLightDiffuseColors ); assert.notEqual( spotLightSpecularColors, - myp5._renderer.spotLightSpecularColors + myp5._renderer.states.spotLightSpecularColors ); - assert.notEqual(spotLightPositions, myp5._renderer.spotLightPositions); - assert.notEqual(spotLightDirections, myp5._renderer.spotLightDirections); - assert.notEqual(spotLightAngle, myp5._renderer.spotLightAngle); - assert.notEqual(spotLightConc, myp5._renderer.spotLightConc); + assert.notEqual(spotLightPositions, myp5._renderer.states.spotLightPositions); + assert.notEqual(spotLightDirections, myp5._renderer.states.spotLightDirections); + assert.notEqual(spotLightAngle, myp5._renderer.states.spotLightAngle); + assert.notEqual(spotLightConc, myp5._renderer.states.spotLightConc); myp5.pop(); assert.deepEqual( spotLightDiffuseColors, - myp5._renderer.spotLightDiffuseColors + myp5._renderer.states.spotLightDiffuseColors ); assert.deepEqual( spotLightSpecularColors, - myp5._renderer.spotLightSpecularColors + myp5._renderer.states.spotLightSpecularColors ); - assert.deepEqual(spotLightPositions, myp5._renderer.spotLightPositions); - assert.deepEqual(spotLightDirections, myp5._renderer.spotLightDirections); - assert.deepEqual(spotLightAngle, myp5._renderer.spotLightAngle); - assert.deepEqual(spotLightConc, myp5._renderer.spotLightConc); + assert.deepEqual(spotLightPositions, myp5._renderer.states.spotLightPositions); + assert.deepEqual(spotLightDirections, myp5._renderer.states.spotLightDirections); + assert.deepEqual(spotLightAngle, myp5._renderer.states.spotLightAngle); + assert.deepEqual(spotLightConc, myp5._renderer.states.spotLightConc); }); test('push/pop and noLights() works', function() { myp5.createCanvas(100, 100, myp5.WEBGL); myp5.ambientLight(0, 0, 200); - var ambColors = myp5._renderer.ambientLightColors.slice(); + var ambColors = myp5._renderer.states.ambientLightColors.slice(); myp5.push(); myp5.ambientLight(0, 200, 0); - var ambPopColors = myp5._renderer.ambientLightColors.slice(); + var ambPopColors = myp5._renderer.states.ambientLightColors.slice(); myp5.noLights(); - assert.notEqual(ambColors, myp5._renderer.ambientLightColors); - assert.notEqual(ambPopColors, myp5._renderer.ambientLightColors); + assert.notEqual(ambColors, myp5._renderer.states.ambientLightColors); + assert.notEqual(ambPopColors, myp5._renderer.states.ambientLightColors); myp5.pop(); - assert.deepEqual(ambColors, myp5._renderer.ambientLightColors); + assert.deepEqual(ambColors, myp5._renderer.states.ambientLightColors); }); test('push/pop and texture() works', function() { myp5.createCanvas(100, 100, myp5.WEBGL); var tex1 = myp5.createGraphics(1, 1); myp5.texture(tex1); - assert.equal(tex1, myp5._renderer._tex); + assert.equal(tex1, myp5._renderer.states._tex); myp5.push(); var tex2 = myp5.createGraphics(2, 2); myp5.texture(tex2); - assert.equal(tex2, myp5._renderer._tex); - assert.notEqual(tex1, myp5._renderer._tex); + assert.equal(tex2, myp5._renderer.states._tex); + assert.notEqual(tex1, myp5._renderer.states._tex); myp5.pop(); - assert.equal(tex1, myp5._renderer._tex); + assert.equal(tex1, myp5._renderer.states._tex); }); test('ambientLight() changes when metalness is applied', function () { @@ -792,7 +792,7 @@ suite('p5.RendererGL', function() { myp5.metalness(100000); myp5.sphere(50); expect(myp5._renderer.mixedAmbientLight).to.not.deep.equal( - myp5._renderer.ambientLightColors); + myp5._renderer.states.ambientLightColors); }); test('specularColor transforms to fill color when metalness is applied', @@ -804,7 +804,7 @@ suite('p5.RendererGL', function() { myp5.metalness(100000); myp5.sphere(50); expect(myp5._renderer.mixedSpecularColor).to.deep.equal( - myp5._renderer.curFillColor); + myp5._renderer.states.curFillColor); }); test('push/pop and shader() works with fill', function() { @@ -835,9 +835,9 @@ suite('p5.RendererGL', function() { } for (var j = i; j > 0; j--) { if (j % 2 === 0) { - assert.deepEqual(col2._array, myp5._renderer.curFillColor); + assert.deepEqual(col2._array, myp5._renderer.states.curFillColor); } else { - assert.deepEqual(col1._array, myp5._renderer.curFillColor); + assert.deepEqual(col1._array, myp5._renderer.states.curFillColor); } myp5.pop(); } @@ -849,7 +849,7 @@ suite('p5.RendererGL', function() { test('changing cameras keeps transforms', function() { myp5.createCanvas(50, 50, myp5.WEBGL); - const origModelMatrix = myp5._renderer.uModelMatrix.copy(); + const origModelMatrix = myp5._renderer.states.uModelMatrix.copy(); const cam2 = myp5.createCamera(); cam2.setPosition(0, 0, -500); @@ -858,21 +858,21 @@ suite('p5.RendererGL', function() { // cam1 is applied right now so technically this is redundant myp5.setCamera(cam1); const cam1Matrix = cam1.cameraMatrix.copy(); - assert.deepEqual(myp5._renderer.uViewMatrix.mat4, cam1Matrix.mat4); + assert.deepEqual(myp5._renderer.states.uViewMatrix.mat4, cam1Matrix.mat4); // Translation only changes the model matrix myp5.translate(100, 0, 0); assert.notDeepEqual( - myp5._renderer.uModelMatrix.mat4, + myp5._renderer.states.uModelMatrix.mat4, origModelMatrix.mat4 ); - assert.deepEqual(myp5._renderer.uViewMatrix.mat4, cam1Matrix.mat4); + assert.deepEqual(myp5._renderer.states.uViewMatrix.mat4, cam1Matrix.mat4); // Switchnig cameras only changes the view matrix - const transformedModel = myp5._renderer.uModelMatrix.copy(); + const transformedModel = myp5._renderer.states.uModelMatrix.copy(); myp5.setCamera(cam2); - assert.deepEqual(myp5._renderer.uModelMatrix.mat4, transformedModel.mat4); - assert.notDeepEqual(myp5._renderer.uViewMatrix.mat4, cam1Matrix.mat4); + assert.deepEqual(myp5._renderer.states.uModelMatrix.mat4, transformedModel.mat4); + assert.notDeepEqual(myp5._renderer.states.uViewMatrix.mat4, cam1Matrix.mat4); }); }); @@ -1163,7 +1163,7 @@ suite('p5.RendererGL', function() { suite('blendMode()', function() { var testBlend = function(mode, intended) { myp5.blendMode(mode); - assert.deepEqual(intended, myp5._renderer.curBlendMode); + assert.deepEqual(intended, myp5._renderer.states.curBlendMode); }; test('blendMode sets _curBlendMode correctly', function() { @@ -1264,10 +1264,10 @@ suite('p5.RendererGL', function() { myp5.blendMode(myp5.MULTIPLY); myp5.push(); myp5.blendMode(myp5.ADD); - assert.equal(myp5._renderer.curBlendMode, myp5.ADD, 'Changed to ADD'); + assert.equal(myp5._renderer.states.curBlendMode, myp5.ADD, 'Changed to ADD'); myp5.pop(); assert.equal( - myp5._renderer.curBlendMode, + myp5._renderer.states.curBlendMode, myp5.MULTIPLY, 'Resets to MULTIPLY' ); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index f952a60da7..6b65349455 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -243,9 +243,9 @@ suite('p5.Shader', function() { }); test('Able to set shininess', function() { - assert.deepEqual(myp5._renderer._useShininess, 1); + assert.deepEqual(myp5._renderer.states._useShininess, 1); myp5.shininess(50); - assert.deepEqual(myp5._renderer._useShininess, 50); + assert.deepEqual(myp5._renderer.states._useShininess, 50); }); test('Shader is reset after resetShader is called', function() { From 94e826780aa0dea6508740f956c0513ac895c903 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 17 Sep 2024 18:48:00 -0400 Subject: [PATCH 005/120] Move more properties into states --- src/core/p5.Renderer.js | 6 +- src/webgl/GeometryBuilder.js | 12 ++-- src/webgl/light.js | 12 ++-- src/webgl/material.js | 18 +++--- src/webgl/p5.Camera.js | 4 ++ src/webgl/p5.Matrix.js | 4 ++ src/webgl/p5.RendererGL.Immediate.js | 6 +- src/webgl/p5.RendererGL.js | 84 ++++++++++++++-------------- test/unit/webgl/p5.RendererGL.js | 8 +-- test/unit/webgl/p5.Shader.js | 6 +- 10 files changed, 85 insertions(+), 75 deletions(-) diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 6a3ae8d6dd..e11dc24b35 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -72,8 +72,10 @@ p5.Renderer = class Renderer { const currentStates = Object.assign({}, this.states); // Clone properties that support it for (const key in currentStates) { - if (currentStates[key] && currentStates[key].copy instanceof Function) { - currentStates[key] = currentStates[key].copy(); + 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); diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 89f115ed76..d5216ccf49 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -11,7 +11,7 @@ class GeometryBuilder { this.renderer = renderer; renderer._pInst.push(); this.identityMatrix = new p5.Matrix(); - renderer.uModelMatrix = new p5.Matrix(); + renderer.states.uModelMatrix = new p5.Matrix(); this.geometry = new p5.Geometry(); this.geometry.gid = `_p5_GeometryBuilder_${GeometryBuilder.nextGeometryId}`; GeometryBuilder.nextGeometryId++; @@ -25,7 +25,7 @@ class GeometryBuilder { transformVertices(vertices) { if (!this.hasTransform) return vertices; - return vertices.map(v => this.renderer.uModelMatrix.multiplyPoint(v)); + return vertices.map(v => this.renderer.states.uModelMatrix.multiplyPoint(v)); } /** @@ -36,7 +36,7 @@ class GeometryBuilder { if (!this.hasTransform) return normals; return normals.map( - v => this.renderer.uNMatrix.multiplyVec3(v) + v => this.renderer.states.uNMatrix.multiplyVec3(v) ); } @@ -46,11 +46,11 @@ class GeometryBuilder { * transformations. */ addGeometry(input) { - this.hasTransform = !this.renderer.uModelMatrix.mat4 + this.hasTransform = !this.renderer.states.uModelMatrix.mat4 .every((v, i) => v === this.identityMatrix.mat4[i]); if (this.hasTransform) { - this.renderer.uNMatrix.inverseTranspose(this.renderer.uModelMatrix); + this.renderer.states.uNMatrix.inverseTranspose(this.renderer.states.uModelMatrix); } let startIdx = this.geometry.vertices.length; @@ -72,7 +72,7 @@ class GeometryBuilder { } const vertexColors = [...input.vertexColors]; while (vertexColors.length < input.vertices.length * 4) { - vertexColors.push(...this.renderer.curFillColor); + vertexColors.push(...this.renderer.states.curFillColor); } this.geometry.vertexColors.push(...vertexColors); } diff --git a/src/webgl/light.js b/src/webgl/light.js index 8057eadb2a..1d17ebdc0c 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -197,7 +197,7 @@ p5.prototype.ambientLight = function (v1, v2, v3, a) { color._array[2] ); - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; return this; }; @@ -674,7 +674,7 @@ p5.prototype.directionalLight = function (v1, v2, v3, x, y, z) { this._renderer.states.specularColors ); - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; return this; }; @@ -947,7 +947,7 @@ p5.prototype.pointLight = function (v1, v2, v3, x, y, z) { this._renderer.states.specularColors ); - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; return this; }; @@ -1014,7 +1014,7 @@ p5.prototype.imageLight = function (img) { // activeImageLight property is checked by _setFillUniforms // for sending uniforms to the fillshader this._renderer.states.activeImageLight = img; - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; }; /** @@ -1677,7 +1677,7 @@ p5.prototype.spotLight = function ( this._renderer.states.spotLightAngle = [Math.cos(angle)]; this._renderer.states.spotLightConc = [concentration]; - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; return this; }; @@ -1745,7 +1745,7 @@ p5.prototype.noLights = function (...args) { p5._validateParameters('noLights', args); this._renderer.states.activeImageLight = null; - this._renderer._enableLighting = false; + this._renderer.states._enableLighting = false; this._renderer.states.ambientLightColors.length = 0; this._renderer.states.specularColors = [1, 1, 1]; diff --git a/src/webgl/material.js b/src/webgl/material.js index 90f7bf308f..6b5976d675 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -761,9 +761,9 @@ p5.prototype.shader = function (s) { s.ensureCompiledOnContext(this); if (s.isStrokeShader()) { - this._renderer.userStrokeShader = s; + this._renderer.states.userStrokeShader = s; } else { - this._renderer.userFillShader = s; + this._renderer.states.userFillShader = s; this._renderer.states._useNormalMaterial = false; } @@ -854,7 +854,7 @@ p5.prototype.shader = function (s) { * */ p5.prototype.resetShader = function () { - this._renderer.userFillShader = this._renderer.userStrokeShader = null; + this._renderer.states.userFillShader = this._renderer.states.userStrokeShader = null; return this; }; @@ -1784,7 +1784,7 @@ p5.prototype.ambientMaterial = function (v1, v2, v3) { this._renderer.states._hasSetAmbient = true; this._renderer.states.curAmbientColor = color._array; this._renderer.states._useNormalMaterial = false; - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; this._renderer.states.doFill = true; return this; }; @@ -1880,7 +1880,7 @@ p5.prototype.emissiveMaterial = function (v1, v2, v3, a) { this._renderer.states.curEmissiveColor = color._array; this._renderer.states._useEmissiveMaterial = true; this._renderer.states._useNormalMaterial = false; - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; return this; }; @@ -2135,7 +2135,7 @@ p5.prototype.specularMaterial = function (v1, v2, v3, alpha) { this._renderer.states.curSpecularColor = color._array; this._renderer.states._useSpecularMaterial = true; this._renderer.states._useNormalMaterial = false; - this._renderer._enableLighting = true; + this._renderer.states._enableLighting = true; return this; }; @@ -2342,9 +2342,9 @@ p5.RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { const isTexture = this.states.drawMode === constants.TEXTURE; const doBlend = hasTransparency || - this.userFillShader || - this.userStrokeShader || - this.userPointShader || + this.states.userFillShader || + this.states.userStrokeShader || + this.states.userPointShader || isTexture || this.states.curBlendMode !== constants.BLEND || colors[colors.length - 1] < 1.0 || diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index c189b566b7..8e446bde83 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -3652,6 +3652,10 @@ p5.Camera = class Camera { 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. diff --git a/src/webgl/p5.Matrix.js b/src/webgl/p5.Matrix.js index 81ed8ef3a4..76deeef6a6 100644 --- a/src/webgl/p5.Matrix.js +++ b/src/webgl/p5.Matrix.js @@ -140,6 +140,10 @@ p5.Matrix = class Matrix { return copied; } + clone() { + return this.copy(); + } + /** * return an identity matrix * @return {p5.Matrix} the result matrix diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index e6283478fc..3e3e3c10c9 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -135,9 +135,9 @@ p5.RendererGL.prototype.vertex = function(x, y) { v /= this.states._tex.height; } } else if ( - this.userFillShader !== undefined || - this.userStrokeShader !== undefined || - this.userPointShader !== undefined + this.states.userFillShader !== undefined || + this.states.userStrokeShader !== undefined || + this.states.userPointShader !== undefined ) { // Do nothing if user-defined shaders are present } else if ( diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index a3c48e963b..9cf2eab1ff 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -554,18 +554,18 @@ p5.RendererGL = class RendererGL extends Renderer { this.executeZoom = false; this.executeRotateAndMove = false; - this.specularShader = undefined; + this.states.specularShader = undefined; this.sphereMapping = undefined; - this.diffusedShader = undefined; + this.states.diffusedShader = undefined; this._defaultLightShader = undefined; this._defaultImmediateModeShader = undefined; this._defaultNormalShader = undefined; this._defaultColorShader = undefined; this._defaultPointShader = undefined; - this.userFillShader = undefined; - this.userStrokeShader = undefined; - this.userPointShader = undefined; + this.states.userFillShader = undefined; + this.states.userStrokeShader = undefined; + this.states.userPointShader = undefined; // Default drawing is done in Retained Mode // Geometry and Material hashes stored here @@ -636,7 +636,7 @@ p5.RendererGL = class RendererGL extends Renderer { this.activeFramebuffers = []; // for post processing step - this.filterShader = undefined; + this.states.filterShader = undefined; this.filterLayer = undefined; this.filterLayerTemp = undefined; this.defaultFilterShaders = {}; @@ -902,7 +902,7 @@ p5.RendererGL = class RendererGL extends Renderer { this.states.spotLightAngle.length = 0; this.states.spotLightConc.length = 0; - this._enableLighting = false; + this.states._enableLighting = false; //reset tint value for new frame this.states.tint = [255, 255, 255, 255]; @@ -1059,12 +1059,12 @@ p5.RendererGL = class RendererGL extends Renderer { filterShaderFrags[operation] ); } - this.filterShader = this.defaultFilterShaders[operation]; + this.states.filterShader = this.defaultFilterShaders[operation]; } // use custom user-supplied shader else { - this.filterShader = args[0]; + this.states.filterShader = args[0]; } // Setting the target to the framebuffer when applying a filter to a framebuffer. @@ -1093,27 +1093,27 @@ p5.RendererGL = class RendererGL extends Renderer { this._pInst.blendMode(constants.BLEND); // draw main to temp buffer - this._pInst.shader(this.filterShader); - this.filterShader.setUniform('texelSize', texelSize); - this.filterShader.setUniform('canvasSize', [target.width, target.height]); - this.filterShader.setUniform('radius', Math.max(1, filterParameter)); + 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.filterShader.setUniform('direction', [1, 0]); - this.filterShader.setUniform('tex0', target); + this.states.filterShader.setUniform('direction', [1, 0]); + this.states.filterShader.setUniform('tex0', target); this._pInst.clear(); - this._pInst.shader(this.filterShader); + 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.filterShader.setUniform('direction', [0, 1]); - this.filterShader.setUniform('tex0', tmp); + this.states.filterShader.setUniform('direction', [0, 1]); + this.states.filterShader.setUniform('tex0', tmp); this._pInst.clear(); - this._pInst.shader(this.filterShader); + this._pInst.shader(this.states.filterShader); this._pInst.noLights(); this._pInst.plane(target.width, target.height); }); @@ -1125,13 +1125,13 @@ p5.RendererGL = class RendererGL extends Renderer { fbo.draw(() => { this._pInst.noStroke(); this._pInst.blendMode(constants.BLEND); - this._pInst.shader(this.filterShader); - this.filterShader.setUniform('tex0', target); - this.filterShader.setUniform('texelSize', texelSize); - this.filterShader.setUniform('canvasSize', [target.width, target.height]); + 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.filterShader.setUniform('filterParameter', filterParameter); + this.states.filterShader.setUniform('filterParameter', filterParameter); this._pInst.noLights(); this._pInst.plane(target.width, target.height); }); @@ -1245,8 +1245,8 @@ p5.RendererGL = class RendererGL extends Renderer { this._pInst.push(); this._pInst.resetShader(); - if (this._doFill) this._pInst.fill(0, 0); - if (this._doStroke) this._pInst.stroke(0, 0); + if (this.states.doFill) this._pInst.fill(0, 0); + if (this.states.doStroke) this._pInst.stroke(0, 0); } endClip() { @@ -1615,7 +1615,7 @@ p5.RendererGL = class RendererGL extends Renderer { _getImmediateStrokeShader() { // select the stroke shader to use - const stroke = this.userStrokeShader; + const stroke = this.states.userStrokeShader; if (!stroke || !stroke.isStrokeShader()) { return this._getLineShader(); } @@ -1647,13 +1647,13 @@ p5.RendererGL = class RendererGL extends Renderer { * for use with begin/endShape and immediate vertex mode. */ _getImmediateFillShader() { - const fill = this.userFillShader; + const fill = this.states.userFillShader; if (this.states._useNormalMaterial) { if (!fill || !fill.isNormalShader()) { return this._getNormalShader(); } } - if (this._enableLighting) { + if (this.states._enableLighting) { if (!fill || !fill.isLightShader()) { return this._getLightShader(); } @@ -1676,8 +1676,8 @@ p5.RendererGL = class RendererGL extends Renderer { return this._getNormalShader(); } - const fill = this.userFillShader; - if (this._enableLighting) { + const fill = this.states.userFillShader; + if (this.states._enableLighting) { if (!fill || !fill.isLightShader()) { return this._getLightShader(); } @@ -1693,7 +1693,7 @@ p5.RendererGL = class RendererGL extends Renderer { _getImmediatePointShader() { // select the point shader to use - const point = this.userPointShader; + const point = this.states.userPointShader; if (!point || !point.isPointShader()) { return this._getPointShader(); } @@ -1880,15 +1880,15 @@ p5.RendererGL = class RendererGL extends Renderer { }); // create framebuffer is like making a new sketch, all functions on main // sketch it would be available on framebuffer - if (!this.diffusedShader) { - this.diffusedShader = this._pInst.createShader( + if (!this.states.diffusedShader) { + this.states.diffusedShader = this._pInst.createShader( defaultShaders.imageLightVert, defaultShaders.imageLightDiffusedFrag ); } newFramebuffer.draw(() => { - this._pInst.shader(this.diffusedShader); - this.diffusedShader.setUniform('environmentMap', input); + this._pInst.shader(this.states.diffusedShader); + this.states.diffusedShader.setUniform('environmentMap', input); this._pInst.noStroke(); this._pInst.rectMode(constants.CENTER); this._pInst.noLights(); @@ -1921,8 +1921,8 @@ p5.RendererGL = class RendererGL extends Renderer { width: size, height: size, density: 1 }); let count = Math.log(size) / Math.log(2); - if (!this.specularShader) { - this.specularShader = this._pInst.createShader( + if (!this.states.specularShader) { + this.states.specularShader = this._pInst.createShader( defaultShaders.imageLightVert, defaultShaders.imageLightSpecularFrag ); @@ -1937,10 +1937,10 @@ p5.RendererGL = class RendererGL extends Renderer { let currCount = Math.log(w) / Math.log(2); let roughness = 1 - currCount / count; framebuffer.draw(() => { - this._pInst.shader(this.specularShader); + this._pInst.shader(this.states.specularShader); this._pInst.clear(); - this.specularShader.setUniform('environmentMap', input); - this.specularShader.setUniform('roughness', roughness); + this.states.specularShader.setUniform('environmentMap', input); + this.states.specularShader.setUniform('roughness', roughness); this._pInst.noStroke(); this._pInst.noLights(); this._pInst.plane(w, w); @@ -2011,7 +2011,7 @@ p5.RendererGL = class RendererGL extends Renderer { this._setImageLightUniforms(fillShader); - fillShader.setUniform('uUseLighting', this._enableLighting); + fillShader.setUniform('uUseLighting', this.states._enableLighting); const pointLightCount = this.states.pointLightDiffuseColors.length / 3; fillShader.setUniform('uPointLightCount', pointLightCount); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 42cf9157dd..d64e2483cd 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -812,13 +812,13 @@ suite('p5.RendererGL', function() { var fillShader1 = myp5._renderer._getLightShader(); var fillShader2 = myp5._renderer._getColorShader(); myp5.shader(fillShader1); - assert.equal(fillShader1, myp5._renderer.userFillShader); + assert.equal(fillShader1, myp5._renderer.states.userFillShader); myp5.push(); myp5.shader(fillShader2); - assert.equal(fillShader2, myp5._renderer.userFillShader); - assert.notEqual(fillShader1, myp5._renderer.userFillShader); + assert.equal(fillShader2, myp5._renderer.states.userFillShader); + assert.notEqual(fillShader1, myp5._renderer.states.userFillShader); myp5.pop(); - assert.equal(fillShader1, myp5._renderer.userFillShader); + assert.equal(fillShader1, myp5._renderer.states.userFillShader); }); test('push/pop builds/unbuilds stack properly', function() { diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 6b65349455..b026491a11 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -236,7 +236,7 @@ suite('p5.Shader', function() { test('Able to setUniform empty arrays', function() { myp5.shader(myp5._renderer._getLightShader()); - var s = myp5._renderer.userFillShader; + var s = myp5._renderer.states.userFillShader; s.setUniform('uMaterialColor', []); s.setUniform('uLightingDirection', []); @@ -250,11 +250,11 @@ suite('p5.Shader', function() { test('Shader is reset after resetShader is called', function() { myp5.shader(myp5._renderer._getColorShader()); - var prevShader = myp5._renderer.userFillShader; + var prevShader = myp5._renderer.states.userFillShader; assert.isTrue(prevShader !== null); myp5.resetShader(); - var curShader = myp5._renderer.userFillShader; + var curShader = myp5._renderer.states.userFillShader; assert.isTrue(curShader === null); }); From f0b3766709cbdc35b23e4c95cdfc378df3c7d0aa Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 18 Sep 2024 10:50:03 -0400 Subject: [PATCH 006/120] Fix some uses of .elt --- src/webgl/p5.Texture.js | 3 ++- test/unit/webgl/p5.Geometry.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 7c1f2fa3ec..1788332670 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -123,11 +123,12 @@ p5.Texture = class Texture { } else if ( this.isSrcMediaElement || this.isSrcP5Graphics || - this.isSrcP5Renderer || 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; } diff --git a/test/unit/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js index 60c0a56a0d..636dd29387 100644 --- a/test/unit/webgl/p5.Geometry.js +++ b/test/unit/webgl/p5.Geometry.js @@ -181,7 +181,7 @@ suite('p5.Geometry', function() { drawGeometry(); myp5.pop(); myp5.resetShader(); - const regularImage = myp5._renderer.elt.toDataURL(); + const regularImage = myp5._renderer.canvas.toDataURL(); // Geometry mode myp5.fill(255); @@ -192,7 +192,7 @@ suite('p5.Geometry', function() { myp5.model(geom); myp5.pop(); myp5.resetShader(); - const geometryImage = myp5._renderer.elt.toDataURL(); + const geometryImage = myp5._renderer.canvas.toDataURL(); assert.equal(regularImage, geometryImage); } From 7299220f1003534003131d9ff7f37fcf53664e18 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 18 Sep 2024 19:35:06 +0100 Subject: [PATCH 007/120] Minor cleanup --- src/core/main.js | 8 ++------ src/core/p5.Renderer.js | 20 ++++++++++---------- src/core/structure.js | 16 ++-------------- 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index 53944de8e3..d0cfe41ddf 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -60,10 +60,6 @@ class p5 { this._loop = true; this._startListener = null; this._initializeInstanceVariables(); - this._defaultCanvasSize = { - width: 100, - height: 100 - }; this._events = { // keep track of user-events for unregistering later mousemove: null, @@ -217,8 +213,8 @@ class p5 { // Later on if the user calls createCanvas, this default one // will be replaced this.createCanvas( - this._defaultCanvasSize.width, - this._defaultCanvasSize.height, + 100, + 100, constants.P2D ); diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index e11dc24b35..387760ba07 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -57,7 +57,8 @@ p5.Renderer = class Renderer { textStyle: constants.NORMAL, textWrap: constants.WORD }; - this.pushPopStack = []; + this._pushPopStack = []; + // NOTE: can use the length of the push pop stack instead this._pushPopDepth = 0; this._clipping = false; @@ -65,9 +66,9 @@ p5.Renderer = class Renderer { this._curveTightness = 0; } - // the renderer should return a 'style' object that it wishes to - // store on the push stack. - push () { + // 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 @@ -78,16 +79,15 @@ p5.Renderer = class Renderer { currentStates[key] = currentStates[key].clone(); } } - this.pushPopStack.push(currentStates); + this._pushPopStack.push(currentStates); return currentStates; } - // a pop() operation is in progress - // the renderer is passed the 'style' object that it returned - // from its push() method. - pop (style) { + // 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()); + Object.assign(this.states, this._pushPopStack.pop()); } beginClip(options = {}) { diff --git a/src/core/structure.js b/src/core/structure.js index b0cbf19677..9387bbd00a 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -542,13 +542,7 @@ p5.prototype.isLooping = function() { * */ p5.prototype.push = function() { - // NOTE: change how state machine is handled from here - this._styles.push({ - props: { - _colorMode: this._colorMode - }, - renderer: this._renderer.push() - }); + this._renderer.push(); }; /** @@ -827,13 +821,7 @@ p5.prototype.push = function() { * */ p5.prototype.pop = function() { - const style = this._styles.pop(); - if (style) { - this._renderer.pop(style.renderer); - Object.assign(this, style.props); - } else { - console.warn('pop() was called without matching push()'); - } + this._renderer.pop(); }; /** From 84ecc61e381710b0ce382215caeadb1bf9a9cb4b Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 18 Sep 2024 17:20:19 +0100 Subject: [PATCH 008/120] Rework how variables are exposed in global mode Properties on the p5 instance should all be assigned regularly, no more using _setProperty. Overwriting globals already set will cause log message to be printed as before but not the new set value will persist instead of being overwritten by p5 again. This likely make more sketches work than break. --- src/core/environment.js | 12 ++++----- src/core/main.js | 45 ++++++++++++++++++++----------- src/core/p5.Element.js | 9 +------ src/core/p5.Renderer.js | 12 ++++----- src/core/p5.Renderer2D.js | 6 ++--- src/core/preload.js | 12 ++++----- src/core/rendering.js | 2 +- src/core/structure.js | 6 ++--- src/dom/dom.js | 4 +-- src/events/acceleration.js | 38 +++++++++++++------------- src/events/keyboard.js | 22 +++++++-------- src/events/mouse.js | 49 +++++++++++++++++----------------- src/events/touch.js | 10 +++---- src/image/p5.Image.js | 8 ------ src/math/trigonometry.js | 12 ++++----- src/webgl/interaction.js | 6 ++--- src/webgl/p5.RendererGL.js | 18 +++++-------- test/unit/math/trigonometry.js | 1 - 18 files changed, 131 insertions(+), 141 deletions(-) diff --git a/src/core/environment.js b/src/core/environment.js index 3f5198662a..2e4b711632 100644 --- a/src/core/environment.js +++ b/src/core/environment.js @@ -410,9 +410,9 @@ p5.prototype.frameRate = function(fps) { if (typeof fps !== 'number' || fps < 0) { return this._frameRate; } else { - this._setProperty('_targetFrameRate', fps); + this._targetFrameRate = fps; if (fps === 0) { - this._setProperty('_frameRate', fps); + this._frameRate = fps; } return this; } @@ -770,8 +770,8 @@ p5.prototype.windowHeight = 0; * This example does not render anything. */ p5.prototype._onresize = function(e) { - this._setProperty('windowWidth', getWindowWidth()); - this._setProperty('windowHeight', getWindowHeight()); + this.windowWidth = getWindowWidth(); + this.windowHeight = getWindowHeight(); const context = this._isGlobal ? window : this; let executeDefault; if (typeof context.windowResized === 'function') { @@ -805,8 +805,8 @@ function getWindowHeight() { * possibility of the window being resized when no sketch is active. */ p5.prototype._updateWindowSize = function() { - this._setProperty('windowWidth', getWindowWidth()); - this._setProperty('windowHeight', getWindowHeight()); + this.windowWidth = getWindowWidth(); + this.windowHeight = getWindowHeight(); }; /** diff --git a/src/core/main.js b/src/core/main.js index d0cfe41ddf..1cc8bf5041 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -83,8 +83,8 @@ class p5 { }; this._millisStart = -1; this._recording = false; - this.touchstart = false; - this.touchend = false; + this._touchstart = false; + this._touchend = false; // States used in the custom random generators this._lcg_random_state = null; // NOTE: move to random.js @@ -103,6 +103,26 @@ class p5 { this._updateWindowSize(); const friendlyBindGlobal = this._createFriendlyGlobalFunctionBinder(); + const bindGlobal = (property) => { + Object.defineProperty(window, property, { + configurable: true, + enumerable: true, + get: () => { + return this[property] + }, + set: (newValue) => { + Object.defineProperty(window, property, { + configurable: true, + enumerable: true, + value: newValue, + writable: true + }); + if (!p5.disableFriendlyErrors) { + console.log(`You just changed the value of "${property}", which was a p5 global value. This could cause problems later if you're not careful.`); + } + } + }) + }; // If the user has created a global setup or draw function, // assume "global" mode and make everything global (i.e. on the window) if (!sketch) { @@ -126,7 +146,8 @@ class p5 { } } } else { - friendlyBindGlobal(p, p5.prototype[p]); + console.log(p); + bindGlobal(p, p5.prototype[p]); } } @@ -134,7 +155,7 @@ class p5 { for (const p in this) { if (this.hasOwnProperty(p)) { if(p[0] === '_') continue; - friendlyBindGlobal(p, this[p]); + bindGlobal(p); } } } else { @@ -158,10 +179,10 @@ class p5 { } const focusHandler = () => { - this._setProperty('focused', true); + this.focused = true; }; const blurHandler = () => { - this._setProperty('focused', false); + this.focused = false; }; window.addEventListener('focus', focusHandler); window.addEventListener('blur', blurHandler); @@ -272,7 +293,6 @@ class p5 { ) { //mandatory update values(matrixes and stack) this.deltaTime = now - this._lastRealFrameTime; - this._setProperty('deltaTime', this.deltaTime); this._frameRate = 1000.0 / this.deltaTime; await this.redraw(); this._lastTargetFrameTime = Math.max(this._lastTargetFrameTime @@ -288,8 +308,8 @@ class p5 { //reset delta values so they reset even if there is no mouse event to set them // for example if the mouse is outside the screen - this._setProperty('movedX', 0); - this._setProperty('movedY', 0); + this.movedX = 0; + this.movedY = 0; } } @@ -424,13 +444,6 @@ class p5 { this._downKeys = {}; //Holds the key codes of currently pressed keys } - _setProperty(prop, value) { - this[prop] = value; - if (this._isGlobal) { - window[prop] = value; - } - } - // create a function which provides a standardized process for binding // globals; this is implemented as a factory primarily so that there's a // way to redefine what "global" means for the binding function so it diff --git a/src/core/p5.Element.js b/src/core/p5.Element.js index 5fa4b47149..b81d211276 100644 --- a/src/core/p5.Element.js +++ b/src/core/p5.Element.js @@ -335,7 +335,7 @@ p5.Element = class { // This is required so that mouseButton is set correctly prior to calling the callback (fxn). // For details, see https://github.com/processing/p5.js/issues/3087. const eventPrependedFxn = function (event) { - this._pInst._setProperty('mouseIsPressed', true); + this._pInst.mouseIsPressed = true; this._pInst._setMouseButton(event); // Pass along the return-value of the callback: return fxn.call(this, event); @@ -942,13 +942,6 @@ p5.Element = class { ctx.elt.removeEventListener(ev, f, false); ctx._events[ev] = null; } - - /** - * Helper fxn for sharing pixel methods - */ - _setProperty(prop, value) { - this[prop] = value; - } }; /** diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 387760ba07..cc5eb60da4 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -28,10 +28,10 @@ p5.Renderer = class Renderer { if (isMainCanvas) { this._isMainCanvas = true; // for pixel method sharing with pimage - this._pInst._setProperty('_curElement', this); - this._pInst._setProperty('canvas', this.canvas); - this._pInst._setProperty('width', this.width); - this._pInst._setProperty('height', this.height); + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + this._pInst.width = this.width; + this._pInst.height = this.height; } else { // hide if offscreen buffer by default this.canvas.style.display = 'none'; @@ -116,8 +116,8 @@ p5.Renderer = class Renderer { this.canvas.style.width = `${w}px`; this.canvas.style.height = `${h}px`; if (this._isMainCanvas) { - this._pInst._setProperty('width', this.width); - this._pInst._setProperty('height', this.height); + this._pInst.width = this.width; + this._pInst.height = this.height; } } diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index e66fb0c404..925b1cea30 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -16,7 +16,7 @@ class Renderer2D extends Renderer { constructor(elt, pInst, isMainCanvas) { super(elt, pInst, isMainCanvas); this.drawingContext = this.canvas.getContext('2d'); - this._pInst._setProperty('drawingContext', this.drawingContext); + this._pInst.drawingContext = this.drawingContext; this.elt = elt; // Extend renderer with methods of p5.Element with getters @@ -435,8 +435,8 @@ class Renderer2D extends Renderer { 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._setProperty('imageData', imageData); - pixelsState._setProperty('pixels', imageData.data); + pixelsState.imageData = imageData; + pixelsState.pixels = imageData.data; } set(x, y, imgOrCol) { diff --git a/src/core/preload.js b/src/core/preload.js index 8381ae1b91..8a7660f5bb 100644 --- a/src/core/preload.js +++ b/src/core/preload.js @@ -202,10 +202,9 @@ p5.prototype._legacyPreloadGenerator = function( }; p5.prototype._decrementPreload = function() { - const context = this._isGlobal ? window : this; - if (!context._preloadDone && typeof context.preload === 'function') { - context._setProperty('_preloadCount', context._preloadCount - 1); - context._runIfPreloadsAreDone(); + if (!this._preloadDone && typeof this.preload === 'function') { + this._preloadCount = context._preloadCount - 1; + this._runIfPreloadsAreDone(); } }; @@ -219,10 +218,9 @@ p5.prototype._wrapPreload = function(obj, fnName) { }; p5.prototype._incrementPreload = function() { - const context = this._isGlobal ? window : this; // Do nothing if we tried to increment preloads outside of `preload` - if (context._preloadDone) return; - context._setProperty('_preloadCount', context._preloadCount + 1); + if (this._preloadDone) return; + this._preloadCount = context._preloadCount + 1; }; p5.prototype._runIfPreloadsAreDone = function() { diff --git a/src/core/rendering.js b/src/core/rendering.js index 7dcd96c2bb..c645c437fc 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -234,7 +234,7 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { // this._elements.push(this._renderer); // } // } - this._setProperty('_renderer', new renderers[r](c, this, true)); + this._renderer = new renderers[r](c, this, true); this._defaultGraphicsCreated = true; this._elements.push(this._renderer); // Instead of resize, just create with the right size in the first place diff --git a/src/core/structure.js b/src/core/structure.js index 9387bbd00a..ff81ff4383 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -929,10 +929,10 @@ p5.prototype.redraw = async function(n) { if (this._accessibleOutputs.grid || this._accessibleOutputs.text) { this._updateAccsOutput(); } - if (context._renderer.isP3D) { - context._renderer._update(); + if (this._renderer.isP3D) { + this._renderer._update(); } - this._setProperty('frameCount', context.frameCount + 1); + this.frameCount = context.frameCount + 1; await this._runLifecycleHook('predraw'); this._inUserDraw = true; try { diff --git a/src/dom/dom.js b/src/dom/dom.js index f383a70215..75a0726f30 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._setProperty('width', aW); - this._pInst._setProperty('height', aH); + this._pInst.width = aW; + this._pInst.height = aH; } } } diff --git a/src/events/acceleration.js b/src/events/acceleration.js index 126a9942de..8a6d1b86b5 100644 --- a/src/events/acceleration.js +++ b/src/events/acceleration.js @@ -122,9 +122,9 @@ function acceleration(p5, fn){ * @private */ fn._updatePAccelerations = function () { - this._setProperty('pAccelerationX', this.accelerationX); - this._setProperty('pAccelerationY', this.accelerationY); - this._setProperty('pAccelerationZ', this.accelerationZ); + this.pAccelerationX = this.accelerationX; + this.pAccelerationY = this.accelerationY; + this.pAccelerationZ = this.accelerationZ; }; /** @@ -369,9 +369,9 @@ function acceleration(p5, fn){ fn.pRotateDirectionZ = undefined; fn._updatePRotations = function () { - this._setProperty('pRotationX', this.rotationX); - this._setProperty('pRotationY', this.rotationY); - this._setProperty('pRotationZ', this.rotationZ); + this.pRotationX = this.rotationX; + this.pRotationY = this.rotationY; + this.pRotationZ = this.rotationZ; }; /** @@ -619,25 +619,25 @@ function acceleration(p5, fn){ this._updatePRotations(); // Convert from degrees into current angle mode - this._setProperty('rotationX', this._fromDegrees(e.beta)); - this._setProperty('rotationY', this._fromDegrees(e.gamma)); - this._setProperty('rotationZ', this._fromDegrees(e.alpha)); + this.rotationX = this._fromDegrees(e.beta); + this.rotationY = this._fromDegrees(e.gamma); + this.rotationZ = this._fromDegrees(e.alpha); this._handleMotion(); }; fn._ondevicemotion = function (e) { this._updatePAccelerations(); - this._setProperty('accelerationX', e.acceleration.x * 2); - this._setProperty('accelerationY', e.acceleration.y * 2); - this._setProperty('accelerationZ', e.acceleration.z * 2); + this.accelerationX = e.acceleration.x * 2; + this.accelerationY = e.acceleration.y * 2; + this.accelerationZ = e.acceleration.z * 2; this._handleMotion(); }; fn._handleMotion = function () { if (window.orientation === 90 || window.orientation === -90) { - this._setProperty('deviceOrientation', 'landscape'); + this.deviceOrientation = 'landscape'; } else if (window.orientation === 0) { - this._setProperty('deviceOrientation', 'portrait'); + this.deviceOrientation = 'portrait'; } else if (window.orientation === undefined) { - this._setProperty('deviceOrientation', 'undefined'); + this.deviceOrientation = 'undefined'; } const context = this._isGlobal ? window : this; if (typeof context.deviceMoved === 'function') { @@ -670,7 +670,7 @@ function acceleration(p5, fn){ } if (Math.abs(wRX - wSAX) > 90 && Math.abs(wRX - wSAX) < 270) { wSAX = wRX; - this._setProperty('turnAxis', 'X'); + this.turnAxis = 'X'; context.deviceTurned(); } this.pRotateDirectionX = rotateDirectionX; @@ -690,7 +690,7 @@ function acceleration(p5, fn){ } if (Math.abs(wRY - wSAY) > 90 && Math.abs(wRY - wSAY) < 270) { wSAY = wRY; - this._setProperty('turnAxis', 'Y'); + this.turnAxis = 'Y'; context.deviceTurned(); } this.pRotateDirectionY = rotateDirectionY; @@ -719,11 +719,11 @@ function acceleration(p5, fn){ Math.abs(rotZ - startAngleZ) < 270 ) { startAngleZ = rotZ; - this._setProperty('turnAxis', 'Z'); + this.turnAxis = 'Z'; context.deviceTurned(); } this.pRotateDirectionZ = rotateDirectionZ; - this._setProperty('turnAxis', undefined); + this.turnAxis = undefined; } if (typeof context.deviceShaken === 'function') { let accelerationChangeX; diff --git a/src/events/keyboard.js b/src/events/keyboard.js index cc6dd45339..fbc03ffad4 100644 --- a/src/events/keyboard.js +++ b/src/events/keyboard.js @@ -444,11 +444,11 @@ function keyboard(p5, fn){ // prevent multiple firings return; } - this._setProperty('isKeyPressed', true); - this._setProperty('keyIsPressed', true); - this._setProperty('keyCode', e.which); + this.isKeyPressed = true; + this.keyIsPressed = true; + this.keyCode = e.which; this._downKeys[e.which] = true; - this._setProperty('key', e.key || String.fromCharCode(e.which) || e.which); + this.key = e.key || String.fromCharCode(e.which) || e.which; const context = this._isGlobal ? window : this; if (typeof context.keyPressed === 'function' && !e.charCode) { const executeDefault = context.keyPressed(e); @@ -617,14 +617,14 @@ function keyboard(p5, fn){ this._downKeys[e.which] = false; if (!this._areDownKeys()) { - this._setProperty('isKeyPressed', false); - this._setProperty('keyIsPressed', false); + this.isKeyPressed = false; + this.keyIsPressed = false; } - this._setProperty('_lastKeyCodeTyped', null); + this._lastKeyCodeTyped = null; - this._setProperty('key', e.key || String.fromCharCode(e.which) || e.which); - this._setProperty('keyCode', e.which); + this.key = e.key || String.fromCharCode(e.which) || e.which; + this.keyCode = e.which; const context = this._isGlobal ? window : this; if (typeof context.keyReleased === 'function') { @@ -770,8 +770,8 @@ function keyboard(p5, fn){ // prevent multiple firings return; } - this._setProperty('_lastKeyCodeTyped', e.which); // track last keyCode - this._setProperty('key', e.key || String.fromCharCode(e.which) || e.which); + this._lastKeyCodeTyped = e.which; // track last keyCode + this.key = e.key || String.fromCharCode(e.which) || e.which; const context = this._isGlobal ? window : this; if (typeof context.keyTyped === 'function') { diff --git a/src/events/mouse.js b/src/events/mouse.js index 5ddb28f4c8..6e9f4a661e 100644 --- a/src/events/mouse.js +++ b/src/events/mouse.js @@ -825,27 +825,26 @@ function mouse(p5, fn){ this.height, e ); - this._setProperty('movedX', e.movementX); - this._setProperty('movedY', e.movementY); - this._setProperty('mouseX', mousePos.x); - this._setProperty('mouseY', mousePos.y); - this._setProperty('winMouseX', mousePos.winX); - this._setProperty('winMouseY', mousePos.winY); + this.movedX = e.movementX; + this.movedY = e.movementY; + this.mouseX = mousePos.x; + this.mouseY = mousePos.y; + this.winMouseX = mousePos.winX; + this.winMouseY = mousePos.winY; } if (!this._hasMouseInteracted) { // For first draw, make previous and next equal this._updateMouseCoords(); - this._setProperty('_hasMouseInteracted', true); + this._hasMouseInteracted = true; } }; fn._updateMouseCoords = function() { - this._setProperty('pmouseX', this.mouseX); - this._setProperty('pmouseY', this.mouseY); - this._setProperty('pwinMouseX', this.winMouseX); - this._setProperty('pwinMouseY', this.winMouseY); - - this._setProperty('_pmouseWheelDeltaY', this._mouseWheelDeltaY); + this.pmouseX = this.mouseX; + this.pmouseY = this.mouseY; + this.pwinMouseX = this.winMouseX; + this.pwinMouseY = this.winMouseY; + this._pmouseWheelDeltaY = this._mouseWheelDeltaY; }; function getMousePos(canvas, w, h, evt) { @@ -871,11 +870,11 @@ function mouse(p5, fn){ fn._setMouseButton = function(e) { if (e.button === 1) { - this._setProperty('mouseButton', constants.CENTER); + this.mouseButton = constants.CENTER; } else if (e.button === 2) { - this._setProperty('mouseButton', constants.RIGHT); + this.mouseButton = constants.RIGHT; } else { - this._setProperty('mouseButton', constants.LEFT); + this.mouseButton = constants.LEFT; } }; @@ -1229,12 +1228,12 @@ function mouse(p5, fn){ fn._onmousedown = function(e) { const context = this._isGlobal ? window : this; let executeDefault; - this._setProperty('mouseIsPressed', true); + this.mouseIsPressed = true; this._setMouseButton(e); this._updateNextMouseCoords(e); - // _ontouchstart triggers first and sets this.touchstart - if (this.touchstart) { + // _ontouchstart triggers first and sets this._touchstart + if (this._touchstart) { return; } @@ -1250,7 +1249,7 @@ function mouse(p5, fn){ } } - this.touchstart = false; + this._touchstart = false; }; /** @@ -1402,10 +1401,10 @@ function mouse(p5, fn){ fn._onmouseup = function(e) { const context = this._isGlobal ? window : this; let executeDefault; - this._setProperty('mouseIsPressed', false); + this.mouseIsPressed = false; - // _ontouchend triggers first and sets this.touchend - if (this.touchend) { + // _ontouchend triggers first and sets this._touchend + if (this._touchend) { return; } @@ -1420,7 +1419,7 @@ function mouse(p5, fn){ e.preventDefault(); } } - this.touchend = false; + this._touchend = false; }; fn._ondragend = fn._onmouseup; @@ -1853,7 +1852,7 @@ function mouse(p5, fn){ */ fn._onwheel = function(e) { const context = this._isGlobal ? window : this; - this._setProperty('_mouseWheelDeltaY', e.deltaY); + this._mouseWheelDeltaY = e.deltaY; if (typeof context.mouseWheel === 'function') { e.delta = e.deltaY; const executeDefault = context.mouseWheel(e); diff --git a/src/events/touch.js b/src/events/touch.js index 6ed8ca89cf..9a9254a511 100644 --- a/src/events/touch.js +++ b/src/events/touch.js @@ -103,7 +103,7 @@ function touch(p5, fn){ i ); } - this._setProperty('touches', touches); + this.touches = touches; } }; @@ -277,7 +277,7 @@ function touch(p5, fn){ fn._ontouchstart = function(e) { const context = this._isGlobal ? window : this; let executeDefault; - this._setProperty('mouseIsPressed', true); + this.mouseIsPressed = true; this._updateTouchCoords(e); this._updateNextMouseCoords(e); this._updateMouseCoords(); // reset pmouseXY at the start of each touch event @@ -287,7 +287,7 @@ function touch(p5, fn){ if (executeDefault === false) { e.preventDefault(); } - this.touchstart = true; + this._touchstart = true; } }; @@ -619,7 +619,7 @@ function touch(p5, fn){ * */ fn._ontouchend = function(e) { - this._setProperty('mouseIsPressed', false); + this.mouseIsPressed = false; this._updateTouchCoords(e); this._updateNextMouseCoords(e); const context = this._isGlobal ? window : this; @@ -629,7 +629,7 @@ function touch(p5, fn){ if (executeDefault === false) { e.preventDefault(); } - this.touchend = true; + this._touchend = true; } }; } diff --git a/src/image/p5.Image.js b/src/image/p5.Image.js index 959e8868c3..54781e20c9 100644 --- a/src/image/p5.Image.js +++ b/src/image/p5.Image.js @@ -193,14 +193,6 @@ function image(p5, fn){ } } - /** - * Helper fxn for sharing pixel methods - */ - _setProperty(prop, value) { - this[prop] = value; - this.setModified(true); - } - /** * Loads the current value of each pixel in the image into the `img.pixels` * array. diff --git a/src/math/trigonometry.js b/src/math/trigonometry.js index a7cd76fc42..8fe2f5f169 100644 --- a/src/math/trigonometry.js +++ b/src/math/trigonometry.js @@ -754,14 +754,14 @@ function trigonometry(p5, fn){ // This is necessary for acceleration events to work properly if(mode === RADIANS) { // Change pRotation to radians - this._setProperty('pRotationX', this.pRotationX * constants.DEG_TO_RAD); - this._setProperty('pRotationY', this.pRotationY * constants.DEG_TO_RAD); - this._setProperty('pRotationZ', this.pRotationZ * constants.DEG_TO_RAD); + this.pRotationX = this.pRotationX * constants.DEG_TO_RAD; + this.pRotationY = this.pRotationY * constants.DEG_TO_RAD; + this.pRotationZ = this.pRotationZ * constants.DEG_TO_RAD; } else { // Change pRotation to degrees - this._setProperty('pRotationX', this.pRotationX * constants.RAD_TO_DEG); - this._setProperty('pRotationY', this.pRotationY * constants.RAD_TO_DEG); - this._setProperty('pRotationZ', this.pRotationZ * constants.RAD_TO_DEG); + this.pRotationX = this.pRotationX * constants.RAD_TO_DEG; + this.pRotationY = this.pRotationY * constants.RAD_TO_DEG; + this.pRotationZ = this.pRotationZ * constants.RAD_TO_DEG; } this._angleMode = mode; diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index 3a53f83c98..7d1b9ba738 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -193,14 +193,14 @@ p5.prototype.orbitControl = function( // flag to p5 instance if (this.contextMenuDisabled !== true) { this.canvas.oncontextmenu = () => false; - this._setProperty('contextMenuDisabled', true); + this.contextMenuDisabled = true; } // disable default scrolling behavior on the canvas element and add // 'wheelDefaultDisabled' flag to p5 instance if (this.wheelDefaultDisabled !== true) { this.canvas.onwheel = () => false; - this._setProperty('wheelDefaultDisabled', true); + this.wheelDefaultDisabled = true; } // disable default touch behavior on the canvas element and add @@ -208,7 +208,7 @@ p5.prototype.orbitControl = function( const { disableTouchActions = true } = options; if (this.touchActionsDisabled !== true && disableTouchActions) { this.canvas.style['touch-action'] = 'none'; - this._setProperty('touchActionsDisabled', true); + this.touchActionsDisabled = true; } // If option.freeRotation is true, the camera always rotates freely in the direction diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 3f8341c32c..02de5880fa 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -445,7 +445,7 @@ 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._setProperty('drawingContext', this.drawingContext); + this._pInst.drawingContext = this.drawingContext; // Push/pop state this.states.uModelMatrix = new p5.Matrix(); @@ -761,7 +761,7 @@ p5.RendererGL = class RendererGL extends Renderer { this.webglVersion = this.drawingContext ? constants.WEBGL2 : constants.WEBGL; // If this is the main canvas, make sure the global `webglVersion` is set - this._pInst._setProperty('webglVersion', this.webglVersion); + 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 @@ -858,7 +858,7 @@ p5.RendererGL = class RendererGL extends Renderer { this._pInst, !isPGraphics ); - this._pInst._setProperty('_renderer', renderer); + this._pInst._renderer = renderer; renderer.resize(w, h); renderer._applyDefaults(); @@ -1362,8 +1362,7 @@ p5.RendererGL = class RendererGL extends Renderer { const pd = this._pInst._pixelDensity; const gl = this.GL; - pixelsState._setProperty( - 'pixels', + pixelsState.pixels = readPixelsWebGL( pixelsState.pixels, gl, @@ -1375,8 +1374,7 @@ p5.RendererGL = class RendererGL extends Renderer { gl.RGBA, gl.UNSIGNED_BYTE, this.height * pd - ) - ); + ); } updatePixels() { @@ -1448,12 +1446,10 @@ p5.RendererGL = class RendererGL extends Renderer { //resize pixels buffer const pixelsState = this._pixelsState; if (typeof pixelsState.pixels !== 'undefined') { - pixelsState._setProperty( - 'pixels', + pixelsState.pixels = new Uint8Array( this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 - ) - ); + ); } for (const framebuffer of this.framebuffers) { diff --git a/test/unit/math/trigonometry.js b/test/unit/math/trigonometry.js index cfe519e7a5..b8c2c86e2a 100644 --- a/test/unit/math/trigonometry.js +++ b/test/unit/math/trigonometry.js @@ -11,7 +11,6 @@ suite('Trigonometry', function() { _validateParameters: vi.fn() }; const mockP5Prototype = { - _setProperty: vi.fn() }; beforeEach(async function() { From 282cabbb11aa757644b3c3cae0e0834f6829ebb4 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 18 Sep 2024 19:59:17 +0100 Subject: [PATCH 009/120] Global functions now also use getter --- src/core/main.js | 94 +++--------------------------------------- test/unit/core/main.js | 3 +- 2 files changed, 8 insertions(+), 89 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index 1cc8bf5041..a4dc7ce29f 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -102,13 +102,16 @@ class p5 { // ensure correct reporting of window dimensions this._updateWindowSize(); - const friendlyBindGlobal = this._createFriendlyGlobalFunctionBinder(); const bindGlobal = (property) => { Object.defineProperty(window, property, { configurable: true, enumerable: true, get: () => { - return this[property] + if(typeof this[property] === 'function'){ + return this[property].bind(this); + }else{ + return this[property]; + } }, set: (newValue) => { Object.defineProperty(window, property, { @@ -133,22 +136,7 @@ class p5 { // All methods and properties with name starting with '_' will be skipped for (const p of Object.getOwnPropertyNames(p5.prototype)) { if(p[0] === '_') continue; - - if (typeof p5.prototype[p] === 'function') { - const ev = p.substring(2); - if (!this._events.hasOwnProperty(ev)) { - if (Math.hasOwnProperty(p) && Math[p] === p5.prototype[p]) { - // Multiple p5 methods are just native Math functions. These can be - // called without any binding. - friendlyBindGlobal(p, p5.prototype[p]); - } else { - friendlyBindGlobal(p, p5.prototype[p].bind(this)); - } - } - } else { - console.log(p); - bindGlobal(p, p5.prototype[p]); - } + bindGlobal(p); } // Attach its properties to the window @@ -443,76 +431,6 @@ class p5 { this._downKeys = {}; //Holds the key codes of currently pressed keys } - - // create a function which provides a standardized process for binding - // globals; this is implemented as a factory primarily so that there's a - // way to redefine what "global" means for the binding function so it - // can be used in scenarios like unit testing where the window object - // might not exist - _createFriendlyGlobalFunctionBinder(options = {}) { - const globalObject = options.globalObject || window; - const log = options.log || console.log.bind(console); - const propsToForciblyOverwrite = { - // p5.print actually always overwrites an existing global function, - // albeit one that is very unlikely to be used: - // - // https://developer.mozilla.org/en-US/docs/Web/API/Window/print - print: true - }; - - return (prop, value) => { - if ( - !p5.disableFriendlyErrors && - typeof IS_MINIFIED === 'undefined' && - typeof value === 'function' - ) { - try { - // Because p5 has so many common function names, it's likely - // that users may accidentally overwrite global p5 functions with - // their own variables. Let's allow this but log a warning to - // help users who may be doing this unintentionally. - // - // For more information, see: - // - // https://github.com/processing/p5.js/issues/1317 - - if (prop in globalObject && !(prop in propsToForciblyOverwrite)) { - throw new Error(`global "${prop}" already exists`); - } - - // It's possible that this might throw an error because there - // are a lot of edge-cases in which `Object.defineProperty` might - // not succeed; since this functionality is only intended to - // help beginners anyways, we'll just catch such an exception - // if it occurs, and fall back to legacy behavior. - Object.defineProperty(globalObject, prop, { - configurable: true, - enumerable: true, - get() { - return value; - }, - set(newValue) { - Object.defineProperty(globalObject, prop, { - configurable: true, - enumerable: true, - value: newValue, - writable: true - }); - log( - `You just changed the value of "${prop}", which was a p5 function. This could cause problems later if you're not careful.` - ); - } - }); - } catch (e) { - let message = `p5 had problems creating the global function "${prop}", possibly because your code is already using that name as a variable. You may want to rename your variable to something else.`; - p5._friendlyError(message, prop); - globalObject[prop] = value; - } - } else { - globalObject[prop] = value; - } - }; - } } // attach constants to p5 prototype diff --git a/test/unit/core/main.js b/test/unit/core/main.js index 0cbcc3cb66..074aa32c7e 100644 --- a/test/unit/core/main.js +++ b/test/unit/core/main.js @@ -71,7 +71,8 @@ suite('Core', function () { }); }); - suite('p5.prototype._createFriendlyGlobalFunctionBinder', function () { + // NOTE: need rewrite or will be taken care of by FES + suite.todo('p5.prototype._createFriendlyGlobalFunctionBinder', function () { var noop = function () {}; var createBinder = p5.prototype._createFriendlyGlobalFunctionBinder; var logMsg, globalObject, bind, iframe; From 3a356d5abdda7ba5c7038fb791006a83a7cabc48 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 19 Sep 2024 16:06:27 +0100 Subject: [PATCH 010/120] Move DOM initialization from p5.Renderer to individual renderers --- src/core/p5.Renderer.js | 27 +++++++++------------------ src/core/p5.Renderer2D.js | 28 +++++++++++++++++++++++++++- src/core/rendering.js | 28 ++++++++++------------------ src/webgl/p5.RendererGL.js | 15 +++++++++++++++ utils/sample-linter.mjs | 3 ++- 5 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index cc5eb60da4..e1dea6e8cf 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -21,21 +21,9 @@ p5.Renderer = class Renderer { constructor(elt, pInst, isMainCanvas) { this._pInst = this._pixelsState = pInst; this._events = {}; - this.canvas = elt; - this.width = this.canvas.offsetWidth; - this.height = this.canvas.offsetHeight; if (isMainCanvas) { this._isMainCanvas = true; - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - this._pInst.width = this.width; - this._pInst.height = this.height; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = 'none'; - this._styles = []; // non-main elt styles stored in p5.Renderer } // Renderer state machine @@ -66,6 +54,13 @@ 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; + } + // Makes a shallow copy of the current states // and push it into the push pop stack push() { @@ -108,20 +103,16 @@ p5.Renderer = class Renderer { /** * Resize our canvas element. */ - resize (w, h) { + resize(w, h) { this.width = w; this.height = h; - this.canvas.width = w * this._pInst._pixelDensity; - this.canvas.height = h * this._pInst._pixelDensity; - this.canvas.style.width = `${w}px`; - this.canvas.style.height = `${h}px`; if (this._isMainCanvas) { this._pInst.width = this.width; this._pInst.height = this.height; } } - get (x, y, w, h) { + get(x, y, w, h) { const pixelsState = this._pixelsState; const pd = pixelsState._pixelDensity; const canvas = this.canvas; diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 925b1cea30..579cd70ebd 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -15,9 +15,19 @@ 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; - this.elt = elt; + + 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); @@ -32,6 +42,18 @@ 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; + + return this.wrappedElt; + } + getFilterGraphicsLayer() { // create hidden webgl renderer if it doesn't exist if (!this.filterGraphicsLayer) { @@ -76,6 +98,10 @@ class Renderer2D extends Renderer { resize(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`; + this.canvas.style.height = `${h}px`; this.drawingContext.scale( this._pInst._pixelDensity, this._pInst._pixelDensity diff --git a/src/core/rendering.js b/src/core/rendering.js index c645c437fc..2729843070 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -133,6 +133,14 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { p5._validateParameters('createCanvas', arguments); //optional: renderer, otherwise defaults to p2d + let selectedRenderer = constants.P2D + // Check third argument whether it is renderer constants + if(Reflect.ownKeys(renderers).includes(renderer)){ + selectedRenderer = renderer; + } + + + let r; if (arguments[2] instanceof HTMLCanvasElement) { renderer = constants.P2D; @@ -217,29 +225,13 @@ p5.prototype.createCanvas = function (w, h, renderer, canvas) { } // Init our graphics renderer - //webgl mode - // if (r === constants.WEBGL) { - // this._setProperty('_renderer', new p5.RendererGL(c, this, true)); - // this._elements.push(this._renderer); - // NOTE: these needs to be taken cared of below - // const dimensions = - // this._renderer._adjustDimensions(w, h); - // w = dimensions.adjustedWidth; - // h = dimensions.adjustedHeight; - // } else { - // //P2D mode - // if (!this._defaultGraphicsCreated) { - // this._setProperty('_renderer', new p5.Renderer2D(c, this, true)); - // this._defaultGraphicsCreated = true; - // this._elements.push(this._renderer); - // } - // } this._renderer = new renderers[r](c, this, true); this._defaultGraphicsCreated = true; this._elements.push(this._renderer); - // Instead of resize, just create with the right size in the first place this._renderer.resize(w, h); this._renderer._applyDefaults(); + this._renderer.createCanvas(w, h, canvas); + // return this._renderer.createCanvas(w, h, canvas); return this._renderer; }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 02de5880fa..a55bf65d0c 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -435,6 +435,8 @@ 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._setAttributeDefaults(pInst); this._initContext(); this.isP3D = true; //lets us know we're in 3d mode @@ -447,6 +449,15 @@ p5.RendererGL = class RendererGL extends Renderer { 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(); this.states.uViewMatrix = new p5.Matrix(); @@ -1432,6 +1443,10 @@ p5.RendererGL = class RendererGL extends Renderer { */ resize(w, h) { Renderer.prototype.resize.call(this, w, h); + this.canvas.width = w * this._pInst._pixelDensity; + this.canvas.height = h * this._pInst._pixelDensity; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; this._origViewport = { width: this.GL.drawingBufferWidth, height: this.GL.drawingBufferHeight diff --git a/utils/sample-linter.mjs b/utils/sample-linter.mjs index 5862add25e..08b4fb94e1 100644 --- a/utils/sample-linter.mjs +++ b/utils/sample-linter.mjs @@ -21,7 +21,7 @@ Object.keys(dataDoc.consts).forEach(c => { }); dataDoc.classitems - .find(ci => ci.name === 'keyCode' && ci.class === 'p5') + .find(ci => ci.name === 'keyCode') .description.match(/[A-Z\r\n, _]{10,}/m)[0] .match(/[A-Z_]+/gm) .forEach(c => { @@ -248,6 +248,7 @@ function splitLines(text) { eslintFiles(null, process.argv.slice(2)) .then(result => { + if(result === true) process.exit(0); console.log(result.output); process.exit(result.report.errorCount === 0 ? 0 : 1); }); From b305b1b0032631b6a7d56a929925b511ff51e96e Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 14 Sep 2024 01:15:34 +0100 Subject: [PATCH 011/120] immediate mode geometry properties are fully deleted --- src/webgl/p5.Geometry.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 8ee028ea67..31bd08d4cf 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -450,6 +450,11 @@ p5.Geometry = class Geometry { this.vertexNormals.length = 0; this.uvs.length = 0; + for (const attr of this.userAttributes){ + delete this[attr.name]; + } + this.userAttributes.length = 0; + this.dirtyFlags = {}; } From 4dcf0d9c5e5e9d796db14f2f09a34766722b9470 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:35:46 +0100 Subject: [PATCH 012/120] updated renderer.setAttribute() call to reflect new method name --- src/core/shape/vertex.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 60c2cf1489..a2e02a976b 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2253,4 +2253,10 @@ p5.prototype.normal = function(x, y, z) { return this; }; +p5.prototype.setAttribute = function(attributeName, data){ + // this._assert3d('setAttribute'); + // p5._validateParameters('setAttribute', arguments); + this._renderer.setAttribute(attributeName, data); +}; + export default p5; From 9d7ab3743fe1c152c086d18314c0fe9ef1497d6f Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:36:18 +0100 Subject: [PATCH 013/120] removed debugging log --- src/webgl/GeometryBuilder.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index ac78ec7e94..4f9a0d4e8d 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -59,6 +59,17 @@ class GeometryBuilder { ...this.transformNormals(input.vertexNormals) ); this.geometry.uvs.push(...input.uvs); + const userAttributes = input.userAttributes; + if (userAttributes.length > 0){ + for (const attr of userAttributes){ + const name = attr.name; + const size = attr.size; + const data = input[name]; + for (let i = 0; i < data.length; i+=size){ + this.geometry.setAttribute(name, data.slice(i, i + size)); + } + } + } if (this.renderer._doFill) { this.geometry.faces.push( From 9b3909a6b8e81a615625420874c3f9d8ad24f5c8 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:37:02 +0100 Subject: [PATCH 014/120] updated to take array or single value --- src/webgl/p5.Geometry.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 31bd08d4cf..9aae274b0d 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1915,6 +1915,22 @@ p5.Geometry = class Geometry { } return this; } + + setAttribute(attributeName, data){ + const size = data.length ? data.length : 1; + if (!this.hasOwnProperty(attributeName)){ + this[attributeName] = []; + this.userAttributes.push({ + name: attributeName, + size: size + }); + } + if (size > 1){ + this[attributeName].push(...data); + } else{ + this[attributeName].push(data); + } + } }; /** From 913131eb35dd4788dc2ec6c34d8bf2b9e5cb91ba Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:37:44 +0100 Subject: [PATCH 015/120] removed empty lines --- src/webgl/p5.RenderBuffer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index 79851e485f..146d63e039 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -32,7 +32,6 @@ p5.RenderBuffer = class { if (!attr) { return; } - // check if the model has the appropriate source array let buffer = geometry[this.dst]; const src = model[this.src]; @@ -53,7 +52,6 @@ p5.RenderBuffer = class { 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; } From 88c7fbcace6e8522f35329a1a4f721b30be6f758 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:38:30 +0100 Subject: [PATCH 016/120] beginShape updated for tesselated shapes and to reset new attributes object --- src/webgl/p5.RendererGL.Immediate.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 47a656fa83..eb74f00898 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -33,6 +33,14 @@ import './p5.RenderBuffer'; p5.RendererGL.prototype.beginShape = function(mode) { this.immediateMode.shapeMode = mode !== undefined ? mode : constants.TESS; + if (this._useUserAttributes === true){ + for (const name of Object.keys(this.userAttributes)){ + delete this.immediateMode.geometry[name]; + } + delete this.userAttributes; + this._useUserAttributes = false; + } + this.tessyVertexSize = 12; this.immediateMode.geometry.reset(); this.immediateMode.contourIndices = []; return this; From 19a3d70558ca7b762bb087cb75e36d734eca7f47 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:40:47 +0100 Subject: [PATCH 017/120] set more than one custom attribute per shape/geometry. back fill with 0's in case not enough attributes set --- src/webgl/p5.RendererGL.Immediate.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index eb74f00898..16bb97d4bf 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -121,6 +121,19 @@ p5.RendererGL.prototype.vertex = function(x, y) { const vert = new p5.Vector(x, y, z); this.immediateMode.geometry.vertices.push(vert); this.immediateMode.geometry.vertexNormals.push(this._currentNormal); + if (this._useUserAttributes){ + const geom = this.immediateMode.geometry; + const verts = geom.vertices; + Object.entries(this.userAttributes).forEach(([name, data]) => { + const size = data.length ? data.length : 1; + if (verts.length > 0 && !geom.hasOwnProperty(name)) { + for (let i = 0; i < verts.length - 1; i++) { + this.immediateMode.geometry.setAttribute(name, Array(size).fill(0)); + } + } + this.immediateMode.geometry.setAttribute(name, data); + }); + } const vertexColor = this.curFillColor || [0.5, 0.5, 0.5, 1.0]; this.immediateMode.geometry.vertexColors.push( vertexColor[0], From ae52039870d88b321db838ea53df892747ec28b8 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:42:38 +0100 Subject: [PATCH 018/120] refactor setAttribute method to update a userAttributes object and increase the tesselation vertex size info --- src/webgl/p5.RendererGL.Immediate.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 16bb97d4bf..73daa1ac6b 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -186,6 +186,29 @@ p5.RendererGL.prototype.vertex = function(x, y) { return this; }; +p5.RendererGL.prototype.setAttribute = function(attributeName, data){ + // if attributeName is in one of default, throw some warning + if(!this._useUserAttributes){ + this._useUserAttributes = true; + this.userAttributes = {}; + } + const size = data.length ? data.length : 1; + if (!this.userAttributes.hasOwnProperty(attributeName)){ + this.tessyVertexSize += size; + } + this.userAttributes[attributeName] = data; + const buff = attributeName.concat('Buffer'); + const bufferExists = this.immediateMode + .buffers + .user + .some(buffer => buffer.dst === buff); + if(!bufferExists){ + this.immediateMode.buffers.user.push( + new p5.RenderBuffer(size, attributeName, buff, attributeName, this) + ); + } +}; + /** * Sets the normal to use for subsequent vertices. * @private From 72cf3bc89aef90c63d862e57ce60abf9bcf9d422 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:44:29 +0100 Subject: [PATCH 019/120] _tesselateShape works with custom attributes --- src/webgl/p5.RendererGL.Immediate.js | 31 ++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 73daa1ac6b..5c3b633383 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -312,6 +312,7 @@ p5.RendererGL.prototype.endShape = function( this.immediateMode._bezierVertex.length = 0; this.immediateMode._quadraticVertex.length = 0; this.immediateMode._curveVertex.length = 0; + return this; }; @@ -326,7 +327,6 @@ p5.RendererGL.prototype.endShape = function( */ p5.RendererGL.prototype._processVertices = function(mode) { if (this.immediateMode.geometry.vertices.length === 0) return; - const calculateStroke = this._doStroke; const shouldClose = mode === constants.CLOSE; if (calculateStroke) { @@ -479,20 +479,47 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals[i].y, this.immediateMode.geometry.vertexNormals[i].z ); + if (this._useUserAttributes){ + const userAttributesArray = Object.entries(this.userAttributes); + for (let [name, data] of userAttributesArray){ + const size = data.length ? data.length : 1; + for (let j = 0; j < size; j++){ + contours[contours.length-1].push( + this.immediateMode.geometry[name][i * size + j] + );} + } + } } const polyTriangles = this._triangulate(contours); const originalVertices = this.immediateMode.geometry.vertices; this.immediateMode.geometry.vertices = []; this.immediateMode.geometry.vertexNormals = []; this.immediateMode.geometry.uvs = []; + if (this._useUserAttributes){ + const userAttributeNames = Object.keys(this.userAttributes); + for (let name of userAttributeNames){ + delete this.immediateMode.geometry[name]; + } + } const colors = []; for ( let j = 0, polyTriLength = polyTriangles.length; j < polyTriLength; - j = j + p5.RendererGL.prototype.tessyVertexSize + j = j + this.tessyVertexSize ) { colors.push(...polyTriangles.slice(j + 5, j + 9)); this.normal(...polyTriangles.slice(j + 9, j + 12)); + if(this._useUserAttributes){ + let offset = 12; + const userAttributesArray = Object.entries(this.userAttributes); + for (let [name, data] of userAttributesArray){ + const size = data.length ? data.length : 1; + const start = j + offset; + const end = start + size; + this.setAttribute(name, polyTriangles.slice(start, end)); + offset = end; + } + } this.vertex(...polyTriangles.slice(j, j + 5)); } if (this.geometryBuilder) { From 03d2e5ef20441ea80d0a4a19adfa65ef8d410fe1 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:45:37 +0100 Subject: [PATCH 020/120] changed references to prototype.tessyvertexSize to this.tessyVertexSize so that custom attributes can increase size of vertex data --- src/webgl/p5.RendererGL.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index f712787233..4c4b274183 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -2475,7 +2475,7 @@ p5.RendererGL = class RendererGL extends Renderer { for ( let j = 0; j < contour.length; - j += p5.RendererGL.prototype.tessyVertexSize + j += this.tessyVertexSize ) { if (contour[j + 2] !== z) { allSameZ = false; @@ -2498,11 +2498,11 @@ p5.RendererGL = class RendererGL extends Renderer { for ( let j = 0; j < contour.length; - j += p5.RendererGL.prototype.tessyVertexSize + j += this.tessyVertexSize ) { const coords = contour.slice( j, - j + p5.RendererGL.prototype.tessyVertexSize + j + this.tessyVertexSize ); this._tessy.gluTessVertex(coords, coords); } From 616d7cac6c05837fcd32b6b452c45f4c6792282d Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:46:30 +0100 Subject: [PATCH 021/120] free user buffers --- src/webgl/p5.RendererGL.Retained.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 49f2dd772b..2405aabe0b 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -62,6 +62,7 @@ p5.RendererGL.prototype._freeBuffers = function(gId) { // free all the buffers freeBuffers(this.retainedMode.buffers.stroke); freeBuffers(this.retainedMode.buffers.fill); + freeBuffers(this.retainedMode.buffers.user); }; /** From 8d2656f9c0d66f1485cc0f5db34bf14db4f297af Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:49:45 +0100 Subject: [PATCH 022/120] moved user attributes section to the bottom of createBuffers() --- src/webgl/p5.RendererGL.Retained.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 2405aabe0b..0a47229225 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -115,6 +115,20 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { ? model.lineVertices.length / 3 : 0; + if (model.userAttributes.length > 0){ + for (const attr of model.userAttributes){ + const buff = attr.name.concat('Buffer'); + const bufferExists = this.retainedMode + .buffers + .user + .some(buffer => buffer.dst === buff); + if(!bufferExists){ + this.retainedMode.buffers.user.push( + new p5.RenderBuffer(attr.size, attr.name, buff, attr.name, this) + ); + } + } + } return buffers; }; From 859ac70625456f2ab99b164b1e29ce8173a4fc31 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 15 Sep 2024 15:50:42 +0100 Subject: [PATCH 023/120] ignore my dev folder --- .gitignore | 51 ++++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 7883f080bb..9af31927d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,26 @@ -*.DS_Store -.project -node_modules/* -experiments/* -lib_old/* -lib/p5.* -lib/modules -docs/reference/* -!*.gitkeep -examples/3d/ -.idea -dist/ -p5.zip -bower-repo/ -p5-website/ -.vscode/settings.json -.nyc_output/* -coverage/ -lib/p5-test.js -release/ -yarn.lock -docs/data.json -analyzer/ -preview/ -__screenshots__/ \ No newline at end of file +*.DS_Store +.project +node_modules/* +experiments/* +lib_old/* +lib/p5.* +lib/modules +docs/reference/* +!*.gitkeep +examples/3d/ +.idea +dist/ +p5.zip +bower-repo/ +p5-website/ +.vscode/settings.json +.nyc_output/* +coverage/ +lib/p5-test.js +release/ +yarn.lock +docs/data.json +analyzer/ +preview/ +__screenshots__/ +attributes-example/ \ No newline at end of file From cafdc1bc30ce98a8734fab47f192a77daf71d0a8 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Thu, 19 Sep 2024 13:10:51 +0100 Subject: [PATCH 024/120] fixed a bug in _tesselateShape miscounting memory offset for custom attributes --- src/webgl/p5.RendererGL.Immediate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 5c3b633383..a36880c56e 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -517,7 +517,7 @@ p5.RendererGL.prototype._tesselateShape = function() { const start = j + offset; const end = start + size; this.setAttribute(name, polyTriangles.slice(start, end)); - offset = end; + offset += size; } } this.vertex(...polyTriangles.slice(j, j + 5)); From 0e35884216e0614e5c37f019f43f4fb79568a313 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 12:07:06 +0100 Subject: [PATCH 025/120] define user attributes array upfront --- src/webgl/p5.Geometry.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 9aae274b0d..0414b0b30c 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -283,6 +283,8 @@ p5.Geometry = class Geometry { // One color per vertex representing the stroke color at that vertex this.vertexStrokeColors = []; + this.userAttributes = []; + // One color per line vertex, generated automatically based on // vertexStrokeColors in _edgesToVertices() this.lineVertexColors = new p5.DataArray(); From d842f056a15a4e3a48347cddeb512b1eb731ee2b Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 12:07:26 +0100 Subject: [PATCH 026/120] prepare immediate user buffers --- src/webgl/p5.RendererGL.Immediate.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index a36880c56e..296103afe6 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -590,6 +590,11 @@ p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { for (const buff of this.immediateMode.buffers.fill) { buff._prepareBuffer(this.immediateMode.geometry, shader); } + if (this._useUserAttributes){ + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + } shader.disableRemainingAttributes(); this._applyColorBlend( @@ -636,6 +641,11 @@ p5.RendererGL.prototype._drawImmediateStroke = function() { for (const buff of this.immediateMode.buffers.stroke) { buff._prepareBuffer(this.immediateMode.geometry, shader); } + if (this._useUserAttributes){ + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + } shader.disableRemainingAttributes(); this._applyColorBlend( this.curStrokeColor, From 33259c01d74521e7cadc3e19cf694ced3c82a4da Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 12:08:18 +0100 Subject: [PATCH 027/120] bool for immediate user attributes, arrays for user attributes p5.Renderbuffers --- src/webgl/p5.RendererGL.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 4c4b274183..6834cf0ece 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -575,7 +575,7 @@ p5.RendererGL = class RendererGL extends Renderer { this.userFillShader = undefined; this.userStrokeShader = undefined; this.userPointShader = undefined; - + this._useUserAttributes = undefined; // Default drawing is done in Retained Mode // Geometry and Material hashes stored here this.retainedMode = { @@ -599,7 +599,8 @@ p5.RendererGL = class RendererGL extends Renderer { text: [ new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ] + ], + user:[] } }; @@ -627,7 +628,8 @@ p5.RendererGL = class RendererGL extends Renderer { new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) ], - point: this.GL.createBuffer() + point: this.GL.createBuffer(), + user:[] } }; From b5c3213dfad49c504bfb3d5efe4044864e1ab88b Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 12:09:00 +0100 Subject: [PATCH 028/120] used const reference to geometry which was already defined but unused --- src/webgl/p5.RendererGL.Retained.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 0a47229225..cffdb35729 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -145,7 +145,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { if ( !this.geometryBuilder && this._doFill && - this.retainedMode.geometry[gId].vertexCount > 0 + geometry.vertexCount > 0 ) { this._useVertexColor = (geometry.model.vertexColors.length > 0); const fillShader = this._getRetainedFillShader(); From e038c9c730d9092b7e208de06c1db4c9d14bb0b3 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 12:09:30 +0100 Subject: [PATCH 029/120] prepare user attribute buffers --- src/webgl/p5.RendererGL.Retained.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index cffdb35729..c30bae7a08 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -153,6 +153,11 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { for (const buff of this.retainedMode.buffers.fill) { buff._prepareBuffer(geometry, fillShader); } + if (geometry.model.userAttributes.length > 0){ + for (const buff of this.retainedMode.buffers.user){ + buff._prepareBuffer(geometry, fillShader); + } + } fillShader.disableRemainingAttributes(); if (geometry.indexBuffer) { //vertex index buffer @@ -173,6 +178,11 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { for (const buff of this.retainedMode.buffers.stroke) { buff._prepareBuffer(geometry, strokeShader); } + if (geometry.model.userAttributes.length > 0){ + for (const buff of this.retainedMode.buffers.user){ + buff._prepareBuffer(geometry, strokeShader); + } + } strokeShader.disableRemainingAttributes(); this._applyColorBlend( this.curStrokeColor, From f7d2b992cf28eed42974963ce7c1cbb334f86ab1 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 15:06:46 +0100 Subject: [PATCH 030/120] handled differences in custom attributes between geometries in the builder --- src/webgl/GeometryBuilder.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 4f9a0d4e8d..256420f9a2 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -59,12 +59,32 @@ class GeometryBuilder { ...this.transformNormals(input.vertexNormals) ); this.geometry.uvs.push(...input.uvs); - const userAttributes = input.userAttributes; - if (userAttributes.length > 0){ - for (const attr of userAttributes){ - const name = attr.name; - const size = attr.size; + + const inputUserAttributes = Object.entries(input.userAttributes); + const currentUserAttributes = Object.entries(this.geometry.userAttributes); + + if (currentUserAttributes.length > 0){ + for (const [name, size] of currentUserAttributes){ + if (name in input.userAttributes){ + continue; + } + const numMissingValues = size * input.vertices.length; + // this.geometry[name].concat(Array(numMissingValues).fill(0)); + this.geometry[name] = (this.geometry[name] || []).concat(Array(numMissingValues).fill(0)); + } + } + if (inputUserAttributes.length > 0){ + for (const [name, size] of inputUserAttributes){ const data = input[name]; + if (!(name in this.geometry.userAttributes) && this.geometry.vertices.length - input.vertices.length > 0){ + const numMissingValues = size * (this.geometry.vertices.length - input.vertices.length) - size; + this.geometry.setAttribute(name, Array(size).fill(0)); + // this.geometry[name].concat(Array(numMissingValues).fill(0)); + this.geometry[name] = this.geometry[name].concat(Array(numMissingValues).fill(0)); + } + if (this.geometry.userAttributes[name] != size){ + console.log("This user attribute has different sizes"); + } for (let i = 0; i < data.length; i+=size){ this.geometry.setAttribute(name, data.slice(i, i + size)); } From 55a37af5097c2e3a36070de3ab7c3334d0bf31a3 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 15:07:19 +0100 Subject: [PATCH 031/120] user attributes is now a single Object instead of an array of them --- src/webgl/p5.Geometry.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 0414b0b30c..fb2328de44 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -283,7 +283,7 @@ p5.Geometry = class Geometry { // One color per vertex representing the stroke color at that vertex this.vertexStrokeColors = []; - this.userAttributes = []; + this.userAttributes = {}; // One color per line vertex, generated automatically based on // vertexStrokeColors in _edgesToVertices() @@ -452,10 +452,10 @@ p5.Geometry = class Geometry { this.vertexNormals.length = 0; this.uvs.length = 0; - for (const attr of this.userAttributes){ + for (const attr of Object.keys(this.userAttributes)){ delete this[attr.name]; } - this.userAttributes.length = 0; + this.userAttributes = {}; this.dirtyFlags = {}; } @@ -1922,10 +1922,7 @@ p5.Geometry = class Geometry { const size = data.length ? data.length : 1; if (!this.hasOwnProperty(attributeName)){ this[attributeName] = []; - this.userAttributes.push({ - name: attributeName, - size: size - }); + this.userAttributes[attributeName] = size; } if (size > 1){ this[attributeName].push(...data); From dd0801467be738ad0742155c35ca1f816e76f5e3 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sun, 22 Sep 2024 15:08:00 +0100 Subject: [PATCH 032/120] reflect the change of geometry custom attributes from objects in an array to single object --- src/webgl/p5.RendererGL.Retained.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index c30bae7a08..4351d7c067 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -115,16 +115,16 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { ? model.lineVertices.length / 3 : 0; - if (model.userAttributes.length > 0){ - for (const attr of model.userAttributes){ - const buff = attr.name.concat('Buffer'); + if (Object.keys(model.userAttributes).length > 0){ + for (const [name, size] of Object.entries(model.userAttributes)){ + const buff = name.concat('Buffer'); const bufferExists = this.retainedMode .buffers .user .some(buffer => buffer.dst === buff); if(!bufferExists){ this.retainedMode.buffers.user.push( - new p5.RenderBuffer(attr.size, attr.name, buff, attr.name, this) + new p5.RenderBuffer(size, name, buff, name, this) ); } } @@ -153,7 +153,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { for (const buff of this.retainedMode.buffers.fill) { buff._prepareBuffer(geometry, fillShader); } - if (geometry.model.userAttributes.length > 0){ + if (Object.keys(geometry.model.userAttributes).length > 0){ for (const buff of this.retainedMode.buffers.user){ buff._prepareBuffer(geometry, fillShader); } @@ -178,7 +178,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { for (const buff of this.retainedMode.buffers.stroke) { buff._prepareBuffer(geometry, strokeShader); } - if (geometry.model.userAttributes.length > 0){ + if (Object.keys(geometry.model.userAttributes).length > 0){ for (const buff of this.retainedMode.buffers.user){ buff._prepareBuffer(geometry, strokeShader); } From a801e4973a9a39c8eff2ccff0b4cba59c1552928 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 11:18:46 +0100 Subject: [PATCH 033/120] Remove Object.keys intermediate array and loop directly on user attributes object --- src/webgl/p5.RendererGL.Immediate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 296103afe6..1ce2c97b9c 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -34,7 +34,7 @@ p5.RendererGL.prototype.beginShape = function(mode) { this.immediateMode.shapeMode = mode !== undefined ? mode : constants.TESS; if (this._useUserAttributes === true){ - for (const name of Object.keys(this.userAttributes)){ + for (const name in this.userAttributes){ delete this.immediateMode.geometry[name]; } delete this.userAttributes; From 6ec85c118074d4b03f56504ab2535b8c2f7d8c56 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 11:21:20 +0100 Subject: [PATCH 034/120] Added an optional size parameter to setAttribute so that bigger chunks of data (without loops) can be submitted so long as the size is defined. --- src/webgl/p5.Geometry.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index fb2328de44..6e41e06efa 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1918,13 +1918,16 @@ p5.Geometry = class Geometry { return this; } - setAttribute(attributeName, data){ - const size = data.length ? data.length : 1; + setAttribute(attributeName, data, size = data.length ? data.length : 1){ if (!this.hasOwnProperty(attributeName)){ this[attributeName] = []; this.userAttributes[attributeName] = size; } - if (size > 1){ + if (size != this.userAttributes[attributeName]){ + p5._friendlyError(`Custom attribute ${attributeName} has been set with various data sizes. You can change it's name, + or if it was an accident, set ${attributeName} to have the same number of inputs each time!`, 'setAttribute()'); + } + if (data.length){ this[attributeName].push(...data); } else{ this[attributeName].push(data); From 59ed53bd1a7d3a0c8a6910ca4cfaa8911ed18fe6 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 11:25:13 +0100 Subject: [PATCH 035/120] Remove object.entries iteration for 'in' loop and changed to use the new optional size parameter on set attribute, which cuts a layer of loops --- src/webgl/GeometryBuilder.js | 78 ++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 256420f9a2..b4380cc65c 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -60,37 +60,65 @@ class GeometryBuilder { ); this.geometry.uvs.push(...input.uvs); - const inputUserAttributes = Object.entries(input.userAttributes); - const currentUserAttributes = Object.entries(this.geometry.userAttributes); + const inputAttrs = input.userAttributes; + const builtAttrs = this.geometry.userAttributes; + const numPreviousVertices = this.geometry.vertices.length - input.vertices.length; - if (currentUserAttributes.length > 0){ - for (const [name, size] of currentUserAttributes){ - if (name in input.userAttributes){ - continue; - } - const numMissingValues = size * input.vertices.length; - // this.geometry[name].concat(Array(numMissingValues).fill(0)); - this.geometry[name] = (this.geometry[name] || []).concat(Array(numMissingValues).fill(0)); + for (const attr in builtAttrs){ + if (attr in inputAttrs){ + continue; } + const size = builtAttrs[attr]; + const numMissingValues = size * input.vertices.length; + const missingValues = Array(numMissingValues).fill(0); + this.geometry.setAttribute(attr, missingValues, size); } - if (inputUserAttributes.length > 0){ - for (const [name, size] of inputUserAttributes){ - const data = input[name]; - if (!(name in this.geometry.userAttributes) && this.geometry.vertices.length - input.vertices.length > 0){ - const numMissingValues = size * (this.geometry.vertices.length - input.vertices.length) - size; - this.geometry.setAttribute(name, Array(size).fill(0)); - // this.geometry[name].concat(Array(numMissingValues).fill(0)); - this.geometry[name] = this.geometry[name].concat(Array(numMissingValues).fill(0)); - } - if (this.geometry.userAttributes[name] != size){ - console.log("This user attribute has different sizes"); - } - for (let i = 0; i < data.length; i+=size){ - this.geometry.setAttribute(name, data.slice(i, i + size)); - } + for (const attr in inputAttrs){ + const data = input[attr]; + const size = inputAttrs[attr]; + if (numPreviousVertices > 0 && !(attr in this.geometry.userAttributes)){ + const numMissingValues = size * numPreviousVertices; + const missingValues = Array(numMissingValues).fill(0); + console.log(`ATTR: ${attr}, SIZE@ ${size}, NUMMISSINVALS: ${numMissingValues}`); + this.geometry.setAttribute(attr, missingValues, size); + } + if (this.geometry.userAttributes[attr] != size){ + console.log("This user attribute has different sizes"); } + this.geometry.setAttribute(attr, data, size); } + // const inputUserAttributes = Object.entries(input.userAttributes); + // const currentUserAttributes = Object.entries(this.geometry.userAttributes); + + // if (currentUserAttributes.length > 0){ + // for (const [name, size] of currentUserAttributes){ + // if (name in input.userAttributes){ + // continue; + // } + // const numMissingValues = size * input.vertices.length; + // // this.geometry[name].concat(Array(numMissingValues).fill(0)); + // this.geometry[name] = (this.geometry[name] || []).concat(Array(numMissingValues).fill(0)); + // } + // } + // if (inputUserAttributes.length > 0){ + // for (const [name, size] of inputUserAttributes){ + // const data = input[name]; + // if (!(name in this.geometry.userAttributes) && this.geometry.vertices.length - input.vertices.length > 0){ + // const numMissingValues = size * (this.geometry.vertices.length - input.vertices.length) - size; + // this.geometry.setAttribute(name, Array(size).fill(0)); + // console.log(`ATTR: ${name}, SIZE@ ${size}, NUMMISSINVALS: ${numMissingValues}`); + // this.geometry[name] = this.geometry[name].concat(Array(numMissingValues).fill(0)); + // } + // if (this.geometry.userAttributes[name] != size){ + // console.log("This user attribute has different sizes"); + // } + // for (let i = 0; i < data.length; i+=size){ + // this.geometry.setAttribute(name, data.slice(i, i + size)); + // } + // } + // } + if (this.renderer._doFill) { this.geometry.faces.push( ...input.faces.map(f => f.map(idx => idx + startIdx)) From 3d195217c34e26cc86edab8208cabaacf7afe52c Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 11:26:32 +0100 Subject: [PATCH 036/120] remove old implementation with extra loop --- src/webgl/GeometryBuilder.js | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index b4380cc65c..d535914635 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -79,46 +79,11 @@ class GeometryBuilder { if (numPreviousVertices > 0 && !(attr in this.geometry.userAttributes)){ const numMissingValues = size * numPreviousVertices; const missingValues = Array(numMissingValues).fill(0); - console.log(`ATTR: ${attr}, SIZE@ ${size}, NUMMISSINVALS: ${numMissingValues}`); this.geometry.setAttribute(attr, missingValues, size); } - if (this.geometry.userAttributes[attr] != size){ - console.log("This user attribute has different sizes"); - } this.geometry.setAttribute(attr, data, size); } - // const inputUserAttributes = Object.entries(input.userAttributes); - // const currentUserAttributes = Object.entries(this.geometry.userAttributes); - - // if (currentUserAttributes.length > 0){ - // for (const [name, size] of currentUserAttributes){ - // if (name in input.userAttributes){ - // continue; - // } - // const numMissingValues = size * input.vertices.length; - // // this.geometry[name].concat(Array(numMissingValues).fill(0)); - // this.geometry[name] = (this.geometry[name] || []).concat(Array(numMissingValues).fill(0)); - // } - // } - // if (inputUserAttributes.length > 0){ - // for (const [name, size] of inputUserAttributes){ - // const data = input[name]; - // if (!(name in this.geometry.userAttributes) && this.geometry.vertices.length - input.vertices.length > 0){ - // const numMissingValues = size * (this.geometry.vertices.length - input.vertices.length) - size; - // this.geometry.setAttribute(name, Array(size).fill(0)); - // console.log(`ATTR: ${name}, SIZE@ ${size}, NUMMISSINVALS: ${numMissingValues}`); - // this.geometry[name] = this.geometry[name].concat(Array(numMissingValues).fill(0)); - // } - // if (this.geometry.userAttributes[name] != size){ - // console.log("This user attribute has different sizes"); - // } - // for (let i = 0; i < data.length; i+=size){ - // this.geometry.setAttribute(name, data.slice(i, i + size)); - // } - // } - // } - if (this.renderer._doFill) { this.geometry.faces.push( ...input.faces.map(f => f.map(idx => idx + startIdx)) From 04201c524535a5f325ca6d8403b6d7c4e2bca5f1 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 11:27:27 +0100 Subject: [PATCH 037/120] Early return if the does not have a custom attribute required by the render buffer --- src/webgl/p5.RenderBuffer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index 146d63e039..96fd2fc5e9 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -35,6 +35,9 @@ p5.RenderBuffer = class { // 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; From 68d9132f30beb53fe2b9c12993dc6983d75d74fb Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 11:28:35 +0100 Subject: [PATCH 038/120] Replaced Object.keys/entries arrays with 'for in' loops and use new setAttribute size parameter to optimize --- src/webgl/p5.RendererGL.Immediate.js | 52 ++++++++++++---------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 1ce2c97b9c..5195d47566 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -121,19 +121,20 @@ p5.RendererGL.prototype.vertex = function(x, y) { const vert = new p5.Vector(x, y, z); this.immediateMode.geometry.vertices.push(vert); this.immediateMode.geometry.vertexNormals.push(this._currentNormal); - if (this._useUserAttributes){ + + for (const attr in this.userAttributes){ const geom = this.immediateMode.geometry; const verts = geom.vertices; - Object.entries(this.userAttributes).forEach(([name, data]) => { - const size = data.length ? data.length : 1; - if (verts.length > 0 && !geom.hasOwnProperty(name)) { - for (let i = 0; i < verts.length - 1; i++) { - this.immediateMode.geometry.setAttribute(name, Array(size).fill(0)); - } - } - this.immediateMode.geometry.setAttribute(name, data); - }); + const data = this.userAttributes[attr]; + const size = data.length ? data.length : 1; + if (!geom.hasOwnProperty(attr) && verts.length > 1) { + const numMissingValues = size * (verts.length - 1); + const missingValues = Array(numMissingValues).fill(0); + geom.setAttribute(attr, missingValues, size); + } + geom.setAttribute(attr, data); } + const vertexColor = this.curFillColor || [0.5, 0.5, 0.5, 1.0]; this.immediateMode.geometry.vertexColors.push( vertexColor[0], @@ -479,15 +480,12 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals[i].y, this.immediateMode.geometry.vertexNormals[i].z ); - if (this._useUserAttributes){ - const userAttributesArray = Object.entries(this.userAttributes); - for (let [name, data] of userAttributesArray){ - const size = data.length ? data.length : 1; - for (let j = 0; j < size; j++){ - contours[contours.length-1].push( - this.immediateMode.geometry[name][i * size + j] - );} - } + for (const attr in this.userAttributes){ + const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; + const start = i * size; + const end = start + size; + const vals = this.immediateMode.geometry[attr].slice(start, end); + contours[contours.length-1].push(...vals); } } const polyTriangles = this._triangulate(contours); @@ -495,11 +493,8 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertices = []; this.immediateMode.geometry.vertexNormals = []; this.immediateMode.geometry.uvs = []; - if (this._useUserAttributes){ - const userAttributeNames = Object.keys(this.userAttributes); - for (let name of userAttributeNames){ - delete this.immediateMode.geometry[name]; - } + for (const attr in this.userAttributes){ + delete this.immediateMode.geometry[attr] } const colors = []; for ( @@ -509,14 +504,13 @@ p5.RendererGL.prototype._tesselateShape = function() { ) { colors.push(...polyTriangles.slice(j + 5, j + 9)); this.normal(...polyTriangles.slice(j + 9, j + 12)); - if(this._useUserAttributes){ + { let offset = 12; - const userAttributesArray = Object.entries(this.userAttributes); - for (let [name, data] of userAttributesArray){ - const size = data.length ? data.length : 1; + for (const attr in this.userAttributes){ + const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; const start = j + offset; const end = start + size; - this.setAttribute(name, polyTriangles.slice(start, end)); + this.setAttribute(attr, polyTriangles.slice(start, end), size); offset += size; } } From 504cf5aefff369671da9af335b33d83992cd87d8 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 11:45:14 +0100 Subject: [PATCH 039/120] simplify for loop and remove unnecessary check --- src/webgl/p5.RendererGL.Retained.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 4351d7c067..630256fe64 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -115,18 +115,17 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { ? model.lineVertices.length / 3 : 0; - if (Object.keys(model.userAttributes).length > 0){ - for (const [name, size] of Object.entries(model.userAttributes)){ - const buff = name.concat('Buffer'); - const bufferExists = this.retainedMode - .buffers - .user - .some(buffer => buffer.dst === buff); - if(!bufferExists){ - this.retainedMode.buffers.user.push( - new p5.RenderBuffer(size, name, buff, name, this) - ); - } + for (const attr in model.userAttributes){ + const buff = attr.concat('Buffer'); + const size = model.userAttributes[attr]; + const bufferExists = this.retainedMode + .buffers + .user + .some(buffer => buffer.dst === buff); + if (!bufferExists){ + this.retainedMode.buffers.user.push( + new p5.RenderBuffer(size, attr, buff, attr, this) + ); } } return buffers; From e64b3f09c2d92c99a39ac52e0e78f2c4acd14213 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 12:46:46 +0100 Subject: [PATCH 040/120] Fixed a bug where if an attribute was set but not applied to any vertices it would not render properly when tessellated --- src/webgl/p5.RendererGL.Immediate.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 5195d47566..5824f06fec 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -484,8 +484,13 @@ p5.RendererGL.prototype._tesselateShape = function() { const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; const start = i * size; const end = start + size; - const vals = this.immediateMode.geometry[attr].slice(start, end); - contours[contours.length-1].push(...vals); + if (this.immediateMode.geometry[attr]){ + const vals = this.immediateMode.geometry[attr].slice(start, end); + contours[contours.length-1].push(...vals); + } else{ + delete this.userAttributes[attr]; + this.tessyVertexSize -= size; + } } } const polyTriangles = this._triangulate(contours); @@ -507,11 +512,11 @@ p5.RendererGL.prototype._tesselateShape = function() { { let offset = 12; for (const attr in this.userAttributes){ - const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; - const start = j + offset; - const end = start + size; - this.setAttribute(attr, polyTriangles.slice(start, end), size); - offset += size; + const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; + const start = j + offset; + const end = start + size; + this.setAttribute(attr, polyTriangles.slice(start, end), size); + offset += size; } } this.vertex(...polyTriangles.slice(j, j + 5)); From f38819b39acc1599227a64236e803844fb5756f1 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 15:35:01 +0100 Subject: [PATCH 041/120] Changed block to backticks in normal documentation --- src/core/shape/vertex.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index a2e02a976b..3e134e7844 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2091,7 +2091,7 @@ p5.prototype.vertex = function(x, y, moveTo, u, v) { * `normal()` will affect all following vertices until `normal()` is called * again: * - * + * ```javascript * beginShape(); * * // Set the vertex normal. @@ -2114,7 +2114,7 @@ p5.prototype.vertex = function(x, y, moveTo, u, v) { * vertex(-30, 30, 0); * * endShape(); - * + * ``` * * @method normal * @param {p5.Vector} vector vertex normal as a p5.Vector object. From f30e3ee8273558ff8495dffd4b8eaeb0d0ee1af6 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 15:37:54 +0100 Subject: [PATCH 042/120] Initial draft of setAttribute() function documentation --- src/core/shape/vertex.js | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 3e134e7844..3f04b8ea3b 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2253,6 +2253,77 @@ p5.prototype.normal = function(x, y, z) { return this; }; +/** Sets the shader's vertex attribute variables. + * + * Shader programs run on the computer's graphics processing unit (GPU) + * They live in a part of the computer's memory that's completely separate from + * the sketch that runs them. Attributes are variables attached to vertices + * within a shader program. They provide a way to attach data to vertices + * and pass values from a sketch running on the CPU to a shader program. + * + * The first parameter, `attributeName`, is a string with the attribute's name. + * + * The second parameter, `data`, is the value that should be assigned to the + * attribute. 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 interpreted according to the WebGL + * specification. Common types include `float`, `vec2`, `vec3`, `vec4` or matrices. + * + * @example + *
+ * + * let vertSrc = ` + * precision highp float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * attribute vec3 aPosition; + * attribute vec2 aOffset; + * + * void main(){ + * vec4 positionVec4 = vec4(aPosition.xyz, 1.0); + * positionVec4.xy += aOffset; + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * let fragSrc = ` + * precision highp float; + * + * void main(){ + * gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0); + * } + * `; + * + * function setup(){ + * createCanvas(100, 100, WEBGL); + * let myShader = createShader(vertSrc, fragSrc); + * shader(myShader); + * noStroke(); + * describe('A wobbly, cyan circle on a grey background.'); + * } + * + * function draw(){ + * background(125); + * beginShape(); + * for (let i = 0; i < 30; i++){ + * let x = 40 * cos(i/30 * TWO_PI); + * let y = 40 * sin(i/30 * TWO_PI); + * let xOff = 10 * noise(x + millis()/1000) - 5; + * let yOff = 10 * noise(y + millis()/1000) - 5; + * setAttribute('aOffset', [xOff, yOff]); + * vertex(x, y); + * } + * endShape(CLOSE); + * } + * + *
+/ +/** + * @method setAttribute + * @param {String} attributeName the name of the vertex attribute. + * @param {Number|Number[]} data the data tied to the vertex attribute. + */ p5.prototype.setAttribute = function(attributeName, data){ // this._assert3d('setAttribute'); // p5._validateParameters('setAttribute', arguments); From fc09ce1505ef35db1372c03cda56dff440708a2a Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 15:41:40 +0100 Subject: [PATCH 043/120] added old documentation files back for compatibility & preview --- docs/documented-method.js | 60 ++++++++ docs/preprocessor.js | 314 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 374 insertions(+) create mode 100644 docs/documented-method.js create mode 100644 docs/preprocessor.js diff --git a/docs/documented-method.js b/docs/documented-method.js new file mode 100644 index 0000000000..1a7e62a283 --- /dev/null +++ b/docs/documented-method.js @@ -0,0 +1,60 @@ +// https://github.com/umdjs/umd/blob/main/templates/returnExports.js +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.DocumentedMethod = factory(); + } + }(this, function () { + function extend(target, src) { + Object.keys(src).forEach(function(prop) { + target[prop] = src[prop]; + }); + return target; + } + + function DocumentedMethod(classitem) { + extend(this, classitem); + + if (this.overloads) { + // Make each overload inherit properties from their parent + // classitem. + this.overloads = this.overloads.map(function(overload) { + return extend(Object.create(this), overload); + }, this); + + if (this.params) { + throw new Error('params for overloaded methods should be undefined'); + } + + this.params = this._getMergedParams(); + } + } + + DocumentedMethod.prototype = { + // Merge parameters across all overloaded versions of this item. + _getMergedParams: function() { + const paramNames = {}; + const params = []; + + this.overloads.forEach(function(overload) { + if (!overload.params) { + return; + } + overload.params.forEach(function(param) { + if (param.name in paramNames) { + return; + } + paramNames[param.name] = param; + params.push(param); + }); + }); + + return params; + } + }; + + return DocumentedMethod; + })); \ No newline at end of file diff --git a/docs/preprocessor.js b/docs/preprocessor.js new file mode 100644 index 0000000000..531b034f28 --- /dev/null +++ b/docs/preprocessor.js @@ -0,0 +1,314 @@ +const marked = require('marked'); +const Entities = require('html-entities').AllHtmlEntities; + +const DocumentedMethod = require('./documented-method'); + +function smokeTestMethods(data) { + data.classitems.forEach(function(classitem) { + if (classitem.itemtype === 'method') { + new DocumentedMethod(classitem); + + if ( + classitem.access !== 'private' && + classitem.file.slice(0, 3) === 'src' && + classitem.name && + !classitem.example + ) { + console.log( + classitem.file + + ':' + + classitem.line + + ': ' + + classitem.itemtype + + ' ' + + classitem.class + + '.' + + classitem.name + + ' missing example' + ); + } + } + }); +} + +function mergeOverloadedMethods(data) { + let methodsByFullName = {}; + let paramsForOverloadedMethods = {}; + + let consts = (data.consts = {}); + + data.classitems = data.classitems.filter(function(classitem) { + if (classitem.access === 'private') { + return false; + } + + const itemClass = data.classes[classitem.class]; + if (!itemClass || itemClass.private) { + return false; + } + + let methodConsts = {}; + + let fullName, method; + + var assertEqual = function(a, b, msg) { + if (a !== b) { + throw new Error( + 'for ' + + fullName + + '() defined in ' + + classitem.file + + ':' + + classitem.line + + ', ' + + msg + + ' (' + + JSON.stringify(a) + + ' !== ' + + JSON.stringify(b) + + ')' + ); + } + }; + + var extractConsts = function(param) { + if (!param.type) { + console.log(param); + } + if (param.type.split('|').indexOf('Constant') >= 0) { + let match; + if (classitem.name === 'endShape' && param.name === 'mode') { + match = 'CLOSE'; + } else { + const constantRe = /either\s+(?:[A-Z0-9_]+\s*,?\s*(?:or)?\s*)+/g; + const execResult = constantRe.exec(param.description); + match = execResult && execResult[0]; + if (!match) { + throw new Error( + classitem.file + + ':' + + classitem.line + + ', Constant-typed parameter ' + + fullName + + '(...' + + param.name + + '...) is missing valid value enumeration. ' + + 'See inline_documentation.md#specify-parameters.' + ); + } + } + if (match) { + const reConst = /[A-Z0-9_]+/g; + let matchConst; + while ((matchConst = reConst.exec(match)) !== null) { + methodConsts[matchConst] = true; + } + } + } + }; + + var processOverloadedParams = function(params) { + let paramNames; + + if (!(fullName in paramsForOverloadedMethods)) { + paramsForOverloadedMethods[fullName] = {}; + } + + paramNames = paramsForOverloadedMethods[fullName]; + + params.forEach(function(param) { + const origParam = paramNames[param.name]; + + if (origParam) { + assertEqual( + origParam.type, + param.type, + 'types for param "' + + param.name + + '" must match ' + + 'across all overloads' + ); + assertEqual( + param.description, + '', + 'description for param "' + + param.name + + '" should ' + + 'only be defined in its first use; subsequent ' + + 'overloads should leave it empty' + ); + } else { + paramNames[param.name] = param; + extractConsts(param); + } + }); + + return params; + }; + + if (classitem.itemtype && classitem.itemtype === 'method') { + fullName = classitem.class + '.' + classitem.name; + if (fullName in methodsByFullName) { + // It's an overloaded version of a method that we've already + // indexed. We need to make sure that we don't list it multiple + // times in our index pages and such. + + method = methodsByFullName[fullName]; + + assertEqual( + method.file, + classitem.file, + 'all overloads must be defined in the same file' + ); + assertEqual( + method.module, + classitem.module, + 'all overloads must be defined in the same module' + ); + assertEqual( + method.submodule, + classitem.submodule, + 'all overloads must be defined in the same submodule' + ); + assertEqual( + classitem.description || '', + '', + 'additional overloads should have no description' + ); + + var makeOverload = function(method) { + const overload = { + line: method.line, + params: processOverloadedParams(method.params || []) + }; + // TODO: the doc renderer assumes (incorrectly) that + // these are the same for all overrides + if (method.static) overload.static = method.static; + if (method.chainable) overload.chainable = method.chainable; + if (method.return) overload.return = method.return; + return overload; + }; + + if (!method.overloads) { + method.overloads = [makeOverload(method)]; + delete method.params; + } + method.overloads.push(makeOverload(classitem)); + return false; + } else { + if (classitem.params) { + classitem.params.forEach(function(param) { + extractConsts(param); + }); + } + methodsByFullName[fullName] = classitem; + } + + Object.keys(methodConsts).forEach(constName => + (consts[constName] || (consts[constName] = [])).push(fullName) + ); + } + return true; + }); +} + +// build a copy of data.json for the FES, restructured for object lookup on +// classitems and removing all the parts not needed by the FES +function buildParamDocs(docs) { + let newClassItems = {}; + // the fields we need for the FES, discard everything else + let allowed = new Set(['name', 'class', 'module', 'params', 'overloads']); + for (let classitem of docs.classitems) { + if (classitem.name && classitem.class) { + for (let key in classitem) { + if (!allowed.has(key)) { + delete classitem[key]; + } + } + if (classitem.hasOwnProperty('overloads')) { + for (let overload of classitem.overloads) { + // remove line number and return type + if (overload.line) { + delete overload.line; + } + + if (overload.return) { + delete overload.return; + } + } + } + if (!newClassItems[classitem.class]) { + newClassItems[classitem.class] = {}; + } + + newClassItems[classitem.class][classitem.name] = classitem; + } + } + + let fs = require('fs'); + let path = require('path'); + let out = fs.createWriteStream( + path.join(process.cwd(), 'docs', 'parameterData.json'), + { + flags: 'w', + mode: '0644' + } + ); + out.write(JSON.stringify(newClassItems, null, 2)); + out.end(); +} + +function renderItemDescriptionsAsMarkdown(item) { + if (item.description) { + const entities = new Entities(); + item.description = entities.decode(marked.parse(item.description)); + } + if (item.params) { + item.params.forEach(renderItemDescriptionsAsMarkdown); + } +} + +function renderDescriptionsAsMarkdown(data) { + Object.keys(data.modules).forEach(function(moduleName) { + renderItemDescriptionsAsMarkdown(data.modules[moduleName]); + }); + Object.keys(data.classes).forEach(function(className) { + renderItemDescriptionsAsMarkdown(data.classes[className]); + }); + data.classitems.forEach(function(classitem) { + renderItemDescriptionsAsMarkdown(classitem); + }); +} + +module.exports = (data, options) => { + data.classitems + .filter( + ci => !ci.itemtype && (ci.params || ci.return) && ci.access !== 'private' + ) + .forEach(ci => { + console.error(ci.file + ':' + ci.line + ': unnamed public member'); + }); + + Object.keys(data.classes) + .filter(k => data.classes[k].access === 'private') + .forEach(k => delete data.classes[k]); + + renderDescriptionsAsMarkdown(data); + mergeOverloadedMethods(data); + smokeTestMethods(data); + buildParamDocs(JSON.parse(JSON.stringify(data))); +}; + +module.exports.mergeOverloadedMethods = mergeOverloadedMethods; +module.exports.renderDescriptionsAsMarkdown = renderDescriptionsAsMarkdown; + +module.exports.register = (Handlebars, options) => { + Handlebars.registerHelper('root', function(context, options) { + // if (this.language === 'en') { + // return ''; + // } else { + // return '/'+this.language; + // } + return window.location.pathname; + }); +}; \ No newline at end of file From d308bd8bb29c57295c59ae6c477df26927447922 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 16:26:42 +0100 Subject: [PATCH 044/120] add custom vertex attributes to the 'TESS preserves vertex data' test --- test/unit/webgl/p5.RendererGL.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 66e9008713..1bc4ed6e0a 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1577,15 +1577,19 @@ suite('p5.RendererGL', function() { renderer.beginShape(myp5.TESS); renderer.fill(255, 255, 255); renderer.normal(-1, -1, 1); + renderer.setAttribute('aCustom', [1, 1, 1]) renderer.vertex(-10, -10, 0, 0); renderer.fill(255, 0, 0); renderer.normal(1, -1, 1); + renderer.setAttribute('aCustom', [1, 0, 0]) renderer.vertex(10, -10, 1, 0); renderer.fill(0, 255, 0); renderer.normal(1, 1, 1); + renderer.setAttribute('aCustom', [0, 1, 0]) renderer.vertex(10, 10, 1, 1); renderer.fill(0, 0, 255); renderer.normal(-1, 1, 1); + renderer.setAttribute('aCustom', [0, 0, 1]) renderer.vertex(-10, 10, 0, 1); renderer.endShape(myp5.CLOSE); @@ -1641,6 +1645,15 @@ suite('p5.RendererGL', function() { [1, 1, 1] ); + assert.deepEqual(renderer.immediateMode.geometry.aCustom, [ + 1, 0, 0, + 0, 0, 1, + 1, 1, 1, + 0, 0, 1, + 1, 0, 0, + 0, 1, 0 + ]); + assert.deepEqual(renderer.immediateMode.geometry.vertexColors, [ 1, 0, 0, 1, 0, 0, 1, 1, From cf0383603b51a8f17cfc8a1359334711a6345036 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 16:33:59 +0100 Subject: [PATCH 045/120] delete custom attributes on reset, change Object.keys to 'for in' loop --- src/webgl/p5.Geometry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 6e41e06efa..f9f93d7306 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -452,8 +452,8 @@ p5.Geometry = class Geometry { this.vertexNormals.length = 0; this.uvs.length = 0; - for (const attr of Object.keys(this.userAttributes)){ - delete this[attr.name]; + for (const attr in this.userAttributes){ + delete this[attr]; } this.userAttributes = {}; From 9f91d519b727436458c33011a4aa6b82296969b3 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 16:59:47 +0100 Subject: [PATCH 046/120] Add check in case there is an incorrect number of attributes on a geometry --- src/webgl/p5.RendererGL.Retained.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 630256fe64..3bebcd3d0a 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -152,10 +152,18 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { for (const buff of this.retainedMode.buffers.fill) { buff._prepareBuffer(geometry, fillShader); } - if (Object.keys(geometry.model.userAttributes).length > 0){ - for (const buff of this.retainedMode.buffers.user){ - buff._prepareBuffer(geometry, fillShader); + for (const buff of this.retainedMode.buffers.user){ + if(!geometry.model[buff.src]){ + continue; } + const adjustedLength = geometry.model[buff.src].length / buff.size; + if(adjustedLength != geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom attribute with + either too many or too few values compared to vertices. + There may be unexpected results from the shaders. + `, 'setAttribute()'); + } + buff._prepareBuffer(geometry, fillShader); } fillShader.disableRemainingAttributes(); if (geometry.indexBuffer) { @@ -177,10 +185,18 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { for (const buff of this.retainedMode.buffers.stroke) { buff._prepareBuffer(geometry, strokeShader); } - if (Object.keys(geometry.model.userAttributes).length > 0){ - for (const buff of this.retainedMode.buffers.user){ - buff._prepareBuffer(geometry, strokeShader); + for (const buff of this.retainedMode.buffers.user){ + if(!geometry.model[buff.src]){ + continue; } + const adjustedLength = geometry.model[buff.src].length / buff.size; + if(adjustedLength != geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom attribute with + either too many or too few values compared to vertices. + There may be unexpected results from the shaders. + `, 'setAttribute()'); + } + buff._prepareBuffer(geometry, strokeShader); } strokeShader.disableRemainingAttributes(); this._applyColorBlend( From fc47dcbf4a91b16b74cb7678eb6c97741694d481 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 17:00:10 +0100 Subject: [PATCH 047/120] remove redundant checks --- src/webgl/p5.RendererGL.Immediate.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 5824f06fec..1643e9bfe3 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -589,10 +589,8 @@ p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { for (const buff of this.immediateMode.buffers.fill) { buff._prepareBuffer(this.immediateMode.geometry, shader); } - if (this._useUserAttributes){ - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); } shader.disableRemainingAttributes(); @@ -640,10 +638,8 @@ p5.RendererGL.prototype._drawImmediateStroke = function() { for (const buff of this.immediateMode.buffers.stroke) { buff._prepareBuffer(this.immediateMode.geometry, shader); } - if (this._useUserAttributes){ - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } + for (const buff of this.immediateMode.buffers.user){ + buff._prepareBuffer(this.immediateMode.geometry, shader); } shader.disableRemainingAttributes(); this._applyColorBlend( From 874c363d1f3c2f6e2eed911d8dbee20999e8da4d Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 23:35:49 +0100 Subject: [PATCH 048/120] preparing to add immediate buffer strides re:custom attributes --- src/webgl/p5.RendererGL.Immediate.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 1643e9bfe3..78734d9d58 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -46,7 +46,7 @@ p5.RendererGL.prototype.beginShape = function(mode) { return this; }; -const immediateBufferStrides = { +p5.RendererGL.prototype.immediateBufferStrides = { vertices: 1, vertexNormals: 1, vertexColors: 4, @@ -86,8 +86,8 @@ p5.RendererGL.prototype.vertex = function(x, y) { // 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 immediateBufferStrides) { - const stride = immediateBufferStrides[key]; + for (const key in this.immediateBufferStrides) { + const stride = this.immediateBufferStrides[key]; const buffer = this.immediateMode.geometry[key]; buffer.push( ...buffer.slice( From 27610ecb7b795b642428202a20ae8bd48cd07dde Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 23 Sep 2024 23:36:46 +0100 Subject: [PATCH 049/120] comment to remind me to convert custom attributes edgesToVertices --- src/webgl/p5.Geometry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index f9f93d7306..45a2127996 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1599,6 +1599,7 @@ p5.Geometry = class Geometry { * @chainable */ _edgesToVertices() { + // probably needs to add something in here for custom attributes this.lineVertices.clear(); this.lineTangentsIn.clear(); this.lineTangentsOut.clear(); From 103d6eb69b35cf1d1ccaba120dd2b9cc131a42fe Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 24 Sep 2024 11:42:04 +0100 Subject: [PATCH 050/120] update the libtess combineCallback to handle custom attributes/ vertex sizes --- src/webgl/p5.RendererGL.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 6834cf0ece..3eb6a69b84 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -671,7 +671,8 @@ p5.RendererGL = class RendererGL extends Renderer { // Used to distinguish between user calls to vertex() and internal calls this.isProcessingVertices = false; - this._tessy = this._initTessy(); + this.tessyVertexSize = 12; + this._tessy = this._initTessy(this.tessyVertexSize); this.fontInfos = {}; @@ -2417,7 +2418,7 @@ p5.RendererGL = class RendererGL extends Renderer { const p = [p1, p2, p3, p4]; return p; } - _initTessy() { + _initTessy(tessyVertexSize) { // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { for (const element of data) { @@ -2437,7 +2438,7 @@ p5.RendererGL = class RendererGL extends Renderer { } // callback for when segments intersect and must be split function combinecallback(coords, data, weight) { - const result = new Array(p5.RendererGL.prototype.tessyVertexSize).fill(0); + const result = new Array(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; @@ -2527,8 +2528,4 @@ p5.prototype._assert3d = function (name) { ); }; -// function to initialize GLU Tesselator - -p5.RendererGL.prototype.tessyVertexSize = 12; - export default p5.RendererGL; From e004a8d12fe434f23d5d27610b62a00bc194f170 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 24 Sep 2024 11:42:54 +0100 Subject: [PATCH 051/120] rebuild the tesselator in case tessyvertexsize changes (custom attributes) --- src/webgl/p5.RendererGL.Immediate.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 78734d9d58..bcc13d936d 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -41,6 +41,7 @@ p5.RendererGL.prototype.beginShape = function(mode) { this._useUserAttributes = false; } this.tessyVertexSize = 12; + this.tessy = this._initTessy(this.tessyVertexSize); this.immediateMode.geometry.reset(); this.immediateMode.contourIndices = []; return this; @@ -456,6 +457,9 @@ p5.RendererGL.prototype._calculateEdges = function( */ p5.RendererGL.prototype._tesselateShape = function() { // TODO: handle non-TESS shape modes that have contours + if (this.tessyVertexSize > 12){ + this._tessy = this._initTessy(this.tessyVertexSize); + } this.immediateMode.shapeMode = constants.TRIANGLES; const contours = [[]]; for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { @@ -490,6 +494,7 @@ p5.RendererGL.prototype._tesselateShape = function() { } else{ delete this.userAttributes[attr]; this.tessyVertexSize -= size; + this._tessy = this._initTessy(this.tessyVertexSize); } } } @@ -499,7 +504,7 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals = []; this.immediateMode.geometry.uvs = []; for (const attr in this.userAttributes){ - delete this.immediateMode.geometry[attr] + this.immediateMode.geometry[attr] = []; } const colors = []; for ( From 7bae702f908331f1179dba93dcb729bb17fcc2f5 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 24 Sep 2024 12:16:54 +0100 Subject: [PATCH 052/120] Added a function to update the libtess tesselator's combine callback in case custom attributes have been added --- src/webgl/p5.RendererGL.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 3eb6a69b84..7b604b6e36 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -671,8 +671,7 @@ p5.RendererGL = class RendererGL extends Renderer { // Used to distinguish between user calls to vertex() and internal calls this.isProcessingVertices = false; - this.tessyVertexSize = 12; - this._tessy = this._initTessy(this.tessyVertexSize); + this._tessy = this._initTessy(); this.fontInfos = {}; @@ -2418,7 +2417,7 @@ p5.RendererGL = class RendererGL extends Renderer { const p = [p1, p2, p3, p4]; return p; } - _initTessy(tessyVertexSize) { + _initTessy() { // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { for (const element of data) { @@ -2438,7 +2437,7 @@ p5.RendererGL = class RendererGL extends Renderer { } // callback for when segments intersect and must be split function combinecallback(coords, data, weight) { - const result = new Array(tessyVertexSize).fill(0); + const result = new Array(p5.RendererGL.prototype.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; @@ -2466,6 +2465,23 @@ p5.RendererGL = class RendererGL extends Renderer { return tessy; } + _updateTessyCombineCallback() { + // If custom attributes have been used, the vertex data which needs to be + // combined has changed, so libtess must have its combine callback updated + 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._tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); + } + _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 @@ -2528,4 +2544,8 @@ p5.prototype._assert3d = function (name) { ); }; +// Initial vertex data size for libtess + +p5.RendererGL.prototype.tessyVertexSize = 12; + export default p5.RendererGL; From c3ad1fc9199852ffc6624f81f8bb7ccad0c78c10 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 24 Sep 2024 12:17:54 +0100 Subject: [PATCH 053/120] update the tesselators combine callback instead of reinitializing the entire tessellator, and only if necessary --- src/webgl/p5.RendererGL.Immediate.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index bcc13d936d..9c221ddfe9 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -40,8 +40,10 @@ p5.RendererGL.prototype.beginShape = function(mode) { delete this.userAttributes; this._useUserAttributes = false; } - this.tessyVertexSize = 12; - this.tessy = this._initTessy(this.tessyVertexSize); + if (this.tessyVertexSize > 12){ + this.tessyVertexSize = 12; + this._updateTessyCombineCallback(); + } this.immediateMode.geometry.reset(); this.immediateMode.contourIndices = []; return this; @@ -457,9 +459,6 @@ p5.RendererGL.prototype._calculateEdges = function( */ p5.RendererGL.prototype._tesselateShape = function() { // TODO: handle non-TESS shape modes that have contours - if (this.tessyVertexSize > 12){ - this._tessy = this._initTessy(this.tessyVertexSize); - } this.immediateMode.shapeMode = constants.TRIANGLES; const contours = [[]]; for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { @@ -494,10 +493,12 @@ p5.RendererGL.prototype._tesselateShape = function() { } else{ delete this.userAttributes[attr]; this.tessyVertexSize -= size; - this._tessy = this._initTessy(this.tessyVertexSize); } } } + if (this.tessyVertexSize > 12){ + this._updateTessyCombineCallback(); + } const polyTriangles = this._triangulate(contours); const originalVertices = this.immediateMode.geometry.vertices; this.immediateMode.geometry.vertices = []; From 5bb41b5bfc6d96fe28ae1e63837597e61f29e160 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 24 Sep 2024 12:32:34 +0100 Subject: [PATCH 054/120] remove old scripts --- docs/documented-method.js | 60 -------- docs/preprocessor.js | 314 -------------------------------------- 2 files changed, 374 deletions(-) delete mode 100644 docs/documented-method.js delete mode 100644 docs/preprocessor.js diff --git a/docs/documented-method.js b/docs/documented-method.js deleted file mode 100644 index 1a7e62a283..0000000000 --- a/docs/documented-method.js +++ /dev/null @@ -1,60 +0,0 @@ -// https://github.com/umdjs/umd/blob/main/templates/returnExports.js -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - define([], factory); - } else if (typeof module === 'object' && module.exports) { - module.exports = factory(); - } else { - root.DocumentedMethod = factory(); - } - }(this, function () { - function extend(target, src) { - Object.keys(src).forEach(function(prop) { - target[prop] = src[prop]; - }); - return target; - } - - function DocumentedMethod(classitem) { - extend(this, classitem); - - if (this.overloads) { - // Make each overload inherit properties from their parent - // classitem. - this.overloads = this.overloads.map(function(overload) { - return extend(Object.create(this), overload); - }, this); - - if (this.params) { - throw new Error('params for overloaded methods should be undefined'); - } - - this.params = this._getMergedParams(); - } - } - - DocumentedMethod.prototype = { - // Merge parameters across all overloaded versions of this item. - _getMergedParams: function() { - const paramNames = {}; - const params = []; - - this.overloads.forEach(function(overload) { - if (!overload.params) { - return; - } - overload.params.forEach(function(param) { - if (param.name in paramNames) { - return; - } - paramNames[param.name] = param; - params.push(param); - }); - }); - - return params; - } - }; - - return DocumentedMethod; - })); \ No newline at end of file diff --git a/docs/preprocessor.js b/docs/preprocessor.js deleted file mode 100644 index 531b034f28..0000000000 --- a/docs/preprocessor.js +++ /dev/null @@ -1,314 +0,0 @@ -const marked = require('marked'); -const Entities = require('html-entities').AllHtmlEntities; - -const DocumentedMethod = require('./documented-method'); - -function smokeTestMethods(data) { - data.classitems.forEach(function(classitem) { - if (classitem.itemtype === 'method') { - new DocumentedMethod(classitem); - - if ( - classitem.access !== 'private' && - classitem.file.slice(0, 3) === 'src' && - classitem.name && - !classitem.example - ) { - console.log( - classitem.file + - ':' + - classitem.line + - ': ' + - classitem.itemtype + - ' ' + - classitem.class + - '.' + - classitem.name + - ' missing example' - ); - } - } - }); -} - -function mergeOverloadedMethods(data) { - let methodsByFullName = {}; - let paramsForOverloadedMethods = {}; - - let consts = (data.consts = {}); - - data.classitems = data.classitems.filter(function(classitem) { - if (classitem.access === 'private') { - return false; - } - - const itemClass = data.classes[classitem.class]; - if (!itemClass || itemClass.private) { - return false; - } - - let methodConsts = {}; - - let fullName, method; - - var assertEqual = function(a, b, msg) { - if (a !== b) { - throw new Error( - 'for ' + - fullName + - '() defined in ' + - classitem.file + - ':' + - classitem.line + - ', ' + - msg + - ' (' + - JSON.stringify(a) + - ' !== ' + - JSON.stringify(b) + - ')' - ); - } - }; - - var extractConsts = function(param) { - if (!param.type) { - console.log(param); - } - if (param.type.split('|').indexOf('Constant') >= 0) { - let match; - if (classitem.name === 'endShape' && param.name === 'mode') { - match = 'CLOSE'; - } else { - const constantRe = /either\s+(?:[A-Z0-9_]+\s*,?\s*(?:or)?\s*)+/g; - const execResult = constantRe.exec(param.description); - match = execResult && execResult[0]; - if (!match) { - throw new Error( - classitem.file + - ':' + - classitem.line + - ', Constant-typed parameter ' + - fullName + - '(...' + - param.name + - '...) is missing valid value enumeration. ' + - 'See inline_documentation.md#specify-parameters.' - ); - } - } - if (match) { - const reConst = /[A-Z0-9_]+/g; - let matchConst; - while ((matchConst = reConst.exec(match)) !== null) { - methodConsts[matchConst] = true; - } - } - } - }; - - var processOverloadedParams = function(params) { - let paramNames; - - if (!(fullName in paramsForOverloadedMethods)) { - paramsForOverloadedMethods[fullName] = {}; - } - - paramNames = paramsForOverloadedMethods[fullName]; - - params.forEach(function(param) { - const origParam = paramNames[param.name]; - - if (origParam) { - assertEqual( - origParam.type, - param.type, - 'types for param "' + - param.name + - '" must match ' + - 'across all overloads' - ); - assertEqual( - param.description, - '', - 'description for param "' + - param.name + - '" should ' + - 'only be defined in its first use; subsequent ' + - 'overloads should leave it empty' - ); - } else { - paramNames[param.name] = param; - extractConsts(param); - } - }); - - return params; - }; - - if (classitem.itemtype && classitem.itemtype === 'method') { - fullName = classitem.class + '.' + classitem.name; - if (fullName in methodsByFullName) { - // It's an overloaded version of a method that we've already - // indexed. We need to make sure that we don't list it multiple - // times in our index pages and such. - - method = methodsByFullName[fullName]; - - assertEqual( - method.file, - classitem.file, - 'all overloads must be defined in the same file' - ); - assertEqual( - method.module, - classitem.module, - 'all overloads must be defined in the same module' - ); - assertEqual( - method.submodule, - classitem.submodule, - 'all overloads must be defined in the same submodule' - ); - assertEqual( - classitem.description || '', - '', - 'additional overloads should have no description' - ); - - var makeOverload = function(method) { - const overload = { - line: method.line, - params: processOverloadedParams(method.params || []) - }; - // TODO: the doc renderer assumes (incorrectly) that - // these are the same for all overrides - if (method.static) overload.static = method.static; - if (method.chainable) overload.chainable = method.chainable; - if (method.return) overload.return = method.return; - return overload; - }; - - if (!method.overloads) { - method.overloads = [makeOverload(method)]; - delete method.params; - } - method.overloads.push(makeOverload(classitem)); - return false; - } else { - if (classitem.params) { - classitem.params.forEach(function(param) { - extractConsts(param); - }); - } - methodsByFullName[fullName] = classitem; - } - - Object.keys(methodConsts).forEach(constName => - (consts[constName] || (consts[constName] = [])).push(fullName) - ); - } - return true; - }); -} - -// build a copy of data.json for the FES, restructured for object lookup on -// classitems and removing all the parts not needed by the FES -function buildParamDocs(docs) { - let newClassItems = {}; - // the fields we need for the FES, discard everything else - let allowed = new Set(['name', 'class', 'module', 'params', 'overloads']); - for (let classitem of docs.classitems) { - if (classitem.name && classitem.class) { - for (let key in classitem) { - if (!allowed.has(key)) { - delete classitem[key]; - } - } - if (classitem.hasOwnProperty('overloads')) { - for (let overload of classitem.overloads) { - // remove line number and return type - if (overload.line) { - delete overload.line; - } - - if (overload.return) { - delete overload.return; - } - } - } - if (!newClassItems[classitem.class]) { - newClassItems[classitem.class] = {}; - } - - newClassItems[classitem.class][classitem.name] = classitem; - } - } - - let fs = require('fs'); - let path = require('path'); - let out = fs.createWriteStream( - path.join(process.cwd(), 'docs', 'parameterData.json'), - { - flags: 'w', - mode: '0644' - } - ); - out.write(JSON.stringify(newClassItems, null, 2)); - out.end(); -} - -function renderItemDescriptionsAsMarkdown(item) { - if (item.description) { - const entities = new Entities(); - item.description = entities.decode(marked.parse(item.description)); - } - if (item.params) { - item.params.forEach(renderItemDescriptionsAsMarkdown); - } -} - -function renderDescriptionsAsMarkdown(data) { - Object.keys(data.modules).forEach(function(moduleName) { - renderItemDescriptionsAsMarkdown(data.modules[moduleName]); - }); - Object.keys(data.classes).forEach(function(className) { - renderItemDescriptionsAsMarkdown(data.classes[className]); - }); - data.classitems.forEach(function(classitem) { - renderItemDescriptionsAsMarkdown(classitem); - }); -} - -module.exports = (data, options) => { - data.classitems - .filter( - ci => !ci.itemtype && (ci.params || ci.return) && ci.access !== 'private' - ) - .forEach(ci => { - console.error(ci.file + ':' + ci.line + ': unnamed public member'); - }); - - Object.keys(data.classes) - .filter(k => data.classes[k].access === 'private') - .forEach(k => delete data.classes[k]); - - renderDescriptionsAsMarkdown(data); - mergeOverloadedMethods(data); - smokeTestMethods(data); - buildParamDocs(JSON.parse(JSON.stringify(data))); -}; - -module.exports.mergeOverloadedMethods = mergeOverloadedMethods; -module.exports.renderDescriptionsAsMarkdown = renderDescriptionsAsMarkdown; - -module.exports.register = (Handlebars, options) => { - Handlebars.registerHelper('root', function(context, options) { - // if (this.language === 'en') { - // return ''; - // } else { - // return '/'+this.language; - // } - return window.location.pathname; - }); -}; \ No newline at end of file From 3ffe8cce0090db04cadc5c0eb46de6245098a904 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 24 Sep 2024 15:05:29 +0100 Subject: [PATCH 055/120] convert tessy combinecallback to arrow function so that custom attributes can work with overlap vertices --- src/webgl/p5.RendererGL.js | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 7b604b6e36..ec7d52e1c6 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -671,6 +671,7 @@ p5.RendererGL = class RendererGL extends Renderer { // Used to distinguish between user calls to vertex() and internal calls this.isProcessingVertices = false; + this.tessyVertexSize = 12; this._tessy = this._initTessy(); this.fontInfos = {}; @@ -2436,8 +2437,8 @@ p5.RendererGL = class RendererGL extends Renderer { console.log(`error number: ${errno}`); } // callback for when segments intersect and must be split - function combinecallback(coords, data, weight) { - const result = new Array(p5.RendererGL.prototype.tessyVertexSize).fill(0); + 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; @@ -2445,7 +2446,7 @@ p5.RendererGL = class RendererGL extends Renderer { } } return result; - } + }; function edgeCallback(flag) { // don't really care about the flag, but need no-strip/no-fan behavior @@ -2465,23 +2466,6 @@ p5.RendererGL = class RendererGL extends Renderer { return tessy; } - _updateTessyCombineCallback() { - // If custom attributes have been used, the vertex data which needs to be - // combined has changed, so libtess must have its combine callback updated - 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._tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); - } - _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 @@ -2544,8 +2528,4 @@ p5.prototype._assert3d = function (name) { ); }; -// Initial vertex data size for libtess - -p5.RendererGL.prototype.tessyVertexSize = 12; - -export default p5.RendererGL; +export default p5.RendererGL; \ No newline at end of file From afdf29d5e021a6a1a3715070c55430b0bf4790f5 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 24 Sep 2024 15:05:59 +0100 Subject: [PATCH 056/120] remove redundant function calls to update tessy combine callback --- src/webgl/p5.RendererGL.Immediate.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 9c221ddfe9..7b8360efd7 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -42,7 +42,6 @@ p5.RendererGL.prototype.beginShape = function(mode) { } if (this.tessyVertexSize > 12){ this.tessyVertexSize = 12; - this._updateTessyCombineCallback(); } this.immediateMode.geometry.reset(); this.immediateMode.contourIndices = []; @@ -496,9 +495,6 @@ p5.RendererGL.prototype._tesselateShape = function() { } } } - if (this.tessyVertexSize > 12){ - this._updateTessyCombineCallback(); - } const polyTriangles = this._triangulate(contours); const originalVertices = this.immediateMode.geometry.vertices; this.immediateMode.geometry.vertices = []; From 69ee7e65ef7c8e9f64b62e318736b6a8fc08e96d Mon Sep 17 00:00:00 2001 From: miaoye que Date: Wed, 25 Sep 2024 20:31:19 -0400 Subject: [PATCH 057/120] Make changes to convert.js --- utils/convert.js | 54 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/utils/convert.js b/utils/convert.js index 0569eec02a..d3b5722bc4 100644 --- a/utils/convert.js +++ b/utils/convert.js @@ -473,24 +473,49 @@ function cleanUpClassItems(data) { } } + // Reduce the amount of information in each function's overloads, while + // keeping all the essential data available. const flattenOverloads = funcObj => { const result = {}; + const processOverload = overload => { + if (overload.params) { + return Object.values(overload.params).map(param => processOptionalParam(param)); + } + return overload; + } + + // To simplify `parameterData.json`, instead of having a separate field for + // optional parameters, we'll add a ? to the end of parameter type to + // indicate that it's optional. + const processOptionalParam = param => { + let type = param.type; + if (param.optional) { + type += '?'; + } + return type; + } + + // In some cases, even when the arguments are intended to mean different + // things, their types and order are identical. Since the exact meaning + // of the arguments is less important for parameter validation, we'll + // perform overload deduplication here. + const removeDuplicateOverloads = (overload, uniqueOverloads) => { + const overloadString = JSON.stringify(overload); + if (uniqueOverloads.has(overloadString)) { + return false; + } + uniqueOverloads.add(overloadString); + return true; + } + for (const [key, value] of Object.entries(funcObj)) { if (value && typeof value === 'object' && value.overloads) { + const uniqueOverloads = new Set(); result[key] = { - overloads: Object.values(value.overloads).map(overload => { - if (overload.params) { - return Object.values(overload.params).map(param => { - let type = param.type; - if (param.optional) { - type += '?'; - } - return type; - }); - } - return overload; - }) + overloads: Object.values(value.overloads) + .map(overload => processOverload(overload)) + .filter(overload => removeDuplicateOverloads(overload, uniqueOverloads)) }; } else { result[key] = value; @@ -517,6 +542,11 @@ function buildParamDocs(docs) { for (let classitem of docs.classitems) { // If `classitem` doesn't have overloads, then it's not a function—skip processing in this case if (classitem.name && classitem.class && classitem.hasOwnProperty('overloads')) { + // Skip if the item already exists in newClassItems + if (newClassItems[classitem.class] && newClassItems[classitem.class][classitem.name]) { + continue; + } + // Clean up fields that will not be used in each classitem's overloads classitem.overloads?.forEach(overload => { delete overload.line; From 81519e65db7dc771f4211030913b027a49bd9aad Mon Sep 17 00:00:00 2001 From: miaoye que Date: Wed, 25 Sep 2024 20:57:42 -0400 Subject: [PATCH 058/120] add parameter validation support for --- src/core/friendly_errors/param_validator.js | 12 ++++++++++++ test/unit/core/param_errors.js | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 2fa02d5525..7849647b38 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -112,6 +112,18 @@ function validateParams(p5, fn) { * @returns {z.ZodSchema} Zod schema */ function generateZodSchemasForFunc(func) { + // A special case for `p5.Color.paletteLerp`, which has an unusual and + // complicated function signature not shared by any other function in p5. + if (func === 'p5.Color.paletteLerp') { + return z.tuple([ + z.array(z.tuple([ + z.instanceof(p5.Color), + z.number() + ])), + z.number() + ]); + } + // Expect global functions like `sin` and class methods like `p5.Vector.add` const ichDot = func.lastIndexOf('.'); const funcName = func.slice(ichDot + 1); diff --git a/test/unit/core/param_errors.js b/test/unit/core/param_errors.js index 04249c2199..ec1d32d697 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -197,4 +197,16 @@ suite('Validate Params', function () { }); }); }); + + suite('validateParams: paletteLerp', function () { + test('paletteLerp(): no firendly-err-msg', function () { + const colorStops = [ + [new mockP5.Color(), 0.2], + [new mockP5.Color(), 0.8], + [new mockP5.Color(), 0.5] + ]; + const result = mockP5Prototype._validateParams('p5.Color.paletteLerp', [colorStops, 0.5]); + assert.isTrue(result.success); + }) + }) }); From 383fe8ea230f1aa5ca157d97ddcdb0f2af620fa9 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Thu, 26 Sep 2024 11:45:43 +0100 Subject: [PATCH 059/120] Changed user attributes source array on geometry to include 'Src' on the end. This avoids name clashes with default p5 attributes. --- src/webgl/GeometryBuilder.js | 3 ++- src/webgl/p5.Geometry.js | 12 +++++++----- src/webgl/p5.RendererGL.Immediate.js | 16 ++++++++++------ src/webgl/p5.RendererGL.Retained.js | 3 ++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index d535914635..0bc98e1b99 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -74,7 +74,8 @@ class GeometryBuilder { this.geometry.setAttribute(attr, missingValues, size); } for (const attr in inputAttrs){ - const data = input[attr]; + const src = attr.concat('Src'); + const data = input[src]; const size = inputAttrs[attr]; if (numPreviousVertices > 0 && !(attr in this.geometry.userAttributes)){ const numMissingValues = size * numPreviousVertices; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 45a2127996..b1f6bc08d3 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -453,7 +453,8 @@ p5.Geometry = class Geometry { this.uvs.length = 0; for (const attr in this.userAttributes){ - delete this[attr]; + const src = attr.concat('Src'); + delete this[src]; } this.userAttributes = {}; @@ -1920,8 +1921,9 @@ p5.Geometry = class Geometry { } setAttribute(attributeName, data, size = data.length ? data.length : 1){ - if (!this.hasOwnProperty(attributeName)){ - this[attributeName] = []; + const attributeSrc = attributeName.concat('Src'); + if (!this.hasOwnProperty(attributeSrc)){ + this[attributeSrc] = []; this.userAttributes[attributeName] = size; } if (size != this.userAttributes[attributeName]){ @@ -1929,9 +1931,9 @@ p5.Geometry = class Geometry { or if it was an accident, set ${attributeName} to have the same number of inputs each time!`, 'setAttribute()'); } if (data.length){ - this[attributeName].push(...data); + this[attributeSrc].push(...data); } else{ - this[attributeName].push(data); + this[attributeSrc].push(data); } } }; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 7b8360efd7..72d4039346 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -128,8 +128,9 @@ p5.RendererGL.prototype.vertex = function(x, y) { const geom = this.immediateMode.geometry; const verts = geom.vertices; const data = this.userAttributes[attr]; + const src = attr.concat('Src'); const size = data.length ? data.length : 1; - if (!geom.hasOwnProperty(attr) && verts.length > 1) { + if (!geom.hasOwnProperty(src) && verts.length > 1) { const numMissingValues = size * (verts.length - 1); const missingValues = Array(numMissingValues).fill(0); geom.setAttribute(attr, missingValues, size); @@ -199,15 +200,16 @@ p5.RendererGL.prototype.setAttribute = function(attributeName, data){ if (!this.userAttributes.hasOwnProperty(attributeName)){ this.tessyVertexSize += size; } - this.userAttributes[attributeName] = data; const buff = attributeName.concat('Buffer'); + const attributeSrc = attributeName.concat('Src'); + this.userAttributes[attributeName] = data; const bufferExists = this.immediateMode .buffers .user .some(buffer => buffer.dst === buff); if(!bufferExists){ this.immediateMode.buffers.user.push( - new p5.RenderBuffer(size, attributeName, buff, attributeName, this) + new p5.RenderBuffer(size, attributeSrc, buff, attributeName, this) ); } }; @@ -483,11 +485,12 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals[i].z ); for (const attr in this.userAttributes){ + const attributeSrc = attr.concat('Src'); const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; const start = i * size; const end = start + size; - if (this.immediateMode.geometry[attr]){ - const vals = this.immediateMode.geometry[attr].slice(start, end); + if (this.immediateMode.geometry[attributeSrc]){ + const vals = this.immediateMode.geometry[attributeSrc].slice(start, end); contours[contours.length-1].push(...vals); } else{ delete this.userAttributes[attr]; @@ -501,7 +504,8 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals = []; this.immediateMode.geometry.uvs = []; for (const attr in this.userAttributes){ - this.immediateMode.geometry[attr] = []; + const attributeSrc = attr.concat('Src'); + this.immediateMode.geometry[attributeSrc] = []; } const colors = []; for ( diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 3bebcd3d0a..37bf95457d 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -117,6 +117,7 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { for (const attr in model.userAttributes){ const buff = attr.concat('Buffer'); + const attributeSrc = attr.concat('Src'); const size = model.userAttributes[attr]; const bufferExists = this.retainedMode .buffers @@ -124,7 +125,7 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { .some(buffer => buffer.dst === buff); if (!bufferExists){ this.retainedMode.buffers.user.push( - new p5.RenderBuffer(size, attr, buff, attr, this) + new p5.RenderBuffer(size, attributeSrc, buff, attr, this) ); } } From c2fd8b7609101808776076e2775af2bac13e1fa0 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Thu, 26 Sep 2024 12:30:51 +0100 Subject: [PATCH 060/120] interpolate custom attributes on quadratic curves --- src/webgl/3d_primitives.js | 52 ++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 9dc5d65622..d87f3647e8 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3018,6 +3018,10 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0] = this.immediateMode.geometry.vertexStrokeColors.slice(-4); strokeColors[3] = this.curStrokeColor.slice(); + // Do the same for custom attributes + const customAttributes = []; + + if (argLength === 6) { this.isBezier = true; @@ -3163,21 +3167,34 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { } 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] = this.immediateMode.geometry.vertexColors.slice(-4); + for (let m = 0; m < 3; m++) fillColors.push([]); + fillColors[0] = immediateGeometry.vertexColors.slice(-4); fillColors[2] = this.curFillColor.slice(); // Do the same for strokeColor. const strokeColors = []; - for (m = 0; m < 3; m++) strokeColors.push([]); - strokeColors[0] = this.immediateMode.geometry.vertexStrokeColors.slice(-4); + for (let m = 0; m < 3; m++) strokeColors.push([]); + strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); strokeColors[2] = this.curStrokeColor.slice(); + // Do the same for custom (user defined) attributes + const userAttributes = {}; + for (const attr in immediateGeometry.userAttributes){ + const attributeSrc = attr.concat('Src'); + const size = immediateGeometry.userAttributes[attr]; + const curData = this.userAttributes[attr]; + userAttributes[attr] = []; + for (let m = 0; m < 3; m++) userAttributes[attr].push([]); + userAttributes[attr][0] = immediateGeometry[attributeSrc].slice(-size); + userAttributes[attr][2] = curData; + } + if (argLength === 4) { this.isQuadratic = true; @@ -3190,7 +3207,7 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); const totalLength = d0 + d1; d0 /= totalLength; - for (k = 0; k < 4; k++) { + for (let k = 0; k < 4; k++) { fillColors[1].push( fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 ); @@ -3198,14 +3215,22 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 ); } + for (const attr in immediateGeometry.userAttributes){ + const size = immediateGeometry.userAttributes[attr]; + for (let k = 0; k < size; k++){ + userAttributes[attr][1].push( + userAttributes[attr][0][k] * (1-d0) + userAttributes[attr][2][k] * d0 + ); + } + } - for (i = 0; i < LUTLength; i++) { + for (let i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; this.curStrokeColor = [0, 0, 0, 0]; _x = _y = 0; - for (m = 0; m < 3; m++) { - for (k = 0; k < 4; k++) { + for (let m = 0; m < 3; m++) { + for (let k = 0; k < 4; k++) { this.curFillColor[k] += this._lookUpTableQuadratic[i][m] * fillColors[m][k]; this.curStrokeColor[k] += @@ -3214,6 +3239,17 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { _x += w_x[m] * this._lookUpTableQuadratic[i][m]; _y += w_y[m] * this._lookUpTableQuadratic[i][m]; } + + for (const attr in immediateGeometry.userAttributes) { + const size = immediateGeometry.userAttributes[attr]; + this.userAttributes[attr] = Array(size).fill(0); + for (let m = 0; m < 3; m++){ + for (let k = 0; k < size; k++){ + this.userAttributes[attr][k] += + this._lookUpTableQuadratic[i][m] * userAttributes[attr][m][k]; + } + } + } this.vertex(_x, _y); } From 7107d7c06977412380b43b488a7c92dfa4c39c17 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Thu, 26 Sep 2024 12:36:28 +0100 Subject: [PATCH 061/120] Update custom attribute test as geometrie's source attribute names now have 'Src' appended to them --- test/unit/webgl/p5.RendererGL.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 1bc4ed6e0a..0fcbf5df6c 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1645,7 +1645,7 @@ suite('p5.RendererGL', function() { [1, 1, 1] ); - assert.deepEqual(renderer.immediateMode.geometry.aCustom, [ + assert.deepEqual(renderer.immediateMode.geometry.aCustomSrc, [ 1, 0, 0, 0, 0, 1, 1, 1, 1, From bb5ac025773a99e9c6186fe2773fd0434513a68c Mon Sep 17 00:00:00 2001 From: 23036879 Date: Thu, 26 Sep 2024 16:57:49 +0100 Subject: [PATCH 062/120] Interpolate user attributes across bezierVertex --- src/webgl/3d_primitives.js | 54 +++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index d87f3647e8..0599c24b6d 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3003,24 +3003,33 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { } 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] = this.immediateMode.geometry.vertexColors.slice(-4); + fillColors[0] = immediateGeometry.vertexColors.slice(-4); fillColors[3] = this.curFillColor.slice(); // Do the same for strokeColor. const strokeColors = []; for (m = 0; m < 4; m++) strokeColors.push([]); - strokeColors[0] = this.immediateMode.geometry.vertexStrokeColors.slice(-4); + strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); strokeColors[3] = this.curStrokeColor.slice(); // Do the same for custom attributes - const customAttributes = []; - + const userAttributes = {}; + for (const attr in immediateGeometry.userAttributes){ + const attributeSrc = attr.concat('Src'); + const size = immediateGeometry.userAttributes[attr]; + const curData = this.userAttributes[attr]; + userAttributes[attr] = []; + for (m = 0; m < 4; m++) userAttributes[attr].push([]); + userAttributes[attr][0] = immediateGeometry[attributeSrc].slice(-size); + userAttributes[attr][3] = curData; + } if (argLength === 6) { this.isBezier = true; @@ -3049,14 +3058,25 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } + for (const attr in immediateGeometry.userAttributes){ + const size = immediateGeometry.userAttributes[attr]; + for (k = 0; k < size; k++){ + userAttributes[attr][1].push( + userAttributes[attr][0][k] * (1-d0) + userAttributes[attr][3][k] * d0 + ); + userAttributes[attr][2].push( + userAttributes[attr][0][k] * (1-d2) + userAttributes[attr][3][k] * d2 + ); + } + } - for (i = 0; i < LUTLength; i++) { + for (let i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; this.curStrokeColor = [0, 0, 0, 0]; _x = _y = 0; - for (m = 0; m < 4; m++) { - for (k = 0; k < 4; k++) { + for (let m = 0; m < 4; m++) { + for (let k = 0; k < 4; k++) { this.curFillColor[k] += this._lookUpTableBezier[i][m] * fillColors[m][k]; this.curStrokeColor[k] += @@ -3065,6 +3085,16 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { _x += w_x[m] * this._lookUpTableBezier[i][m]; _y += w_y[m] * this._lookUpTableBezier[i][m]; } + for (const attr in immediateGeometry.userAttributes){ + const size = immediateGeometry.userAttributes[attr]; + this.userAttributes[attr] = Array(size).fill(0); + for (let m = 0; m < 4; m++){ + for (let k = 0; k < size; k++){ + this.userAttributes[attr][k] += + this._lookUpTableBezier[i][m] * userAttributes[attr][m][k]; + } + } + } this.vertex(_x, _y); } // so that we leave currentColor with the last value the user set it to @@ -3086,7 +3116,7 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { const totalLength = d0 + d1 + d2; d0 /= totalLength; d2 /= totalLength; - for (k = 0; k < 4; k++) { + for (let k = 0; k < 4; k++) { fillColors[1].push( fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 ); @@ -3100,7 +3130,7 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } - for (i = 0; i < LUTLength; i++) { + for (let i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; this.curStrokeColor = [0, 0, 0, 0]; @@ -3173,13 +3203,13 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { // fillColors[1]: control point color // fillColors[2]: end point color const fillColors = []; - for (let m = 0; m < 3; m++) fillColors.push([]); + for (m = 0; m < 3; m++) fillColors.push([]); fillColors[0] = immediateGeometry.vertexColors.slice(-4); fillColors[2] = this.curFillColor.slice(); // Do the same for strokeColor. const strokeColors = []; - for (let m = 0; m < 3; m++) strokeColors.push([]); + for (m = 0; m < 3; m++) strokeColors.push([]); strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); strokeColors[2] = this.curStrokeColor.slice(); @@ -3190,7 +3220,7 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { const size = immediateGeometry.userAttributes[attr]; const curData = this.userAttributes[attr]; userAttributes[attr] = []; - for (let m = 0; m < 3; m++) userAttributes[attr].push([]); + for (m = 0; m < 3; m++) userAttributes[attr].push([]); userAttributes[attr][0] = immediateGeometry[attributeSrc].slice(-size); userAttributes[attr][2] = curData; } From 233ed4b51938502ccde2e611a696ad3fc409cb37 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Thu, 26 Sep 2024 17:24:53 +0100 Subject: [PATCH 063/120] updated setAttribute documentation --- src/core/shape/vertex.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 3f04b8ea3b..1eab0fc475 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2255,30 +2255,31 @@ p5.prototype.normal = function(x, y, z) { /** Sets the shader's vertex attribute variables. * - * Shader programs run on the computer's graphics processing unit (GPU) - * They live in a part of the computer's memory that's completely separate from - * the sketch that runs them. Attributes are variables attached to vertices - * within a shader program. They provide a way to attach data to vertices - * and pass values from a sketch running on the CPU to a shader program. + * An attribute is a variable belonging to a vertex in a shader. p5.js provides some + * default attributes, such as `aPosition`, `aNormal`, `aVertexColor`, etc. Custom + * attributes can also be defined within `beginShape()` and `endShape()`. * * The first parameter, `attributeName`, is a string with the attribute's name. + * This is the same variable name which should be declared in the shader, similar to + * `setUniform()`. * - * The second parameter, `data`, is the value that should be assigned to the - * attribute. This value will be applied to subsequent vertices created with + * The second parameter, `data`, is the value assigned to the attribute. 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 interpreted according to the WebGL + * and in the shader program the type can be declared according to the WebGL * specification. Common types include `float`, `vec2`, `vec3`, `vec4` or matrices. * * @example *
* * let vertSrc = ` + * #version 300 es * precision highp float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * - * attribute vec3 aPosition; - * attribute vec2 aOffset; + * in vec3 aPosition; + * in vec2 aOffset; * * void main(){ * vec4 positionVec4 = vec4(aPosition.xyz, 1.0); @@ -2288,6 +2289,7 @@ p5.prototype.normal = function(x, y, z) { * `; * * let fragSrc = ` + * #version 300 es * precision highp float; * * void main(){ From b558be06e768dc1fb7d4c3a756b1dea0b2621503 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Thu, 26 Sep 2024 22:28:19 -0400 Subject: [PATCH 064/120] major changes --- package.json | 5 +- src/core/friendly_errors/param_validator.js | 133 ++++++++++++++--- test/js/chai_helpers.js | 14 +- test/unit/core/param_errors.js | 151 +++++++++++--------- 4 files changed, 202 insertions(+), 101 deletions(-) diff --git a/package.json b/package.json index 802c048821..ef709e7e47 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,7 @@ "gifenc": "^1.0.3", "libtess": "^1.2.2", "omggif": "^1.0.10", - "opentype.js": "^1.3.1", - "zod-validation-error": "^3.3.1" + "opentype.js": "^1.3.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", @@ -83,4 +82,4 @@ "pre-commit": "lint-staged" } } -} +} \ No newline at end of file diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 7849647b38..8c6820f202 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -84,6 +84,10 @@ function validateParams(p5, fn) { // Add web API schemas to the schema map. Object.assign(schemaMap, webAPISchemas); + // For mapping 0-indexed parameters to their ordinal representation, e.g. + // "first" for 0, "second" for 1, "third" for 2, etc. + const ordinals = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth"]; + /** * This is a helper function that generates Zod schemas for a function based on * the parameter data from `docs/parameterData.json`. @@ -102,12 +106,6 @@ function validateParams(p5, fn) { * Where each array in `overloads` represents a set of valid overloaded * parameters, and `?` is a shorthand for `Optional`. * - * TODO: - * - [ ] Support for p5 constructors - * - [ ] Support for more obscure types, such as `lerpPalette` and optional - * objects in `p5.Geometry.computeNormals()` - * (see https://github.com/processing/p5.js/pull/7186#discussion_r1724983249) - * * @param {String} func - Name of the function. * @returns {z.ZodSchema} Zod schema */ @@ -152,9 +150,7 @@ function validateParams(p5, fn) { } // All p5 objects start with `p5` in the documentation, i.e. `p5.Camera`. else if (baseType.startsWith('p5')) { - console.log('type', baseType); const className = baseType.substring(baseType.indexOf('.') + 1); - console.log('className', p5Constructors[className]); typeSchema = z.instanceof(p5Constructors[className]); } // For primitive types and web API objects. @@ -244,7 +240,6 @@ function validateParams(p5, fn) { /** * This is a helper function to print out the Zod schema in a readable format. - * This is for debugging purposes only and will be removed in the future. * * @param {z.ZodSchema} schema - Zod schema. * @param {number} indent - Indentation level. @@ -289,14 +284,27 @@ function validateParams(p5, fn) { // Helper function that scores how close the input arguments are to a schema. // Lower score means closer match. const scoreSchema = schema => { + let score = Infinity; if (!(schema instanceof z.ZodTuple)) { console.warn('Schema below is not a tuple: '); printZodSchema(schema); - return Infinity; + return score; } + const numArgs = args.length; const schemaItems = schema.items; - let score = Math.abs(schemaItems.length - args.length) * 2; + const numSchemaItems = schemaItems.length; + const numRequiredSchemaItems = schemaItems.filter(item => !item.isOptional()).length; + + if (numArgs >= numRequiredSchemaItems && numArgs <= numSchemaItems) { + score = 0; + } + + else { + score = Math.abs( + numArgs < numRequiredSchemaItems ? numRequiredSchemaItems - numArgs : numArgs - numSchemaItems + ) * 4; + } for (let i = 0; i < Math.min(schemaItems.length, args.length); i++) { const paramSchema = schemaItems[i]; @@ -325,6 +333,97 @@ function validateParams(p5, fn) { return closestSchema; } + /** + * Prints a friendly error message after parameter validation, if validation + * has failed. + * + * @method _friendlyParamError + * @private + * @param {z.ZodError} zodErrorObj - The Zod error object containing validation errors. + * @returns {String} The friendly error message. + */ + p5._friendlyParamError = function (zodErrorObj) { + let message; + // The `zodErrorObj` might contain multiple errors of equal importance + // (after scoring the schema closeness in `findClosestSchema`). Here, we + // always print the first error so that user can work through the errors + // one by one. + let currentError = zodErrorObj.errors[0]; + + // Helper function to build a type mismatch message. + const buildTypeMismatchMessage = (actualType, expectedTypeStr, position) => { + const positionStr = position ? `at the ${ordinals[position]} parameter` : ''; + const actualTypeStr = actualType ? `, but received ${actualType}` : ''; + return `Expected ${expectedTypeStr} ${positionStr}${actualTypeStr}.`; + } + + // Union errors occur when a parameter can be of multiple types but is not + // of any of them. In this case, aggregate all possible types and print + // a friendly error message that indicates what the expected types are at + // which position (position is not 0-indexed, for accessibility reasons). + const processUnionError = (error) => { + const expectedTypes = new Set(); + let actualType; + + error.unionErrors.forEach(err => { + const issue = err.issues[0]; + if (issue) { + if (!actualType) { + actualType = issue.received; + } + + if (issue.code === 'invalid_type') { + expectedTypes.add(issue.expected); + } + // The case for constants. Since we don't want to print out the actual + // constant values in the error message, the error message will + // direct users to the documentation. + else if (issue.code === 'invalid_literal') { + expectedTypes.add("constant (please refer to documentation for allowed values)"); + } else if (issue.code === 'custom') { + const match = issue.message.match(/Input not instance of (\w+)/); + if (match) expectedTypes.add(match[1]); + } + } + }); + + if (expectedTypes.size > 0) { + const expectedTypesStr = Array.from(expectedTypes).join(' or '); + const position = error.path.join('.'); + + message = buildTypeMismatchMessage(actualType, expectedTypesStr, position); + } + + return message; + } + + switch (currentError.code) { + case 'invalid_union': { + processUnionError(currentError); + break; + } + case 'too_small': { + const minArgs = currentError.minimum; + message = `Expected at least ${minArgs} argument${minArgs > 1 ? 's' : ''}, but received fewer. Please add more arguments!`; + break; + } + case 'invalid_type': { + message = buildTypeMismatchMessage(currentError.received, currentError.expected, currentError.path.join('.')); + break; + } + case 'too_big': { + const maxArgs = currentError.maximum; + message = `Expected at most ${maxArgs} argument${maxArgs > 1 ? 's' : ''}, but received more. Please delete some arguments!`; + break; + } + default: { + console.log('Zod error object', currentError); + } + } + + return message; + } + /** * Runs parameter validation by matching the input parameters to Zod schemas * generated from the parameter data from `docs/parameterData.json`. @@ -334,7 +433,7 @@ function validateParams(p5, fn) { * @returns {Object} The validation result. * @returns {Boolean} result.success - Whether the validation was successful. * @returns {any} [result.data] - The parsed data if validation was successful. - * @returns {import('zod-validation-error').ZodValidationError} [result.error] - The validation error if validation failed. + * @returns {String} [result.error] - The validation error message if validation has failed. */ fn._validateParams = function (func, args) { if (p5.disableFriendlyErrors) { @@ -346,12 +445,11 @@ function validateParams(p5, fn) { // user intended to call the function with non-undefined arguments. Skip // regular workflow and return a friendly error message right away. if (Array.isArray(args) && args.every(arg => arg === undefined)) { - const undefinedError = new Error(`All arguments for function ${func} are undefined. There is likely an error in the code.`); - const zodUndefinedError = fromError(undefinedError); + const undefinedErrorMessage = `All arguments for function ${func} are undefined. There is likely an error in the code.`; return { success: false, - error: zodUndefinedError + error: undefinedErrorMessage }; } @@ -368,11 +466,12 @@ function validateParams(p5, fn) { }; } catch (error) { const closestSchema = findClosestSchema(funcSchemas, args); - const validationError = fromError(closestSchema.safeParse(args).error); + const zodError = closestSchema.safeParse(args).error; + const errorMessage = p5._friendlyParamError(zodError); return { success: false, - error: validationError + error: errorMessage }; } }; diff --git a/test/js/chai_helpers.js b/test/js/chai_helpers.js index d6ba5c92ca..8bed1efc12 100644 --- a/test/js/chai_helpers.js +++ b/test/js/chai_helpers.js @@ -12,7 +12,7 @@ assert.arrayApproximately = function (arr1, arr2, delta, desc) { } } -assert.deepCloseTo = function(actual, expected, digits = 4) { +assert.deepCloseTo = function (actual, expected, digits = 4) { expect(actual.length).toBe(expected.length); for (let i = 0; i < actual.length; i++) { expect(actual[i]).withContext(`[${i}]`).toBeCloseTo(expected[i], digits); @@ -27,14 +27,4 @@ assert.validationError = function (fn) { } else { assert.doesNotThrow(fn, Error, 'got unwanted exception'); } -}; - -// A custom assertion for validation results for the new parameter validation -// system. -assert.validationResult = function (result, expectSuccess) { - if (expectSuccess) { - assert.isTrue(result.success); - } else { - assert.instanceOf(result.error, ValidationError); - } -}; +}; \ No newline at end of file diff --git a/test/unit/core/param_errors.js b/test/unit/core/param_errors.js index ec1d32d697..b80a5477c5 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -1,10 +1,6 @@ import validateParams from '../../../src/core/friendly_errors/param_validator.js'; import * as constants from '../../../src/core/constants.js'; -import '../../js/chai_helpers' -import { vi } from 'vitest'; -import { ValidationError } from 'zod-validation-error'; - suite('Validate Params', function () { const mockP5 = { disableFriendlyErrors: false, @@ -46,57 +42,63 @@ suite('Validate Params', function () { invalidInputs.forEach(({ input }) => { const result = mockP5Prototype._validateParams('p5.saturation', input); - assert.instanceOf(result.error, ValidationError); + assert.isTrue(result.error.startsWith("Expected Color or array or string at the first parameter, but received")); }); }); }); suite('validateParams: constant as parameter', function () { + const validInputs = [ + { name: 'BLEND, no friendly-err-msg', input: constants.BLEND }, + { name: 'HARD_LIGHT, no friendly-err-msg', input: constants.HARD_LIGHT } + ]; + + validInputs.forEach(({ name, input }) => { + test(`blendMode(): ${name}`, () => { + const result = mockP5Prototype._validateParams('p5.blendMode', [input]); + assert.isTrue(result.success); + }); + }); + const FAKE_CONSTANT = 'fake-constant'; - const testCases = [ - { name: 'BLEND, no friendly-err-msg', input: constants.BLEND, expectSuccess: true }, - { name: 'HARD_LIGHT, no friendly-err-msg', input: constants.HARD_LIGHT, expectSuccess: true }, - { name: 'invalid constant', input: FAKE_CONSTANT, expectSuccess: false }, - { name: 'non-constant parameter', input: 100, expectSuccess: false } + const invalidInputs = [ + { name: 'invalid constant', input: FAKE_CONSTANT }, + { name: 'non-constant parameter', input: 100 } ]; - testCases.forEach(({ name, input, expectSuccess }) => { + invalidInputs.forEach(({ name, input }) => { test(`blendMode(): ${name}`, () => { const result = mockP5Prototype._validateParams('p5.blendMode', [input]); - assert.validationResult(result, expectSuccess); + const expectedError = "Expected constant (please refer to documentation for allowed values) at the first parameter, but received " + input + "."; + assert.equal(result.error, expectedError); }); }); }); suite('validateParams: numbers + optional constant for arc()', function () { - const testCases = [ - { name: 'no friendly-err-msg', input: [200, 100, 100, 80, 0, Math.PI, constants.PIE, 30], expectSuccess: true }, - { name: 'missing optional param #6 & #7, no friendly-err-msg', input: [200, 100, 100, 80, 0, Math.PI], expectSuccess: true }, - { name: 'missing required arc parameters #4, #5', input: [200, 100, 100, 80], expectSuccess: false }, - { name: 'missing required param #0', input: [undefined, 100, 100, 80, 0, Math.PI, constants.PIE, 30], expectSuccess: false }, - { name: 'missing required param #4', input: [200, 100, 100, 80, undefined, 0], expectSuccess: false }, - { name: 'missing optional param #5', input: [200, 100, 100, 80, 0, undefined, Math.PI], expectSuccess: false }, - { name: 'wrong param type at #0', input: ['a', 100, 100, 80, 0, Math.PI, constants.PIE, 30], expectSuccess: false } + const validInputs = [ + { name: 'no friendly-err-msg', input: [200, 100, 100, 80, 0, Math.PI, constants.PIE, 30] }, + { name: 'missing optional param #6 & #7, no friendly-err-msg', input: [200, 100, 100, 80, 0, Math.PI] } ]; - - testCases.forEach(({ name, input, expectSuccess }) => { + validInputs.forEach(({ name, input }) => { test(`arc(): ${name}`, () => { const result = mockP5Prototype._validateParams('p5.arc', input); - assert.validationResult(result, expectSuccess); + assert.isTrue(result.success); }); }); - }); - suite('validateParams: numbers + optional constant for rect()', function () { - const testCases = [ - { name: 'no friendly-err-msg', input: [1, 1, 10.5, 10], expectSuccess: true }, - { name: 'wrong param type at #0', input: ['a', 1, 10.5, 10, 0, Math.PI], expectSuccess: false } + const invalidInputs = [ + { name: 'missing required arc parameters #4, #5', input: [200, 100, 100, 80], msg: 'Expected at least 6 arguments, but received fewer. Please add more arguments!' }, + { name: 'missing required param #0', input: [undefined, 100, 100, 80, 0, Math.PI, constants.PIE, 30], msg: 'Expected number at the first parameter, but received undefined.' }, + { name: 'missing required param #4', input: [200, 100, 100, 80, undefined, 0], msg: 'Expected number at the fifth parameter, but received undefined.' }, + { name: 'missing optional param #5', input: [200, 100, 100, 80, 0, undefined, Math.PI], msg: 'Expected number at the sixth parameter, but received undefined.' }, + { name: 'wrong param type at #0', input: ['a', 100, 100, 80, 0, Math.PI, constants.PIE, 30], msg: 'Expected number at the first parameter, but received string.' } ]; - testCases.forEach(({ name, input, expectSuccess }) => { - test(`rect(): ${name}`, () => { - const result = mockP5Prototype._validateParams('p5.rect', input); - assert.validationResult(result, expectSuccess); + invalidInputs.forEach(({ name, input, msg }) => { + test(`arc(): ${name}`, () => { + const result = mockP5Prototype._validateParams('p5.arc', input); + assert.equal(result.error, msg); }); }); }); @@ -109,76 +111,87 @@ suite('Validate Params', function () { }) suite('validateParams: a few edge cases', function () { - const testCases = [ - { fn: 'color', name: 'wrong type for optional parameter', input: [0, 0, 0, 'A'] }, - { fn: 'color', name: 'superfluous parameter', input: [[0, 0, 0], 0] }, - { fn: 'color', name: 'wrong element types', input: [['A', 'B', 'C']] }, - { fn: 'rect', name: 'null, non-trailing, optional parameter', input: [0, 0, 0, 0, null, 0, 0, 0] }, - { fn: 'color', name: 'too many args + wrong types too', input: ['A', 'A', 0, 0, 0, 0, 0, 0, 0, 0] }, - { fn: 'line', name: 'null string given', input: [1, 2, 4, 'null'] }, - { fn: 'line', name: 'NaN value given', input: [1, 2, 4, NaN] } + const invalidInputs = [ + { fn: 'color', name: 'wrong type for optional parameter', input: [0, 0, 0, 'A'], msg: 'Expected number at the fourth parameter, but received string.' }, + { fn: 'color', name: 'superfluous parameter', input: [[0, 0, 0], 0], msg: 'Expected number at the first parameter, but received array.' }, + { fn: 'color', name: 'wrong element types', input: [['A', 'B', 'C']], msg: 'Expected number at the first parameter, but received array.' }, + { fn: 'rect', name: 'null, non-trailing, optional parameter', input: [0, 0, 0, 0, null, 0, 0, 0], msg: 'Expected number at the fifth parameter, but received null.' }, + { fn: 'color', name: 'too many args + wrong types too', input: ['A', 'A', 0, 0, 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments!' }, + { fn: 'line', name: 'null string given', input: [1, 2, 4, 'null'], msg: 'Expected number at the fourth parameter, but received string.' }, + { fn: 'line', name: 'NaN value given', input: [1, 2, 4, NaN], msg: 'Expected number at the fourth parameter, but received nan.' } ]; - testCases.forEach(({ name, input, fn }) => { + invalidInputs.forEach(({ name, input, fn, msg }) => { test(`${fn}(): ${name}`, () => { const result = mockP5Prototype._validateParams(`p5.${fn}`, input); - console.log(result); - assert.validationResult(result, false); + assert.equal(result.error, msg); }); }); }); suite('validateParams: trailing undefined arguments', function () { - const testCases = [ - { fn: 'color', name: 'missing params #1, #2', input: [12, undefined, undefined] }, + const invalidInputs = [ + { fn: 'color', name: 'missing params #1, #2', input: [12, undefined, undefined], msg: 'Expected number at the second parameter, but received undefined.' }, // Even though the undefined arguments are technically allowed for // optional parameters, it is more likely that the user wanted to call // the function with meaningful arguments. - { fn: 'random', name: 'missing params #0, #1', input: [undefined, undefined] }, - { fn: 'circle', name: 'missing compulsory parameter #2', input: [5, 5, undefined] } + { fn: 'random', name: 'missing params #0, #1', input: [undefined, undefined], msg: 'All arguments for function p5.random are undefined. There is likely an error in the code.' }, + { fn: 'circle', name: 'missing compulsory parameter #2', input: [5, 5, undefined], msg: 'Expected number at the third parameter, but received undefined.' } ]; - testCases.forEach(({ fn, name, input }) => { + invalidInputs.forEach(({ fn, name, input, msg }) => { test(`${fn}(): ${name}`, () => { const result = mockP5Prototype._validateParams(`p5.${fn}`, input); - assert.validationResult(result, false); + assert.equal(result.error, msg); }); }); }); suite('validateParams: multi-format', function () { - const testCases = [ - { name: 'no friendly-err-msg', input: [65], expectSuccess: true }, - { name: 'no friendly-err-msg', input: [65, 100], expectSuccess: true }, - { name: 'no friendly-err-msg', input: [65, 100, 100], expectSuccess: true }, - { name: 'optional parameter, incorrect type', input: [65, 100, 100, 'a'], expectSuccess: false }, - { name: 'extra parameter', input: [[65, 100, 100], 100], expectSuccess: false }, - { name: 'incorrect element type', input: ['A', 'B', 'C'], expectSuccess: false }, - { name: 'incorrect parameter count', input: ['A', 'A', 0, 0, 0, 0, 0, 0], expectSuccess: false } + const validInputs = [ + { name: 'no friendly-err-msg', input: [65] }, + { name: 'no friendly-err-msg', input: [65, 100] }, + { name: 'no friendly-err-msg', input: [65, 100, 100] } ]; + validInputs.forEach(({ name, input }) => { + test(`color(): ${name}`, () => { + const result = mockP5Prototype._validateParams('p5.color', input); + assert.isTrue(result.success); + }); + }); - testCases.forEach(({ name, input, expectSuccess }) => { + const invalidInputs = [ + { name: 'optional parameter, incorrect type', input: [65, 100, 100, 'a'], msg: 'Expected number at the fourth parameter, but received string.' }, + { name: 'extra parameter', input: [[65, 100, 100], 100], msg: 'Expected number at the first parameter, but received array.' }, + { name: 'incorrect element type', input: ['A', 'B', 'C'], msg: 'Expected number at the first parameter, but received string.' }, + { name: 'incorrect parameter count', input: ['A', 'A', 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments!' } + ]; + + invalidInputs.forEach(({ name, input, msg }) => { test(`color(): ${name}`, () => { const result = mockP5Prototype._validateParams('p5.color', input); - assert.validationResult(result, expectSuccess); + assert.equal(result.error, msg); }); }); }); suite('validateParameters: union types', function () { - const testCases = [ - { name: 'set() with Number', input: [0, 0, 0], expectSuccess: true }, - { name: 'set() with Number[]', input: [0, 0, [0, 0, 0, 255]], expectSuccess: true }, - { name: 'set() with Object', input: [0, 0, new mockP5.Color()], expectSuccess: true }, - { name: 'set() with Boolean (invalid)', input: [0, 0, true], expectSuccess: false } + const validInputs = [ + { name: 'set() with Number', input: [0, 0, 0] }, + { name: 'set() with Number[]', input: [0, 0, [0, 0, 0, 255]] }, + { name: 'set() with Object', input: [0, 0, new mockP5.Color()] } ]; - - testCases.forEach(({ name, input, expectSuccess }) => { - test(`set(): ${name}`, function () { + validInputs.forEach(({ name, input }) => { + test(`${name}`, function () { const result = mockP5Prototype._validateParams('p5.set', input); - assert.validationResult(result, expectSuccess); + assert.isTrue(result.success); }); }); + + test(`set() with Boolean (invalid)`, function () { + const result = mockP5Prototype._validateParams('p5.set', [0, 0, true]); + assert.equal(result.error, 'Expected number or array or object at the third parameter, but received boolean.'); + }); }); suite('validateParams: web API objects', function () { @@ -193,7 +206,7 @@ suite('Validate Params', function () { testCases.forEach(({ fn, name, input }) => { test(`${fn}(): ${name}`, function () { const result = mockP5Prototype._validateParams(fn, input); - assert.validationResult(result, true); + assert.isTrue(result.success); }); }); }); From 194956e397fc87fe458fb94487464f3d8a488fa5 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Thu, 26 Sep 2024 22:34:17 -0400 Subject: [PATCH 065/120] delete printZodSchema function because it's not necessary and doesn't work in production anyway --- src/core/friendly_errors/param_validator.js | 40 ++++++--------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 8c6820f202..40778a3d13 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -5,7 +5,6 @@ import p5 from '../main.js'; import * as constants from '../constants.js'; import { z } from 'zod'; -import { fromError } from 'zod-validation-error'; import dataDoc from '../../../docs/parameterData.json'; function validateParams(p5, fn) { @@ -238,33 +237,6 @@ function validateParams(p5, fn) { : z.union(overloadSchemas); } - /** - * This is a helper function to print out the Zod schema in a readable format. - * - * @param {z.ZodSchema} schema - Zod schema. - * @param {number} indent - Indentation level. - */ - function printZodSchema(schema, indent = 0) { - const i = ' '.repeat(indent); - const log = msg => console.log(`${i}${msg}`); - - if (schema instanceof z.ZodUnion || schema instanceof z.ZodTuple) { - const type = schema instanceof z.ZodUnion ? 'Union' : 'Tuple'; - log(`${type}: [`); - - const items = schema instanceof z.ZodUnion - ? schema._def.options - : schema.items; - items.forEach((item, index) => { - log(` ${type === 'Union' ? 'Option' : 'Item'} ${index + 1}:`); - printZodSchema(item, indent + 4); - }); - log(']'); - } else { - log(schema.constructor.name); - } - } - /** * Finds the closest schema to the input arguments. * @@ -299,7 +271,17 @@ function validateParams(p5, fn) { if (numArgs >= numRequiredSchemaItems && numArgs <= numSchemaItems) { score = 0; } - + // Here, give more weight to mismatch in number of arguments. + // + // For example, color() can either take [Number, Number?] or + // [Number, Number, Number, Number?] as list of parameters. + // If the user passed in 3 arguments, [10, undefined, undefined], it's + // more than likely that they intended to pass in 3 arguments, but the + // last two arguments are invalid. + // + // If there's no bias towards matching the number of arguments, the error + // message will show that we're expecting at most 2 arguments, but more + // are received. else { score = Math.abs( numArgs < numRequiredSchemaItems ? numRequiredSchemaItems - numArgs : numArgs - numSchemaItems From b2d98d7d480ceb2b62ca7a1b652e3b50de317952 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Thu, 26 Sep 2024 22:38:37 -0400 Subject: [PATCH 066/120] Re-trigger GitHub Actions From b6e436643b49841f790b601aed6d1cac60e4db5c Mon Sep 17 00:00:00 2001 From: miaoye que Date: Thu, 26 Sep 2024 23:01:34 -0400 Subject: [PATCH 067/120] remove zod validation error import --- test/js/chai_helpers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/js/chai_helpers.js b/test/js/chai_helpers.js index 8bed1efc12..7018aa9c26 100644 --- a/test/js/chai_helpers.js +++ b/test/js/chai_helpers.js @@ -1,5 +1,4 @@ import p5 from '../../src/app.js'; -import { ValidationError } from 'zod-validation-error'; // Setup chai var expect = chai.expect; From 9f2457619220351ab54305f4e3099344c4a6670f Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 12:28:12 +0100 Subject: [PATCH 068/120] Added a second example for setAttribute() --- src/core/shape/vertex.js | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 1eab0fc475..3f4afdea22 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2320,6 +2320,76 @@ p5.prototype.normal = function(x, y, z) { * } * *
+ * + *
+ * + * let myShader; + * const cols = 10; + * const rows = 10; + * const cellSize = 16; + * + * 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; + * 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); + * } + * `; + * + * async function setup(){ + * myShader = createShader(vertSrc, fragSrc); + * createCanvas(200, 200, WEBGL); + * shader(myShader); + * noStroke(); + * describe('A blue grid, which moves away from the mouse position, on a grey background.'); + * } + * + * function draw(){ + * background(200); + * translate(-cols*cellSize/2, -rows*cellSize/2); + * beginShape(QUADS); + * for (let x = 0; x < cols; x++) { + * for (let y = 0; y < rows; y++) { + * let x1 = x * cellSize; + * let y1 = y * cellSize; + * let x2 = x1 + cellSize; + * let y2 = y1 + cellSize; + * fill(x/rows*255, y/cols*255, 255); + * let distance = dist(x1,y1, mouseX, mouseY); + * setAttribute('aDistance', min(distance, 200)); + * vertex(x1, y1); + * vertex(x2, y1); + * vertex(x2, y2); + * vertex(x1, y2); + * } + * } + * endShape(); + * } + * + *
/ /** * @method setAttribute From b41b0136fa7d7e67b761cad94f1c25f8688f2f34 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 12:28:45 +0100 Subject: [PATCH 069/120] add custom attributes to buffer strides for quad strip/ quad --- src/webgl/p5.RendererGL.Immediate.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 72d4039346..965904ff86 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -36,11 +36,10 @@ p5.RendererGL.prototype.beginShape = function(mode) { if (this._useUserAttributes === true){ for (const name in this.userAttributes){ delete this.immediateMode.geometry[name]; + delete this.immediateBufferStrides[name.concat('Src')]; } delete this.userAttributes; this._useUserAttributes = false; - } - if (this.tessyVertexSize > 12){ this.tessyVertexSize = 12; } this.immediateMode.geometry.reset(); @@ -196,12 +195,12 @@ p5.RendererGL.prototype.setAttribute = function(attributeName, data){ this._useUserAttributes = true; this.userAttributes = {}; } - const size = data.length ? data.length : 1; + const buff = attributeName.concat('Buffer'); + const attributeSrc = attributeName.concat('Src'); const size = data.length ? data.length : 1; if (!this.userAttributes.hasOwnProperty(attributeName)){ this.tessyVertexSize += size; + this.immediateBufferStrides[attributeSrc] = size; } - const buff = attributeName.concat('Buffer'); - const attributeSrc = attributeName.concat('Src'); this.userAttributes[attributeName] = data; const bufferExists = this.immediateMode .buffers From 3eef4cbaa5e8a9da8b8b7df43425f61f6322ac8b Mon Sep 17 00:00:00 2001 From: miaoye que Date: Fri, 27 Sep 2024 09:20:05 -0400 Subject: [PATCH 070/120] add documentation link in error message for cases where the user provided too few/too many arguments --- src/core/friendly_errors/param_validator.js | 36 ++++++++++++++++----- test/unit/core/param_errors.js | 6 ++-- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index 40778a3d13..d1e20218b8 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -87,6 +87,13 @@ function validateParams(p5, fn) { // "first" for 0, "second" for 1, "third" for 2, etc. const ordinals = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth"]; + function extractFuncNameAndClass(func) { + const ichDot = func.lastIndexOf('.'); + const funcName = func.slice(ichDot + 1); + const funcClass = func.slice(0, ichDot !== -1 ? ichDot : 0) || 'p5'; + return { funcName, funcClass }; + } + /** * This is a helper function that generates Zod schemas for a function based on * the parameter data from `docs/parameterData.json`. @@ -105,7 +112,7 @@ function validateParams(p5, fn) { * Where each array in `overloads` represents a set of valid overloaded * parameters, and `?` is a shorthand for `Optional`. * - * @param {String} func - Name of the function. + * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add` * @returns {z.ZodSchema} Zod schema */ function generateZodSchemasForFunc(func) { @@ -121,11 +128,7 @@ function validateParams(p5, fn) { ]); } - // Expect global functions like `sin` and class methods like `p5.Vector.add` - const ichDot = func.lastIndexOf('.'); - const funcName = func.slice(ichDot + 1); - const funcClass = func.slice(0, ichDot !== -1 ? ichDot : 0) || 'p5'; - + const { funcName, funcClass } = extractFuncNameAndClass(func); let funcInfo = dataDoc[funcClass][funcName]; let overloads = []; @@ -322,9 +325,10 @@ function validateParams(p5, fn) { * @method _friendlyParamError * @private * @param {z.ZodError} zodErrorObj - The Zod error object containing validation errors. + * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add` * @returns {String} The friendly error message. */ - p5._friendlyParamError = function (zodErrorObj) { + p5._friendlyParamError = function (zodErrorObj, func) { let message; // The `zodErrorObj` might contain multiple errors of equal importance // (after scoring the schema closeness in `findClosestSchema`). Here, we @@ -403,6 +407,22 @@ function validateParams(p5, fn) { } } + // Generates a link to the documentation based on the given function name. + // TODO: Check if the link is reachable before appending it to the error + // message. + const generateDocumentationLink = (func) => { + const { funcName, funcClass } = extractFuncNameAndClass(func); + const p5BaseUrl = 'https://p5js.org/reference'; + const url = `${p5BaseUrl}/${funcClass}/${funcName}`; + + return url; + } + + if (currentError.code === 'too_big' || currentError.code === 'too_small') { + const documentationLink = generateDocumentationLink(func); + message += ` For more information, see ${documentationLink}.`; + } + return message; } @@ -449,7 +469,7 @@ function validateParams(p5, fn) { } catch (error) { const closestSchema = findClosestSchema(funcSchemas, args); const zodError = closestSchema.safeParse(args).error; - const errorMessage = p5._friendlyParamError(zodError); + const errorMessage = p5._friendlyParamError(zodError, func); return { success: false, diff --git a/test/unit/core/param_errors.js b/test/unit/core/param_errors.js index b80a5477c5..e9e9bf4ff4 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -88,7 +88,7 @@ suite('Validate Params', function () { }); const invalidInputs = [ - { name: 'missing required arc parameters #4, #5', input: [200, 100, 100, 80], msg: 'Expected at least 6 arguments, but received fewer. Please add more arguments!' }, + { name: 'missing required arc parameters #4, #5', input: [200, 100, 100, 80], msg: 'Expected at least 6 arguments, but received fewer. Please add more arguments! For more information, see https://p5js.org/reference/p5/arc.' }, { name: 'missing required param #0', input: [undefined, 100, 100, 80, 0, Math.PI, constants.PIE, 30], msg: 'Expected number at the first parameter, but received undefined.' }, { name: 'missing required param #4', input: [200, 100, 100, 80, undefined, 0], msg: 'Expected number at the fifth parameter, but received undefined.' }, { name: 'missing optional param #5', input: [200, 100, 100, 80, 0, undefined, Math.PI], msg: 'Expected number at the sixth parameter, but received undefined.' }, @@ -116,7 +116,7 @@ suite('Validate Params', function () { { fn: 'color', name: 'superfluous parameter', input: [[0, 0, 0], 0], msg: 'Expected number at the first parameter, but received array.' }, { fn: 'color', name: 'wrong element types', input: [['A', 'B', 'C']], msg: 'Expected number at the first parameter, but received array.' }, { fn: 'rect', name: 'null, non-trailing, optional parameter', input: [0, 0, 0, 0, null, 0, 0, 0], msg: 'Expected number at the fifth parameter, but received null.' }, - { fn: 'color', name: 'too many args + wrong types too', input: ['A', 'A', 0, 0, 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments!' }, + { fn: 'color', name: 'too many args + wrong types too', input: ['A', 'A', 0, 0, 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments! For more information, see https://p5js.org/reference/p5/color.' }, { fn: 'line', name: 'null string given', input: [1, 2, 4, 'null'], msg: 'Expected number at the fourth parameter, but received string.' }, { fn: 'line', name: 'NaN value given', input: [1, 2, 4, NaN], msg: 'Expected number at the fourth parameter, but received nan.' } ]; @@ -164,7 +164,7 @@ suite('Validate Params', function () { { name: 'optional parameter, incorrect type', input: [65, 100, 100, 'a'], msg: 'Expected number at the fourth parameter, but received string.' }, { name: 'extra parameter', input: [[65, 100, 100], 100], msg: 'Expected number at the first parameter, but received array.' }, { name: 'incorrect element type', input: ['A', 'B', 'C'], msg: 'Expected number at the first parameter, but received string.' }, - { name: 'incorrect parameter count', input: ['A', 'A', 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments!' } + { name: 'incorrect parameter count', input: ['A', 'A', 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments! For more information, see https://p5js.org/reference/p5/color.' } ]; invalidInputs.forEach(({ name, input, msg }) => { From ebce73e8e5fd522de245ecaf1945ebf59028b327 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 14:23:44 +0100 Subject: [PATCH 071/120] added visual tests for setAttribute() --- test/unit/visual/cases/webgl.js | 96 ++++++++++++++++++ .../setAttribute/on QUADS shape mode/000.png | Bin 0 -> 421 bytes .../on QUADS shape mode/metadata.json | 3 + .../setAttribute/on TESS shape mode/000.png | Bin 0 -> 883 bytes .../on TESS shape mode/metadata.json | 3 + .../000.png | Bin 0 -> 630 bytes .../metadata.json | 3 + 7 files changed, 105 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png create mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png create mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png create mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index e5e8a9be7a..def61f978e 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -130,4 +130,100 @@ visualSuite('WebGL', function() { } ); }); + + visualSuite('setAttribute', function(){ + const vertSrc = `#version 300 es + precision mediump float; + uniform mat4 uProjectionMatrix; + uniform mat4 uModelViewMatrix; + in vec3 aPosition; + in vec3 aCol; + out vec3 vCol; + void main(){ + vCol = aCol; + gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); + }`; + const fragSrc = `#version 300 es + precision mediump float; + in vec3 vCol; + out vec4 outColor; + void main(){ + outColor = vec4(vCol, 1.0); + }`; + visualTest( + 'on TESS shape mode', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.background('white'); + const myShader = p5.createShader(vertSrc, fragSrc); + p5.shader(myShader); + p5.beginShape(p5.TESS); + p5.noStroke(); + for (let i = 0; i < 20; i++){ + let x = 20 * p5.sin(i/20*p5.TWO_PI); + let y = 20 * p5.cos(i/20*p5.TWO_PI); + p5.setAttribute('aCol', [x/20, -y/20, 0]); + p5.vertex(x, y); + } + p5.endShape(); + screenshot(); + } + ); + visualTest( + 'on QUADS shape mode', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.background('white'); + const myShader = p5.createShader(vertSrc, fragSrc); + p5.shader(myShader) + p5.beginShape(p5.QUADS); + p5.noStroke(); + p5.translate(-25,-25); + for (let i = 0; i < 5; i++){ + for (let j = 0; j < 5; j++){ + let x1 = i * 10; + let x2 = x1 + 10; + let y1 = j * 10; + let y2 = y1 + 10; + p5.setAttribute('aCol', [1, 0, 0]); + p5.vertex(x1, y1); + p5.setAttribute('aCol', [0, 0, 1]); + p5.vertex(x2, y1); + p5.setAttribute('aCol', [0, 1, 1]); + p5.vertex(x2, y2); + p5.setAttribute('aCol', [1, 1, 1]); + p5.vertex(x1, y2); + } + } + p5.endShape(); + screenshot(); + } + ); + visualTest( + 'on buildGeometry outputs containing 3D primitives', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.background('white'); + const myShader = p5.createShader(vertSrc, fragSrc); + p5.shader(myShader); + const shape = p5.buildGeometry(() => { + p5.push(); + p5.translate(15,-10,0); + p5.sphere(5); + p5.pop(); + p5.beginShape(p5.TRIANGLES); + p5.setAttribute('aCol', [1,0,0]) + p5.vertex(-5, 5, 0); + p5.setAttribute('aCol', [0,1,0]) + p5.vertex(5, 5, 0); + p5.setAttribute('aCol', [0,0,1]) + p5.vertex(0, -5, 0); + p5.endShape(p5.CLOSE); + p5.push(); + p5.translate(-15,10,0); + p5.box(10); + p5.pop(); + }) + p5.model(shape); + screenshot(); + } + ); + }); }); diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png new file mode 100644 index 0000000000000000000000000000000000000000..75018122a4f9c6100832d4a7c6fcee5ace5965d4 GIT binary patch literal 421 zcmV;W0b2fvP)Px$Ur9tkRA@u(nXz%gKoCX$7C-^Eb1HBFR6tGzE`SQ0Q-KSh0_Rjf0Ti%Sz761E zbVdkiPZ!+G-gthbfX@tnPw4Ic5?igV9bWK*SLCjWx9sPC-;dIXt=HEMC@}JlmUUvA zO)`Ro6WJysZR*5!y9Xow$tXS^jI^f{+wY%@%uihL`DCOmo!H?pV`MsU#n+6Hc64IL zqhn+|amBY|qz#?e>69@tOk8o!7>U=3T`mho`iU#L1tYOKF|z_Afa3N;ZkJB7Rkz(I z*-7@)=GV^lY~y_(vW<5VXITfp%+Aby2V&j#_H5(X#-~24_Ivw(li7a(-B$ZOUeb?w P00000NkvXXu0mjfI)lLG literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png new file mode 100644 index 0000000000000000000000000000000000000000..5a5164da2f41e42175fa6b9d21f38e0a11a3587b GIT binary patch literal 883 zcmV-(1C0EMP)Px&ElET{RA@u(*}rQOVHgMS@1>@#t(H{L3N;`Nf~{C$hg7iG_a%#i(5?mlfr6lm zgVXcg*j0rtu5N-jI^CN=aFQbU1EdcA7lmr4H+$H(k_j&Gl z?-F&p-7b#6BN{?Vmf|pxk_Cd`iO78k*_AzKXTQ^XJqqAv#4rKiMr9St5yLS6ixn2^ zcm#onUjp&(IP*s-$jQFviT|&!RT(yp6E0Q#L0%BCClK#t15#NQB*k4FXSh`(%#FK) zoFL+dK>V{9U z!Jl)hdOf(YZI2x6nYk8Wrj?H=gz%>v^2@&g=lbx4DA<(;`HS>ii2N+UpZKNWTUx5T z6r|oJ)Y{ppeLejZ1|98t?8oxImH;JG>zUn@L7K=glzawiOePms+zLOg@i1x5RfyB;ao-< zg^*CGgn(R35{zf0Q3wf@iZ5h_G0f$pVFU@0iZ5g_Nidm{h7m+<m(j9y-u8<}X8U_gCM0_zWkQNaZ3=!54*D|w(tPr7TfpCJTozz-Pt&UBX`Zka@ z5n7fAXGmIVrXUl@IuY6qfb_Q*H+$Z-ZxJXDa+?V29)O^b{)E!f>vTH3w-vJY_cUYR ztE(6Tu*?{i3g&|CO@knb*na6@Y(In?#!>b8%wZxvNAEE9{R?;Ux_YR26U+br002ov JPDHLkV1ffDkUIbX literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png new file mode 100644 index 0000000000000000000000000000000000000000..596ec54c90d7b36a606d8145e1332a4822a29f51 GIT binary patch literal 630 zcmV-+0*U>JP)Px%FiAu~RA@u(mp@CxKor1V?4W}~K^G-L#l;rCfPaRb5}`U2EGUA~#jzqC6o*LC zPvIBP(GMV4+DQ=HGPo!R6&+l}O&lUGn9wGd_G<6aLoOgKa=m-M_j~W|qPec?!asO4 z1tOV6WkfO|po}OZf-;~=OHg8iX>HpMzUT9Kz`t&fbVi!ZCJ;g*F(Zg&S+PJ;fy6%zRpaW}#{K-T#b2xf}bBdi(ZJx&=>MtIlM?|iDXq%SQD zkoDAM9Ve)ol6gZ+BKY<-xZi%02JqKYAj8Uh+`7t$G9oAgsM*`v3p{ literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From e46c2d7777389bdba8c5d8566adc8a9dc0f24afa Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 16:22:29 +0100 Subject: [PATCH 072/120] improved error message for when working directly with geometry.setattribute --- src/webgl/p5.RendererGL.Retained.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 37bf95457d..ddb4c8d70e 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -158,11 +158,12 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { continue; } const adjustedLength = geometry.model[buff.src].length / buff.size; - if(adjustedLength != geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute with - either too many or too few values compared to vertices. - There may be unexpected results from the shaders. - `, 'setAttribute()'); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom attribute with more values than vertices. + This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom attribute with less values than vertices. + This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } buff._prepareBuffer(geometry, fillShader); } @@ -191,11 +192,12 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { continue; } const adjustedLength = geometry.model[buff.src].length / buff.size; - if(adjustedLength != geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute with - either too many or too few values compared to vertices. - There may be unexpected results from the shaders. - `, 'setAttribute()'); + if(adjustedLength > geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom attribute with more values than vertices. + This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + } else if(adjustedLength < geometry.model.vertices.length){ + p5._friendlyError(`One of the geometries has a custom attribute with fewer values than vertices. + This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } buff._prepareBuffer(geometry, strokeShader); } From d8318e1ba42d01902493b78a6a2fbca206d42466 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 16:22:53 +0100 Subject: [PATCH 073/120] reset userattributes object instead of deleting (its likely to be used again --- src/webgl/p5.RendererGL.Immediate.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 965904ff86..a9386be051 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -38,9 +38,10 @@ p5.RendererGL.prototype.beginShape = function(mode) { delete this.immediateMode.geometry[name]; delete this.immediateBufferStrides[name.concat('Src')]; } - delete this.userAttributes; + this.userAttributes = {}; this._useUserAttributes = false; this.tessyVertexSize = 12; + this.immediateMode.buffers.user = []; } this.immediateMode.geometry.reset(); this.immediateMode.contourIndices = []; From 968a05ec8b338f58ef1440bf85b3187d1f6ab1f4 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 17:44:17 +0100 Subject: [PATCH 074/120] remove old user defined render buffers when model is called --- src/webgl/p5.RendererGL.Retained.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index ddb4c8d70e..5adff5010d 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -63,6 +63,7 @@ p5.RendererGL.prototype._freeBuffers = function(gId) { freeBuffers(this.retainedMode.buffers.stroke); freeBuffers(this.retainedMode.buffers.fill); freeBuffers(this.retainedMode.buffers.user); + this.retainedMode.buffers.user = []; }; /** @@ -162,7 +163,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { p5._friendlyError(`One of the geometries has a custom attribute with more values than vertices. This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute with less values than vertices. + p5._friendlyError(`One of the geometries has a custom attribute with fewer values than vertices. This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } buff._prepareBuffer(geometry, fillShader); @@ -213,7 +214,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { if (this.geometryBuilder) { this.geometryBuilder.addRetained(geometry); } - + return this; }; From a66af6955c533958677d31f16b230e181dacd24e Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 17:44:42 +0100 Subject: [PATCH 075/120] Added a bunch of tests for setAttribute() --- test/unit/webgl/p5.RendererGL.js | 176 +++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 0fcbf5df6c..962b734186 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2503,4 +2503,180 @@ suite('p5.RendererGL', function() { } ); }); + + suite('setAttribute()', function() { + test('Immediate mode data and buffers created in beginShape', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + + myp5.beginShape(); + myp5.setAttribute('aCustom', 1); + myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + assert.deepEqual(myp5._renderer.userAttributes,{ + aCustom: 1, + aCustomVec3: [1,2,3] + }); + assert.deepEqual(myp5._renderer.immediateMode.geometry.aCustomSrc, [1]); + assert.deepEqual(myp5._renderer.immediateMode.geometry.aCustomVec3Src, [1,2,3]); + assert.deepEqual(myp5._renderer.immediateMode.geometry.userAttributes, { + aCustom: 1, + aCustomVec3: 3 + }); + assert.deepEqual(myp5._renderer.immediateMode.buffers.user, [ + { + size: 1, + src: 'aCustomSrc', + dst: 'aCustomBuffer', + attr: 'aCustom', + _renderer: myp5._renderer, + map: undefined + }, + { + size: 3, + src: 'aCustomVec3Src', + dst: 'aCustomVec3Buffer', + attr: 'aCustomVec3', + _renderer: myp5._renderer, + map: undefined + } + ]); + myp5.endShape(); + + } + ); + test('Immediate mode data and buffers deleted after beginShape', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + + myp5.beginShape(); + myp5.setAttribute('aCustom', 1); + myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + myp5.endShape(); + + myp5.beginShape(); + assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomSrc); + assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomVec3Src); + assert.deepEqual(myp5._renderer.immediateMode.geometry.userAttributes, {}); + assert.deepEqual(myp5._renderer.userAttributes, {}); + assert.deepEqual(myp5._renderer.immediateMode.buffers.user, []); + myp5.endShape(); + } + ); + test('Data copied over from beginGeometry', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.beginGeometry(); + myp5.beginShape(); + myp5.setAttribute('aCustom', 1); + myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertex(0,1,0); + myp5.vertex(-1,0,0); + myp5.vertex(1,0,0); + const immediateCopy = myp5._renderer.immediateMode.geometry; + myp5.endShape(); + const myGeo = myp5.endGeometry(); + assert.deepEqual(immediateCopy.aCustomSrc, myGeo.aCustomSrc); + assert.deepEqual(immediateCopy.aCustomVec3Src, myGeo.aCustomVec3Src); + assert.deepEqual(immediateCopy.userAttributes, myGeo.userAttributes); + } + ); + test('Retained mode buffers are created for rendering', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.beginGeometry(); + myp5.beginShape(); + myp5.setAttribute('aCustom', 1); + myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + myp5.vertex(1,0,0); + myp5.endShape(); + const myGeo = myp5.endGeometry(); + myp5._renderer.createBuffers(myGeo.gId, myGeo); + assert.deepEqual(myp5._renderer.retainedMode.buffers.user, [ + { + size: 1, + src: 'aCustomSrc', + dst: 'aCustomBuffer', + attr: 'aCustom', + _renderer: myp5._renderer, + map: undefined + }, + { + size: 3, + src: 'aCustomVec3Src', + dst: 'aCustomVec3Buffer', + attr: 'aCustomVec3', + _renderer: myp5._renderer, + map: undefined + } + ]); + } + ); + test('Retained mode buffers deleted after rendering', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.beginGeometry(); + myp5.beginShape(); + myp5.setAttribute('aCustom', 1); + myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + myp5.vertex(1,0,0); + myp5.endShape(); + const myGeo = myp5.endGeometry(); + myp5.model(myGeo); + assert.equal(myp5._renderer.retainedMode.buffers.user.length, 0); + } + ); + test('Friendly error if different sizes used', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + const logs = []; + const myLog = (...data) => logs.push(data.join(', ')); + const oldLog = console.log; + console.log = myLog; + myp5.beginShape(); + myp5.setAttribute('aCustom', [1,2,3]); + myp5.vertex(0,0,0); + myp5.setAttribute('aCustom', [1,2]); + myp5.vertex(1,0,0); + myp5.endShape(); + console.log = oldLog; + expect(logs.join('\n')).to.match(/Custom attribute aCustom has been set with various data sizes/); + } + ); + test('Friendly error too many values set', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + const logs = []; + const myLog = (...data) => logs.push(data.join(', ')); + const oldLog = console.log; + console.log = myLog; + let myGeo = new p5.Geometry(); + myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.setAttribute('aCustom', 1); + myGeo.setAttribute('aCustom', 2); + myp5.model(myGeo); + console.log = oldLog; + expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute with more values than vertices./); + } + ); + test('Friendly error if too few values set', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + const logs = []; + const myLog = (...data) => logs.push(data.join(', ')); + const oldLog = console.log; + console.log = myLog; + let myGeo = new p5.Geometry(); + myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.setAttribute('aCustom', 1); + myp5.model(myGeo); + console.log = oldLog; + expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute with fewer values than vertices./); + } + ); + }) }); From 62a4afaa6e8463dfe903c45f4da8b846ad8dd14f Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 17:53:52 +0100 Subject: [PATCH 076/120] fixed some bugs in examples for setAttribute --- src/core/shape/vertex.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 3f4afdea22..6ebdff7b77 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2272,9 +2272,8 @@ p5.prototype.normal = function(x, y, z) { * @example *
* - * let vertSrc = ` - * #version 300 es - * precision highp float; + * const vertSrc = `#version 300 es + * precision mediump float; * uniform mat4 uModelViewMatrix; * uniform mat4 uProjectionMatrix; * @@ -2288,18 +2287,17 @@ p5.prototype.normal = function(x, y, z) { * } * `; * - * let fragSrc = ` - * #version 300 es - * precision highp float; - * + * const fragSrc = `#version 300 es + * precision mediump float; + * out vec4 outColor; * void main(){ - * gl_FragColor = vec4(0.0, 1.0, 1.0, 1.0); + * outColor = vec4(0.0, 1.0, 1.0, 1.0); * } * `; * * function setup(){ * createCanvas(100, 100, WEBGL); - * let myShader = createShader(vertSrc, fragSrc); + * const myShader = createShader(vertSrc, fragSrc); * shader(myShader); * noStroke(); * describe('A wobbly, cyan circle on a grey background.'); @@ -2309,10 +2307,10 @@ p5.prototype.normal = function(x, y, z) { * background(125); * beginShape(); * for (let i = 0; i < 30; i++){ - * let x = 40 * cos(i/30 * TWO_PI); - * let y = 40 * sin(i/30 * TWO_PI); - * let xOff = 10 * noise(x + millis()/1000) - 5; - * let yOff = 10 * noise(y + millis()/1000) - 5; + * const x = 40 * cos(i/30 * TWO_PI); + * const y = 40 * sin(i/30 * TWO_PI); + * const xOff = 10 * noise(x + millis()/1000) - 5; + * const yOff = 10 * noise(y + millis()/1000) - 5; * setAttribute('aOffset', [xOff, yOff]); * vertex(x, y); * } @@ -2361,7 +2359,7 @@ p5.prototype.normal = function(x, y, z) { * * async function setup(){ * myShader = createShader(vertSrc, fragSrc); - * createCanvas(200, 200, WEBGL); + * createCanvas(100, 100, WEBGL); * shader(myShader); * noStroke(); * describe('A blue grid, which moves away from the mouse position, on a grey background.'); From 01c2d3ba5b961bd5b165c2bc36948553821ce132 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 17:59:44 +0100 Subject: [PATCH 077/120] scaled down the example canvas size to correct (100,100) size --- src/core/shape/vertex.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 6ebdff7b77..3916aa5722 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2324,7 +2324,7 @@ p5.prototype.normal = function(x, y, z) { * let myShader; * const cols = 10; * const rows = 10; - * const cellSize = 16; + * const cellSize = 9; * * const vertSrc = `#version 300 es * precision mediump float; @@ -2377,7 +2377,7 @@ p5.prototype.normal = function(x, y, z) { * let y2 = y1 + cellSize; * fill(x/rows*255, y/cols*255, 255); * let distance = dist(x1,y1, mouseX, mouseY); - * setAttribute('aDistance', min(distance, 200)); + * setAttribute('aDistance', min(distance, 100)); * vertex(x1, y1); * vertex(x2, y1); * vertex(x2, y2); From d8db76e4ffc71309e119a73b7d2bc54249c84da2 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 18:11:08 +0100 Subject: [PATCH 078/120] Added explanation to the setAttribute method in geometry. --- src/webgl/p5.Geometry.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index b1f6bc08d3..eb14c1deaa 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1920,6 +1920,35 @@ p5.Geometry = class Geometry { return this; } +/** Sets the shader's vertex attribute variables. + * + * An attribute is a variable belonging to a vertex in a shader. p5.js provides some + * default attributes, such as `aPosition`, `aNormal`, `aVertexColor`, etc. Custom + * attributes can also be defined within `beginShape()` and `endShape()`. + * + * The first parameter, `attributeName`, is a string with the attribute's name. + * This is the same variable name which should be declared in the shader, similar to + * `setUniform()`. + * + * The second parameter, `data`, is the value assigned to the attribute. This value + * will be pushed directly onto the Geometry object. There should be the same number + * of custom attribute values as vertices. + * + * 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. + * + * @example + *
+ * + * + *
+/ +/** + * @method setAttribute + * @param {String} attributeName the name of the vertex attribute. + * @param {Number|Number[]} data the data tied to the vertex attribute. + */ setAttribute(attributeName, data, size = data.length ? data.length : 1){ const attributeSrc = attributeName.concat('Src'); if (!this.hasOwnProperty(attributeSrc)){ From 8d97dc1d00ae2415308153b5b3212fe507b208d1 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 18:11:37 +0100 Subject: [PATCH 079/120] added initial docs for Geometry.setAttribute --- src/webgl/p5.Geometry.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index eb14c1deaa..4c123b6f05 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1938,6 +1938,8 @@ p5.Geometry = class Geometry { * can be declared according to the WebGL specification. Common types include `float`, * `vec2`, `vec3`, `vec4` or matrices. * + * See also the global setAttribute() function. + * * @example *
* From 65f8a08fd6692f38e6302d2bdc1df8c5423183db Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 18:12:06 +0100 Subject: [PATCH 080/120] added a reference to the geometry method at the end of setAttribute inline docs --- src/core/shape/vertex.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 3916aa5722..4d9de88075 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2269,6 +2269,9 @@ p5.prototype.normal = function(x, y, z) { * 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 setAttribute() method on + * Geometry objects. + * * @example *
* From eb18e6fd67324bd46c98140e22c178e4286024d8 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 18:54:54 +0100 Subject: [PATCH 081/120] added an example to set geometry method --- src/webgl/p5.Geometry.js | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 4c123b6f05..51b3ea42ca 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1943,6 +1943,56 @@ p5.Geometry = class Geometry { * @example *
* + * let geo; + * + * function cartesianToSpherical(x, y, z) { + * let r = sqrt(x * x + y * y + z * z); + * let theta = acos(z / r); + * let phi = atan2(y, x); + * return { theta, phi }; + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * 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; + * }` + * }); + * beginGeometry(); + * fill('hotpink'); + * sphere(45, 50, 50); + * geo = endGeometry(); + * for (let v of geo.vertices){ + * let spherical = cartesianToSpherical(v.x, v.y, v.z); + * let roughness = noise(spherical.theta*5, spherical.phi*5); + * geo.setAttribute('aRoughness', roughness); + * } + * shader(myShader); + * noStroke(); + * describe('A rough pink sphere rotating on a blue background.'); + * } + * + * function draw() { + * rotateY(millis()*0.001); + * background('lightblue'); + * directionalLight('white', -1, 1, -1); + * ambientLight(320); + * shininess(2); + * specularMaterial(255,125,100); + * model(geo); + * } * *
/ From 6b0ad95a6469a4854bd54012e6acbec3b7dbba85 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Fri, 27 Sep 2024 18:56:50 +0100 Subject: [PATCH 082/120] fixed syntax error in set attribute example --- src/webgl/p5.Geometry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 51b3ea42ca..38608e6e69 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1954,7 +1954,7 @@ p5.Geometry = class Geometry { * * function setup() { * createCanvas(100, 100, WEBGL); - * myShader = materialShader().modify({ + * const myShader = materialShader().modify({ * vertexDeclarations:`in float aRoughness; * out float vRoughness;`, * fragmentDeclarations: 'in float vRoughness;', From 70d64c76becad6b4184b7608caaae18b1f4914a6 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 10:20:55 +0100 Subject: [PATCH 083/120] changed some tests from assert.deepEqual() to expect().to.containSubset --- test/unit/webgl/p5.RendererGL.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 962b734186..d8977adf0a 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2523,22 +2523,18 @@ suite('p5.RendererGL', function() { aCustom: 1, aCustomVec3: 3 }); - assert.deepEqual(myp5._renderer.immediateMode.buffers.user, [ + expect(myp5._renderer.immediateMode.buffers.user).to.containSubset([ { size: 1, src: 'aCustomSrc', dst: 'aCustomBuffer', attr: 'aCustom', - _renderer: myp5._renderer, - map: undefined }, { size: 3, src: 'aCustomVec3Src', dst: 'aCustomVec3Buffer', attr: 'aCustomVec3', - _renderer: myp5._renderer, - map: undefined } ]); myp5.endShape(); @@ -2594,22 +2590,18 @@ suite('p5.RendererGL', function() { myp5.endShape(); const myGeo = myp5.endGeometry(); myp5._renderer.createBuffers(myGeo.gId, myGeo); - assert.deepEqual(myp5._renderer.retainedMode.buffers.user, [ + expect(myp5._renderer.retainedMode.buffers.user).to.containSubset([ { size: 1, src: 'aCustomSrc', dst: 'aCustomBuffer', attr: 'aCustom', - _renderer: myp5._renderer, - map: undefined }, { size: 3, src: 'aCustomVec3Src', dst: 'aCustomVec3Buffer', attr: 'aCustomVec3', - _renderer: myp5._renderer, - map: undefined } ]); } From 9d1cd6ee3a85b1ffeb8e1739d9639cab4852adfe Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 10:30:44 +0100 Subject: [PATCH 084/120] add some comments to the example --- src/webgl/p5.Geometry.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 38608e6e69..f06e159f63 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1943,7 +1943,7 @@ p5.Geometry = class Geometry { * @example *
* - * let geo; + * let geo; * * function cartesianToSpherical(x, y, z) { * let r = sqrt(x * x + y * y + z * z); @@ -1954,6 +1954,8 @@ p5.Geometry = class Geometry { * * function setup() { * createCanvas(100, 100, WEBGL); + * + * // Modify the material shader to display roughness. * const myShader = materialShader().modify({ * vertexDeclarations:`in float aRoughness; * out float vRoughness;`, @@ -1970,27 +1972,40 @@ p5.Geometry = class Geometry { * 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); * let roughness = noise(spherical.theta*5, spherical.phi*5); * geo.setAttribute('aRoughness', roughness); * } + * + * // Use the custom shader. * shader(myShader); - * noStroke(); * describe('A rough pink sphere rotating on a blue background.'); * } * * function draw() { - * rotateY(millis()*0.001); + * // Set some styles and lighting * background('lightblue'); + * noStroke(); + * + * specularMaterial(255,125,100); + * shininess(2); + * * directionalLight('white', -1, 1, -1); * ambientLight(320); - * shininess(2); - * specularMaterial(255,125,100); + * + * rotateY(millis()*0.001); + * + * // Draw the geometry * model(geo); * } * From 656d37fd841d45a6436c420f2346ff2338794234 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 10:33:48 +0100 Subject: [PATCH 085/120] change x * x to pow(x, x) --- src/webgl/p5.Geometry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index f06e159f63..98faa330b7 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1946,7 +1946,7 @@ p5.Geometry = class Geometry { * let geo; * * function cartesianToSpherical(x, y, z) { - * let r = sqrt(x * x + y * y + z * 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 }; From 5c967dceb89976e2780b73c9d9350afc71f401da Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 10:43:22 +0100 Subject: [PATCH 086/120] Updated setAttribute Documentation. --- src/core/shape/vertex.js | 50 ++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 4d9de88075..f224c65bd6 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2256,8 +2256,11 @@ p5.prototype.normal = function(x, y, z) { /** Sets the shader's vertex attribute variables. * * An attribute is a variable belonging to a vertex in a shader. p5.js provides some - * default attributes, such as `aPosition`, `aNormal`, `aVertexColor`, etc. Custom - * attributes can also be defined within `beginShape()` and `endShape()`. + * default attributes, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are + * set using vertex(), normal() + * and fill() respectively. Custom attribute data can also + * be defined within beginShape() and + * endShape(). * * The first parameter, `attributeName`, is a string with the attribute's name. * This is the same variable name which should be declared in the shader, similar to @@ -2300,20 +2303,30 @@ p5.prototype.normal = function(x, y, z) { * * function setup(){ * createCanvas(100, 100, WEBGL); + * + * // Create and use the custom shader. * const myShader = createShader(vertSrc, fragSrc); * shader(myShader); - * noStroke(); + * * describe('A wobbly, cyan circle on a grey 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. * setAttribute('aOffset', [xOff, yOff]); * vertex(x, y); * } @@ -2360,9 +2373,11 @@ p5.prototype.normal = function(x, y, z) { * } * `; * - * async function setup(){ - * myShader = createShader(vertSrc, fragSrc); + * 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 grey background.'); @@ -2370,21 +2385,26 @@ p5.prototype.normal = function(x, y, z) { * * function draw(){ * background(200); + * + * // Draw the grid in the middle of the screen. * translate(-cols*cellSize/2, -rows*cellSize/2); * beginShape(QUADS); - * for (let x = 0; x < cols; x++) { - * for (let y = 0; y < rows; y++) { - * let x1 = x * cellSize; - * let y1 = y * cellSize; - * let x2 = x1 + cellSize; - * let y2 = y1 + cellSize; + * for (let i = 0; i < cols; i++) { + * for (let j = 0; j < rows; j++) { + * let x = i * cellSize; + * let y = j * cellSize; * fill(x/rows*255, y/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. * setAttribute('aDistance', min(distance, 100)); - * vertex(x1, y1); - * vertex(x2, y1); - * vertex(x2, y2); - * vertex(x1, y2); + * + * vertex(x, y); + * vertex(x + cellsize, y); + * vertex(x + cellsize, y1 + cellsize); + * vertex(x, y + cellsize); * } * } * endShape(); From a2dd8e4005c559b5d6fd55a9f87b5d5b5a7d1331 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 11:22:05 +0100 Subject: [PATCH 087/120] Changed 'grey' to 'gray' --- src/core/shape/vertex.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index f224c65bd6..71c8229bea 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2308,7 +2308,7 @@ p5.prototype.normal = function(x, y, z) { * const myShader = createShader(vertSrc, fragSrc); * shader(myShader); * - * describe('A wobbly, cyan circle on a grey background.'); + * describe('A wobbly, cyan circle on a gray background.'); * } * * function draw(){ @@ -2380,7 +2380,7 @@ p5.prototype.normal = function(x, y, z) { * myShader = createShader(vertSrc, fragSrc); * shader(myShader); * noStroke(); - * describe('A blue grid, which moves away from the mouse position, on a grey background.'); + * describe('A blue grid, which moves away from the mouse position, on a gray background.'); * } * * function draw(){ From 86cabd4af3b61e7227d9195dcc6c93cea268b2d9 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 11:46:09 +0100 Subject: [PATCH 088/120] fix some bugs in one example --- src/core/shape/vertex.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 71c8229bea..fe815cb339 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2356,7 +2356,7 @@ p5.prototype.normal = function(x, y, z) { * * void main(){ * vec4 positionVec4 = vec4(aPosition, 1.0); - * positionVec4.xyz += aDistance * aNormal; + * positionVec4.xyz += aDistance * aNormal * 2.0;; * vVertexColor = aVertexColor; * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; * } @@ -2391,9 +2391,12 @@ p5.prototype.normal = function(x, y, z) { * 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(x/rows*255, y/cols*255, 255); + * + * 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); @@ -2402,9 +2405,9 @@ p5.prototype.normal = function(x, y, z) { * setAttribute('aDistance', min(distance, 100)); * * vertex(x, y); - * vertex(x + cellsize, y); - * vertex(x + cellsize, y1 + cellsize); - * vertex(x, y + cellsize); + * vertex(x + cellSize, y); + * vertex(x + cellSize, y + cellSize); + * vertex(x, y + cellSize); * } * } * endShape(); From 7539a7b84edaf1c293fef9164df387ca85a1babd Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 11:49:11 +0100 Subject: [PATCH 089/120] change cellSize in example --- src/core/shape/vertex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index fe815cb339..0027509860 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2340,7 +2340,7 @@ p5.prototype.normal = function(x, y, z) { * let myShader; * const cols = 10; * const rows = 10; - * const cellSize = 9; + * const cellSize = 6; * * const vertSrc = `#version 300 es * precision mediump float; From 3b7d8f928cfeff946963b1012404e1aff53bb93b Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 15:22:40 +0100 Subject: [PATCH 090/120] add a custom attribute helper type and refactor to use it --- src/webgl/p5.Geometry.js | 84 +++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 98faa330b7..a8086da024 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -453,8 +453,7 @@ p5.Geometry = class Geometry { this.uvs.length = 0; for (const attr in this.userAttributes){ - const src = attr.concat('Src'); - delete this[src]; + this.userAttributes[attr].delete(); } this.userAttributes = {}; @@ -2016,22 +2015,79 @@ p5.Geometry = class Geometry { * @param {String} attributeName the name of the vertex attribute. * @param {Number|Number[]} data the data tied to the vertex attribute. */ - setAttribute(attributeName, data, size = data.length ? data.length : 1){ - const attributeSrc = attributeName.concat('Src'); - if (!this.hasOwnProperty(attributeSrc)){ - this[attributeSrc] = []; - this.userAttributes[attributeName] = size; + setAttribute(attributeName, data, size){ + let attr; + if (!this.userAttributes[attributeName]){ + attr = this.userAttributes[attributeName] = + this._createUserAttributeHelper(attributeName, data, this); } - if (size != this.userAttributes[attributeName]){ - p5._friendlyError(`Custom attribute ${attributeName} has been set with various data sizes. You can change it's name, - or if it was an accident, set ${attributeName} to have the same number of inputs each time!`, 'setAttribute()'); - } - if (data.length){ - this[attributeSrc].push(...data); + attr = this.userAttributes[attributeName] + if (size){ + attr.pushDirect(data); } else{ - this[attributeSrc].push(data); + attr.setCurrentData(data); + attr.pushCurrentData(); } } + + _createUserAttributeHelper(attributeName, data){ + const geometryInstace = this; + const attr = this.userAttributes[attributeName] = { + name: attributeName, + currentData: data, + dataSize: data.length ? data.length : 1, + geometry: geometryInstace, + // Getters + 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 attribute ${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!`, 'setAttribute()'); + } + 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[attr.getSrcName()] = []; + return this.userAttributes[attributeName]; + } }; /** From 3bcce8bba48a42f6fb74423719ff30783a0b4b60 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 15:23:41 +0100 Subject: [PATCH 091/120] Refactor to use the geometry user attributes helper object --- src/webgl/p5.RendererGL.Immediate.js | 95 +++++++++++++--------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index a9386be051..4664a300b8 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -34,14 +34,7 @@ p5.RendererGL.prototype.beginShape = function(mode) { this.immediateMode.shapeMode = mode !== undefined ? mode : constants.TESS; if (this._useUserAttributes === true){ - for (const name in this.userAttributes){ - delete this.immediateMode.geometry[name]; - delete this.immediateBufferStrides[name.concat('Src')]; - } - this.userAttributes = {}; - this._useUserAttributes = false; - this.tessyVertexSize = 12; - this.immediateMode.buffers.user = []; + this._resetUserAttributes(); } this.immediateMode.geometry.reset(); this.immediateMode.contourIndices = []; @@ -124,18 +117,16 @@ p5.RendererGL.prototype.vertex = function(x, y) { this.immediateMode.geometry.vertices.push(vert); this.immediateMode.geometry.vertexNormals.push(this._currentNormal); - for (const attr in this.userAttributes){ + for (const attrName in this.immediateMode.geometry.userAttributes){ const geom = this.immediateMode.geometry; + const attr = geom.userAttributes[attrName]; const verts = geom.vertices; - const data = this.userAttributes[attr]; - const src = attr.concat('Src'); - const size = data.length ? data.length : 1; - if (!geom.hasOwnProperty(src) && verts.length > 1) { - const numMissingValues = size * (verts.length - 1); + if (!attr.getSrcArray() && verts.length > 1) { + const numMissingValues = attr.getDataSize() * (verts.length - 1); const missingValues = Array(numMissingValues).fill(0); - geom.setAttribute(attr, missingValues, size); + attr.pushDirect(missingValues); } - geom.setAttribute(attr, data); + attr.pushCurrentData(); } const vertexColor = this.curFillColor || [0.5, 0.5, 0.5, 1.0]; @@ -191,27 +182,35 @@ p5.RendererGL.prototype.vertex = function(x, y) { }; p5.RendererGL.prototype.setAttribute = function(attributeName, data){ - // if attributeName is in one of default, throw some warning if(!this._useUserAttributes){ this._useUserAttributes = true; - this.userAttributes = {}; - } - const buff = attributeName.concat('Buffer'); - const attributeSrc = attributeName.concat('Src'); const size = data.length ? data.length : 1; - if (!this.userAttributes.hasOwnProperty(attributeName)){ - this.tessyVertexSize += size; - this.immediateBufferStrides[attributeSrc] = size; } - this.userAttributes[attributeName] = data; - const bufferExists = this.immediateMode - .buffers - .user - .some(buffer => buffer.dst === buff); - if(!bufferExists){ + const attrExists = this.immediateMode.geometry.userAttributes[attributeName]; + let attr; + if (attrExists){ + attr = this.immediateMode.geometry.userAttributes[attributeName]; + } + else { + attr = this.immediateMode.geometry._createUserAttributeHelper(attributeName, data); + this.tessyVertexSize += attr.getDataSize(); + this.immediateBufferStrides[attr.getSrcName()] = attr.getDataSize(); this.immediateMode.buffers.user.push( - new p5.RenderBuffer(size, attributeSrc, buff, attributeName, this) + new p5.RenderBuffer(attr.getDataSize(), attr.getSrcName(), attr.getDstName(), attributeName, this) ); } + attr.setCurrentData(data); +}; + +p5.RendererGL.prototype._resetUserAttributes = function(){ + const attributes = this.immediateMode.geometry.userAttributes; + for (const attrName in attributes){ + const attr = attributes[attrName]; + delete this.immediateBufferStrides[attr.getSrcName()]; + attr.delete(); + } + this._userUserAttributes = false; + this.tessyVertexSize = 12; + this.immediateMode.buffers.user = []; }; /** @@ -484,18 +483,12 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals[i].y, this.immediateMode.geometry.vertexNormals[i].z ); - for (const attr in this.userAttributes){ - const attributeSrc = attr.concat('Src'); - const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; - const start = i * size; - const end = start + size; - if (this.immediateMode.geometry[attributeSrc]){ - const vals = this.immediateMode.geometry[attributeSrc].slice(start, end); - contours[contours.length-1].push(...vals); - } else{ - delete this.userAttributes[attr]; - this.tessyVertexSize -= size; - } + for (const attrName in this.immediateMode.geometry.userAttributes){ + const attr = this.immediateMode.geometry.userAttributes[attrName]; + const start = i * attr.getDataSize(); + const end = start + attr.getDataSize(); + const vals = attr.getSrcArray().slice(start, end); + contours[contours.length-1].push(...vals); } } const polyTriangles = this._triangulate(contours); @@ -503,9 +496,9 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertices = []; this.immediateMode.geometry.vertexNormals = []; this.immediateMode.geometry.uvs = []; - for (const attr in this.userAttributes){ - const attributeSrc = attr.concat('Src'); - this.immediateMode.geometry[attributeSrc] = []; + for (const attrName in this.immediateMode.geometry.userAttributes){ + const attr = this.immediateMode.geometry.userAttributes[attrName]; + attr.resetSrcArray(); } const colors = []; for ( @@ -517,12 +510,12 @@ p5.RendererGL.prototype._tesselateShape = function() { this.normal(...polyTriangles.slice(j + 9, j + 12)); { let offset = 12; - for (const attr in this.userAttributes){ - const size = this.userAttributes[attr].length ? this.userAttributes[attr].length : 1; + for (const attrName in this.immediateMode.geometry.userAttributes){ + const attr = this.immediateMode.geometry.userAttributes[attrName]; const start = j + offset; - const end = start + size; - this.setAttribute(attr, polyTriangles.slice(start, end), size); - offset += size; + const end = start + attr.getDataSize(); + attr.setCurrentData(polyTriangles.slice(start, end)) + offset += attr.getDataSize(); } } this.vertex(...polyTriangles.slice(j, j + 5)); From 0d6b6c9ea9538aa09c0abc98ebcce06e95e95da8 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 15:56:34 +0100 Subject: [PATCH 092/120] added getName to user attributes --- src/webgl/p5.Geometry.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index a8086da024..3f6d9cac4d 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -2038,6 +2038,9 @@ p5.Geometry = class Geometry { dataSize: data.length ? data.length : 1, geometry: geometryInstace, // Getters + getName(){ + return this.name; + }, getCurrentData(){ return this.currentData; }, From 1b9e569eb9b2e7edb34618165afa3f088daf086b Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 15:57:54 +0100 Subject: [PATCH 093/120] use new user Attribute Helper implementation of setAttribute --- src/webgl/p5.RendererGL.Retained.js | 42 +++++++++-------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 5adff5010d..86d0358ef2 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -116,19 +116,11 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { ? model.lineVertices.length / 3 : 0; - for (const attr in model.userAttributes){ - const buff = attr.concat('Buffer'); - const attributeSrc = attr.concat('Src'); - const size = model.userAttributes[attr]; - const bufferExists = this.retainedMode - .buffers - .user - .some(buffer => buffer.dst === buff); - if (!bufferExists){ - this.retainedMode.buffers.user.push( - new p5.RenderBuffer(size, attributeSrc, buff, attr, this) - ); - } + for (const attrName in model.userAttributes){ + const attr = model.userAttributes[attrName]; + this.retainedMode.buffers.user.push( + new p5.RenderBuffer(attr.getDataSize(), attr.getSrcName(), attr.getDstName(), attr.getName(), this) + ); } return buffers; }; @@ -155,16 +147,12 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { buff._prepareBuffer(geometry, fillShader); } for (const buff of this.retainedMode.buffers.user){ - if(!geometry.model[buff.src]){ - continue; - } - const adjustedLength = geometry.model[buff.src].length / buff.size; + const attr = geometry.model.userAttributes[buff.attr]; + const adjustedLength = attr.getSrcArray().length / attr.getDataSize(); if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute with more values than vertices. - This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + p5._friendlyError(`One of the geometries has a custom attribute ${attr.name} with more values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute with fewer values than vertices. - This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + p5._friendlyError(`One of the geometries has a custom attribute ${attr.name} with fewer values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } buff._prepareBuffer(geometry, fillShader); } @@ -189,16 +177,12 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { buff._prepareBuffer(geometry, strokeShader); } for (const buff of this.retainedMode.buffers.user){ - if(!geometry.model[buff.src]){ - continue; - } - const adjustedLength = geometry.model[buff.src].length / buff.size; + const attr = geometry.model.userAttributes[buff.attr]; + const adjustedLength = attr.getSrcArray().length / attr.getDataSize(); if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute with more values than vertices. - This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + p5._friendlyError(`One of the geometries has a custom attribute ${attr.name} with more values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute with fewer values than vertices. - This is probably from directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + p5._friendlyError(`One of the geometries has a custom attribute ${attr.name} with fewer values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } buff._prepareBuffer(geometry, strokeShader); } From 69e3f0817eb16273daef5f0934acf3ed62181e26 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Sat, 28 Sep 2024 16:01:49 +0100 Subject: [PATCH 094/120] use new implementation for setAttribute with helper --- src/webgl/GeometryBuilder.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 0bc98e1b99..8aaaec7188 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -68,15 +68,15 @@ class GeometryBuilder { if (attr in inputAttrs){ continue; } - const size = builtAttrs[attr]; + const size = builtAttrs[attr].getDataSize(); const numMissingValues = size * input.vertices.length; const missingValues = Array(numMissingValues).fill(0); this.geometry.setAttribute(attr, missingValues, size); } for (const attr in inputAttrs){ const src = attr.concat('Src'); - const data = input[src]; - const size = inputAttrs[attr]; + const data = input.userAttributes[attr].getSrcArray(); + const size = inputAttrs[attr].getDataSize(); if (numPreviousVertices > 0 && !(attr in this.geometry.userAttributes)){ const numMissingValues = size * numPreviousVertices; const missingValues = Array(numMissingValues).fill(0); From 9bb71ef46905642be4768e5a09a73785575ecc1b Mon Sep 17 00:00:00 2001 From: miaoye que Date: Sun, 29 Sep 2024 15:43:47 -0400 Subject: [PATCH 095/120] add function name in error message, remove suggestion to add/remove parameters, use decorator pattern --- src/core/friendly_errors/param_validator.js | 31 +++++---- test/unit/core/param_errors.js | 75 +++++++++++---------- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src/core/friendly_errors/param_validator.js b/src/core/friendly_errors/param_validator.js index d1e20218b8..27a5420695 100644 --- a/src/core/friendly_errors/param_validator.js +++ b/src/core/friendly_errors/param_validator.js @@ -19,7 +19,7 @@ function validateParams(p5, fn) { // and so on. const p5Constructors = {}; - fn._loadP5Constructors = function () { + fn.loadP5Constructors = function () { // Make a list of all p5 classes to be used for argument validation // This must be done only when everything has loaded otherwise we get // an empty array @@ -112,10 +112,11 @@ function validateParams(p5, fn) { * Where each array in `overloads` represents a set of valid overloaded * parameters, and `?` is a shorthand for `Optional`. * + * @method generateZodSchemasForFunc * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add` * @returns {z.ZodSchema} Zod schema */ - function generateZodSchemasForFunc(func) { + fn.generateZodSchemasForFunc = function (func) { // A special case for `p5.Color.paletteLerp`, which has an unusual and // complicated function signature not shared by any other function in p5. if (func === 'p5.Color.paletteLerp') { @@ -251,7 +252,7 @@ function validateParams(p5, fn) { * @param {Array} args - User input arguments. * @returns {z.ZodSchema} Closest schema matching the input arguments. */ - function findClosestSchema(schema, args) { + fn.findClosestSchema = function (schema, args) { if (!(schema instanceof z.ZodUnion)) { return schema; } @@ -328,7 +329,7 @@ function validateParams(p5, fn) { * @param {String} func - Name of the function. Expect global functions like `sin` and class methods like `p5.Vector.add` * @returns {String} The friendly error message. */ - p5._friendlyParamError = function (zodErrorObj, func) { + fn.friendlyParamError = function (zodErrorObj, func) { let message; // The `zodErrorObj` might contain multiple errors of equal importance // (after scoring the schema closeness in `findClosestSchema`). Here, we @@ -340,7 +341,7 @@ function validateParams(p5, fn) { const buildTypeMismatchMessage = (actualType, expectedTypeStr, position) => { const positionStr = position ? `at the ${ordinals[position]} parameter` : ''; const actualTypeStr = actualType ? `, but received ${actualType}` : ''; - return `Expected ${expectedTypeStr} ${positionStr}${actualTypeStr}.`; + return `Expected ${expectedTypeStr} ${positionStr}${actualTypeStr}`; } // Union errors occur when a parameter can be of multiple types but is not @@ -390,7 +391,7 @@ function validateParams(p5, fn) { } case 'too_small': { const minArgs = currentError.minimum; - message = `Expected at least ${minArgs} argument${minArgs > 1 ? 's' : ''}, but received fewer. Please add more arguments!`; + message = `Expected at least ${minArgs} argument${minArgs > 1 ? 's' : ''}, but received fewer`; break; } case 'invalid_type': { @@ -399,7 +400,7 @@ function validateParams(p5, fn) { } case 'too_big': { const maxArgs = currentError.maximum; - message = `Expected at most ${maxArgs} argument${maxArgs > 1 ? 's' : ''}, but received more. Please delete some arguments!`; + message = `Expected at most ${maxArgs} argument${maxArgs > 1 ? 's' : ''}, but received more`; break; } default: { @@ -407,6 +408,9 @@ function validateParams(p5, fn) { } } + // Let the user know which function is generating the error. + message += ` in ${func}().`; + // Generates a link to the documentation based on the given function name. // TODO: Check if the link is reachable before appending it to the error // message. @@ -423,6 +427,7 @@ function validateParams(p5, fn) { message += ` For more information, see ${documentationLink}.`; } + console.log(message); return message; } @@ -437,7 +442,7 @@ function validateParams(p5, fn) { * @returns {any} [result.data] - The parsed data if validation was successful. * @returns {String} [result.error] - The validation error message if validation has failed. */ - fn._validateParams = function (func, args) { + fn.validate = function (func, args) { if (p5.disableFriendlyErrors) { return; // skip FES } @@ -447,7 +452,7 @@ function validateParams(p5, fn) { // user intended to call the function with non-undefined arguments. Skip // regular workflow and return a friendly error message right away. if (Array.isArray(args) && args.every(arg => arg === undefined)) { - const undefinedErrorMessage = `All arguments for function ${func} are undefined. There is likely an error in the code.`; + const undefinedErrorMessage = `All arguments for ${func}() are undefined. There is likely an error in the code.`; return { success: false, @@ -457,7 +462,7 @@ function validateParams(p5, fn) { let funcSchemas = schemaRegistry.get(func); if (!funcSchemas) { - funcSchemas = generateZodSchemasForFunc(func); + funcSchemas = fn.generateZodSchemasForFunc(func); schemaRegistry.set(func, funcSchemas); } @@ -467,9 +472,9 @@ function validateParams(p5, fn) { data: funcSchemas.parse(args) }; } catch (error) { - const closestSchema = findClosestSchema(funcSchemas, args); + const closestSchema = fn.findClosestSchema(funcSchemas, args); const zodError = closestSchema.safeParse(args).error; - const errorMessage = p5._friendlyParamError(zodError, func); + const errorMessage = fn.friendlyParamError(zodError, func); return { success: false, @@ -483,5 +488,5 @@ export default validateParams; if (typeof p5 !== 'undefined') { validateParams(p5, p5.prototype); - p5.prototype._loadP5Constructors(); + p5.prototype.loadP5Constructors(); } \ No newline at end of file diff --git a/test/unit/core/param_errors.js b/test/unit/core/param_errors.js index e9e9bf4ff4..6456ceb6d3 100644 --- a/test/unit/core/param_errors.js +++ b/test/unit/core/param_errors.js @@ -12,7 +12,7 @@ suite('Validate Params', function () { beforeAll(function () { validateParams(mockP5, mockP5Prototype); - mockP5Prototype._loadP5Constructors(); + mockP5Prototype.loadP5Constructors(); }); afterAll(function () { @@ -27,7 +27,7 @@ suite('Validate Params', function () { ]; validInputs.forEach(({ input }) => { - const result = mockP5Prototype._validateParams('saturation', input); + const result = mockP5Prototype.validate('saturation', input); assert.isTrue(result.success); }); }); @@ -41,7 +41,7 @@ suite('Validate Params', function () { ]; invalidInputs.forEach(({ input }) => { - const result = mockP5Prototype._validateParams('p5.saturation', input); + const result = mockP5Prototype.validate('p5.saturation', input); assert.isTrue(result.error.startsWith("Expected Color or array or string at the first parameter, but received")); }); }); @@ -55,7 +55,7 @@ suite('Validate Params', function () { validInputs.forEach(({ name, input }) => { test(`blendMode(): ${name}`, () => { - const result = mockP5Prototype._validateParams('p5.blendMode', [input]); + const result = mockP5Prototype.validate('p5.blendMode', [input]); assert.isTrue(result.success); }); }); @@ -68,8 +68,8 @@ suite('Validate Params', function () { invalidInputs.forEach(({ name, input }) => { test(`blendMode(): ${name}`, () => { - const result = mockP5Prototype._validateParams('p5.blendMode', [input]); - const expectedError = "Expected constant (please refer to documentation for allowed values) at the first parameter, but received " + input + "."; + const result = mockP5Prototype.validate('p5.blendMode', [input]); + const expectedError = "Expected constant (please refer to documentation for allowed values) at the first parameter, but received " + input + " in p5.blendMode()."; assert.equal(result.error, expectedError); }); }); @@ -82,22 +82,22 @@ suite('Validate Params', function () { ]; validInputs.forEach(({ name, input }) => { test(`arc(): ${name}`, () => { - const result = mockP5Prototype._validateParams('p5.arc', input); + const result = mockP5Prototype.validate('p5.arc', input); assert.isTrue(result.success); }); }); const invalidInputs = [ - { name: 'missing required arc parameters #4, #5', input: [200, 100, 100, 80], msg: 'Expected at least 6 arguments, but received fewer. Please add more arguments! For more information, see https://p5js.org/reference/p5/arc.' }, - { name: 'missing required param #0', input: [undefined, 100, 100, 80, 0, Math.PI, constants.PIE, 30], msg: 'Expected number at the first parameter, but received undefined.' }, - { name: 'missing required param #4', input: [200, 100, 100, 80, undefined, 0], msg: 'Expected number at the fifth parameter, but received undefined.' }, - { name: 'missing optional param #5', input: [200, 100, 100, 80, 0, undefined, Math.PI], msg: 'Expected number at the sixth parameter, but received undefined.' }, - { name: 'wrong param type at #0', input: ['a', 100, 100, 80, 0, Math.PI, constants.PIE, 30], msg: 'Expected number at the first parameter, but received string.' } + { name: 'missing required arc parameters #4, #5', input: [200, 100, 100, 80], msg: 'Expected at least 6 arguments, but received fewer in p5.arc(). For more information, see https://p5js.org/reference/p5/arc.' }, + { name: 'missing required param #0', input: [undefined, 100, 100, 80, 0, Math.PI, constants.PIE, 30], msg: 'Expected number at the first parameter, but received undefined in p5.arc().' }, + { name: 'missing required param #4', input: [200, 100, 100, 80, undefined, 0], msg: 'Expected number at the fifth parameter, but received undefined in p5.arc().' }, + { name: 'missing optional param #5', input: [200, 100, 100, 80, 0, undefined, Math.PI], msg: 'Expected number at the sixth parameter, but received undefined in p5.arc().' }, + { name: 'wrong param type at #0', input: ['a', 100, 100, 80, 0, Math.PI, constants.PIE, 30], msg: 'Expected number at the first parameter, but received string in p5.arc().' } ]; invalidInputs.forEach(({ name, input, msg }) => { test(`arc(): ${name}`, () => { - const result = mockP5Prototype._validateParams('p5.arc', input); + const result = mockP5Prototype.validate('p5.arc', input); assert.equal(result.error, msg); }); }); @@ -105,25 +105,25 @@ suite('Validate Params', function () { suite('validateParams: class, multi-types + optional numbers', function () { test('ambientLight(): no firendly-err-msg', function () { - const result = mockP5Prototype._validateParams('p5.ambientLight', [new mockP5.Color()]); + const result = mockP5Prototype.validate('p5.ambientLight', [new mockP5.Color()]); assert.isTrue(result.success); }) }) suite('validateParams: a few edge cases', function () { const invalidInputs = [ - { fn: 'color', name: 'wrong type for optional parameter', input: [0, 0, 0, 'A'], msg: 'Expected number at the fourth parameter, but received string.' }, - { fn: 'color', name: 'superfluous parameter', input: [[0, 0, 0], 0], msg: 'Expected number at the first parameter, but received array.' }, - { fn: 'color', name: 'wrong element types', input: [['A', 'B', 'C']], msg: 'Expected number at the first parameter, but received array.' }, - { fn: 'rect', name: 'null, non-trailing, optional parameter', input: [0, 0, 0, 0, null, 0, 0, 0], msg: 'Expected number at the fifth parameter, but received null.' }, - { fn: 'color', name: 'too many args + wrong types too', input: ['A', 'A', 0, 0, 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments! For more information, see https://p5js.org/reference/p5/color.' }, - { fn: 'line', name: 'null string given', input: [1, 2, 4, 'null'], msg: 'Expected number at the fourth parameter, but received string.' }, - { fn: 'line', name: 'NaN value given', input: [1, 2, 4, NaN], msg: 'Expected number at the fourth parameter, but received nan.' } + { fn: 'color', name: 'wrong type for optional parameter', input: [0, 0, 0, 'A'], msg: 'Expected number at the fourth parameter, but received string in p5.color().' }, + { fn: 'color', name: 'superfluous parameter', input: [[0, 0, 0], 0], msg: 'Expected number at the first parameter, but received array in p5.color().' }, + { fn: 'color', name: 'wrong element types', input: [['A', 'B', 'C']], msg: 'Expected number at the first parameter, but received array in p5.color().' }, + { fn: 'rect', name: 'null, non-trailing, optional parameter', input: [0, 0, 0, 0, null, 0, 0, 0], msg: 'Expected number at the fifth parameter, but received null in p5.rect().' }, + { fn: 'color', name: 'too many args + wrong types too', input: ['A', 'A', 0, 0, 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more in p5.color(). For more information, see https://p5js.org/reference/p5/color.' }, + { fn: 'line', name: 'null string given', input: [1, 2, 4, 'null'], msg: 'Expected number at the fourth parameter, but received string in p5.line().' }, + { fn: 'line', name: 'NaN value given', input: [1, 2, 4, NaN], msg: 'Expected number at the fourth parameter, but received nan in p5.line().' } ]; invalidInputs.forEach(({ name, input, fn, msg }) => { test(`${fn}(): ${name}`, () => { - const result = mockP5Prototype._validateParams(`p5.${fn}`, input); + const result = mockP5Prototype.validate(`p5.${fn}`, input); assert.equal(result.error, msg); }); }); @@ -131,17 +131,17 @@ suite('Validate Params', function () { suite('validateParams: trailing undefined arguments', function () { const invalidInputs = [ - { fn: 'color', name: 'missing params #1, #2', input: [12, undefined, undefined], msg: 'Expected number at the second parameter, but received undefined.' }, + { fn: 'color', name: 'missing params #1, #2', input: [12, undefined, undefined], msg: 'Expected number at the second parameter, but received undefined in p5.color().' }, // Even though the undefined arguments are technically allowed for // optional parameters, it is more likely that the user wanted to call // the function with meaningful arguments. - { fn: 'random', name: 'missing params #0, #1', input: [undefined, undefined], msg: 'All arguments for function p5.random are undefined. There is likely an error in the code.' }, - { fn: 'circle', name: 'missing compulsory parameter #2', input: [5, 5, undefined], msg: 'Expected number at the third parameter, but received undefined.' } + { fn: 'random', name: 'missing params #0, #1', input: [undefined, undefined], msg: 'All arguments for p5.random() are undefined. There is likely an error in the code.' }, + { fn: 'circle', name: 'missing compulsory parameter #2', input: [5, 5, undefined], msg: 'Expected number at the third parameter, but received undefined in p5.circle().' } ]; invalidInputs.forEach(({ fn, name, input, msg }) => { test(`${fn}(): ${name}`, () => { - const result = mockP5Prototype._validateParams(`p5.${fn}`, input); + const result = mockP5Prototype.validate(`p5.${fn}`, input); assert.equal(result.error, msg); }); }); @@ -155,21 +155,22 @@ suite('Validate Params', function () { ]; validInputs.forEach(({ name, input }) => { test(`color(): ${name}`, () => { - const result = mockP5Prototype._validateParams('p5.color', input); + const result = mockP5Prototype.validate('p5.color', input); assert.isTrue(result.success); }); }); const invalidInputs = [ - { name: 'optional parameter, incorrect type', input: [65, 100, 100, 'a'], msg: 'Expected number at the fourth parameter, but received string.' }, - { name: 'extra parameter', input: [[65, 100, 100], 100], msg: 'Expected number at the first parameter, but received array.' }, - { name: 'incorrect element type', input: ['A', 'B', 'C'], msg: 'Expected number at the first parameter, but received string.' }, - { name: 'incorrect parameter count', input: ['A', 'A', 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more. Please delete some arguments! For more information, see https://p5js.org/reference/p5/color.' } + { name: 'optional parameter, incorrect type', input: [65, 100, 100, 'a'], msg: 'Expected number at the fourth parameter, but received string in p5.color().' }, + { name: 'extra parameter', input: [[65, 100, 100], 100], msg: 'Expected number at the first parameter, but received array in p5.color().' }, + { name: 'incorrect element type', input: ['A', 'B', 'C'], msg: 'Expected number at the first parameter, but received string in p5.color().' }, + { name: 'incorrect parameter count', input: ['A', 'A', 0, 0, 0, 0, 0, 0], msg: 'Expected at most 4 arguments, but received more in p5.color(). For more information, see https://p5js.org/reference/p5/color.' } ]; invalidInputs.forEach(({ name, input, msg }) => { test(`color(): ${name}`, () => { - const result = mockP5Prototype._validateParams('p5.color', input); + const result = mockP5Prototype.validate('p5.color', input); + assert.equal(result.error, msg); }); }); @@ -183,14 +184,14 @@ suite('Validate Params', function () { ]; validInputs.forEach(({ name, input }) => { test(`${name}`, function () { - const result = mockP5Prototype._validateParams('p5.set', input); + const result = mockP5Prototype.validate('p5.set', input); assert.isTrue(result.success); }); }); test(`set() with Boolean (invalid)`, function () { - const result = mockP5Prototype._validateParams('p5.set', [0, 0, true]); - assert.equal(result.error, 'Expected number or array or object at the third parameter, but received boolean.'); + const result = mockP5Prototype.validate('p5.set', [0, 0, true]); + assert.equal(result.error, 'Expected number or array or object at the third parameter, but received boolean in p5.set().'); }); }); @@ -205,7 +206,7 @@ suite('Validate Params', function () { testCases.forEach(({ fn, name, input }) => { test(`${fn}(): ${name}`, function () { - const result = mockP5Prototype._validateParams(fn, input); + const result = mockP5Prototype.validate(fn, input); assert.isTrue(result.success); }); }); @@ -218,7 +219,7 @@ suite('Validate Params', function () { [new mockP5.Color(), 0.8], [new mockP5.Color(), 0.5] ]; - const result = mockP5Prototype._validateParams('p5.Color.paletteLerp', [colorStops, 0.5]); + const result = mockP5Prototype.validate('p5.Color.paletteLerp', [colorStops, 0.5]); assert.isTrue(result.success); }) }) From 0ccfb5d274b6166428cdb44873c71b76082ae7d2 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 12:37:39 +0100 Subject: [PATCH 096/120] fix silling in missing values for custom attribs --- src/webgl/p5.RendererGL.Immediate.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 4664a300b8..e6bb51c2a1 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -121,7 +121,7 @@ p5.RendererGL.prototype.vertex = function(x, y) { const geom = this.immediateMode.geometry; const attr = geom.userAttributes[attrName]; const verts = geom.vertices; - if (!attr.getSrcArray() && verts.length > 1) { + if (attr.getSrcArray().length === 0 && verts.length > 1) { const numMissingValues = attr.getDataSize() * (verts.length - 1); const missingValues = Array(numMissingValues).fill(0); attr.pushDirect(missingValues); @@ -184,6 +184,7 @@ p5.RendererGL.prototype.vertex = function(x, y) { p5.RendererGL.prototype.setAttribute = function(attributeName, data){ if(!this._useUserAttributes){ this._useUserAttributes = true; + this.immediateMode.geometry.userAttributes = {}; } const attrExists = this.immediateMode.geometry.userAttributes[attributeName]; let attr; @@ -205,11 +206,12 @@ p5.RendererGL.prototype._resetUserAttributes = function(){ const attributes = this.immediateMode.geometry.userAttributes; for (const attrName in attributes){ const attr = attributes[attrName]; - delete this.immediateBufferStrides[attr.getSrcName()]; + delete this.immediateBufferStrides[attrName]; attr.delete(); } this._userUserAttributes = false; this.tessyVertexSize = 12; + this.immediateMode.geometry.userAttributes = {}; this.immediateMode.buffers.user = []; }; @@ -512,10 +514,11 @@ p5.RendererGL.prototype._tesselateShape = function() { let offset = 12; for (const attrName in this.immediateMode.geometry.userAttributes){ const attr = this.immediateMode.geometry.userAttributes[attrName]; + const size = attr.getDataSize(); const start = j + offset; - const end = start + attr.getDataSize(); - attr.setCurrentData(polyTriangles.slice(start, end)) - offset += attr.getDataSize(); + const end = start + size; + this.setAttribute(attrName, polyTriangles.slice(start, end), size); + offset += size; } } this.vertex(...polyTriangles.slice(j, j + 5)); From f5c667f2d1c48b6f9c3ee67717647f6279a5bd96 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 12:38:04 +0100 Subject: [PATCH 097/120] fix custom attributes geom building --- src/webgl/GeometryBuilder.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 8aaaec7188..9366bdf786 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -64,25 +64,26 @@ class GeometryBuilder { const builtAttrs = this.geometry.userAttributes; const numPreviousVertices = this.geometry.vertices.length - input.vertices.length; - for (const attr in builtAttrs){ - if (attr in inputAttrs){ + for (const attrName in builtAttrs){ + if (attrName in inputAttrs){ continue; } - const size = builtAttrs[attr].getDataSize(); + const attr = builtAttrs[attrName] + const size = attr.getDataSize(); const numMissingValues = size * input.vertices.length; const missingValues = Array(numMissingValues).fill(0); - this.geometry.setAttribute(attr, missingValues, size); + attr.pushDirect(missingValues); } - for (const attr in inputAttrs){ - const src = attr.concat('Src'); - const data = input.userAttributes[attr].getSrcArray(); - const size = inputAttrs[attr].getDataSize(); - if (numPreviousVertices > 0 && !(attr in this.geometry.userAttributes)){ + for (const attrName in inputAttrs){ + const attr = inputAttrs[attrName]; + const data = attr.getSrcArray(); + const size = attr.getDataSize(); + if (numPreviousVertices > 0 && !(attrName in builtAttrs)){ const numMissingValues = size * numPreviousVertices; const missingValues = Array(numMissingValues).fill(0); - this.geometry.setAttribute(attr, missingValues, size); + this.geometry.setAttribute(attrName, missingValues, size); } - this.geometry.setAttribute(attr, data, size); + this.geometry.setAttribute(attrName, data, size); } if (this.renderer._doFill) { From 550f666a5fba2788af3e05ff28db626200848f6b Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 12:39:14 +0100 Subject: [PATCH 098/120] add size parameter for geometry.setattribute --- src/webgl/p5.Geometry.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 3f6d9cac4d..d62d847d43 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -452,8 +452,8 @@ p5.Geometry = class Geometry { this.vertexNormals.length = 0; this.uvs.length = 0; - for (const attr in this.userAttributes){ - this.userAttributes[attr].delete(); + for (const attrName in this.userAttributes){ + this.userAttributes[attrName].delete(); } this.userAttributes = {}; @@ -2019,7 +2019,7 @@ p5.Geometry = class Geometry { let attr; if (!this.userAttributes[attributeName]){ attr = this.userAttributes[attributeName] = - this._createUserAttributeHelper(attributeName, data, this); + this._createUserAttributeHelper(attributeName, data, size); } attr = this.userAttributes[attributeName] if (size){ @@ -2030,12 +2030,12 @@ p5.Geometry = class Geometry { } } - _createUserAttributeHelper(attributeName, data){ + _createUserAttributeHelper(attributeName, data, size){ const geometryInstace = this; const attr = this.userAttributes[attributeName] = { name: attributeName, currentData: data, - dataSize: data.length ? data.length : 1, + dataSize: size ? size : data.length ? data.length : 1, geometry: geometryInstace, // Getters getName(){ From 5fc85022e28f1a67ba50749c90f283d0086576be Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 12:53:35 +0100 Subject: [PATCH 099/120] Typo --- src/webgl/p5.Geometry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index d62d847d43..23a92c7330 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -2031,12 +2031,12 @@ p5.Geometry = class Geometry { } _createUserAttributeHelper(attributeName, data, size){ - const geometryInstace = this; + const geometryInstance = this; const attr = this.userAttributes[attributeName] = { name: attributeName, currentData: data, dataSize: size ? size : data.length ? data.length : 1, - geometry: geometryInstace, + geometry: geometryInstance, // Getters getName(){ return this.name; From 2fc756764b40806f513ff971b641af9d880b2b03 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 13:25:36 +0100 Subject: [PATCH 100/120] Fixed documentation --- src/core/shape/vertex.js | 3 +-- src/webgl/p5.Geometry.js | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 0027509860..02b964c199 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2414,8 +2414,7 @@ p5.prototype.normal = function(x, y, z) { * } * *
-/ -/** + * * @method setAttribute * @param {String} attributeName the name of the vertex attribute. * @param {Number|Number[]} data the data tied to the vertex attribute. diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 98faa330b7..8b60da6994 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -2010,11 +2010,10 @@ p5.Geometry = class Geometry { * } *
*
-/ -/** - * @method setAttribute + * * @param {String} attributeName the name of the vertex attribute. * @param {Number|Number[]} data the data tied to the vertex attribute. + * @param {Number} size optional size of each unit of data */ setAttribute(attributeName, data, size = data.length ? data.length : 1){ const attributeSrc = attributeName.concat('Src'); From 1244d808d38ca2d8bc5309253536a81383868596 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 13:43:25 +0100 Subject: [PATCH 101/120] remove auto current data setting --- src/webgl/p5.Geometry.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 23a92c7330..214b3f90c6 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -2021,7 +2021,7 @@ p5.Geometry = class Geometry { attr = this.userAttributes[attributeName] = this._createUserAttributeHelper(attributeName, data, size); } - attr = this.userAttributes[attributeName] + attr = this.userAttributes[attributeName]; if (size){ attr.pushDirect(data); } else{ @@ -2034,7 +2034,6 @@ p5.Geometry = class Geometry { const geometryInstance = this; const attr = this.userAttributes[attributeName] = { name: attributeName, - currentData: data, dataSize: size ? size : data.length ? data.length : 1, geometry: geometryInstance, // Getters From daf9cf04534a6a10e010276728a0d6a89f760a4a Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 14:50:56 +0100 Subject: [PATCH 102/120] changed method call for more direct version --- src/webgl/p5.RendererGL.Immediate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index e6bb51c2a1..9e6695ee04 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -517,7 +517,7 @@ p5.RendererGL.prototype._tesselateShape = function() { const size = attr.getDataSize(); const start = j + offset; const end = start + size; - this.setAttribute(attrName, polyTriangles.slice(start, end), size); + attr.setCurrentData(polyTriangles.slice(start, end)); offset += size; } } From 4ad477034920fd4c05540ba1a0f6420c7c11c50f Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 14:51:19 +0100 Subject: [PATCH 103/120] Fixed bug where geometry custom src array was not reset properly --- src/webgl/p5.Geometry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 214b3f90c6..042aca84e6 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -2079,7 +2079,7 @@ p5.Geometry = class Geometry { } }, resetSrcArray(){ - this.geometry[this.getSrcName] = []; + this.geometry[this.getSrcName()] = []; }, delete() { const srcName = this.getSrcName(); From a91088f0bc00f3f004f408f807823b64e9d6879f Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 14:51:54 +0100 Subject: [PATCH 104/120] moved this.tessyVertexSize declaration into _initTessy() --- 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 ec7d52e1c6..65375fa48e 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -671,7 +671,6 @@ p5.RendererGL = class RendererGL extends Renderer { // Used to distinguish between user calls to vertex() and internal calls this.isProcessingVertices = false; - this.tessyVertexSize = 12; this._tessy = this._initTessy(); this.fontInfos = {}; @@ -2419,6 +2418,7 @@ p5.RendererGL = class RendererGL extends Renderer { return p; } _initTessy() { + this.tessyVertexSize = 12; // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { for (const element of data) { From cdf73007f8b7558bda424169a9123e7e54d1a733 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 15:50:19 +0100 Subject: [PATCH 105/120] quadraticVertex working again. --- src/webgl/3d_primitives.js | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 0599c24b6d..fd2d00aaf3 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3215,14 +3215,13 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { // Do the same for custom (user defined) attributes const userAttributes = {}; - for (const attr in immediateGeometry.userAttributes){ - const attributeSrc = attr.concat('Src'); - const size = immediateGeometry.userAttributes[attr]; - const curData = this.userAttributes[attr]; - userAttributes[attr] = []; - for (m = 0; m < 3; m++) userAttributes[attr].push([]); - userAttributes[attr][0] = immediateGeometry[attributeSrc].slice(-size); - userAttributes[attr][2] = curData; + for (const attrName in immediateGeometry.userAttributes){ + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.getDataSize(); + userAttributes[attrName] = []; + for (m = 0; m < 3; m++) userAttributes[attrName].push([]); + userAttributes[attrName][0] = attr.getSrcArray().slice(-size); + userAttributes[attrName][2] = attr.getCurrentData(); } if (argLength === 4) { @@ -3245,11 +3244,12 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 ); } - for (const attr in immediateGeometry.userAttributes){ - const size = immediateGeometry.userAttributes[attr]; + for (const attrName in immediateGeometry.userAttributes){ + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.getDataSize(); for (let k = 0; k < size; k++){ - userAttributes[attr][1].push( - userAttributes[attr][0][k] * (1-d0) + userAttributes[attr][2][k] * d0 + userAttributes[attrName][1].push( + userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][2][k] * d0 ); } } @@ -3270,15 +3270,16 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { _y += w_y[m] * this._lookUpTableQuadratic[i][m]; } - for (const attr in immediateGeometry.userAttributes) { - const size = immediateGeometry.userAttributes[attr]; - this.userAttributes[attr] = Array(size).fill(0); + for (const attrName in immediateGeometry.userAttributes) { + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.getDataSize(); + let newValues = Array(size).fill(0); for (let m = 0; m < 3; m++){ for (let k = 0; k < size; k++){ - this.userAttributes[attr][k] += - this._lookUpTableQuadratic[i][m] * userAttributes[attr][m][k]; + newValues[k] += this._lookUpTableQuadratic[i][m] * userAttributes[attrName][m][k]; } - } + } + attr.setCurrentData(newValues); } this.vertex(_x, _y); } From 556502328f7c70ef204ead5aef8175275738b8d6 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 15:59:16 +0100 Subject: [PATCH 106/120] custom attributes also on 6 vertices quadratic vertex --- src/webgl/3d_primitives.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index fd2d00aaf3..96cdff2be9 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3287,6 +3287,10 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[2]; this.curStrokeColor = strokeColors[2]; + for (const attrName in immediateGeometry.userAttributes) { + const attr = immediateGeometry.userAttributes[attrName]; + attr.setCurrentData(userAttributes[attrName][2]); + } this.immediateMode._quadraticVertex[0] = args[2]; this.immediateMode._quadraticVertex[1] = args[3]; } else if (argLength === 6) { @@ -3311,6 +3315,16 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { ); } + for (const attrName in immediateGeometry.userAttributes){ + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.getDataSize(); + for (let k = 0; k < size; k++){ + userAttributes[attrName][1].push( + userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][2][k] * d0 + ); + } + } + for (i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; @@ -3327,12 +3341,27 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { _y += w_y[m] * this._lookUpTableQuadratic[i][m]; _z += w_z[m] * this._lookUpTableQuadratic[i][m]; } + for (const attrName in immediateGeometry.userAttributes) { + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.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] * userAttributes[attrName][m][k]; + } + } + attr.setCurrentData(newValues); + } this.vertex(_x, _y, _z); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[2]; this.curStrokeColor = strokeColors[2]; + for (const attrName in immediateGeometry.userAttributes) { + const attr = immediateGeometry.userAttributes[attrName]; + attr.setCurrentData(userAttributes[attrName][2]); + } this.immediateMode._quadraticVertex[0] = args[3]; this.immediateMode._quadraticVertex[1] = args[4]; this.immediateMode._quadraticVertex[2] = args[5]; From 6b183b40860bc06e6df467b330e79c9fbc64e7ff Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 16:13:56 +0100 Subject: [PATCH 107/120] bezier vertex interpolates custom attributes --- src/webgl/3d_primitives.js | 68 +++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 96cdff2be9..05b067937e 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3021,14 +3021,13 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { // Do the same for custom attributes const userAttributes = {}; - for (const attr in immediateGeometry.userAttributes){ - const attributeSrc = attr.concat('Src'); - const size = immediateGeometry.userAttributes[attr]; - const curData = this.userAttributes[attr]; - userAttributes[attr] = []; - for (m = 0; m < 4; m++) userAttributes[attr].push([]); - userAttributes[attr][0] = immediateGeometry[attributeSrc].slice(-size); - userAttributes[attr][3] = curData; + for (const attrName in immediateGeometry.userAttributes){ + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.getDataSize(); + userAttributes[attrName] = []; + for (m = 0; m < 4; m++) userAttributes[attrName].push([]); + userAttributes[attrName][0] = attr.getSrcArray().slice(-size); + userAttributes[attrName][3] = attr.getCurrentData(); } if (argLength === 6) { @@ -3058,14 +3057,14 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } - for (const attr in immediateGeometry.userAttributes){ - const size = immediateGeometry.userAttributes[attr]; + for (const attrName in immediateGeometry.userAttributes){ + const size = immediateGeometry.userAttributes[attrName].getDataSize(); for (k = 0; k < size; k++){ - userAttributes[attr][1].push( - userAttributes[attr][0][k] * (1-d0) + userAttributes[attr][3][k] * d0 + userAttributes[attrName][1].push( + userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][3][k] * d0 ); - userAttributes[attr][2].push( - userAttributes[attr][0][k] * (1-d2) + userAttributes[attr][3][k] * d2 + userAttributes[attrName][2].push( + userAttributes[attrName][0][k] * (1-d2) + userAttributes[attrName][3][k] * d2 ); } } @@ -3085,21 +3084,26 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { _x += w_x[m] * this._lookUpTableBezier[i][m]; _y += w_y[m] * this._lookUpTableBezier[i][m]; } - for (const attr in immediateGeometry.userAttributes){ - const size = immediateGeometry.userAttributes[attr]; - this.userAttributes[attr] = Array(size).fill(0); + for (const attrName in immediateGeometry.userAttributes){ + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.getDataSize(); + let newValues = Array(size).fill(0); for (let m = 0; m < 4; m++){ for (let k = 0; k < size; k++){ - this.userAttributes[attr][k] += - this._lookUpTableBezier[i][m] * userAttributes[attr][m][k]; + newValues[k] += this._lookUpTableBezier[i][m] * userAttributes[attrName][m][k]; } } + attr.setCurrentData(newValues); } this.vertex(_x, _y); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[3]; this.curStrokeColor = strokeColors[3]; + for (const attrName in immediateGeometry.userAttributes) { + const attr = immediateGeometry.userAttributes[attrName]; + attr.setCurrentData(userAttributes[attrName][2]); + } this.immediateMode._bezierVertex[0] = args[4]; this.immediateMode._bezierVertex[1] = args[5]; } else if (argLength === 9) { @@ -3130,6 +3134,17 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } + for (const attrName in immediateGeometry.userAttributes){ + const size = immediateGeometry.userAttributes[attrName].getDataSize(); + for (k = 0; k < size; k++){ + userAttributes[attrName][1].push( + userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][3][k] * d0 + ); + userAttributes[attrName][2].push( + userAttributes[attrName][0][k] * (1-d2) + userAttributes[attrName][3][k] * d2 + ); + } + } for (let i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; @@ -3146,11 +3161,26 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { _y += w_y[m] * this._lookUpTableBezier[i][m]; _z += w_z[m] * this._lookUpTableBezier[i][m]; } + for (const attrName in immediateGeometry.userAttributes){ + const attr = immediateGeometry.userAttributes[attrName]; + const size = attr.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] * userAttributes[attrName][m][k]; + } + } + attr.setCurrentData(newValues); + } this.vertex(_x, _y, _z); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[3]; this.curStrokeColor = strokeColors[3]; + for (const attrName in immediateGeometry.userAttributes) { + const attr = immediateGeometry.userAttributes[attrName]; + attr.setCurrentData(userAttributes[attrName][2]); + } this.immediateMode._bezierVertex[0] = args[6]; this.immediateMode._bezierVertex[1] = args[7]; this.immediateMode._bezierVertex[2] = args[8]; From cff40744b890f4295fa11a62d3af9843b85edb60 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 16:55:07 +0100 Subject: [PATCH 108/120] updated tests for new custom attribute helper object --- test/unit/webgl/p5.RendererGL.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index d8977adf0a..400a1f62b0 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2511,18 +2511,20 @@ suite('p5.RendererGL', function() { myp5.beginShape(); myp5.setAttribute('aCustom', 1); - myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.setAttribute('aCustomVec3', [1, 2, 3]); myp5.vertex(0,0,0); - assert.deepEqual(myp5._renderer.userAttributes,{ - aCustom: 1, - aCustomVec3: [1,2,3] + expect(myp5._renderer.immediateMode.geometry.userAttributes.aCustom).to.containSubset({ + name: 'aCustom', + currentData: 1, + dataSize: 1 + }); + expect(myp5._renderer.immediateMode.geometry.userAttributes.aCustomVec3).to.containSubset({ + name: 'aCustomVec3', + currentData: [1, 2, 3], + dataSize: 3 }); assert.deepEqual(myp5._renderer.immediateMode.geometry.aCustomSrc, [1]); assert.deepEqual(myp5._renderer.immediateMode.geometry.aCustomVec3Src, [1,2,3]); - assert.deepEqual(myp5._renderer.immediateMode.geometry.userAttributes, { - aCustom: 1, - aCustomVec3: 3 - }); expect(myp5._renderer.immediateMode.buffers.user).to.containSubset([ { size: 1, @@ -2538,7 +2540,6 @@ suite('p5.RendererGL', function() { } ]); myp5.endShape(); - } ); test('Immediate mode data and buffers deleted after beginShape', @@ -2555,7 +2556,6 @@ suite('p5.RendererGL', function() { assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomSrc); assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomVec3Src); assert.deepEqual(myp5._renderer.immediateMode.geometry.userAttributes, {}); - assert.deepEqual(myp5._renderer.userAttributes, {}); assert.deepEqual(myp5._renderer.immediateMode.buffers.user, []); myp5.endShape(); } @@ -2575,7 +2575,6 @@ suite('p5.RendererGL', function() { const myGeo = myp5.endGeometry(); assert.deepEqual(immediateCopy.aCustomSrc, myGeo.aCustomSrc); assert.deepEqual(immediateCopy.aCustomVec3Src, myGeo.aCustomVec3Src); - assert.deepEqual(immediateCopy.userAttributes, myGeo.userAttributes); } ); test('Retained mode buffers are created for rendering', @@ -2635,7 +2634,7 @@ suite('p5.RendererGL', function() { myp5.vertex(1,0,0); myp5.endShape(); console.log = oldLog; - expect(logs.join('\n')).to.match(/Custom attribute aCustom has been set with various data sizes/); + expect(logs.join('\n')).to.match(/Custom attribute 'aCustom' has been set with various data sizes/); } ); test('Friendly error too many values set', @@ -2651,7 +2650,7 @@ suite('p5.RendererGL', function() { myGeo.setAttribute('aCustom', 2); myp5.model(myGeo); console.log = oldLog; - expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute with more values than vertices./); + expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute 'aCustom' with more values than vertices./); } ); test('Friendly error if too few values set', @@ -2667,7 +2666,7 @@ suite('p5.RendererGL', function() { myGeo.setAttribute('aCustom', 1); myp5.model(myGeo); console.log = oldLog; - expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute with fewer values than vertices./); + expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute 'aCustom' with fewer values than vertices./); } ); }) From b086feac22cd82c1fda30eadee0e45f434097331 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 16:56:21 +0100 Subject: [PATCH 109/120] updated documentation --- src/webgl/p5.Geometry.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 042aca84e6..702fbdd75b 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -2009,11 +2009,11 @@ p5.Geometry = class Geometry { * } * *
-/ -/** + * * @method setAttribute * @param {String} attributeName the name of the vertex attribute. * @param {Number|Number[]} data the data tied to the vertex attribute. + * @param {Number} [size] optional size of each unit of data. */ setAttribute(attributeName, data, size){ let attr; @@ -2062,7 +2062,7 @@ p5.Geometry = class Geometry { setCurrentData(data) { const size = data.length ? data.length : 1; if (size != this.getDataSize()){ - p5._friendlyError(`Custom attribute ${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!`, 'setAttribute()'); + p5._friendlyError(`Custom attribute '${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!`, 'setAttribute()'); } this.currentData = data; }, From 9ab620f5d2c685b02c65909d6bc1aee31cfe213a Mon Sep 17 00:00:00 2001 From: 23036879 Date: Mon, 30 Sep 2024 16:56:58 +0100 Subject: [PATCH 110/120] Put quote marks in error message --- src/webgl/p5.RendererGL.Retained.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 86d0358ef2..6bcb3e8a2a 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -150,9 +150,9 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { const attr = geometry.model.userAttributes[buff.attr]; const adjustedLength = attr.getSrcArray().length / attr.getDataSize(); if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute ${attr.name} with more values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + p5._friendlyError(`One of the geometries has a custom attribute '${attr.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom attribute ${attr.name} with fewer values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + p5._friendlyError(`One of the geometries has a custom attribute '${attr.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); } buff._prepareBuffer(geometry, fillShader); } From 0da4073f33099df57ca0172667ce42f2583471b4 Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 1 Oct 2024 10:49:44 +0100 Subject: [PATCH 111/120] changed 'setAttribute()' to 'vertexProperty()' and all references, variables & helpers to match --- src/core/shape/vertex.js | 32 +++--- src/webgl/3d_primitives.js | 144 +++++++++++++-------------- src/webgl/GeometryBuilder.js | 28 +++--- src/webgl/p5.Geometry.js | 75 +++++++------- src/webgl/p5.RendererGL.Immediate.js | 82 +++++++-------- src/webgl/p5.RendererGL.Retained.js | 22 ++-- test/unit/visual/cases/webgl.js | 18 ++-- test/unit/webgl/p5.RendererGL.js | 52 +++++----- 8 files changed, 230 insertions(+), 223 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 02b964c199..f1dd9ff214 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2253,26 +2253,26 @@ p5.prototype.normal = function(x, y, z) { return this; }; -/** Sets the shader's vertex attribute variables. +/** Sets the shader's vertex property or attribute variables. * - * An attribute is a variable belonging to a vertex in a shader. p5.js provides some - * default attributes, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are + * 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 attribute data can also + * and fill() respectively. Custom properties can also * be defined within beginShape() and * endShape(). * - * The first parameter, `attributeName`, is a string with the attribute's name. - * This is the same variable name which should be declared in the shader, similar to - * `setUniform()`. + * 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 attribute. This + * 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 setAttribute() method on + * See also the vertexProperty() method on * Geometry objects. * * @example @@ -2327,7 +2327,7 @@ p5.prototype.normal = function(x, y, z) { * const yOff = 10 * noise(y + millis()/1000) - 5; * * // Apply these noise values to the following vertex. - * setAttribute('aOffset', [xOff, yOff]); + * vertexProperty('aOffset', [xOff, yOff]); * vertex(x, y); * } * endShape(CLOSE); @@ -2402,7 +2402,7 @@ p5.prototype.normal = function(x, y, z) { * let distance = dist(x1,y1, mouseX, mouseY); * * // Send the distance to the shader. - * setAttribute('aDistance', min(distance, 100)); + * vertexProperty('aDistance', min(distance, 100)); * * vertex(x, y); * vertex(x + cellSize, y); @@ -2415,14 +2415,14 @@ p5.prototype.normal = function(x, y, z) { * *
* - * @method setAttribute + * @method vertexProperty * @param {String} attributeName the name of the vertex attribute. * @param {Number|Number[]} data the data tied to the vertex attribute. */ -p5.prototype.setAttribute = function(attributeName, data){ - // this._assert3d('setAttribute'); - // p5._validateParameters('setAttribute', arguments); - this._renderer.setAttribute(attributeName, data); +p5.prototype.vertexProperty = function(attributeName, data){ + // this._assert3d('vertexProperty'); + // p5._validateParameters('vertexProperty', arguments); + this._renderer.vertexProperty(attributeName, data); }; export default p5; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 05b067937e..eea99ee2d8 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3019,15 +3019,15 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); strokeColors[3] = this.curStrokeColor.slice(); - // Do the same for custom attributes - const userAttributes = {}; - for (const attrName in immediateGeometry.userAttributes){ - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); - userAttributes[attrName] = []; - for (m = 0; m < 4; m++) userAttributes[attrName].push([]); - userAttributes[attrName][0] = attr.getSrcArray().slice(-size); - userAttributes[attrName][3] = attr.getCurrentData(); + // 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) { @@ -3057,14 +3057,14 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } - for (const attrName in immediateGeometry.userAttributes){ - const size = immediateGeometry.userAttributes[attrName].getDataSize(); + for (const propName in immediateGeometry.userVertexProperties){ + const size = immediateGeometry.userVertexProperties[propName].getDataSize(); for (k = 0; k < size; k++){ - userAttributes[attrName][1].push( - userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][3][k] * d0 + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 ); - userAttributes[attrName][2].push( - userAttributes[attrName][0][k] * (1-d2) + userAttributes[attrName][3][k] * d2 + userVertexProperties[propName][2].push( + userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 ); } } @@ -3084,25 +3084,25 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { _x += w_x[m] * this._lookUpTableBezier[i][m]; _y += w_y[m] * this._lookUpTableBezier[i][m]; } - for (const attrName in immediateGeometry.userAttributes){ - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); + 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] * userAttributes[attrName][m][k]; + newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; } } - attr.setCurrentData(newValues); + prop.setCurrentData(newValues); } this.vertex(_x, _y); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[3]; this.curStrokeColor = strokeColors[3]; - for (const attrName in immediateGeometry.userAttributes) { - const attr = immediateGeometry.userAttributes[attrName]; - attr.setCurrentData(userAttributes[attrName][2]); + 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]; @@ -3134,14 +3134,14 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } - for (const attrName in immediateGeometry.userAttributes){ - const size = immediateGeometry.userAttributes[attrName].getDataSize(); + for (const propName in immediateGeometry.userVertexProperties){ + const size = immediateGeometry.userVertexProperties[propName].getDataSize(); for (k = 0; k < size; k++){ - userAttributes[attrName][1].push( - userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][3][k] * d0 + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 ); - userAttributes[attrName][2].push( - userAttributes[attrName][0][k] * (1-d2) + userAttributes[attrName][3][k] * d2 + userVertexProperties[propName][2].push( + userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 ); } } @@ -3161,25 +3161,25 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { _y += w_y[m] * this._lookUpTableBezier[i][m]; _z += w_z[m] * this._lookUpTableBezier[i][m]; } - for (const attrName in immediateGeometry.userAttributes){ - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); + 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] * userAttributes[attrName][m][k]; + newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; } } - attr.setCurrentData(newValues); + prop.setCurrentData(newValues); } this.vertex(_x, _y, _z); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[3]; this.curStrokeColor = strokeColors[3]; - for (const attrName in immediateGeometry.userAttributes) { - const attr = immediateGeometry.userAttributes[attrName]; - attr.setCurrentData(userAttributes[attrName][2]); + 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]; @@ -3243,15 +3243,15 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); strokeColors[2] = this.curStrokeColor.slice(); - // Do the same for custom (user defined) attributes - const userAttributes = {}; - for (const attrName in immediateGeometry.userAttributes){ - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); - userAttributes[attrName] = []; - for (m = 0; m < 3; m++) userAttributes[attrName].push([]); - userAttributes[attrName][0] = attr.getSrcArray().slice(-size); - userAttributes[attrName][2] = attr.getCurrentData(); + // 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) { @@ -3274,12 +3274,12 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 ); } - for (const attrName in immediateGeometry.userAttributes){ - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); for (let k = 0; k < size; k++){ - userAttributes[attrName][1].push( - userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][2][k] * d0 + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 ); } } @@ -3300,16 +3300,16 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { _y += w_y[m] * this._lookUpTableQuadratic[i][m]; } - for (const attrName in immediateGeometry.userAttributes) { - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); + 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] * userAttributes[attrName][m][k]; + newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; } } - attr.setCurrentData(newValues); + prop.setCurrentData(newValues); } this.vertex(_x, _y); } @@ -3317,9 +3317,9 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[2]; this.curStrokeColor = strokeColors[2]; - for (const attrName in immediateGeometry.userAttributes) { - const attr = immediateGeometry.userAttributes[attrName]; - attr.setCurrentData(userAttributes[attrName][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]; @@ -3345,12 +3345,12 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { ); } - for (const attrName in immediateGeometry.userAttributes){ - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); for (let k = 0; k < size; k++){ - userAttributes[attrName][1].push( - userAttributes[attrName][0][k] * (1-d0) + userAttributes[attrName][2][k] * d0 + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 ); } } @@ -3371,16 +3371,16 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { _y += w_y[m] * this._lookUpTableQuadratic[i][m]; _z += w_z[m] * this._lookUpTableQuadratic[i][m]; } - for (const attrName in immediateGeometry.userAttributes) { - const attr = immediateGeometry.userAttributes[attrName]; - const size = attr.getDataSize(); + 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] * userAttributes[attrName][m][k]; + newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; } } - attr.setCurrentData(newValues); + prop.setCurrentData(newValues); } this.vertex(_x, _y, _z); } @@ -3388,9 +3388,9 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[2]; this.curStrokeColor = strokeColors[2]; - for (const attrName in immediateGeometry.userAttributes) { - const attr = immediateGeometry.userAttributes[attrName]; - attr.setCurrentData(userAttributes[attrName][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]; diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 9366bdf786..195da6b181 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -60,30 +60,30 @@ class GeometryBuilder { ); this.geometry.uvs.push(...input.uvs); - const inputAttrs = input.userAttributes; - const builtAttrs = this.geometry.userAttributes; + const inputUserVertexProps = input.userVertexProperties; + const builtUserVertexProps = this.geometry.userVertexProperties; const numPreviousVertices = this.geometry.vertices.length - input.vertices.length; - for (const attrName in builtAttrs){ - if (attrName in inputAttrs){ + for (const propName in builtUserVertexProps){ + if (propName in inputUserVertexProps){ continue; } - const attr = builtAttrs[attrName] - const size = attr.getDataSize(); + const prop = builtUserVertexProps[propName] + const size = prop.getDataSize(); const numMissingValues = size * input.vertices.length; const missingValues = Array(numMissingValues).fill(0); - attr.pushDirect(missingValues); + prop.pushDirect(missingValues); } - for (const attrName in inputAttrs){ - const attr = inputAttrs[attrName]; - const data = attr.getSrcArray(); - const size = attr.getDataSize(); - if (numPreviousVertices > 0 && !(attrName in builtAttrs)){ + for (const propName in inputUserVertexProps){ + const prop = inputUserVertexProps[propName]; + const data = prop.getSrcArray(); + const size = prop.getDataSize(); + if (numPreviousVertices > 0 && !(propName in builtUserVertexProps)){ const numMissingValues = size * numPreviousVertices; const missingValues = Array(numMissingValues).fill(0); - this.geometry.setAttribute(attrName, missingValues, size); + this.geometry.vertexProperty(propName, missingValues, size); } - this.geometry.setAttribute(attrName, data, size); + this.geometry.vertexProperty(propName, data, size); } if (this.renderer._doFill) { diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 702fbdd75b..12689acc86 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -283,7 +283,7 @@ p5.Geometry = class Geometry { // One color per vertex representing the stroke color at that vertex this.vertexStrokeColors = []; - this.userAttributes = {}; + this.userVertexProperties = {}; // One color per line vertex, generated automatically based on // vertexStrokeColors in _edgesToVertices() @@ -452,10 +452,10 @@ p5.Geometry = class Geometry { this.vertexNormals.length = 0; this.uvs.length = 0; - for (const attrName in this.userAttributes){ - this.userAttributes[attrName].delete(); + for (const propName in this.userVertexProperties){ + this.userVertexProperties[propName].delete(); } - this.userAttributes = {}; + this.userVertexProperties = {}; this.dirtyFlags = {}; } @@ -1599,7 +1599,6 @@ p5.Geometry = class Geometry { * @chainable */ _edgesToVertices() { - // probably needs to add something in here for custom attributes this.lineVertices.clear(); this.lineTangentsIn.clear(); this.lineTangentsOut.clear(); @@ -1919,25 +1918,29 @@ p5.Geometry = class Geometry { return this; } -/** Sets the shader's vertex attribute variables. +/** Sets the shader's vertex property or attribute variables. * - * An attribute is a variable belonging to a vertex in a shader. p5.js provides some - * default attributes, such as `aPosition`, `aNormal`, `aVertexColor`, etc. Custom - * attributes can also be defined within `beginShape()` and `endShape()`. + * 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, `attributeName`, is a string with the attribute's name. - * This is the same variable name which should be declared in the shader, similar to - * `setUniform()`. + * 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 attribute. This value + * 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 attribute values as vertices. + * 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 setAttribute() function. + * See also the global vertexProperty() function. * * @example *
@@ -1980,14 +1983,18 @@ p5.Geometry = class Geometry { * * // 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.setAttribute('aRoughness', roughness); + * geo.vertexProperty('aRoughness', roughness); * } * * // Use the custom shader. * shader(myShader); + * * describe('A rough pink sphere rotating on a blue background.'); * } * @@ -2010,30 +2017,30 @@ p5.Geometry = class Geometry { * *
* - * @method setAttribute - * @param {String} attributeName the name of the vertex attribute. - * @param {Number|Number[]} data the data tied to the vertex attribute. + * @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. */ - setAttribute(attributeName, data, size){ - let attr; - if (!this.userAttributes[attributeName]){ - attr = this.userAttributes[attributeName] = - this._createUserAttributeHelper(attributeName, data, size); + vertexProperty(propertyName, data, size){ + let prop; + if (!this.userVertexProperties[propertyName]){ + prop = this.userVertexProperties[propertyName] = + this._userVertexPropertyHelper(propertyName, data, size); } - attr = this.userAttributes[attributeName]; + prop = this.userVertexProperties[propertyName]; if (size){ - attr.pushDirect(data); + prop.pushDirect(data); } else{ - attr.setCurrentData(data); - attr.pushCurrentData(); + prop.setCurrentData(data); + prop.pushCurrentData(); } } - _createUserAttributeHelper(attributeName, data, size){ + _userVertexPropertyHelper(propertyName, data, size){ const geometryInstance = this; - const attr = this.userAttributes[attributeName] = { - name: attributeName, + const prop = this.userVertexProperties[propertyName] = { + name: propertyName, dataSize: size ? size : data.length ? data.length : 1, geometry: geometryInstance, // Getters @@ -2062,7 +2069,7 @@ p5.Geometry = class Geometry { setCurrentData(data) { const size = data.length ? data.length : 1; if (size != this.getDataSize()){ - p5._friendlyError(`Custom attribute '${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!`, 'setAttribute()'); + 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; }, @@ -2087,8 +2094,8 @@ p5.Geometry = class Geometry { delete this; } }; - this[attr.getSrcName()] = []; - return this.userAttributes[attributeName]; + this[prop.getSrcName()] = []; + return this.userVertexProperties[propertyName]; } }; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 9e6695ee04..357c1c5527 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -33,8 +33,8 @@ import './p5.RenderBuffer'; p5.RendererGL.prototype.beginShape = function(mode) { this.immediateMode.shapeMode = mode !== undefined ? mode : constants.TESS; - if (this._useUserAttributes === true){ - this._resetUserAttributes(); + if (this._useUserVertexProperties === true){ + this._resetUserVertexProperties(); } this.immediateMode.geometry.reset(); this.immediateMode.contourIndices = []; @@ -117,16 +117,16 @@ p5.RendererGL.prototype.vertex = function(x, y) { this.immediateMode.geometry.vertices.push(vert); this.immediateMode.geometry.vertexNormals.push(this._currentNormal); - for (const attrName in this.immediateMode.geometry.userAttributes){ + for (const propName in this.immediateMode.geometry.userVertexProperties){ const geom = this.immediateMode.geometry; - const attr = geom.userAttributes[attrName]; + const prop = geom.userVertexProperties[propName]; const verts = geom.vertices; - if (attr.getSrcArray().length === 0 && verts.length > 1) { - const numMissingValues = attr.getDataSize() * (verts.length - 1); + if (prop.getSrcArray().length === 0 && verts.length > 1) { + const numMissingValues = prop.getDataSize() * (verts.length - 1); const missingValues = Array(numMissingValues).fill(0); - attr.pushDirect(missingValues); + prop.pushDirect(missingValues); } - attr.pushCurrentData(); + prop.pushCurrentData(); } const vertexColor = this.curFillColor || [0.5, 0.5, 0.5, 1.0]; @@ -181,37 +181,37 @@ p5.RendererGL.prototype.vertex = function(x, y) { return this; }; -p5.RendererGL.prototype.setAttribute = function(attributeName, data){ - if(!this._useUserAttributes){ - this._useUserAttributes = true; - this.immediateMode.geometry.userAttributes = {}; +p5.RendererGL.prototype.vertexProperty = function(propertyName, data){ + if(!this._useUserVertexProperties){ + this._useUserVertexProperties = true; + this.immediateMode.geometry.userVertexProperties = {}; } - const attrExists = this.immediateMode.geometry.userAttributes[attributeName]; - let attr; - if (attrExists){ - attr = this.immediateMode.geometry.userAttributes[attributeName]; + const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; + let prop; + if (propertyExists){ + prop = this.immediateMode.geometry.userVertexProperties[propertyName]; } else { - attr = this.immediateMode.geometry._createUserAttributeHelper(attributeName, data); - this.tessyVertexSize += attr.getDataSize(); - this.immediateBufferStrides[attr.getSrcName()] = attr.getDataSize(); + 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(attr.getDataSize(), attr.getSrcName(), attr.getDstName(), attributeName, this) + new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) ); } - attr.setCurrentData(data); + prop.setCurrentData(data); }; -p5.RendererGL.prototype._resetUserAttributes = function(){ - const attributes = this.immediateMode.geometry.userAttributes; - for (const attrName in attributes){ - const attr = attributes[attrName]; - delete this.immediateBufferStrides[attrName]; - attr.delete(); +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._userUserAttributes = false; + this._useUserVertexProperties = false; this.tessyVertexSize = 12; - this.immediateMode.geometry.userAttributes = {}; + this.immediateMode.geometry.userVertexProperties = {}; this.immediateMode.buffers.user = []; }; @@ -485,11 +485,11 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals[i].y, this.immediateMode.geometry.vertexNormals[i].z ); - for (const attrName in this.immediateMode.geometry.userAttributes){ - const attr = this.immediateMode.geometry.userAttributes[attrName]; - const start = i * attr.getDataSize(); - const end = start + attr.getDataSize(); - const vals = attr.getSrcArray().slice(start, end); + 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); } } @@ -498,9 +498,9 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertices = []; this.immediateMode.geometry.vertexNormals = []; this.immediateMode.geometry.uvs = []; - for (const attrName in this.immediateMode.geometry.userAttributes){ - const attr = this.immediateMode.geometry.userAttributes[attrName]; - attr.resetSrcArray(); + for (const propName in this.immediateMode.geometry.userVertexProperties){ + const prop = this.immediateMode.geometry.userVertexProperties[propName]; + prop.resetSrcArray(); } const colors = []; for ( @@ -512,12 +512,12 @@ p5.RendererGL.prototype._tesselateShape = function() { this.normal(...polyTriangles.slice(j + 9, j + 12)); { let offset = 12; - for (const attrName in this.immediateMode.geometry.userAttributes){ - const attr = this.immediateMode.geometry.userAttributes[attrName]; - const size = attr.getDataSize(); + 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; - attr.setCurrentData(polyTriangles.slice(start, end)); + prop.setCurrentData(polyTriangles.slice(start, end)); offset += size; } } diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 6bcb3e8a2a..57ce2a9d15 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -116,10 +116,10 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { ? model.lineVertices.length / 3 : 0; - for (const attrName in model.userAttributes){ - const attr = model.userAttributes[attrName]; + for (const propName in model.userVertexProperties){ + const prop = model.userVertexProperties[propName]; this.retainedMode.buffers.user.push( - new p5.RenderBuffer(attr.getDataSize(), attr.getSrcName(), attr.getDstName(), attr.getName(), this) + new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) ); } return buffers; @@ -147,12 +147,12 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { buff._prepareBuffer(geometry, fillShader); } for (const buff of this.retainedMode.buffers.user){ - const attr = geometry.model.userAttributes[buff.attr]; - const adjustedLength = attr.getSrcArray().length / attr.getDataSize(); + 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 attribute '${attr.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + 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 attribute '${attr.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + 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); } @@ -177,12 +177,12 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { buff._prepareBuffer(geometry, strokeShader); } for (const buff of this.retainedMode.buffers.user){ - const attr = geometry.model.userAttributes[buff.attr]; - const adjustedLength = attr.getSrcArray().length / attr.getDataSize(); + 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 attribute ${attr.name} with more values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + 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 attribute ${attr.name} with fewer values than vertices. This is probably caused by directly using the Geometry.setAttribute() method.`, 'setAttribute()'); + 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); } diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index def61f978e..105a49047f 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -131,7 +131,7 @@ visualSuite('WebGL', function() { ); }); - visualSuite('setAttribute', function(){ + visualSuite('vertexProperty', function(){ const vertSrc = `#version 300 es precision mediump float; uniform mat4 uProjectionMatrix; @@ -161,7 +161,7 @@ visualSuite('WebGL', function() { for (let i = 0; i < 20; i++){ let x = 20 * p5.sin(i/20*p5.TWO_PI); let y = 20 * p5.cos(i/20*p5.TWO_PI); - p5.setAttribute('aCol', [x/20, -y/20, 0]); + p5.vertexProperty('aCol', [x/20, -y/20, 0]); p5.vertex(x, y); } p5.endShape(); @@ -183,13 +183,13 @@ visualSuite('WebGL', function() { let x2 = x1 + 10; let y1 = j * 10; let y2 = y1 + 10; - p5.setAttribute('aCol', [1, 0, 0]); + p5.vertexProperty('aCol', [1, 0, 0]); p5.vertex(x1, y1); - p5.setAttribute('aCol', [0, 0, 1]); + p5.vertexProperty('aCol', [0, 0, 1]); p5.vertex(x2, y1); - p5.setAttribute('aCol', [0, 1, 1]); + p5.vertexProperty('aCol', [0, 1, 1]); p5.vertex(x2, y2); - p5.setAttribute('aCol', [1, 1, 1]); + p5.vertexProperty('aCol', [1, 1, 1]); p5.vertex(x1, y2); } } @@ -209,11 +209,11 @@ visualSuite('WebGL', function() { p5.sphere(5); p5.pop(); p5.beginShape(p5.TRIANGLES); - p5.setAttribute('aCol', [1,0,0]) + p5.vertexProperty('aCol', [1,0,0]) p5.vertex(-5, 5, 0); - p5.setAttribute('aCol', [0,1,0]) + p5.vertexProperty('aCol', [0,1,0]) p5.vertex(5, 5, 0); - p5.setAttribute('aCol', [0,0,1]) + p5.vertexProperty('aCol', [0,0,1]) p5.vertex(0, -5, 0); p5.endShape(p5.CLOSE); p5.push(); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 400a1f62b0..6bfae7eb93 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1577,19 +1577,19 @@ suite('p5.RendererGL', function() { renderer.beginShape(myp5.TESS); renderer.fill(255, 255, 255); renderer.normal(-1, -1, 1); - renderer.setAttribute('aCustom', [1, 1, 1]) + renderer.vertexProperty('aCustom', [1, 1, 1]) renderer.vertex(-10, -10, 0, 0); renderer.fill(255, 0, 0); renderer.normal(1, -1, 1); - renderer.setAttribute('aCustom', [1, 0, 0]) + renderer.vertexProperty('aCustom', [1, 0, 0]) renderer.vertex(10, -10, 1, 0); renderer.fill(0, 255, 0); renderer.normal(1, 1, 1); - renderer.setAttribute('aCustom', [0, 1, 0]) + renderer.vertexProperty('aCustom', [0, 1, 0]) renderer.vertex(10, 10, 1, 1); renderer.fill(0, 0, 255); renderer.normal(-1, 1, 1); - renderer.setAttribute('aCustom', [0, 0, 1]) + renderer.vertexProperty('aCustom', [0, 0, 1]) renderer.vertex(-10, 10, 0, 1); renderer.endShape(myp5.CLOSE); @@ -2504,21 +2504,21 @@ suite('p5.RendererGL', function() { ); }); - suite('setAttribute()', function() { + suite('vertexProperty()', function() { test('Immediate mode data and buffers created in beginShape', function() { myp5.createCanvas(50, 50, myp5.WEBGL); myp5.beginShape(); - myp5.setAttribute('aCustom', 1); - myp5.setAttribute('aCustomVec3', [1, 2, 3]); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1, 2, 3]); myp5.vertex(0,0,0); - expect(myp5._renderer.immediateMode.geometry.userAttributes.aCustom).to.containSubset({ + expect(myp5._renderer.immediateMode.geometry.userVertexProperties.aCustom).to.containSubset({ name: 'aCustom', currentData: 1, dataSize: 1 }); - expect(myp5._renderer.immediateMode.geometry.userAttributes.aCustomVec3).to.containSubset({ + expect(myp5._renderer.immediateMode.geometry.userVertexProperties.aCustomVec3).to.containSubset({ name: 'aCustomVec3', currentData: [1, 2, 3], dataSize: 3 @@ -2547,15 +2547,15 @@ suite('p5.RendererGL', function() { myp5.createCanvas(50, 50, myp5.WEBGL); myp5.beginShape(); - myp5.setAttribute('aCustom', 1); - myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); myp5.vertex(0,0,0); myp5.endShape(); myp5.beginShape(); assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomSrc); assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomVec3Src); - assert.deepEqual(myp5._renderer.immediateMode.geometry.userAttributes, {}); + assert.deepEqual(myp5._renderer.immediateMode.geometry.userVertexProperties, {}); assert.deepEqual(myp5._renderer.immediateMode.buffers.user, []); myp5.endShape(); } @@ -2565,8 +2565,8 @@ suite('p5.RendererGL', function() { myp5.createCanvas(50, 50, myp5.WEBGL); myp5.beginGeometry(); myp5.beginShape(); - myp5.setAttribute('aCustom', 1); - myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); myp5.vertex(0,1,0); myp5.vertex(-1,0,0); myp5.vertex(1,0,0); @@ -2582,8 +2582,8 @@ suite('p5.RendererGL', function() { myp5.createCanvas(50, 50, myp5.WEBGL); myp5.beginGeometry(); myp5.beginShape(); - myp5.setAttribute('aCustom', 1); - myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); myp5.vertex(0,0,0); myp5.vertex(1,0,0); myp5.endShape(); @@ -2610,8 +2610,8 @@ suite('p5.RendererGL', function() { myp5.createCanvas(50, 50, myp5.WEBGL); myp5.beginGeometry(); myp5.beginShape(); - myp5.setAttribute('aCustom', 1); - myp5.setAttribute('aCustomVec3', [1,2,3]); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); myp5.vertex(0,0,0); myp5.vertex(1,0,0); myp5.endShape(); @@ -2628,13 +2628,13 @@ suite('p5.RendererGL', function() { const oldLog = console.log; console.log = myLog; myp5.beginShape(); - myp5.setAttribute('aCustom', [1,2,3]); + myp5.vertexProperty('aCustom', [1,2,3]); myp5.vertex(0,0,0); - myp5.setAttribute('aCustom', [1,2]); + myp5.vertexProperty('aCustom', [1,2]); myp5.vertex(1,0,0); myp5.endShape(); console.log = oldLog; - expect(logs.join('\n')).to.match(/Custom attribute 'aCustom' has been set with various data sizes/); + expect(logs.join('\n')).to.match(/Custom vertex property 'aCustom' has been set with various data sizes/); } ); test('Friendly error too many values set', @@ -2646,11 +2646,11 @@ suite('p5.RendererGL', function() { console.log = myLog; let myGeo = new p5.Geometry(); myGeo.vertices.push(new p5.Vector(0,0,0)); - myGeo.setAttribute('aCustom', 1); - myGeo.setAttribute('aCustom', 2); + myGeo.vertexProperty('aCustom', 1); + myGeo.vertexProperty('aCustom', 2); myp5.model(myGeo); console.log = oldLog; - expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute 'aCustom' with more values than vertices./); + expect(logs.join('\n')).to.match(/One of the geometries has a custom vertex property 'aCustom' with more values than vertices./); } ); test('Friendly error if too few values set', @@ -2663,10 +2663,10 @@ suite('p5.RendererGL', function() { let myGeo = new p5.Geometry(); myGeo.vertices.push(new p5.Vector(0,0,0)); myGeo.vertices.push(new p5.Vector(0,0,0)); - myGeo.setAttribute('aCustom', 1); + myGeo.vertexProperty('aCustom', 1); myp5.model(myGeo); console.log = oldLog; - expect(logs.join('\n')).to.match(/One of the geometries has a custom attribute 'aCustom' with fewer values than vertices./); + expect(logs.join('\n')).to.match(/One of the geometries has a custom vertex property 'aCustom' with fewer values than vertices./); } ); }) From 22cad2b3ba6fa07bf3850a9e8ab267b33309157c Mon Sep 17 00:00:00 2001 From: 23036879 Date: Tue, 1 Oct 2024 10:53:59 +0100 Subject: [PATCH 112/120] remove my temporary gitignore for dev folder --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9af31927d2..42f1e447dc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,4 @@ yarn.lock docs/data.json analyzer/ preview/ -__screenshots__/ -attributes-example/ \ No newline at end of file +__screenshots__/ \ No newline at end of file From 1b03c6bb08845160f9acfe47716c59eb80638b47 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 1 Oct 2024 17:51:34 -0500 Subject: [PATCH 113/120] Lock in 1x ratio --- .../setAttribute/on QUADS shape mode/000.png | Bin 421 -> 0 bytes .../on QUADS shape mode/metadata.json | 3 --- .../setAttribute/on TESS shape mode/000.png | Bin 883 -> 0 bytes .../on TESS shape mode/metadata.json | 3 --- .../000.png | Bin 630 -> 0 bytes .../metadata.json | 3 --- .../vertexProperty/on QUADS shape mode/000.png | Bin 1098 -> 421 bytes .../vertexProperty/on TESS shape mode/000.png | Bin 1746 -> 879 bytes .../000.png | Bin 1182 -> 619 bytes test/unit/visual/visualTest.js | 9 ++++++++- 10 files changed, 8 insertions(+), 10 deletions(-) delete mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png delete mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json delete mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png delete mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json delete mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png delete mode 100644 test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png deleted file mode 100644 index 75018122a4f9c6100832d4a7c6fcee5ace5965d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 421 zcmV;W0b2fvP)Px$Ur9tkRA@u(nXz%gKoCX$7C-^Eb1HBFR6tGzE`SQ0Q-KSh0_Rjf0Ti%Sz761E zbVdkiPZ!+G-gthbfX@tnPw4Ic5?igV9bWK*SLCjWx9sPC-;dIXt=HEMC@}JlmUUvA zO)`Ro6WJysZR*5!y9Xow$tXS^jI^f{+wY%@%uihL`DCOmo!H?pV`MsU#n+6Hc64IL zqhn+|amBY|qz#?e>69@tOk8o!7>U=3T`mho`iU#L1tYOKF|z_Afa3N;ZkJB7Rkz(I z*-7@)=GV^lY~y_(vW<5VXITfp%+Aby2V&j#_H5(X#-~24_Ivw(li7a(-B$ZOUeb?w P00000NkvXXu0mjfI)lLG diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json deleted file mode 100644 index 2d4bfe30da..0000000000 --- a/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "numScreenshots": 1 -} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png deleted file mode 100644 index 5a5164da2f41e42175fa6b9d21f38e0a11a3587b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 883 zcmV-(1C0EMP)Px&ElET{RA@u(*}rQOVHgMS@1>@#t(H{L3N;`Nf~{C$hg7iG_a%#i(5?mlfr6lm zgVXcg*j0rtu5N-jI^CN=aFQbU1EdcA7lmr4H+$H(k_j&Gl z?-F&p-7b#6BN{?Vmf|pxk_Cd`iO78k*_AzKXTQ^XJqqAv#4rKiMr9St5yLS6ixn2^ zcm#onUjp&(IP*s-$jQFviT|&!RT(yp6E0Q#L0%BCClK#t15#NQB*k4FXSh`(%#FK) zoFL+dK>V{9U z!Jl)hdOf(YZI2x6nYk8Wrj?H=gz%>v^2@&g=lbx4DA<(;`HS>ii2N+UpZKNWTUx5T z6r|oJ)Y{ppeLejZ1|98t?8oxImH;JG>zUn@L7K=glzawiOePms+zLOg@i1x5RfyB;ao-< zg^*CGgn(R35{zf0Q3wf@iZ5h_G0f$pVFU@0iZ5g_Nidm{h7m+<m(j9y-u8<}X8U_gCM0_zWkQNaZ3=!54*D|w(tPr7TfpCJTozz-Pt&UBX`Zka@ z5n7fAXGmIVrXUl@IuY6qfb_Q*H+$Z-ZxJXDa+?V29)O^b{)E!f>vTH3w-vJY_cUYR ztE(6Tu*?{i3g&|CO@knb*na6@Y(In?#!>b8%wZxvNAEE9{R?;Ux_YR26U+br002ov JPDHLkV1ffDkUIbX diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json deleted file mode 100644 index 2d4bfe30da..0000000000 --- a/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "numScreenshots": 1 -} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png deleted file mode 100644 index 596ec54c90d7b36a606d8145e1332a4822a29f51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 630 zcmV-+0*U>JP)Px%FiAu~RA@u(mp@CxKor1V?4W}~K^G-L#l;rCfPaRb5}`U2EGUA~#jzqC6o*LC zPvIBP(GMV4+DQ=HGPo!R6&+l}O&lUGn9wGd_G<6aLoOgKa=m-M_j~W|qPec?!asO4 z1tOV6WkfO|po}OZf-;~=OHg8iX>HpMzUT9Kz`t&fbVi!ZCJ;g*F(Zg&S+PJ;fy6%zRpaW}#{K-T#b2xf}bBdi(ZJx&=>MtIlM?|iDXq%SQD zkoDAM9Ve)ol6gZ+BKY<-xZi%02JqKYAj8Uh+`7t$G9oAgsM*`v3p{ diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json deleted file mode 100644 index 2d4bfe30da..0000000000 --- a/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "numScreenshots": 1 -} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/vertexProperty/on QUADS shape mode/000.png b/test/unit/visual/screenshots/WebGL/vertexProperty/on QUADS shape mode/000.png index e75495b342d848f0b754ae7aaee0a555d84e4624..75018122a4f9c6100832d4a7c6fcee5ace5965d4 100644 GIT binary patch literal 421 zcmV;W0b2fvP)Px$Ur9tkRA@u(nXz%gKoCX$7C-^Eb1HBFR6tGzE`SQ0Q-KSh0_Rjf0Ti%Sz761E zbVdkiPZ!+G-gthbfX@tnPw4Ic5?igV9bWK*SLCjWx9sPC-;dIXt=HEMC@}JlmUUvA zO)`Ro6WJysZR*5!y9Xow$tXS^jI^f{+wY%@%uihL`DCOmo!H?pV`MsU#n+6Hc64IL zqhn+|amBY|qz#?e>69@tOk8o!7>U=3T`mho`iU#L1tYOKF|z_Afa3N;ZkJB7Rkz(I z*-7@)=GV^lY~y_(vW<5VXITfp%+Aby2V&j#_H5(X#-~24_Ivw(li7a(-B$ZOUeb?w P00000NkvXXu0mjfI)lLG literal 1098 zcmeAS@N?(olHy`uVBq!ia0vp^DImU}5oeaSW-5 zdplP#;Esbx+kL5!)ditDv|@PIafP?8K6LfLDopOtHH(Ui%O~uPnRK`I|J|08>UnL+ zZS!aTxw$80|M&eZTeg1}dAnBEzW>{^pU-c;_u6m1HS1s9-~Yb8`b+-)U7n$4@?`G9 z@cO3ql7bBeDS2KmyE5iqd@tU%AiUVd*~%tIB8kb_&VF%OaOfdbTT;nkw9S&gXpL^v*{uB~`bQJt}!S zzAZg2@TD!w%OgFVxoyhThKzW2psDMZwUpG&OZ2D%D$qSF@MRZJfw=h0wkJRZ_llrWBiAgoZZ3dc98=L4+4m4r)A%QQ` zfc)a%%oERm8p@X{zBuc{`SRPfM33EUF25feugTrkMC&j4UEQd7 zw)uyD!ikcvPE{2GlACNg0#i$X5HKcmmyi%e6Uzuby71Bw0m9Q$C;^sJ5-TX`>j6F6`sJnwWi5118w z-ZORk!B-$P$Jnp`2@5C%=`B`QT)%8ZHAlj$z0uZ(OQ{jwt?Re8KVO``=89Tn*;U+q(0{-Q`QqJw1G7Gf(46 z^LfXXPpxiWnZABP<=gD?{n^Do4ZbEhC%jrTYx;&|w@t zdo7B--AX$-Ik;TddB(2E5?PWKK6h)~vL!1_*2-Cy7QI@^5k8w){&!AQ5zryt;U~6$ z6ss@4cc<(`r>uYWG#ejTprhXBGbeqkKK9MdXYWQ^qqUKro?PkPcK^1y;;YE-249tc zPB7aF77Rb}syM_%cX9j5aA_yM+Kb{(XY1O314-RI&^>3D@WU*y&71Gv)pj}k;^fNR zyp1cZ^&_8L5uaUNSzNH)Hd=O>ol%;vUu}wI!PhlqTje13tGDg(-`g(n%p@`6zWn(& z-w&QWdiNPnKflzWH{YkS?_arhzU;DjTb<(aTyF8NyCq}mv-gC+3@e|#|K6TkugtzK z?Ln69jH-{St2-y0Tp9hBD=+=j^UdDJj=uQMET}kn%l}Px&DM>^@RA@u(+0APcK^O+`XYoE|eMAQWwi*bC5Aad=a3}`Ur(dN+f^;32m+r8r7~*{}@5Uz5wl; zA<{q;tXU(}Yx)>%hStOua#yW!!7kp7U!=!Evp^x19_*yJ95wR;kyXgm@>p3z!S1LP%{LXlv zghKc>hkWU#KU|kq0!vd)gA>Y+JYpJs0Jfj2A_mN|o15urI zJckW$dto&^5El*uEw8i^2Bsn|l67{h8z8d?w+sn|l+d>`eQG_)XUBU_9Oq)vot$N(H&m?qxW z3U;u;q=htyP}4wYCt{0918EYWu8A;)q%AX3$R-gQ28dJ;wUb(lsZ!Hj>YG5?L}(f! zQbU4L(*@~3Iz(uv00h59yV>KeeUm_OkQ+qkSOA&1n11Obwx2>Kaa3(SbDD_F(VN7+e*k%6x_U66QeFT6002ovPDHLk FV1h`Vg!BLa literal 1746 zcmV;@1}*uCP)Px*j7da6RCr$P-9czvMHmP0|JyWXldWkRY%NVqZA{zPl%)-cp~jk-Qfw~`2#p7e zg@QeJ6IAR$$Tv4d!IOdr_Rx!BLF~!fo1g~;saU8KT3T9KDJY6Up$F^AB(5xRH+}Qo z%$qMT8v-HhZYJ}~|C?{#d%H;*jYb3U#%2bR2*So>T#YbW;{g*KME$8if8>X7 z0b-If)+2Cq-8@Em!j`gNAnG>-`n@=WD-e?w12fty@Z5%ZZ0`*NQ#y$HRe^qS2jLo| zS;ltu3CwJq$53CnFr|U0I|_8iJ%lfiW&<$1R^aK)^VqVs9GFr-)NKX2?GMAC6j{_e zbz!Y7^wc0>e9-^G?;S+lRG=GvG0FfjIFs85^&!6~UP06~1-kAZqbv}UqBLs*k zU%SQ#!((nyoPwx}3Usl2j8GsZDLd+fp}IR1XCUf=0{svMBP57H8Qw=29Cw4_1VsO= zA-|M94}w1Mxqa_Lnuk`}9o{$PYiN?Nd7?Oq6A*b$Lf`6quHu2S>EI5bK@8O1SBqbj zTNp&&)R3QsO-JaG)!gZCohCdoT`VPqLF8EpeIsl-xPTa#C*LZTl0qQ*x`zBDd^$LR zJSqtT2lJ<-5QzLpLSGA?4o)CuEqUUd{3*!|qOWSm4|t@58_34Pg#IJ>Qj!})zAvG# z@JI(Y5R;O}KFpVrTp;Sa0-X=H32fITb7*^8uWA{XCDdl^Q<4iLdC6Sa#OuZl#Ncc? zYo8Kp5cRnNo#T}bjwQ>0^dBQs=j>8q4U#<1FCAP#%v!Sk3%it9f#{1G@-&~$s>_C3hkgr9MP6ry>U0Yl`LFhh_y=+;5s51(5hTr_5GB+ z)5;8@ztfP%c`oSeL9Y4$`Bp=|5J5Vf!d>YUZ|`R*lKt6Q-^>Y7?x&K{6h6;ClGo z09J`mnRnb}nQ~>y7S|hRklr*!A{=iF4Mf6tVUHlpqQV)ZFJp(b_(yN^zP+3~Ev_I{ zAyCcOVOfK4?zAFEELs11Z?3lI)QQlYvBQc9S@vdUMRwwy4!`%ODJmH|ENc+XtuNP- z&q2ny+wy*oe=W?t_wRykbWXmE!K}*AY4ObIuT0V32a(~%p5 zV@f!IJS+q@S!@PdcMw)P_0|V+O9>~Ct!auLi&obO2)C4Q0a+&mw&YAlp_0X|q0pYTiZ~)nvrsyk>4ksX7QW6?uKnRQ!OouZN4k-x@ zvO7(&wqQD(fpA#hLxKzwVKB!gu-zGq-20GrPZlyMp+I&JVaO4N(}XmELZO5J*+qn5 zhZtT!LY1$wLG}<~#4(09kPs+kg4BsH>H)(mNZBZ5f$Sr~m?sSHAZ4PI0WwL1x<`ys zK*~b#4KhW9anBf~ftVj;E|4%Cn6H;K_yl=|2ooh>lnT;pi+#cP0FgwPE(xMJKTbD> z?KX>@zULaoCCFhS94r|kIEd@SxB!_Y!jV7_Y(QM?@QZ-V5n(nk1UrzTFnK_Z6X6&W z2(}=trObT2u(0r7XSPy@wi3v+5SS8T@jtnl&%HA}RZnI`)y>GBZJ?j9M->-?X z9Lh}eAt4~d(tn1Yo|(aZrjx$X@4o6l@GY) oA_%h?U^bkXkO;zT2AB=!AM~_q0@Hr*x&QzG07*qoM6N<$f`Nh$YXATM diff --git a/test/unit/visual/screenshots/WebGL/vertexProperty/on buildGeometry outputs containing 3D primitives/000.png b/test/unit/visual/screenshots/WebGL/vertexProperty/on buildGeometry outputs containing 3D primitives/000.png index 1b0d8e95d24f7e8955467299b4c2c29864091bee..9a8353804d06ee322d9d9ab7c36875577c794e74 100644 GIT binary patch literal 619 zcmV-x0+juUP)Px%B}qgQIKorMcaM0op1VL1=73$)kI4KCymVls(iqcU8ry`;*aWS+7 z{~c#R)V7mbpdvU0L_tMx@W-ZDc_|5LdTB4cUVD{0C}|<@@;=}1yL*=^rBbN`|6nK* zL@*bnBZ3(M(h=zhCk13_aT3;4-F`8_dY#!;81Kb0!>f1rL0E3`e$~Y;ZFl7$>j*&#g@O~} z-ELc2TA0kTdIaAC^PnFLRqg#j!0O%0)e&^#!B~ZtP!Y7$L}!Aa8%cE+Cd0?@JPPpm zehPYWni~u^N6?KPV+wvFQ47R;JqK-Vp|e2HjU9ChR>DaO;%N-ZuH0RNzPQ8%hKnQU z#*EPgy^-Mx5VLp-+T2EGfS?=u>N*^TXDc8aSFY|sk8U>|rsV2SW_aSW-5 zdpp-ULnKh7?Yz+s2KC01KRUR)Smq0MCoEhvsiAc)%bO!^8?X2UsYPqvis0Z95~vXg zn-$a{yCnX`DEe1dqI*4()$oi2ZJ&dr&7=L!FBbl@m|Ht+BLpYLYgys5G@ zYHgTuPLhxYV~2!Tp(2A)H+uueV@3rAC0)kEuM654IySJhOl5XvaEV|#bgB`kK!-8$ z=>Y)-!D$T}iV`>)IJ_MqEDcy3T2vL*$Vf0L91=Pa;kPqp*~*nGe`xRI<>R~b;zhHkf;)-HC1BqtM-WGO}Cg7Gy}IYD_5RaBmatV zLTCdIFR$s>t~IM9oVBKMRhu(JDz4J8Rn~p0W6SCg=3)Fk=xRm8yGyJJx(zeDqc+Eg z?kv=0oFKMR{GzoKKevLV-}^<=wWGPxZ#F6F9$@t7+q_eFHeqRLM{pFaZ z4`7bjt)o?dJ#NcSb2(>udg(yXXOj?oXFu?taZ# zwM%XZO?6l!Ya3_(wf*T0zTcs>wQ*ZjwHhO={eCHLQM7RA{+F-d9qUrOSzqJVkC?Tp zuNoq(w^dia45`SKbjdj(Z*Vm|{A$!9zK*Sr=hV0EYTCD>TxI&29n))OEqU>FiI;Z> zt@l_j`G!`-Y{d?=xPhPURQz-4mYL$p!S!;W+-@7WeSC zvc10hsy;5&@(y9xSa#>j@0I#5;V3-e`r+wab-au3$G{NmfHs-sY8 zU%By;^UN*J#opHJP+ba?m9jv}CQk55XXt~HJ z!0>anLlnQ@w5gMe5;`W&_&J$@!(Llmnem9{nLnNkE%T>Np2+Y>CGDRI!=dx1PI@pD z3K`c6F+5CvnxfKR(K*wev*E+$A|oM(IY-XSXL0xuX(`F6;Ft8wpGo1*nmIiz0_ujJ z)je60_`cfP-G1gC@y0_;z&Q1BJ@biZw&(foPv<&6{+FX56+h?LEJhazQwBzXL{1KE h_BJwdra;&^{iGZFm@;nY{Q?#^44$rjF6*2UngB%J0D}Mk diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js index b7a831e81b..9f084cdabd 100644 --- a/test/unit/visual/visualTest.js +++ b/test/unit/visual/visualTest.js @@ -62,15 +62,20 @@ export function visualSuite( } suiteFn(name, () => { let lastPrefix; + let lastDeviceRatio = window.devicePixelRatio; beforeAll(() => { lastPrefix = namePrefix; namePrefix += escapeName(name) + '/'; + + // Force everything to be 1x + window.devicePixelRatio = 1; }) callback() afterAll(() => { namePrefix = lastPrefix; + window.devicePixelRatio = lastDeviceRatio; }); }); } @@ -186,7 +191,9 @@ export function visualTest( // Generate screenshots await callback(myp5, () => { - actual.push(myp5.get()); + const img = myp5.get(); + img.pixelDensity(1); + actual.push(img); }); From 0a7cec82c333c79d88b00f0b3ab9f802f1a5b879 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Mon, 30 Sep 2024 15:42:31 -0400 Subject: [PATCH 114/120] add new sketch verifier to FES; it currently can read all user-defined variables and functions --- package-lock.json | 76 ++++++++++---- package.json | 3 +- preview/index.html | 52 ++++++---- src/core/friendly_errors/index.js | 2 + src/core/friendly_errors/sketch_verifier.js | 107 ++++++++++++++++++++ 5 files changed, 195 insertions(+), 45 deletions(-) create mode 100644 src/core/friendly_errors/sketch_verifier.js diff --git a/package-lock.json b/package-lock.json index 96a28156d2..e753dbafb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "license": "LGPL-2.1", "dependencies": { "colorjs.io": "^0.5.2", + "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", "omggif": "^1.0.10", - "opentype.js": "^1.3.1", - "zod-validation-error": "^3.3.1" + "opentype.js": "^1.3.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", @@ -828,6 +828,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2547,7 +2564,6 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2559,7 +2575,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -4563,6 +4578,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4603,17 +4635,27 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -11606,21 +11648,11 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-validation-error": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.1.tgz", - "integrity": "sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA==", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.18.0" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index ef709e7e47..750620c1ea 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "version": "1.9.4", "dependencies": { "colorjs.io": "^0.5.2", + "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", @@ -82,4 +83,4 @@ "pre-commit": "lint-staged" } } -} \ No newline at end of file +} diff --git a/preview/index.html b/preview/index.html index 847423dc4d..c8357b5675 100644 --- a/preview/index.html +++ b/preview/index.html @@ -1,5 +1,6 @@ + P5 test @@ -7,35 +8,42 @@ + - + new p5(sketch); + + \ No newline at end of file diff --git a/src/core/friendly_errors/index.js b/src/core/friendly_errors/index.js index 4cf7db60ba..8f1b0e56e0 100644 --- a/src/core/friendly_errors/index.js +++ b/src/core/friendly_errors/index.js @@ -1,5 +1,7 @@ import validateParams from './param_validator.js'; +import sketchVerifier from './sketch_verifier.js'; export default function (p5) { p5.registerAddon(validateParams); + p5.registerAddon(sketchVerifier); } \ No newline at end of file diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js new file mode 100644 index 0000000000..fe88ab9fda --- /dev/null +++ b/src/core/friendly_errors/sketch_verifier.js @@ -0,0 +1,107 @@ +import * as espree from 'espree'; + +/** + * @for p5 + * @requires core + */ +function sketchVerifier(p5, fn) { + /** + * Fetches the contents of a script element in the user's sketch. + * + * @method fetchScript + * @param {HTMLScriptElement} script + * @returns {Promise} + */ + fn.fetchScript = async function (script) { + if (script.src) { + const contents = await fetch(script.src).then((res) => res.text()); + return contents; + } else { + return script.textContent; + } + } + + /** + * Extracts the user's code from the script fetched. Note that this method + * assumes that the user's code is always the last script element in the + * sketch. + * + * @method getUserCode + * @returns {Promise} The user's code as a string. + */ + fn.getUserCode = async function () { + const scripts = document.querySelectorAll('script'); + const userCodeScript = scripts[scripts.length - 1]; + const userCode = await fn.fetchScript(userCodeScript); + + return userCode; + } + + fn.extractUserDefinedVariablesAndFuncs = function (codeStr) { + const userDefinitions = { + variables: [], + functions: [] + }; + + try { + const ast = espree.parse(codeStr, { + ecmaVersion: 2021, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }); + + function traverse(node) { + switch (node.type) { + case 'VariableDeclaration': + node.declarations.forEach(declaration => { + if (declaration.id.type === 'Identifier') { + userDefinitions.variables.push(declaration.id.name); + } + }); + break; + case 'FunctionDeclaration': + if (node.id && node.id.type === 'Identifier') { + userDefinitions.functions.push(node.id.name); + } + break; + case 'ArrowFunctionExpression': + case 'FunctionExpression': + if (node.parent && node.parent.type === 'VariableDeclarator') { + userDefinitions.functions.push(node.parent.id.name); + } + break; + } + + for (const key in node) { + if (node[key] && typeof node[key] === 'object') { + if (Array.isArray(node[key])) { + node[key].forEach(child => traverse(child)); + } else { + traverse(node[key]); + } + } + } + } + + traverse(ast); + } catch (error) { + console.error('Error parsing code:', error); + } + + return userDefinitions; + } + + fn.run = async function () { + const userCode = await fn.getUserCode(); + const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); + console.log(userDefinedVariablesAndFuncs); + } +} + +export default sketchVerifier; + +if (typeof p5 !== 'undefined') { + sketchVerifier(p5, p5.prototype); +} \ No newline at end of file From 135187f1f429334a9d82cd67529f59a2c3805f98 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Tue, 1 Oct 2024 22:23:34 -0400 Subject: [PATCH 115/120] add test file for sketch verifier --- src/core/friendly_errors/sketch_verifier.js | 37 +++-- test/unit/core/sketch_overrides.js | 142 ++++++++++++++++++++ test/unit/spec.js | 1 + 3 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 test/unit/core/sketch_overrides.js diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js index fe88ab9fda..c646360f81 100644 --- a/src/core/friendly_errors/sketch_verifier.js +++ b/src/core/friendly_errors/sketch_verifier.js @@ -37,6 +37,16 @@ function sketchVerifier(p5, fn) { return userCode; } + /** + * Extracts the user-defined variables and functions from the user code with + * the help of Espree parser. + * + * @method extractUserDefinedVariablesAndFuncs + * @param {string} codeStr - The code to extract variables and functions from. + * @returns {Object} An object containing the user's defined variables and functions. + * @returns {string[]} [userDefinitions.variables] Array of user-defined variable names. + * @returns {strings[]} [userDefinitions.functions] Array of user-defined function names. + */ fn.extractUserDefinedVariablesAndFuncs = function (codeStr) { const userDefinitions = { variables: [], @@ -53,23 +63,22 @@ function sketchVerifier(p5, fn) { }); function traverse(node) { - switch (node.type) { + const { type, declarations, id, init } = node; + + switch (type) { case 'VariableDeclaration': - node.declarations.forEach(declaration => { - if (declaration.id.type === 'Identifier') { - userDefinitions.variables.push(declaration.id.name); + declarations.forEach(({ id, init }) => { + if (id.type === 'Identifier') { + const category = init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(init.type) + ? 'functions' + : 'variables'; + userDefinitions[category].push(id.name); } }); break; case 'FunctionDeclaration': - if (node.id && node.id.type === 'Identifier') { - userDefinitions.functions.push(node.id.name); - } - break; - case 'ArrowFunctionExpression': - case 'FunctionExpression': - if (node.parent && node.parent.type === 'VariableDeclarator') { - userDefinitions.functions.push(node.parent.id.name); + if (id?.type === 'Identifier') { + userDefinitions.functions.push(id.name); } break; } @@ -87,6 +96,7 @@ function sketchVerifier(p5, fn) { traverse(ast); } catch (error) { + // TODO: Replace this with a friendly error message. console.error('Error parsing code:', error); } @@ -96,7 +106,8 @@ function sketchVerifier(p5, fn) { fn.run = async function () { const userCode = await fn.getUserCode(); const userDefinedVariablesAndFuncs = fn.extractUserDefinedVariablesAndFuncs(userCode); - console.log(userDefinedVariablesAndFuncs); + + return userDefinedVariablesAndFuncs; } } diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js new file mode 100644 index 0000000000..7c183ffd73 --- /dev/null +++ b/test/unit/core/sketch_overrides.js @@ -0,0 +1,142 @@ +import sketchVerifier from '../../../src/core/friendly_errors/sketch_verifier.js'; + +suite('Validate Params', function () { + const mockP5 = { + _validateParameters: vi.fn() + }; + const mockP5Prototype = {}; + + beforeAll(function () { + sketchVerifier(mockP5, mockP5Prototype); + }); + + afterAll(function () { + }); + + suite('fetchScript()', function () { + const url = 'https://www.p5test.com/sketch.js'; + const code = 'p.createCanvas(200, 200);'; + + test('Fetches script content from src', async function () { + const mockFetch = vi.fn(() => + Promise.resolve({ + text: () => Promise.resolve(code) + }) + ); + vi.stubGlobal('fetch', mockFetch); + + const mockScript = { src: url }; + const result = await mockP5Prototype.fetchScript(mockScript); + + expect(mockFetch).toHaveBeenCalledWith(url); + expect(result).toBe(code); + + vi.unstubAllGlobals(); + }); + + test('Fetches code when there is no src attribute', async function () { + const mockScript = { textContent: code }; + const result = await mockP5Prototype.fetchScript(mockScript); + + expect(result).toBe(code); + }); + }); + + suite('getUserCode()', function () { + const userCode = "let c = p5.Color(20, 20, 20);"; + + test('fetches the last script element', async function () { + document.body.innerHTML = ` + + + + `; + + mockP5Prototype.fetchScript = vi.fn(() => Promise.resolve(userCode)); + + const result = await mockP5Prototype.getUserCode(); + + expect(mockP5Prototype.fetchScript).toHaveBeenCalledTimes(1); + expect(result).toBe(userCode); + }); + }); + + suite('extractUserDefinedVariablesAndFuncs()', function () { + test('Extracts user-defined variables and functions', function () { + const code = ` + let x = 5; + const y = 10; + var z = 15; + let v1, v2, v3 + function foo() {} + const bar = () => {}; + const baz = (x) => x * 2; + `; + + const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); + + expect(result.variables).toEqual(['x', 'y', 'z', 'v1', 'v2', 'v3']); + expect(result.functions).toEqual(['foo', 'bar', 'baz']); + }); + + // Sketch verifier should ignore the following types of lines: + // - Comments (both single line and multi-line) + // - Function calls + // - Non-declaration code + test('Ignores other lines', function () { + const code = ` + // This is a comment + let x = 5; + /* This is a multi-line comment. + * This is a multi-line comment. + */ + const y = 10; + console.log("This is a statement"); + foo(5); + p5.Math.random(); + if (true) { + let z = 15; + } + for (let i = 0; i < 5; i++) {} + `; + + const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); + + expect(result.variables).toEqual(['x', 'y', 'z', 'i']); + expect(result.functions).toEqual([]); + }); + + test('Handles parsing errors', function () { + const invalidCode = 'let x = ;'; + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); + + const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(invalidCode); + + expect(consoleSpy).toHaveBeenCalled(); + expect(result).toEqual({ variables: [], functions: [] }); + + consoleSpy.mockRestore(); + }); + }); + + suite('run()', function () { + test('Returns extracted variables and functions', async function () { + const mockScript = ` + let x = 5; + const y = 10; + function foo() {} + const bar = () => {}; + `; + mockP5Prototype.getUserCode = vi.fn(() => Promise.resolve(mockScript)); + + const result = await mockP5Prototype.run(); + + expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1); + + expect(result).toEqual({ + variables: ['x', 'y'], + functions: ['foo', 'bar'] + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/spec.js b/test/unit/spec.js index 995b152693..8df31317f2 100644 --- a/test/unit/spec.js +++ b/test/unit/spec.js @@ -13,6 +13,7 @@ var spec = { 'param_errors', 'preload', 'rendering', + 'sketch_overrides', 'structure', 'transform', 'version', From 9067152ef239a9a5e74f578efacaaee571470f10 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Tue, 1 Oct 2024 22:26:25 -0400 Subject: [PATCH 116/120] remove all the silly definitions in index.html --- preview/index.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/preview/index.html b/preview/index.html index c8357b5675..299cf2ccb0 100644 --- a/preview/index.html +++ b/preview/index.html @@ -22,14 +22,9 @@ // p5.registerAddon(calculation); - let apple = 10; - function banana() { - console.log('banana'); - } const sketch = function (p) { p.setup = function () { p.createCanvas(200, 200); - p.run(); }; p.draw = function () { From 99342de0c6b4bfd703b132e7a33f1e21266b637f Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 2 Oct 2024 16:53:34 +0100 Subject: [PATCH 117/120] Convert most of webgl module to use new syntax --- src/app.js | 30 +- src/webgl/3d_primitives.js | 6840 +++++++++++------------ src/webgl/index.js | 31 + src/webgl/interaction.js | 1691 +++--- src/webgl/light.js | 3498 ++++++------ src/webgl/loading.js | 2095 +++---- src/webgl/material.js | 6488 +++++++++++----------- src/webgl/p5.Camera.js | 7672 +++++++++++++------------- src/webgl/p5.DataArray.js | 200 +- src/webgl/p5.Framebuffer.js | 3239 +++++------ src/webgl/p5.Geometry.js | 4459 +++++++-------- src/webgl/p5.Matrix.js | 1869 +++---- src/webgl/p5.Quat.js | 168 +- src/webgl/p5.RenderBuffer.js | 138 +- src/webgl/p5.RendererGL.Immediate.js | 1 - src/webgl/p5.RendererGL.Retained.js | 2 - src/webgl/p5.RendererGL.js | 4 - src/webgl/p5.Shader.js | 2600 ++++----- src/webgl/text.js | 1355 ++--- 19 files changed, 21231 insertions(+), 21149 deletions(-) create mode 100644 src/webgl/index.js diff --git a/src/app.js b/src/app.js index ca17271be7..e5189e7eb1 100644 --- a/src/app.js +++ b/src/app.js @@ -68,24 +68,26 @@ import utilities from './utilities'; utilities(p5); // webgl -import './webgl/3d_primitives'; -import './webgl/interaction'; -import './webgl/light'; -import './webgl/loading'; -import './webgl/material'; -import './webgl/p5.Camera'; -import './webgl/p5.DataArray'; -import './webgl/p5.Geometry'; -import './webgl/p5.Matrix'; -import './webgl/p5.Quat'; +import webgl from './webgl'; +webgl(p5); +// import './webgl/3d_primitives'; +// import './webgl/interaction'; +// import './webgl/light'; +// import './webgl/loading'; +// import './webgl/material'; +// import './webgl/p5.Camera'; +// import './webgl/p5.DataArray'; +// import './webgl/p5.Geometry'; +// import './webgl/p5.Matrix'; +// import './webgl/p5.Quat'; import './webgl/p5.RendererGL.Immediate'; import './webgl/p5.RendererGL'; import './webgl/p5.RendererGL.Retained'; -import './webgl/p5.Framebuffer'; -import './webgl/p5.Shader'; -import './webgl/p5.RenderBuffer'; +// import './webgl/p5.Framebuffer'; +// import './webgl/p5.Shader'; +// import './webgl/p5.RenderBuffer'; import './webgl/p5.Texture'; -import './webgl/text'; +// import './webgl/text'; import './core/init'; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 9c552d1ed7..d34e7890cb 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -6,2679 +6,2619 @@ * @requires p5.Geometry */ -import p5 from '../core/main'; -import './p5.Geometry'; import * as constants from '../core/constants'; -/** - * Begins adding shapes to a new - * p5.Geometry object. - * - * The `beginGeometry()` and endGeometry() - * functions help with creating complex 3D shapes from simpler ones such as - * sphere(). `beginGeometry()` begins adding shapes - * to a custom p5.Geometry object and - * endGeometry() stops adding them. - * - * `beginGeometry()` and endGeometry() can help - * to make sketches more performant. For example, if a complex 3D shape - * doesn’t change while a sketch runs, then it can be created with - * `beginGeometry()` and endGeometry(). - * Creating a p5.Geometry object once and then - * drawing it will run faster than repeatedly drawing the individual pieces. - * - * See buildGeometry() for another way to - * build 3D shapes. - * - * Note: `beginGeometry()` can only be used in WebGL mode. - * - * @method beginGeometry - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * // Add a cone. - * cone(); - * - * // Stop building the p5.Geometry object. - * shape = endGeometry(); - * - * describe('A white cone drawn on a gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the p5.Geometry object. - * noStroke(); - * - * // Draw the p5.Geometry object. - * model(shape); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Geometry object. - * createArrow(); - * - * describe('A white arrow drawn on a gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the p5.Geometry object. - * noStroke(); - * - * // Draw the p5.Geometry object. - * model(shape); - * } - * - * function createArrow() { - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * // Add shapes. - * push(); - * rotateX(PI); - * cone(10); - * translate(0, -10, 0); - * cylinder(3, 20); - * pop(); - * - * // Stop building the p5.Geometry object. - * shape = endGeometry(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let blueArrow; - * let redArrow; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the arrows. - * redArrow = createArrow('red'); - * blueArrow = createArrow('blue'); - * - * describe('A red arrow and a blue arrow drawn on a gray background. The blue arrow rotates slowly.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the arrows. - * noStroke(); - * - * // Draw the red arrow. - * model(redArrow); - * - * // Translate and rotate the coordinate system. - * translate(30, 0, 0); - * rotateZ(frameCount * 0.01); - * - * // Draw the blue arrow. - * model(blueArrow); - * } - * - * function createArrow(fillColor) { - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * fill(fillColor); - * - * // Add shapes to the p5.Geometry object. - * push(); - * rotateX(PI); - * cone(10); - * translate(0, -10, 0); - * cylinder(3, 20); - * pop(); - * - * // Stop building the p5.Geometry object. - * let shape = endGeometry(); - * - * return shape; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let button; - * let particles; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a button to reset the particle system. - * button = createButton('Reset'); - * - * // Call resetModel() when the user presses the button. - * button.mousePressed(resetModel); - * - * // Add the original set of particles. - * resetModel(); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the particles. - * noStroke(); - * - * // Draw the particles. - * model(particles); - * } - * - * function resetModel() { - * // If the p5.Geometry object has already been created, - * // free those resources. - * if (particles) { - * freeGeometry(particles); - * } - * - * // Create a new p5.Geometry object with random spheres. - * particles = createParticles(); - * } - * - * function createParticles() { - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * // Add shapes. - * for (let i = 0; i < 60; i += 1) { - * // Calculate random coordinates. - * let x = randomGaussian(0, 20); - * let y = randomGaussian(0, 20); - * let z = randomGaussian(0, 20); - * - * push(); - * // Translate to the particle's coordinates. - * translate(x, y, z); - * // Draw the particle. - * sphere(5); - * pop(); - * } - * - * // Stop building the p5.Geometry object. - * let shape = endGeometry(); - * - * return shape; - * } - * - *
- */ -p5.prototype.beginGeometry = function() { - return this._renderer.beginGeometry(); -}; - -/** - * Stops adding shapes to a new - * p5.Geometry object and returns the object. - * - * The `beginGeometry()` and endGeometry() - * functions help with creating complex 3D shapes from simpler ones such as - * sphere(). `beginGeometry()` begins adding shapes - * to a custom p5.Geometry object and - * endGeometry() stops adding them. - * - * `beginGeometry()` and endGeometry() can help - * to make sketches more performant. For example, if a complex 3D shape - * doesn’t change while a sketch runs, then it can be created with - * `beginGeometry()` and endGeometry(). - * Creating a p5.Geometry object once and then - * drawing it will run faster than repeatedly drawing the individual pieces. - * - * See buildGeometry() for another way to - * build 3D shapes. - * - * Note: `endGeometry()` can only be used in WebGL mode. - * - * @method endGeometry - * @returns {p5.Geometry} new 3D shape. - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * // Add a cone. - * cone(); - * - * // Stop building the p5.Geometry object. - * shape = endGeometry(); - * - * describe('A white cone drawn on a gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the p5.Geometry object. - * noStroke(); - * - * // Draw the p5.Geometry object. - * model(shape); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Geometry object. - * createArrow(); - * - * describe('A white arrow drawn on a gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the p5.Geometry object. - * noStroke(); - * - * // Draw the p5.Geometry object. - * model(shape); - * } - * - * function createArrow() { - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * // Add shapes. - * push(); - * rotateX(PI); - * cone(10); - * translate(0, -10, 0); - * cylinder(3, 20); - * pop(); - * - * // Stop building the p5.Geometry object. - * shape = endGeometry(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let blueArrow; - * let redArrow; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the arrows. - * redArrow = createArrow('red'); - * blueArrow = createArrow('blue'); - * - * describe('A red arrow and a blue arrow drawn on a gray background. The blue arrow rotates slowly.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the arrows. - * noStroke(); - * - * // Draw the red arrow. - * model(redArrow); - * - * // Translate and rotate the coordinate system. - * translate(30, 0, 0); - * rotateZ(frameCount * 0.01); - * - * // Draw the blue arrow. - * model(blueArrow); - * } - * - * function createArrow(fillColor) { - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * fill(fillColor); - * - * // Add shapes to the p5.Geometry object. - * push(); - * rotateX(PI); - * cone(10); - * translate(0, -10, 0); - * cylinder(3, 20); - * pop(); - * - * // Stop building the p5.Geometry object. - * let shape = endGeometry(); - * - * return shape; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let button; - * let particles; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a button to reset the particle system. - * button = createButton('Reset'); - * - * // Call resetModel() when the user presses the button. - * button.mousePressed(resetModel); - * - * // Add the original set of particles. - * resetModel(); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the particles. - * noStroke(); - * - * // Draw the particles. - * model(particles); - * } - * - * function resetModel() { - * // If the p5.Geometry object has already been created, - * // free those resources. - * if (particles) { - * freeGeometry(particles); - * } - * - * // Create a new p5.Geometry object with random spheres. - * particles = createParticles(); - * } - * - * function createParticles() { - * // Start building the p5.Geometry object. - * beginGeometry(); - * - * // Add shapes. - * for (let i = 0; i < 60; i += 1) { - * // Calculate random coordinates. - * let x = randomGaussian(0, 20); - * let y = randomGaussian(0, 20); - * let z = randomGaussian(0, 20); - * - * push(); - * // Translate to the particle's coordinates. - * translate(x, y, z); - * // Draw the particle. - * sphere(5); - * pop(); - * } - * - * // Stop building the p5.Geometry object. - * let shape = endGeometry(); - * - * return shape; - * } - * - *
- */ -p5.prototype.endGeometry = function() { - return this._renderer.endGeometry(); -}; - -/** - * Creates a custom p5.Geometry object from - * simpler 3D shapes. - * - * `buildGeometry()` helps with creating complex 3D shapes from simpler ones - * such as sphere(). It can help to make sketches - * more performant. For example, if a complex 3D shape doesn’t change while a - * sketch runs, then it can be created with `buildGeometry()`. Creating a - * p5.Geometry object once and then drawing it - * will run faster than repeatedly drawing the individual pieces. - * - * The parameter, `callback`, is a function with the drawing instructions for - * the new p5.Geometry object. It will be called - * once to create the new 3D shape. - * - * See beginGeometry() and - * endGeometry() for another way to build 3D - * shapes. - * - * Note: `buildGeometry()` can only be used in WebGL mode. - * - * @method buildGeometry - * @param {Function} callback function that draws the shape. - * @returns {p5.Geometry} new 3D shape. - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Geometry object. - * shape = buildGeometry(createShape); - * - * describe('A white cone drawn on a gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the p5.Geometry object. - * noStroke(); - * - * // Draw the p5.Geometry object. - * model(shape); - * } - * - * // Create p5.Geometry object from a single cone. - * function createShape() { - * cone(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the arrow. - * shape = buildGeometry(createArrow); - * - * describe('A white arrow drawn on a gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the arrow. - * noStroke(); - * - * // Draw the arrow. - * model(shape); - * } - * - * function createArrow() { - * // Add shapes to the p5.Geometry object. - * push(); - * rotateX(PI); - * cone(10); - * translate(0, -10, 0); - * cylinder(3, 20); - * pop(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Geometry object. - * shape = buildGeometry(createArrow); - * - * describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the arrows. - * noStroke(); - * - * // Draw the p5.Geometry object. - * model(shape); - * - * // Translate and rotate the coordinate system. - * translate(30, 0, 0); - * rotateZ(frameCount * 0.01); - * - * // Draw the p5.Geometry object again. - * model(shape); - * } - * - * function createArrow() { - * // Add shapes to the p5.Geometry object. - * push(); - * rotateX(PI); - * cone(10); - * translate(0, -10, 0); - * cylinder(3, 20); - * pop(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let button; - * let particles; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a button to reset the particle system. - * button = createButton('Reset'); - * - * // Call resetModel() when the user presses the button. - * button.mousePressed(resetModel); - * - * // Add the original set of particles. - * resetModel(); - * - * describe('A set of white spheres on a gray background. The spheres are positioned randomly. Their positions reset when the user presses the Reset button.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the particles. - * noStroke(); - * - * // Draw the particles. - * model(particles); - * } - * - * function resetModel() { - * // If the p5.Geometry object has already been created, - * // free those resources. - * if (particles) { - * freeGeometry(particles); - * } - * - * // Create a new p5.Geometry object with random spheres. - * particles = buildGeometry(createParticles); - * } - * - * function createParticles() { - * for (let i = 0; i < 60; i += 1) { - * // Calculate random coordinates. - * let x = randomGaussian(0, 20); - * let y = randomGaussian(0, 20); - * let z = randomGaussian(0, 20); - * - * push(); - * // Translate to the particle's coordinates. - * translate(x, y, z); - * // Draw the particle. - * sphere(5); - * pop(); - * } - * } - * - *
- */ -p5.prototype.buildGeometry = function(callback) { - return this._renderer.buildGeometry(callback); -}; +function primitives3D(p5, fn){ + /** + * Begins adding shapes to a new + * p5.Geometry object. + * + * The `beginGeometry()` and endGeometry() + * functions help with creating complex 3D shapes from simpler ones such as + * sphere(). `beginGeometry()` begins adding shapes + * to a custom p5.Geometry object and + * endGeometry() stops adding them. + * + * `beginGeometry()` and endGeometry() can help + * to make sketches more performant. For example, if a complex 3D shape + * doesn’t change while a sketch runs, then it can be created with + * `beginGeometry()` and endGeometry(). + * Creating a p5.Geometry object once and then + * drawing it will run faster than repeatedly drawing the individual pieces. + * + * See buildGeometry() for another way to + * build 3D shapes. + * + * Note: `beginGeometry()` can only be used in WebGL mode. + * + * @method beginGeometry + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * // Add a cone. + * cone(); + * + * // Stop building the p5.Geometry object. + * shape = endGeometry(); + * + * describe('A white cone drawn on a gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the p5.Geometry object. + * noStroke(); + * + * // Draw the p5.Geometry object. + * model(shape); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Geometry object. + * createArrow(); + * + * describe('A white arrow drawn on a gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the p5.Geometry object. + * noStroke(); + * + * // Draw the p5.Geometry object. + * model(shape); + * } + * + * function createArrow() { + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * // Add shapes. + * push(); + * rotateX(PI); + * cone(10); + * translate(0, -10, 0); + * cylinder(3, 20); + * pop(); + * + * // Stop building the p5.Geometry object. + * shape = endGeometry(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let blueArrow; + * let redArrow; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the arrows. + * redArrow = createArrow('red'); + * blueArrow = createArrow('blue'); + * + * describe('A red arrow and a blue arrow drawn on a gray background. The blue arrow rotates slowly.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the arrows. + * noStroke(); + * + * // Draw the red arrow. + * model(redArrow); + * + * // Translate and rotate the coordinate system. + * translate(30, 0, 0); + * rotateZ(frameCount * 0.01); + * + * // Draw the blue arrow. + * model(blueArrow); + * } + * + * function createArrow(fillColor) { + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * fill(fillColor); + * + * // Add shapes to the p5.Geometry object. + * push(); + * rotateX(PI); + * cone(10); + * translate(0, -10, 0); + * cylinder(3, 20); + * pop(); + * + * // Stop building the p5.Geometry object. + * let shape = endGeometry(); + * + * return shape; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let button; + * let particles; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a button to reset the particle system. + * button = createButton('Reset'); + * + * // Call resetModel() when the user presses the button. + * button.mousePressed(resetModel); + * + * // Add the original set of particles. + * resetModel(); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the particles. + * noStroke(); + * + * // Draw the particles. + * model(particles); + * } + * + * function resetModel() { + * // If the p5.Geometry object has already been created, + * // free those resources. + * if (particles) { + * freeGeometry(particles); + * } + * + * // Create a new p5.Geometry object with random spheres. + * particles = createParticles(); + * } + * + * function createParticles() { + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * // Add shapes. + * for (let i = 0; i < 60; i += 1) { + * // Calculate random coordinates. + * let x = randomGaussian(0, 20); + * let y = randomGaussian(0, 20); + * let z = randomGaussian(0, 20); + * + * push(); + * // Translate to the particle's coordinates. + * translate(x, y, z); + * // Draw the particle. + * sphere(5); + * pop(); + * } + * + * // Stop building the p5.Geometry object. + * let shape = endGeometry(); + * + * return shape; + * } + * + *
+ */ + fn.beginGeometry = function() { + return this._renderer.beginGeometry(); + }; + + /** + * Stops adding shapes to a new + * p5.Geometry object and returns the object. + * + * The `beginGeometry()` and endGeometry() + * functions help with creating complex 3D shapes from simpler ones such as + * sphere(). `beginGeometry()` begins adding shapes + * to a custom p5.Geometry object and + * endGeometry() stops adding them. + * + * `beginGeometry()` and endGeometry() can help + * to make sketches more performant. For example, if a complex 3D shape + * doesn’t change while a sketch runs, then it can be created with + * `beginGeometry()` and endGeometry(). + * Creating a p5.Geometry object once and then + * drawing it will run faster than repeatedly drawing the individual pieces. + * + * See buildGeometry() for another way to + * build 3D shapes. + * + * Note: `endGeometry()` can only be used in WebGL mode. + * + * @method endGeometry + * @returns {p5.Geometry} new 3D shape. + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * // Add a cone. + * cone(); + * + * // Stop building the p5.Geometry object. + * shape = endGeometry(); + * + * describe('A white cone drawn on a gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the p5.Geometry object. + * noStroke(); + * + * // Draw the p5.Geometry object. + * model(shape); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Geometry object. + * createArrow(); + * + * describe('A white arrow drawn on a gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the p5.Geometry object. + * noStroke(); + * + * // Draw the p5.Geometry object. + * model(shape); + * } + * + * function createArrow() { + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * // Add shapes. + * push(); + * rotateX(PI); + * cone(10); + * translate(0, -10, 0); + * cylinder(3, 20); + * pop(); + * + * // Stop building the p5.Geometry object. + * shape = endGeometry(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let blueArrow; + * let redArrow; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the arrows. + * redArrow = createArrow('red'); + * blueArrow = createArrow('blue'); + * + * describe('A red arrow and a blue arrow drawn on a gray background. The blue arrow rotates slowly.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the arrows. + * noStroke(); + * + * // Draw the red arrow. + * model(redArrow); + * + * // Translate and rotate the coordinate system. + * translate(30, 0, 0); + * rotateZ(frameCount * 0.01); + * + * // Draw the blue arrow. + * model(blueArrow); + * } + * + * function createArrow(fillColor) { + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * fill(fillColor); + * + * // Add shapes to the p5.Geometry object. + * push(); + * rotateX(PI); + * cone(10); + * translate(0, -10, 0); + * cylinder(3, 20); + * pop(); + * + * // Stop building the p5.Geometry object. + * let shape = endGeometry(); + * + * return shape; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let button; + * let particles; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a button to reset the particle system. + * button = createButton('Reset'); + * + * // Call resetModel() when the user presses the button. + * button.mousePressed(resetModel); + * + * // Add the original set of particles. + * resetModel(); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the particles. + * noStroke(); + * + * // Draw the particles. + * model(particles); + * } + * + * function resetModel() { + * // If the p5.Geometry object has already been created, + * // free those resources. + * if (particles) { + * freeGeometry(particles); + * } + * + * // Create a new p5.Geometry object with random spheres. + * particles = createParticles(); + * } + * + * function createParticles() { + * // Start building the p5.Geometry object. + * beginGeometry(); + * + * // Add shapes. + * for (let i = 0; i < 60; i += 1) { + * // Calculate random coordinates. + * let x = randomGaussian(0, 20); + * let y = randomGaussian(0, 20); + * let z = randomGaussian(0, 20); + * + * push(); + * // Translate to the particle's coordinates. + * translate(x, y, z); + * // Draw the particle. + * sphere(5); + * pop(); + * } + * + * // Stop building the p5.Geometry object. + * let shape = endGeometry(); + * + * return shape; + * } + * + *
+ */ + fn.endGeometry = function() { + return this._renderer.endGeometry(); + }; + + /** + * Creates a custom p5.Geometry object from + * simpler 3D shapes. + * + * `buildGeometry()` helps with creating complex 3D shapes from simpler ones + * such as sphere(). It can help to make sketches + * more performant. For example, if a complex 3D shape doesn’t change while a + * sketch runs, then it can be created with `buildGeometry()`. Creating a + * p5.Geometry object once and then drawing it + * will run faster than repeatedly drawing the individual pieces. + * + * The parameter, `callback`, is a function with the drawing instructions for + * the new p5.Geometry object. It will be called + * once to create the new 3D shape. + * + * See beginGeometry() and + * endGeometry() for another way to build 3D + * shapes. + * + * Note: `buildGeometry()` can only be used in WebGL mode. + * + * @method buildGeometry + * @param {Function} callback function that draws the shape. + * @returns {p5.Geometry} new 3D shape. + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Geometry object. + * shape = buildGeometry(createShape); + * + * describe('A white cone drawn on a gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the p5.Geometry object. + * noStroke(); + * + * // Draw the p5.Geometry object. + * model(shape); + * } + * + * // Create p5.Geometry object from a single cone. + * function createShape() { + * cone(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the arrow. + * shape = buildGeometry(createArrow); + * + * describe('A white arrow drawn on a gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the arrow. + * noStroke(); + * + * // Draw the arrow. + * model(shape); + * } + * + * function createArrow() { + * // Add shapes to the p5.Geometry object. + * push(); + * rotateX(PI); + * cone(10); + * translate(0, -10, 0); + * cylinder(3, 20); + * pop(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Geometry object. + * shape = buildGeometry(createArrow); + * + * describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the arrows. + * noStroke(); + * + * // Draw the p5.Geometry object. + * model(shape); + * + * // Translate and rotate the coordinate system. + * translate(30, 0, 0); + * rotateZ(frameCount * 0.01); + * + * // Draw the p5.Geometry object again. + * model(shape); + * } + * + * function createArrow() { + * // Add shapes to the p5.Geometry object. + * push(); + * rotateX(PI); + * cone(10); + * translate(0, -10, 0); + * cylinder(3, 20); + * pop(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let button; + * let particles; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a button to reset the particle system. + * button = createButton('Reset'); + * + * // Call resetModel() when the user presses the button. + * button.mousePressed(resetModel); + * + * // Add the original set of particles. + * resetModel(); + * + * describe('A set of white spheres on a gray background. The spheres are positioned randomly. Their positions reset when the user presses the Reset button.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the particles. + * noStroke(); + * + * // Draw the particles. + * model(particles); + * } + * + * function resetModel() { + * // If the p5.Geometry object has already been created, + * // free those resources. + * if (particles) { + * freeGeometry(particles); + * } + * + * // Create a new p5.Geometry object with random spheres. + * particles = buildGeometry(createParticles); + * } + * + * function createParticles() { + * for (let i = 0; i < 60; i += 1) { + * // Calculate random coordinates. + * let x = randomGaussian(0, 20); + * let y = randomGaussian(0, 20); + * let z = randomGaussian(0, 20); + * + * push(); + * // Translate to the particle's coordinates. + * translate(x, y, z); + * // Draw the particle. + * sphere(5); + * pop(); + * } + * } + * + *
+ */ + fn.buildGeometry = function(callback) { + return this._renderer.buildGeometry(callback); + }; + + /** + * Clears a p5.Geometry object from the graphics + * processing unit (GPU) memory. + * + * p5.Geometry objects can contain lots of data + * about their vertices, surface normals, colors, and so on. Complex 3D shapes + * can use lots of memory which is a limited resource in many GPUs. Calling + * `freeGeometry()` can improve performance by freeing a + * p5.Geometry object’s resources from GPU memory. + * `freeGeometry()` works with p5.Geometry objects + * created with beginGeometry() and + * endGeometry(), + * buildGeometry(), and + * loadModel(). + * + * The parameter, `geometry`, is the p5.Geometry + * object to be freed. + * + * Note: A p5.Geometry object can still be drawn + * after its resources are cleared from GPU memory. It may take longer to draw + * the first time it’s redrawn. + * + * Note: `freeGeometry()` can only be used in WebGL mode. + * + * @method freeGeometry + * @param {p5.Geometry} geometry 3D shape whose resources should be freed. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * background(200); + * + * // Create a p5.Geometry object. + * beginGeometry(); + * cone(); + * let shape = endGeometry(); + * + * // Draw the shape. + * model(shape); + * + * // Free the shape's resources. + * freeGeometry(shape); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let button; + * let particles; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a button to reset the particle system. + * button = createButton('Reset'); + * + * // Call resetModel() when the user presses the button. + * button.mousePressed(resetModel); + * + * // Add the original set of particles. + * resetModel(); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the particles. + * noStroke(); + * + * // Draw the particles. + * model(particles); + * } + * + * function resetModel() { + * // If the p5.Geometry object has already been created, + * // free those resources. + * if (particles) { + * freeGeometry(particles); + * } + * + * // Create a new p5.Geometry object with random spheres. + * particles = buildGeometry(createParticles); + * } + * + * function createParticles() { + * for (let i = 0; i < 60; i += 1) { + * // Calculate random coordinates. + * let x = randomGaussian(0, 20); + * let y = randomGaussian(0, 20); + * let z = randomGaussian(0, 20); + * + * push(); + * // Translate to the particle's coordinates. + * translate(x, y, z); + * // Draw the particle. + * sphere(5); + * pop(); + * } + * } + * + *
+ */ + fn.freeGeometry = function(geometry) { + this._renderer._freeBuffers(geometry.gid); + }; + + /** + * Draws a plane. + * + * A plane is a four-sided, flat shape with every angle measuring 90˚. It’s + * similar to a rectangle and offers advanced drawing features in WebGL mode. + * + * The first parameter, `width`, is optional. If a `Number` is passed, as in + * `plane(20)`, it sets the plane’s width and height. By default, `width` is + * 50. + * + * The second parameter, `height`, is also optional. If a `Number` is passed, + * as in `plane(20, 30)`, it sets the plane’s height. By default, `height` is + * set to the plane’s `width`. + * + * The third parameter, `detailX`, is also optional. If a `Number` is passed, + * as in `plane(20, 30, 5)` it sets the number of triangle subdivisions to use + * along the x-axis. All 3D shapes are made by connecting triangles to form + * their surfaces. By default, `detailX` is 1. + * + * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, + * as in `plane(20, 30, 5, 7)` it sets the number of triangle subdivisions to + * use along the y-axis. All 3D shapes are made by connecting triangles to + * form their surfaces. By default, `detailY` is 1. + * + * Note: `plane()` can only be used in WebGL mode. + * + * @method plane + * @param {Number} [width] width of the plane. + * @param {Number} [height] height of the plane. + * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. + * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white plane on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the plane. + * plane(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white plane on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the plane. + * // Set its width and height to 30. + * plane(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white plane on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the plane. + * // Set its width to 30 and height to 50. + * plane(30, 50); + * } + * + *
+ */ + fn.plane = function( + width = 50, + height = width, + detailX = 1, + detailY = 1 + ) { + this._assert3d('plane'); + p5._validateParameters('plane', arguments); -/** - * Clears a p5.Geometry object from the graphics - * processing unit (GPU) memory. - * - * p5.Geometry objects can contain lots of data - * about their vertices, surface normals, colors, and so on. Complex 3D shapes - * can use lots of memory which is a limited resource in many GPUs. Calling - * `freeGeometry()` can improve performance by freeing a - * p5.Geometry object’s resources from GPU memory. - * `freeGeometry()` works with p5.Geometry objects - * created with beginGeometry() and - * endGeometry(), - * buildGeometry(), and - * loadModel(). - * - * The parameter, `geometry`, is the p5.Geometry - * object to be freed. - * - * Note: A p5.Geometry object can still be drawn - * after its resources are cleared from GPU memory. It may take longer to draw - * the first time it’s redrawn. - * - * Note: `freeGeometry()` can only be used in WebGL mode. - * - * @method freeGeometry - * @param {p5.Geometry} geometry 3D shape whose resources should be freed. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Geometry object. - * beginGeometry(); - * cone(); - * let shape = endGeometry(); - * - * // Draw the shape. - * model(shape); - * - * // Free the shape's resources. - * freeGeometry(shape); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let button; - * let particles; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a button to reset the particle system. - * button = createButton('Reset'); - * - * // Call resetModel() when the user presses the button. - * button.mousePressed(resetModel); - * - * // Add the original set of particles. - * resetModel(); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the particles. - * noStroke(); - * - * // Draw the particles. - * model(particles); - * } - * - * function resetModel() { - * // If the p5.Geometry object has already been created, - * // free those resources. - * if (particles) { - * freeGeometry(particles); - * } - * - * // Create a new p5.Geometry object with random spheres. - * particles = buildGeometry(createParticles); - * } - * - * function createParticles() { - * for (let i = 0; i < 60; i += 1) { - * // Calculate random coordinates. - * let x = randomGaussian(0, 20); - * let y = randomGaussian(0, 20); - * let z = randomGaussian(0, 20); - * - * push(); - * // Translate to the particle's coordinates. - * translate(x, y, z); - * // Draw the particle. - * sphere(5); - * pop(); - * } - * } - * - *
- */ -p5.prototype.freeGeometry = function(geometry) { - this._renderer._freeBuffers(geometry.gid); -}; + const gId = `plane|${detailX}|${detailY}`; -/** - * Draws a plane. - * - * A plane is a four-sided, flat shape with every angle measuring 90˚. It’s - * similar to a rectangle and offers advanced drawing features in WebGL mode. - * - * The first parameter, `width`, is optional. If a `Number` is passed, as in - * `plane(20)`, it sets the plane’s width and height. By default, `width` is - * 50. - * - * The second parameter, `height`, is also optional. If a `Number` is passed, - * as in `plane(20, 30)`, it sets the plane’s height. By default, `height` is - * set to the plane’s `width`. - * - * The third parameter, `detailX`, is also optional. If a `Number` is passed, - * as in `plane(20, 30, 5)` it sets the number of triangle subdivisions to use - * along the x-axis. All 3D shapes are made by connecting triangles to form - * their surfaces. By default, `detailX` is 1. - * - * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, - * as in `plane(20, 30, 5, 7)` it sets the number of triangle subdivisions to - * use along the y-axis. All 3D shapes are made by connecting triangles to - * form their surfaces. By default, `detailY` is 1. - * - * Note: `plane()` can only be used in WebGL mode. - * - * @method plane - * @param {Number} [width] width of the plane. - * @param {Number} [height] height of the plane. - * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. - * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white plane on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the plane. - * plane(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white plane on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the plane. - * // Set its width and height to 30. - * plane(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white plane on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the plane. - * // Set its width to 30 and height to 50. - * plane(30, 50); - * } - * - *
- */ -p5.prototype.plane = function( - width = 50, - height = width, - detailX = 1, - detailY = 1 -) { - 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 p5.Vector(u - 0.5, v - 0.5, 0); - this.vertices.push(p); - this.uvs.push(u, v); + 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 p5.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); + 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' + ); } - }; - const planeGeom = new p5.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.createBuffers(gId, planeGeom); - } - - this._renderer.drawBuffersScaled(gId, width, height, 1); - return this; -}; -/** - * Draws a box (rectangular prism). - * - * A box is a 3D shape with six faces. Each face makes a 90˚ with four - * neighboring faces. - * - * The first parameter, `width`, is optional. If a `Number` is passed, as in - * `box(20)`, it sets the box’s width and height. By default, `width` is 50. - * - * The second parameter, `height`, is also optional. If a `Number` is passed, - * as in `box(20, 30)`, it sets the box’s height. By default, `height` is set - * to the box’s `width`. - * - * The third parameter, `depth`, is also optional. If a `Number` is passed, as - * in `box(20, 30, 40)`, it sets the box’s depth. By default, `depth` is set - * to the box’s `height`. - * - * The fourth parameter, `detailX`, is also optional. If a `Number` is passed, - * as in `box(20, 30, 40, 5)`, it sets the number of triangle subdivisions to - * use along the x-axis. All 3D shapes are made by connecting triangles to - * form their surfaces. By default, `detailX` is 1. - * - * The fifth parameter, `detailY`, is also optional. If a number is passed, as - * in `box(20, 30, 40, 5, 7)`, it sets the number of triangle subdivisions to - * use along the y-axis. All 3D shapes are made by connecting triangles to - * form their surfaces. By default, `detailY` is 1. - * - * Note: `box()` can only be used in WebGL mode. - * - * @method box - * @param {Number} [width] width of the box. - * @param {Number} [height] height of the box. - * @param {Number} [depth] depth of the box. - * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. - * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white box on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white box on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the box. - * // Set its width and height to 30. - * box(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white box on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the box. - * // Set its width to 30 and height to 50. - * box(30, 50); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white box on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the box. - * // Set its width to 30, height to 50, and depth to 10. - * box(30, 50, 10); - * } - * - *
- */ -p5.prototype.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 p5.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 p5.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' - ); + this._renderer.drawBuffersScaled(gId, width, height, 1); + return this; + }; + + /** + * Draws a box (rectangular prism). + * + * A box is a 3D shape with six faces. Each face makes a 90˚ with four + * neighboring faces. + * + * The first parameter, `width`, is optional. If a `Number` is passed, as in + * `box(20)`, it sets the box’s width and height. By default, `width` is 50. + * + * The second parameter, `height`, is also optional. If a `Number` is passed, + * as in `box(20, 30)`, it sets the box’s height. By default, `height` is set + * to the box’s `width`. + * + * The third parameter, `depth`, is also optional. If a `Number` is passed, as + * in `box(20, 30, 40)`, it sets the box’s depth. By default, `depth` is set + * to the box’s `height`. + * + * The fourth parameter, `detailX`, is also optional. If a `Number` is passed, + * as in `box(20, 30, 40, 5)`, it sets the number of triangle subdivisions to + * use along the x-axis. All 3D shapes are made by connecting triangles to + * form their surfaces. By default, `detailX` is 1. + * + * The fifth parameter, `detailY`, is also optional. If a number is passed, as + * in `box(20, 30, 40, 5, 7)`, it sets the number of triangle subdivisions to + * use along the y-axis. All 3D shapes are made by connecting triangles to + * form their surfaces. By default, `detailY` is 1. + * + * Note: `box()` can only be used in WebGL mode. + * + * @method box + * @param {Number} [width] width of the box. + * @param {Number} [height] height of the box. + * @param {Number} [depth] depth of the box. + * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. + * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white box on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white box on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the box. + * // Set its width and height to 30. + * box(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white box on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the box. + * // Set its width to 30 and height to 50. + * box(30, 50); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white box on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the box. + * // Set its width to 30, height to 50, and depth to 10. + * box(30, 50, 10); + * } + * + *
+ */ + fn.box = function(width, height, depth, detailX, detailY) { + this._assert3d('box'); + p5._validateParameters('box', arguments); + if (typeof width === 'undefined') { + width = 50; } - //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); - - return this; -}; - -/** - * Draws a sphere. - * - * A sphere is a 3D shape with triangular faces that connect to form a round - * surface. Spheres with few faces look like crystals. Spheres with many faces - * have smooth surfaces and look like balls. - * - * The first parameter, `radius`, is optional. If a `Number` is passed, as in - * `sphere(20)`, it sets the radius of the sphere. By default, `radius` is 50. - * - * The second parameter, `detailX`, is also optional. If a `Number` is passed, - * as in `sphere(20, 5)`, it sets the number of triangle subdivisions to use - * along the x-axis. All 3D shapes are made by connecting triangles to form - * their surfaces. By default, `detailX` is 24. - * - * The third parameter, `detailY`, is also optional. If a `Number` is passed, - * as in `sphere(20, 5, 2)`, it sets the number of triangle subdivisions to - * use along the y-axis. All 3D shapes are made by connecting triangles to - * form their surfaces. By default, `detailY` is 16. - * - * Note: `sphere()` can only be used in WebGL mode. - * - * @method sphere - * @param {Number} [radius] radius of the sphere. Defaults to 50. - * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24. - * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. - * - * @chainable - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the sphere. - * sphere(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the sphere. - * // Set its radius to 30. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the sphere. - * // Set its radius to 30. - * // Set its detailX to 6. - * sphere(30, 6); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the sphere. - * // Set its radius to 30. - * // Set its detailX to 24. - * // Set its detailY to 4. - * sphere(30, 24, 4); - * } - * - *
- */ -p5.prototype.sphere = function(radius = 50, detailX = 24, detailY = 16) { - this._assert3d('sphere'); - p5._validateParameters('sphere', arguments); - - this.ellipsoid(radius, radius, 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 (typeof height === 'undefined') { + height = width; } - if (yy === -2 || yy === detailY + 2) { - //center of bottom or top caps - ringRadius = 0; + if (typeof depth === 'undefined') { + depth = height; } - 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 p5.Vector(sur * ringRadius, y, cur * ringRadius)); + 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; + } - //VERTEX NORMALS - let vertexNormal; + 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 p5.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 p5.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); + + return this; + }; + + /** + * Draws a sphere. + * + * A sphere is a 3D shape with triangular faces that connect to form a round + * surface. Spheres with few faces look like crystals. Spheres with many faces + * have smooth surfaces and look like balls. + * + * The first parameter, `radius`, is optional. If a `Number` is passed, as in + * `sphere(20)`, it sets the radius of the sphere. By default, `radius` is 50. + * + * The second parameter, `detailX`, is also optional. If a `Number` is passed, + * as in `sphere(20, 5)`, it sets the number of triangle subdivisions to use + * along the x-axis. All 3D shapes are made by connecting triangles to form + * their surfaces. By default, `detailX` is 24. + * + * The third parameter, `detailY`, is also optional. If a `Number` is passed, + * as in `sphere(20, 5, 2)`, it sets the number of triangle subdivisions to + * use along the y-axis. All 3D shapes are made by connecting triangles to + * form their surfaces. By default, `detailY` is 16. + * + * Note: `sphere()` can only be used in WebGL mode. + * + * @method sphere + * @param {Number} [radius] radius of the sphere. Defaults to 50. + * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24. + * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. + * + * @chainable + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the sphere. + * sphere(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the sphere. + * // Set its radius to 30. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the sphere. + * // Set its radius to 30. + * // Set its detailX to 6. + * sphere(30, 6); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the sphere. + * // Set its radius to 30. + * // Set its detailX to 24. + * // Set its detailY to 4. + * sphere(30, 24, 4); + * } + * + *
+ */ + fn.sphere = function(radius = 50, detailX = 24, detailY = 16) { + this._assert3d('sphere'); + p5._validateParameters('sphere', arguments); + + this.ellipsoid(radius, radius, 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) { - vertexNormal = new p5.Vector(0, -1, 0); - } else if (yy > detailY && topRadius) { - vertexNormal = new p5.Vector(0, 1, 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 { - vertexNormal = new p5.Vector(sur * cosSlant, sinSlant, cur * cosSlant); + //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 p5.Vector(sur * ringRadius, y, cur * ringRadius)); + + //VERTEX NORMALS + let vertexNormal; + if (yy < 0) { + vertexNormal = new p5.Vector(0, -1, 0); + } else if (yy > detailY && topRadius) { + vertexNormal = new p5.Vector(0, 1, 0); + } else { + vertexNormal = new p5.Vector(sur * cosSlant, sinSlant, cur * cosSlant); + } + this.vertexNormals.push(vertexNormal); + //UVs + this.uvs.push(u, v); } - 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 - ]); + + 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; } - startIndex += detailX; - } - if (topCap) { - startIndex += detailX; - for (ii = 0; ii < detailX; ++ii) { - this.faces.push([ - startIndex + ii, - startIndex + (ii + 1) % detailX, - startIndex + detailX - ]); + 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; } - } -}; - -/** - * Draws a cylinder. - * - * A cylinder is a 3D shape with triangular faces that connect a flat bottom - * to a flat top. Cylinders with few faces look like boxes. Cylinders with - * many faces have smooth surfaces. - * - * The first parameter, `radius`, is optional. If a `Number` is passed, as in - * `cylinder(20)`, it sets the radius of the cylinder’s base. By default, - * `radius` is 50. - * - * The second parameter, `height`, is also optional. If a `Number` is passed, - * as in `cylinder(20, 30)`, it sets the cylinder’s height. By default, - * `height` is set to the cylinder’s `radius`. - * - * The third parameter, `detailX`, is also optional. If a `Number` is passed, - * as in `cylinder(20, 30, 5)`, it sets the number of edges used to form the - * cylinder's top and bottom. Using more edges makes the top and bottom look - * more like circles. By default, `detailX` is 24. - * - * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, - * as in `cylinder(20, 30, 5, 2)`, it sets the number of triangle subdivisions - * to use along the y-axis, between cylinder's the top and bottom. All 3D - * shapes are made by connecting triangles to form their surfaces. By default, - * `detailY` is 1. - * - * The fifth parameter, `bottomCap`, is also optional. If a `false` is passed, - * as in `cylinder(20, 30, 5, 2, false)` the cylinder’s bottom won’t be drawn. - * By default, `bottomCap` is `true`. - * - * The sixth parameter, `topCap`, is also optional. If a `false` is passed, as - * in `cylinder(20, 30, 5, 2, false, false)` the cylinder’s top won’t be - * drawn. By default, `topCap` is `true`. - * - * Note: `cylinder()` can only be used in WebGL mode. - * - * @method cylinder - * @param {Number} [radius] radius of the cylinder. Defaults to 50. - * @param {Number} [height] height of the cylinder. Defaults to the value of `radius`. - * @param {Integer} [detailX] number of edges along the top and bottom. Defaults to 24. - * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 1. - * @param {Boolean} [bottomCap] whether to draw the cylinder's bottom. Defaults to `true`. - * @param {Boolean} [topCap] whether to draw the cylinder's top. Defaults to `true`. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cylinder on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cylinder. - * cylinder(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cylinder on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cylinder. - * // Set its radius and height to 30. - * cylinder(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cylinder on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cylinder. - * // Set its radius to 30 and height to 50. - * cylinder(30, 50); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white box on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cylinder. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 5. - * cylinder(30, 50, 5); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cylinder on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cylinder. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 24 and detailY to 2. - * cylinder(30, 50, 24, 2); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cylinder on a gray background. Its top is missing.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cylinder. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 24 and detailY to 1. - * // Don't draw its bottom. - * cylinder(30, 50, 24, 1, false); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cylinder on a gray background. Its top and bottom are missing.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cylinder. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 24 and detailY to 1. - * // Don't draw its bottom or top. - * cylinder(30, 50, 24, 1, false, false); - * } - * - *
- */ -p5.prototype.cylinder = function( - radius = 50, - height = radius, - detailX = 24, - detailY = 1, - bottomCap = true, - topCap = true -) { - 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' - ); + if (topCap) { + startIndex += detailX; + for (ii = 0; ii < detailX; ++ii) { + this.faces.push([ + startIndex + ii, + startIndex + (ii + 1) % detailX, + startIndex + detailX + ]); + } } - this._renderer.createBuffers(gId, cylinderGeom); - } - - this._renderer.drawBuffersScaled(gId, radius, height, radius); - - return this; -}; - -/** - * Draws a cone. - * - * A cone is a 3D shape with triangular faces that connect a flat bottom to a - * single point. Cones with few faces look like pyramids. Cones with many - * faces have smooth surfaces. - * - * The first parameter, `radius`, is optional. If a `Number` is passed, as in - * `cone(20)`, it sets the radius of the cone’s base. By default, `radius` is - * 50. - * - * The second parameter, `height`, is also optional. If a `Number` is passed, - * as in `cone(20, 30)`, it sets the cone’s height. By default, `height` is - * set to the cone’s `radius`. - * - * The third parameter, `detailX`, is also optional. If a `Number` is passed, - * as in `cone(20, 30, 5)`, it sets the number of edges used to form the - * cone's base. Using more edges makes the base look more like a circle. By - * default, `detailX` is 24. - * - * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, - * as in `cone(20, 30, 5, 7)`, it sets the number of triangle subdivisions to - * use along the y-axis connecting the base to the tip. All 3D shapes are made - * by connecting triangles to form their surfaces. By default, `detailY` is 1. - * - * The fifth parameter, `cap`, is also optional. If a `false` is passed, as - * in `cone(20, 30, 5, 7, false)` the cone’s base won’t be drawn. By default, - * `cap` is `true`. - * - * Note: `cone()` can only be used in WebGL mode. - * - * @method cone - * @param {Number} [radius] radius of the cone's base. Defaults to 50. - * @param {Number} [height] height of the cone. Defaults to the value of `radius`. - * @param {Integer} [detailX] number of edges used to draw the base. Defaults to 24. - * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 1. - * @param {Boolean} [cap] whether to draw the cone's base. Defaults to `true`. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cone on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cone. - * cone(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cone on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cone. - * // Set its radius and height to 30. - * cone(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cone on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cone. - * // Set its radius to 30 and height to 50. - * cone(30, 50); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cone on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cone. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 5. - * cone(30, 50, 5); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white pyramid on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cone. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 5. - * cone(30, 50, 5); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cone on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cone. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 24 and detailY to 2. - * cone(30, 50, 24, 2); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cone on a gray background. Its base is missing.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the cone. - * // Set its radius to 30 and height to 50. - * // Set its detailX to 24 and detailY to 1. - * // Don't draw its base. - * cone(30, 50, 24, 1, false); - * } - * - *
- */ -p5.prototype.cone = function( - radius = 50, - height = radius, - detailX = 24, - detailY = 1, - cap = true -) { - this._assert3d('cone'); - p5._validateParameters('cone', arguments); - - const gId = `cone|${detailX}|${detailY}|${cap}`; - if (!this._renderer.geometryInHash(gId)) { - const coneGeom = new p5.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' + }; + + /** + * Draws a cylinder. + * + * A cylinder is a 3D shape with triangular faces that connect a flat bottom + * to a flat top. Cylinders with few faces look like boxes. Cylinders with + * many faces have smooth surfaces. + * + * The first parameter, `radius`, is optional. If a `Number` is passed, as in + * `cylinder(20)`, it sets the radius of the cylinder’s base. By default, + * `radius` is 50. + * + * The second parameter, `height`, is also optional. If a `Number` is passed, + * as in `cylinder(20, 30)`, it sets the cylinder’s height. By default, + * `height` is set to the cylinder’s `radius`. + * + * The third parameter, `detailX`, is also optional. If a `Number` is passed, + * as in `cylinder(20, 30, 5)`, it sets the number of edges used to form the + * cylinder's top and bottom. Using more edges makes the top and bottom look + * more like circles. By default, `detailX` is 24. + * + * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, + * as in `cylinder(20, 30, 5, 2)`, it sets the number of triangle subdivisions + * to use along the y-axis, between cylinder's the top and bottom. All 3D + * shapes are made by connecting triangles to form their surfaces. By default, + * `detailY` is 1. + * + * The fifth parameter, `bottomCap`, is also optional. If a `false` is passed, + * as in `cylinder(20, 30, 5, 2, false)` the cylinder’s bottom won’t be drawn. + * By default, `bottomCap` is `true`. + * + * The sixth parameter, `topCap`, is also optional. If a `false` is passed, as + * in `cylinder(20, 30, 5, 2, false, false)` the cylinder’s top won’t be + * drawn. By default, `topCap` is `true`. + * + * Note: `cylinder()` can only be used in WebGL mode. + * + * @method cylinder + * @param {Number} [radius] radius of the cylinder. Defaults to 50. + * @param {Number} [height] height of the cylinder. Defaults to the value of `radius`. + * @param {Integer} [detailX] number of edges along the top and bottom. Defaults to 24. + * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 1. + * @param {Boolean} [bottomCap] whether to draw the cylinder's bottom. Defaults to `true`. + * @param {Boolean} [topCap] whether to draw the cylinder's top. Defaults to `true`. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cylinder on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cylinder. + * cylinder(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cylinder on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cylinder. + * // Set its radius and height to 30. + * cylinder(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cylinder on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cylinder. + * // Set its radius to 30 and height to 50. + * cylinder(30, 50); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white box on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cylinder. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 5. + * cylinder(30, 50, 5); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cylinder on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cylinder. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 24 and detailY to 2. + * cylinder(30, 50, 24, 2); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cylinder on a gray background. Its top is missing.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cylinder. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 24 and detailY to 1. + * // Don't draw its bottom. + * cylinder(30, 50, 24, 1, false); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cylinder on a gray background. Its top and bottom are missing.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cylinder. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 24 and detailY to 1. + * // Don't draw its bottom or top. + * cylinder(30, 50, 24, 1, false, false); + * } + * + *
+ */ + fn.cylinder = function( + radius = 50, + height = radius, + detailX = 24, + detailY = 1, + bottomCap = true, + topCap = true + ) { + 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.createBuffers(gId, coneGeom); - } - this._renderer.drawBuffersScaled(gId, radius, height, radius); - - return this; -}; - -/** - * Draws an ellipsoid. - * - * An ellipsoid is a 3D shape with triangular faces that connect to form a - * round surface. Ellipsoids with few faces look like crystals. Ellipsoids - * with many faces have smooth surfaces and look like eggs. `ellipsoid()` - * defines a shape by its radii. This is different from - * ellipse() which uses diameters - * (width and height). - * - * The first parameter, `radiusX`, is optional. If a `Number` is passed, as in - * `ellipsoid(20)`, it sets the radius of the ellipsoid along the x-axis. By - * default, `radiusX` is 50. - * - * The second parameter, `radiusY`, is also optional. If a `Number` is passed, - * as in `ellipsoid(20, 30)`, it sets the ellipsoid’s radius along the y-axis. - * By default, `radiusY` is set to the ellipsoid’s `radiusX`. - * - * The third parameter, `radiusZ`, is also optional. If a `Number` is passed, - * as in `ellipsoid(20, 30, 40)`, it sets the ellipsoid’s radius along the - * z-axis. By default, `radiusZ` is set to the ellipsoid’s `radiusY`. - * - * The fourth parameter, `detailX`, is also optional. If a `Number` is passed, - * as in `ellipsoid(20, 30, 40, 5)`, it sets the number of triangle - * subdivisions to use along the x-axis. All 3D shapes are made by connecting - * triangles to form their surfaces. By default, `detailX` is 24. - * - * The fifth parameter, `detailY`, is also optional. If a `Number` is passed, - * as in `ellipsoid(20, 30, 40, 5, 7)`, it sets the number of triangle - * subdivisions to use along the y-axis. All 3D shapes are made by connecting - * triangles to form their surfaces. By default, `detailY` is 16. - * - * Note: `ellipsoid()` can only be used in WebGL mode. - * - * @method ellipsoid - * @param {Number} [radiusX] radius of the ellipsoid along the x-axis. Defaults to 50. - * @param {Number} [radiusY] radius of the ellipsoid along the y-axis. Defaults to `radiusX`. - * @param {Number} [radiusZ] radius of the ellipsoid along the z-axis. Defaults to `radiusY`. - * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24. - * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the ellipsoid. - * // Set its radiusX to 30. - * ellipsoid(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white ellipsoid on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the ellipsoid. - * // Set its radiusX to 30. - * // Set its radiusY to 40. - * ellipsoid(30, 40); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white ellipsoid on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the ellipsoid. - * // Set its radiusX to 30. - * // Set its radiusY to 40. - * // Set its radiusZ to 50. - * ellipsoid(30, 40, 50); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white ellipsoid on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the ellipsoid. - * // Set its radiusX to 30. - * // Set its radiusY to 40. - * // Set its radiusZ to 50. - * // Set its detailX to 4. - * ellipsoid(30, 40, 50, 4); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white ellipsoid on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the ellipsoid. - * // Set its radiusX to 30. - * // Set its radiusY to 40. - * // Set its radiusZ to 50. - * // Set its detailX to 4. - * // Set its detailY to 3. - * ellipsoid(30, 40, 50, 4, 3); - * } - * - *
- */ -p5.prototype.ellipsoid = function( - radiusX = 50, - radiusY = radiusX, - radiusZ = radiusX, - detailX = 24, - detailY = 16 -) { - 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); - } + this._renderer.drawBuffersScaled(gId, radius, height, radius); + + return this; + }; + + /** + * Draws a cone. + * + * A cone is a 3D shape with triangular faces that connect a flat bottom to a + * single point. Cones with few faces look like pyramids. Cones with many + * faces have smooth surfaces. + * + * The first parameter, `radius`, is optional. If a `Number` is passed, as in + * `cone(20)`, it sets the radius of the cone’s base. By default, `radius` is + * 50. + * + * The second parameter, `height`, is also optional. If a `Number` is passed, + * as in `cone(20, 30)`, it sets the cone’s height. By default, `height` is + * set to the cone’s `radius`. + * + * The third parameter, `detailX`, is also optional. If a `Number` is passed, + * as in `cone(20, 30, 5)`, it sets the number of edges used to form the + * cone's base. Using more edges makes the base look more like a circle. By + * default, `detailX` is 24. + * + * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, + * as in `cone(20, 30, 5, 7)`, it sets the number of triangle subdivisions to + * use along the y-axis connecting the base to the tip. All 3D shapes are made + * by connecting triangles to form their surfaces. By default, `detailY` is 1. + * + * The fifth parameter, `cap`, is also optional. If a `false` is passed, as + * in `cone(20, 30, 5, 7, false)` the cone’s base won’t be drawn. By default, + * `cap` is `true`. + * + * Note: `cone()` can only be used in WebGL mode. + * + * @method cone + * @param {Number} [radius] radius of the cone's base. Defaults to 50. + * @param {Number} [height] height of the cone. Defaults to the value of `radius`. + * @param {Integer} [detailX] number of edges used to draw the base. Defaults to 24. + * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 1. + * @param {Boolean} [cap] whether to draw the cone's base. Defaults to `true`. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cone on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cone. + * cone(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cone on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cone. + * // Set its radius and height to 30. + * cone(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cone on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cone. + * // Set its radius to 30 and height to 50. + * cone(30, 50); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cone on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cone. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 5. + * cone(30, 50, 5); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white pyramid on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cone. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 5. + * cone(30, 50, 5); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cone on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cone. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 24 and detailY to 2. + * cone(30, 50, 24, 2); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cone on a gray background. Its base is missing.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the cone. + * // Set its radius to 30 and height to 50. + * // Set its detailX to 24 and detailY to 1. + * // Don't draw its base. + * cone(30, 50, 24, 1, false); + * } + * + *
+ */ + fn.cone = function( + radius = 50, + height = radius, + detailX = 24, + detailY = 1, + cap = true + ) { + this._assert3d('cone'); + p5._validateParameters('cone', arguments); + + const gId = `cone|${detailX}|${detailY}|${cap}`; + if (!this._renderer.geometryInHash(gId)) { + const coneGeom = new p5.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' + ); } - }; - const ellipsoidGeom = new p5.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, coneGeom); } - this._renderer.createBuffers(gId, ellipsoidGeom); - } - - this._renderer.drawBuffersScaled(gId, radiusX, radiusY, radiusZ); - return this; -}; + this._renderer.drawBuffersScaled(gId, radius, height, radius); + + return this; + }; + + /** + * Draws an ellipsoid. + * + * An ellipsoid is a 3D shape with triangular faces that connect to form a + * round surface. Ellipsoids with few faces look like crystals. Ellipsoids + * with many faces have smooth surfaces and look like eggs. `ellipsoid()` + * defines a shape by its radii. This is different from + * ellipse() which uses diameters + * (width and height). + * + * The first parameter, `radiusX`, is optional. If a `Number` is passed, as in + * `ellipsoid(20)`, it sets the radius of the ellipsoid along the x-axis. By + * default, `radiusX` is 50. + * + * The second parameter, `radiusY`, is also optional. If a `Number` is passed, + * as in `ellipsoid(20, 30)`, it sets the ellipsoid’s radius along the y-axis. + * By default, `radiusY` is set to the ellipsoid’s `radiusX`. + * + * The third parameter, `radiusZ`, is also optional. If a `Number` is passed, + * as in `ellipsoid(20, 30, 40)`, it sets the ellipsoid’s radius along the + * z-axis. By default, `radiusZ` is set to the ellipsoid’s `radiusY`. + * + * The fourth parameter, `detailX`, is also optional. If a `Number` is passed, + * as in `ellipsoid(20, 30, 40, 5)`, it sets the number of triangle + * subdivisions to use along the x-axis. All 3D shapes are made by connecting + * triangles to form their surfaces. By default, `detailX` is 24. + * + * The fifth parameter, `detailY`, is also optional. If a `Number` is passed, + * as in `ellipsoid(20, 30, 40, 5, 7)`, it sets the number of triangle + * subdivisions to use along the y-axis. All 3D shapes are made by connecting + * triangles to form their surfaces. By default, `detailY` is 16. + * + * Note: `ellipsoid()` can only be used in WebGL mode. + * + * @method ellipsoid + * @param {Number} [radiusX] radius of the ellipsoid along the x-axis. Defaults to 50. + * @param {Number} [radiusY] radius of the ellipsoid along the y-axis. Defaults to `radiusX`. + * @param {Number} [radiusZ] radius of the ellipsoid along the z-axis. Defaults to `radiusY`. + * @param {Integer} [detailX] number of triangle subdivisions along the x-axis. Defaults to 24. + * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the ellipsoid. + * // Set its radiusX to 30. + * ellipsoid(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white ellipsoid on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the ellipsoid. + * // Set its radiusX to 30. + * // Set its radiusY to 40. + * ellipsoid(30, 40); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white ellipsoid on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the ellipsoid. + * // Set its radiusX to 30. + * // Set its radiusY to 40. + * // Set its radiusZ to 50. + * ellipsoid(30, 40, 50); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white ellipsoid on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the ellipsoid. + * // Set its radiusX to 30. + * // Set its radiusY to 40. + * // Set its radiusZ to 50. + * // Set its detailX to 4. + * ellipsoid(30, 40, 50, 4); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white ellipsoid on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the ellipsoid. + * // Set its radiusX to 30. + * // Set its radiusY to 40. + * // Set its radiusZ to 50. + * // Set its detailX to 4. + * // Set its detailY to 3. + * ellipsoid(30, 40, 50, 4, 3); + * } + * + *
+ */ + fn.ellipsoid = function( + radiusX = 50, + radiusY = radiusX, + radiusZ = radiusX, + detailX = 24, + detailY = 16 + ) { + this._assert3d('ellipsoid'); + p5._validateParameters('ellipsoid', arguments); -/** - * Draws a torus. - * - * A torus is a 3D shape with triangular faces that connect to form a ring. - * Toruses with few faces look flattened. Toruses with many faces have smooth - * surfaces. - * - * The first parameter, `radius`, is optional. If a `Number` is passed, as in - * `torus(30)`, it sets the radius of the ring. By default, `radius` is 50. - * - * The second parameter, `tubeRadius`, is also optional. If a `Number` is - * passed, as in `torus(30, 15)`, it sets the radius of the tube. By default, - * `tubeRadius` is 10. - * - * The third parameter, `detailX`, is also optional. If a `Number` is passed, - * as in `torus(30, 15, 5)`, it sets the number of edges used to draw the hole - * of the torus. Using more edges makes the hole look more like a circle. By - * default, `detailX` is 24. - * - * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, - * as in `torus(30, 15, 5, 7)`, it sets the number of triangle subdivisions to - * use while filling in the torus’ height. By default, `detailY` is 16. - * - * Note: `torus()` can only be used in WebGL mode. - * - * @method torus - * @param {Number} [radius] radius of the torus. Defaults to 50. - * @param {Number} [tubeRadius] radius of the tube. Defaults to 10. - * @param {Integer} [detailX] number of edges that form the hole. Defaults to 24. - * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white torus on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the torus. - * torus(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white torus on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the torus. - * // Set its radius to 30. - * torus(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white torus on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the torus. - * // Set its radius to 30 and tubeRadius to 15. - * torus(30, 15); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white torus on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the torus. - * // Set its radius to 30 and tubeRadius to 15. - * // Set its detailX to 5. - * torus(30, 15, 5); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white torus on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the torus. - * // Set its radius to 30 and tubeRadius to 15. - * // Set its detailX to 5. - * // Set its detailY to 3. - * torus(30, 15, 5, 3); - * } - * - *
- */ -p5.prototype.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 p5.Vector( - r * cosTheta, - r * sinTheta, - tubeRatio * sinPhi - ); + const gId = `ellipsoid|${detailX}|${detailY}`; - const n = new p5.Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); + 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); - this.vertices.push(p); - this.vertexNormals.push(n); - this.uvs.push(u, v); + 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 p5.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' + ); } - }; - const torusGeom = new p5.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, ellipsoidGeom); } - this._renderer.createBuffers(gId, torusGeom); - } - this._renderer.drawBuffersScaled(gId, radius, radius, radius); - return this; -}; + this._renderer.drawBuffersScaled(gId, radiusX, radiusY, radiusZ); + + return this; + }; + + /** + * Draws a torus. + * + * A torus is a 3D shape with triangular faces that connect to form a ring. + * Toruses with few faces look flattened. Toruses with many faces have smooth + * surfaces. + * + * The first parameter, `radius`, is optional. If a `Number` is passed, as in + * `torus(30)`, it sets the radius of the ring. By default, `radius` is 50. + * + * The second parameter, `tubeRadius`, is also optional. If a `Number` is + * passed, as in `torus(30, 15)`, it sets the radius of the tube. By default, + * `tubeRadius` is 10. + * + * The third parameter, `detailX`, is also optional. If a `Number` is passed, + * as in `torus(30, 15, 5)`, it sets the number of edges used to draw the hole + * of the torus. Using more edges makes the hole look more like a circle. By + * default, `detailX` is 24. + * + * The fourth parameter, `detailY`, is also optional. If a `Number` is passed, + * as in `torus(30, 15, 5, 7)`, it sets the number of triangle subdivisions to + * use while filling in the torus’ height. By default, `detailY` is 16. + * + * Note: `torus()` can only be used in WebGL mode. + * + * @method torus + * @param {Number} [radius] radius of the torus. Defaults to 50. + * @param {Number} [tubeRadius] radius of the tube. Defaults to 10. + * @param {Integer} [detailX] number of edges that form the hole. Defaults to 24. + * @param {Integer} [detailY] number of triangle subdivisions along the y-axis. Defaults to 16. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white torus on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the torus. + * torus(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white torus on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the torus. + * // Set its radius to 30. + * torus(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white torus on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the torus. + * // Set its radius to 30 and tubeRadius to 15. + * torus(30, 15); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white torus on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the torus. + * // Set its radius to 30 and tubeRadius to 15. + * // Set its detailX to 5. + * torus(30, 15, 5); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white torus on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the torus. + * // Set its radius to 30 and tubeRadius to 15. + * // Set its detailX to 5. + * // Set its detailY to 3. + * torus(30, 15, 5, 3); + * } + * + *
+ */ + 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 + } -/////////////////////// -/// 2D primitives -///////////////////////// -// -// Note: Documentation is not generated on the p5.js website for functions on -// the p5.RendererGL prototype. + if (typeof tubeRadius === 'undefined') { + tubeRadius = 10; + } else if (!tubeRadius) { + return; // nothing to draw + } -/** - * 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 p5.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 p5.Vector(0, 0, 0)); - vertices.push(new p5.Vector(1, 0, 0)); - vertices.push(new p5.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); - 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 p5.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; + if (typeof detailX === 'undefined') { + detailX = 24; + } + if (typeof detailY === 'undefined') { + detailY = 16; + } - 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 p5.Vector(0.5, 0.5, 0)); - this.uvs.push([0.5, 0.5]); - } + const tubeRatio = (tubeRadius / radius).toPrecision(4); + const gId = `torus|${tubeRatio}|${detailX}|${detailY}`; - // vertices for the perimeter of the circle - for (let i = 0; i <= detail; i++) { - const u = i / detail; - const theta = (stop - start) * u + start; + 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 _x = 0.5 + Math.cos(theta) / 2; - const _y = 0.5 + Math.sin(theta) / 2; + const p = new p5.Vector( + r * cosTheta, + r * sinTheta, + tubeRatio * sinPhi + ); - this.vertices.push(new p5.Vector(_x, _y, 0)); - this.uvs.push([_x, _y]); + const n = new p5.Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); - if (i < detail - 1) { - this.faces.push([0, i + 1, i + 2]); - this.edges.push([i + 1, i + 2]); + this.vertices.push(p); + this.vertexNormals.push(n); + this.uvs.push(u, v); } } + }; + const torusGeom = new p5.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); - // 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; + return this; + }; - case constants.CHORD: - this.edges.push([0, 1]); - this.edges.push([0, this.vertices.length - 1]); - break; + /////////////////////// + /// 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 p5.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 p5.Vector(0, 0, 0)); + vertices.push(new p5.Vector(1, 0, 0)); + vertices.push(new p5.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); + triGeom._edgesToVertices(); + triGeom.computeNormals(); + this.createBuffers(gId, triGeom); + } - case constants.OPEN: - this.edges.push([0, 1]); - break; + // 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 p5.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); - default: - this.faces.push([ - 0, - this.vertices.length - 2, - this.vertices.length - 1 - ]); - this.edges.push([ - this.vertices.length - 2, - this.vertices.length - 1 - ]); - } - } - }; + this.states.uModelMatrix = mult; - const arcGeom = new p5.Geometry(detail, 1, _arc); - arcGeom.computeNormals(); + this.drawBuffers(gId); + } finally { + this.states.uModelMatrix = uModelMatrix; + } - 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; + }; + + 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}|`; } - this.createBuffers(gId, arcGeom); - } + 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 p5.Vector(0.5, 0.5, 0)); + this.uvs.push([0.5, 0.5]); + } - const uModelMatrix = this.states.uModelMatrix.copy(); + // vertices for the perimeter of the circle + for (let i = 0; i <= detail; i++) { + const u = i / detail; + const theta = (stop - start) * u + start; - try { - this.states.uModelMatrix.translate([x, y, 0]); - this.states.uModelMatrix.scale(width, height, 1); + const _x = 0.5 + Math.cos(theta) / 2; + const _y = 0.5 + Math.sin(theta) / 2; - 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 p5.Vector(u, v, 0); - this.vertices.push(p); - this.uvs.push(u, v); + this.vertices.push(new p5.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 + ]); } - } - // 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 p5.Geometry(detailX, detailY, _rect); - rectGeom - .computeFaces() - .computeNormals() - ._edgesToVertices(); - this.createBuffers(gId, rectGeom); + + const arcGeom = new p5.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); } - // 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); @@ -2687,537 +2627,802 @@ p5.RendererGL.prototype.rect = function(args) { } 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; - } + 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 p5.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 p5.Geometry(detailX, detailY, _rect); + rectGeom + .computeFaces() + .computeNormals() + ._edgesToVertices(); + this.createBuffers(gId, rectGeom); + } - 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; + // 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; + } - let x1 = a; - let y1 = b; - let x2 = c; - let y2 = d; + if (b > d) { + const temp = b; + b = d; + d = temp; + } - 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); - } + 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.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; + }; - 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 p5.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 p5.Vector(ptx, pty, ptz)); - this.uvs.push([pctx, pcty]); + /* 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 p5.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 p5.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.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); } - 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]); + 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 + ); } - 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); + 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; -}; - -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; + 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; + }; - 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; + 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); - break; + 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; } - 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) - ); - } + 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 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 + 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) ); - userVertexProperties[propName][2].push( - userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * 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]; + 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]; } - _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); } - for (const propName in immediateGeometry.userVertexProperties){ + // 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]; - 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(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); } - prop.setCurrentData(newValues); + this.vertex(_x, _y, _z); } - 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]); + // 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]; } - 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) - ); + } + }; + + 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 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 + 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 ); - userVertexProperties[propName][2].push( - userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 + strokeColors[1].push( + strokeColors[0][k] * (1-d0) + strokeColors[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 = _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]; + 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 + ); } - _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 (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(); - 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]; + 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); } - prop.setCurrentData(newValues); + this.vertex(_x, _y, _z); } - 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]); + + // 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]; } - 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 { + p5.RendererGL.prototype.curveVertex = function(...args) { 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; - + let t, _x, _y, _z, i; t = 0; + const argLength = args.length; if ( - this._lookUpTableQuadratic.length === 0 || - this._lutQuadraticDetail !== this._pInst._curveDetail + this._lookUpTableBezier.length === 0 || + this._lutBezierDetail !== this._pInst._curveDetail ) { - this._lookUpTableQuadratic = []; - this._lutQuadraticDetail = this._pInst._curveDetail; - const step = 1 / this._lutQuadraticDetail; + 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._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); + this._lookUpTableBezier[j] = this._bezierCoefficients(t); if (end.toFixed(6) === step.toFixed(6)) { t = parseFloat(end.toFixed(6)) + parseFloat(start.toFixed(6)); ++j; - this._lookUpTableQuadratic[j] = this._quadraticCoefficients(t); + this._lookUpTableBezier[j] = this._bezierCoefficients(t); break; } start += step; @@ -3226,352 +3431,151 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { } } - 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(); - } + const LUTLength = this._lookUpTableBezier.length; - 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 - ); + 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(); } } - - 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]; + } 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 (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); + for (i = 0; i < argLength; i++) { + this.immediateMode._curveVertex.shift(); } - this.vertex(_x, _y); } + } + }; + + p5.RendererGL.prototype.image = function( + img, + sx, + sy, + sWidth, + sHeight, + dx, + dy, + dWidth, + dHeight + ) { + if (this._isErasing) { + this.blendMode(this._cachedBlendMode); + } - // 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 - ); - } + this._pInst.push(); - 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 - ); - } - } + this._pInst.noLights(); + this._pInst.noStroke(); - 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); - } + this._pInst.texture(img); + this._pInst.textureMode(constants.NORMAL); - // 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]; + let u0 = 0; + if (sx <= img.width) { + u0 = sx / img.width; } - } -}; - -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; + + let u1 = 1; + if (sx + sWidth <= img.width) { + u1 = (sx + sWidth) / img.width; } - } - - 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(); - } + + let v0 = 0; + if (sy <= img.height) { + v0 = sy / img.height; } - } 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(); - } + + 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); } - } -}; - -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); - } -}; - -export default p5; + }; +} + +export default primitives3D; + +if(typeof p5 !== 'undefined'){ + primitives3D(p5, p5.prototype); +} diff --git a/src/webgl/index.js b/src/webgl/index.js new file mode 100644 index 0000000000..3fcb0efe6e --- /dev/null +++ b/src/webgl/index.js @@ -0,0 +1,31 @@ +import primitives3D from './3d_primitives'; +import interaction from './interaction'; +import light from './light'; +import loading from './loading'; +import material from './material'; +import text from './text'; +import renderBuffer from './p5.RenderBuffer'; +import quat from './p5.Quat'; +import matrix from './p5.Matrix'; +import geometry from './p5.Geometry'; +import framebuffer from './p5.Framebuffer'; +import dataArray from './p5.DataArray'; +import shader from './p5.Shader'; +import camera from './p5.Camera'; + +export default function(p5){ + primitives3D(p5, p5.prototype); + interaction(p5, p5.prototype); + light(p5, p5.prototype); + loading(p5, p5.prototype); + material(p5, p5.prototype); + text(p5, p5.prototype); + renderBuffer(p5, p5.prototype); + quat(p5, p5.prototype); + matrix(p5, p5.prototype); + geometry(p5, p5.prototype); + camera(p5, p5.prototype); + framebuffer(p5, p5.prototype); + dataArray(p5, p5.prototype); + shader(p5, p5.prototype); +} diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js index 7d1b9ba738..8ff849fa2f 100644 --- a/src/webgl/interaction.js +++ b/src/webgl/interaction.js @@ -5,884 +5,889 @@ * @requires core */ -import p5 from '../core/main'; import * as constants from '../core/constants'; -/** - * Allows the user to orbit around a 3D sketch using a mouse, trackpad, or - * touchscreen. - * - * 3D sketches are viewed through an imaginary camera. Calling - * `orbitControl()` within the draw() function allows - * the user to change the camera’s position: - * - * ```js - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Rest of sketch. - * } - * ``` - * - * Left-clicking and dragging or swipe motion will rotate the camera position - * about the center of the sketch. Right-clicking and dragging or multi-swipe - * will pan the camera position without rotation. Using the mouse wheel - * (scrolling) or pinch in/out will move the camera further or closer from the - * center of the sketch. - * - * The first three parameters, `sensitivityX`, `sensitivityY`, and - * `sensitivityZ`, are optional. They’re numbers that set the sketch’s - * sensitivity to movement along each axis. For example, calling - * `orbitControl(1, 2, -1)` keeps movement along the x-axis at its default - * value, makes the sketch twice as sensitive to movement along the y-axis, - * and reverses motion along the z-axis. By default, all sensitivity values - * are 1. - * - * The fourth parameter, `options`, is also optional. It’s an object that - * changes the behavior of orbiting. For example, calling - * `orbitControl(1, 1, 1, options)` keeps the default sensitivity values while - * changing the behaviors set with `options`. The object can have the - * following properties: - * - * ```js - * let options = { - * // Setting this to false makes mobile interactions smoother by - * // preventing accidental interactions with the page while orbiting. - * // By default, it's true. - * disableTouchActions: true, - * - * // Setting this to true makes the camera always rotate in the - * // direction the mouse/touch is moving. - * // By default, it's false. - * freeRotation: false - * }; - * - * orbitControl(1, 1, 1, options); - * ``` - * - * @method orbitControl - * @for p5 - * @param {Number} [sensitivityX] sensitivity to movement along the x-axis. Defaults to 1. - * @param {Number} [sensitivityY] sensitivity to movement along the y-axis. Defaults to 1. - * @param {Number} [sensitivityZ] sensitivity to movement along the z-axis. Defaults to 1. - * @param {Object} [options] object with two optional properties, `disableTouchActions` - * and `freeRotation`. Both are `Boolean`s. `disableTouchActions` - * defaults to `true` and `freeRotation` defaults to `false`. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. - * box(30, 50); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * // Make the interactions 3X sensitive. - * orbitControl(3, 3, 3); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. - * box(30, 50); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.'); - * } - * - * function draw() { - * background(200); - * - * // Create an options object. - * let options = { - * disableTouchActions: false, - * freeRotation: true - * }; - * - * // Enable orbiting with the mouse. - * // Prevent accidental touch actions on touchscreen devices - * // and enable free rotation. - * orbitControl(1, 1, 1, options); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. - * box(30, 50); - * } - * - *
- */ +function interaction(p5, fn){ + /** + * Allows the user to orbit around a 3D sketch using a mouse, trackpad, or + * touchscreen. + * + * 3D sketches are viewed through an imaginary camera. Calling + * `orbitControl()` within the draw() function allows + * the user to change the camera’s position: + * + * ```js + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Rest of sketch. + * } + * ``` + * + * Left-clicking and dragging or swipe motion will rotate the camera position + * about the center of the sketch. Right-clicking and dragging or multi-swipe + * will pan the camera position without rotation. Using the mouse wheel + * (scrolling) or pinch in/out will move the camera further or closer from the + * center of the sketch. + * + * The first three parameters, `sensitivityX`, `sensitivityY`, and + * `sensitivityZ`, are optional. They’re numbers that set the sketch’s + * sensitivity to movement along each axis. For example, calling + * `orbitControl(1, 2, -1)` keeps movement along the x-axis at its default + * value, makes the sketch twice as sensitive to movement along the y-axis, + * and reverses motion along the z-axis. By default, all sensitivity values + * are 1. + * + * The fourth parameter, `options`, is also optional. It’s an object that + * changes the behavior of orbiting. For example, calling + * `orbitControl(1, 1, 1, options)` keeps the default sensitivity values while + * changing the behaviors set with `options`. The object can have the + * following properties: + * + * ```js + * let options = { + * // Setting this to false makes mobile interactions smoother by + * // preventing accidental interactions with the page while orbiting. + * // By default, it's true. + * disableTouchActions: true, + * + * // Setting this to true makes the camera always rotate in the + * // direction the mouse/touch is moving. + * // By default, it's false. + * freeRotation: false + * }; + * + * orbitControl(1, 1, 1, options); + * ``` + * + * @method orbitControl + * @for p5 + * @param {Number} [sensitivityX] sensitivity to movement along the x-axis. Defaults to 1. + * @param {Number} [sensitivityY] sensitivity to movement along the y-axis. Defaults to 1. + * @param {Number} [sensitivityZ] sensitivity to movement along the z-axis. Defaults to 1. + * @param {Object} [options] object with two optional properties, `disableTouchActions` + * and `freeRotation`. Both are `Boolean`s. `disableTouchActions` + * defaults to `true` and `freeRotation` defaults to `false`. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. + * box(30, 50); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * // Make the interactions 3X sensitive. + * orbitControl(3, 3, 3); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. + * box(30, 50); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A multicolor box on a gray background. The camera angle changes when the user interacts using a mouse, trackpad, or touchscreen.'); + * } + * + * function draw() { + * background(200); + * + * // Create an options object. + * let options = { + * disableTouchActions: false, + * freeRotation: true + * }; + * + * // Enable orbiting with the mouse. + * // Prevent accidental touch actions on touchscreen devices + * // and enable free rotation. + * orbitControl(1, 1, 1, options); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. + * box(30, 50); + * } + * + *
+ */ + + // implementation based on three.js 'orbitControls': + // https://github.com/mrdoob/three.js/blob/6afb8595c0bf8b2e72818e42b64e6fe22707d896/examples/jsm/controls/OrbitControls.js#L22 + fn.orbitControl = function( + sensitivityX, + sensitivityY, + sensitivityZ, + options + ) { + this._assert3d('orbitControl'); + p5._validateParameters('orbitControl', arguments); + + const cam = this._renderer.states.curCamera; + + if (typeof sensitivityX === 'undefined') { + sensitivityX = 1; + } + if (typeof sensitivityY === 'undefined') { + sensitivityY = sensitivityX; + } + if (typeof sensitivityZ === 'undefined') { + sensitivityZ = 1; + } + if (typeof options !== 'object') { + options = {}; + } -// implementation based on three.js 'orbitControls': -// https://github.com/mrdoob/three.js/blob/6afb8595c0bf8b2e72818e42b64e6fe22707d896/examples/jsm/controls/OrbitControls.js#L22 -p5.prototype.orbitControl = function( - sensitivityX, - sensitivityY, - sensitivityZ, - options -) { - this._assert3d('orbitControl'); - p5._validateParameters('orbitControl', arguments); - - const cam = this._renderer.states.curCamera; - - if (typeof sensitivityX === 'undefined') { - sensitivityX = 1; - } - if (typeof sensitivityY === 'undefined') { - sensitivityY = sensitivityX; - } - if (typeof sensitivityZ === 'undefined') { - sensitivityZ = 1; - } - if (typeof options !== 'object') { - options = {}; - } - - // default right-mouse and mouse-wheel behaviors (context menu and scrolling, - // respectively) are disabled here to allow use of those events for panning and - // zooming. However, whether or not to disable touch actions is an option. - - // disable context menu for canvas element and add 'contextMenuDisabled' - // flag to p5 instance - if (this.contextMenuDisabled !== true) { - this.canvas.oncontextmenu = () => false; - this.contextMenuDisabled = true; - } - - // disable default scrolling behavior on the canvas element and add - // 'wheelDefaultDisabled' flag to p5 instance - if (this.wheelDefaultDisabled !== true) { - this.canvas.onwheel = () => false; - this.wheelDefaultDisabled = true; - } - - // disable default touch behavior on the canvas element and add - // 'touchActionsDisabled' flag to p5 instance - const { disableTouchActions = true } = options; - if (this.touchActionsDisabled !== true && disableTouchActions) { - this.canvas.style['touch-action'] = 'none'; - this.touchActionsDisabled = true; - } - - // If option.freeRotation is true, the camera always rotates freely in the direction - // the pointer moves. default value is false (normal behavior) - const { freeRotation = false } = options; - - // get moved touches. - const movedTouches = []; - - this.touches.forEach(curTouch => { - this._renderer.prevTouches.forEach(prevTouch => { - if (curTouch.id === prevTouch.id) { - const movedTouch = { - x: curTouch.x, - y: curTouch.y, - px: prevTouch.x, - py: prevTouch.y - }; - movedTouches.push(movedTouch); - } - }); - }); - - this._renderer.prevTouches = this.touches; - - // The idea of using damping is based on the following website. thank you. - // https://github.com/freshfork/p5.EasyCam/blob/9782964680f6a5c4c9bee825c475d9f2021d5134/p5.easycam.js#L1124 - - // variables for interaction - let deltaRadius = 0; - let deltaTheta = 0; - let deltaPhi = 0; - let moveDeltaX = 0; - let moveDeltaY = 0; - // constants for dampingProcess - const damping = 0.85; - const rotateAccelerationFactor = 0.6; - const moveAccelerationFactor = 0.15; - // For touches, the appropriate scale is different - // because the distance difference is multiplied. - const mouseZoomScaleFactor = 0.01; - const touchZoomScaleFactor = 0.0004; - const scaleFactor = this.height < this.width ? this.height : this.width; - // Flag whether the mouse or touch pointer is inside the canvas - let pointersInCanvas = false; - - // calculate and determine flags and variables. - if (movedTouches.length > 0) { - /* for touch */ - // if length === 1, rotate - // if length > 1, zoom and move - - // for touch, it is calculated based on one moved touch pointer position. - pointersInCanvas = - movedTouches[0].x > 0 && movedTouches[0].x < this.width && - movedTouches[0].y > 0 && movedTouches[0].y < this.height; - - if (movedTouches.length === 1) { - const t = movedTouches[0]; - deltaTheta = -sensitivityX * (t.x - t.px) / scaleFactor; - deltaPhi = sensitivityY * (t.y - t.py) / scaleFactor; - } else { - const t0 = movedTouches[0]; - const t1 = movedTouches[1]; - const distWithTouches = Math.hypot(t0.x - t1.x, t0.y - t1.y); - const prevDistWithTouches = Math.hypot(t0.px - t1.px, t0.py - t1.py); - const changeDist = distWithTouches - prevDistWithTouches; - // move the camera farther when the distance between the two touch points - // decreases, move the camera closer when it increases. - deltaRadius = -changeDist * sensitivityZ * touchZoomScaleFactor; - // Move the center of the camera along with the movement of - // the center of gravity of the two touch points. - moveDeltaX = 0.5 * (t0.x + t1.x) - 0.5 * (t0.px + t1.px); - moveDeltaY = 0.5 * (t0.y + t1.y) - 0.5 * (t0.py + t1.py); + // default right-mouse and mouse-wheel behaviors (context menu and scrolling, + // respectively) are disabled here to allow use of those events for panning and + // zooming. However, whether or not to disable touch actions is an option. + + // disable context menu for canvas element and add 'contextMenuDisabled' + // flag to p5 instance + if (this.contextMenuDisabled !== true) { + this.canvas.oncontextmenu = () => false; + this.contextMenuDisabled = true; } - if (this.touches.length > 0) { - if (pointersInCanvas) { - // Initiate an interaction if touched in the canvas - this._renderer.executeRotateAndMove = true; - this._renderer.executeZoom = true; + + // disable default scrolling behavior on the canvas element and add + // 'wheelDefaultDisabled' flag to p5 instance + if (this.wheelDefaultDisabled !== true) { + this.canvas.onwheel = () => false; + this.wheelDefaultDisabled = true; + } + + // disable default touch behavior on the canvas element and add + // 'touchActionsDisabled' flag to p5 instance + const { disableTouchActions = true } = options; + if (this.touchActionsDisabled !== true && disableTouchActions) { + this.canvas.style['touch-action'] = 'none'; + this.touchActionsDisabled = true; + } + + // If option.freeRotation is true, the camera always rotates freely in the direction + // the pointer moves. default value is false (normal behavior) + const { freeRotation = false } = options; + + // get moved touches. + const movedTouches = []; + + this.touches.forEach(curTouch => { + this._renderer.prevTouches.forEach(prevTouch => { + if (curTouch.id === prevTouch.id) { + const movedTouch = { + x: curTouch.x, + y: curTouch.y, + px: prevTouch.x, + py: prevTouch.y + }; + movedTouches.push(movedTouch); + } + }); + }); + + this._renderer.prevTouches = this.touches; + + // The idea of using damping is based on the following website. thank you. + // https://github.com/freshfork/p5.EasyCam/blob/9782964680f6a5c4c9bee825c475d9f2021d5134/p5.easycam.js#L1124 + + // variables for interaction + let deltaRadius = 0; + let deltaTheta = 0; + let deltaPhi = 0; + let moveDeltaX = 0; + let moveDeltaY = 0; + // constants for dampingProcess + const damping = 0.85; + const rotateAccelerationFactor = 0.6; + const moveAccelerationFactor = 0.15; + // For touches, the appropriate scale is different + // because the distance difference is multiplied. + const mouseZoomScaleFactor = 0.01; + const touchZoomScaleFactor = 0.0004; + const scaleFactor = this.height < this.width ? this.height : this.width; + // Flag whether the mouse or touch pointer is inside the canvas + let pointersInCanvas = false; + + // calculate and determine flags and variables. + if (movedTouches.length > 0) { + /* for touch */ + // if length === 1, rotate + // if length > 1, zoom and move + + // for touch, it is calculated based on one moved touch pointer position. + pointersInCanvas = + movedTouches[0].x > 0 && movedTouches[0].x < this.width && + movedTouches[0].y > 0 && movedTouches[0].y < this.height; + + if (movedTouches.length === 1) { + const t = movedTouches[0]; + deltaTheta = -sensitivityX * (t.x - t.px) / scaleFactor; + deltaPhi = sensitivityY * (t.y - t.py) / scaleFactor; + } else { + const t0 = movedTouches[0]; + const t1 = movedTouches[1]; + const distWithTouches = Math.hypot(t0.x - t1.x, t0.y - t1.y); + const prevDistWithTouches = Math.hypot(t0.px - t1.px, t0.py - t1.py); + const changeDist = distWithTouches - prevDistWithTouches; + // move the camera farther when the distance between the two touch points + // decreases, move the camera closer when it increases. + deltaRadius = -changeDist * sensitivityZ * touchZoomScaleFactor; + // Move the center of the camera along with the movement of + // the center of gravity of the two touch points. + moveDeltaX = 0.5 * (t0.x + t1.x) - 0.5 * (t0.px + t1.px); + moveDeltaY = 0.5 * (t0.y + t1.y) - 0.5 * (t0.py + t1.py); + } + if (this.touches.length > 0) { + if (pointersInCanvas) { + // Initiate an interaction if touched in the canvas + this._renderer.executeRotateAndMove = true; + this._renderer.executeZoom = true; + } + } else { + // End an interaction when the touch is released + this._renderer.executeRotateAndMove = false; + this._renderer.executeZoom = false; } } else { - // End an interaction when the touch is released - this._renderer.executeRotateAndMove = false; - this._renderer.executeZoom = false; + /* for mouse */ + // if wheelDeltaY !== 0, zoom + // if mouseLeftButton is down, rotate + // if mouseRightButton is down, move + + // For mouse, it is calculated based on the mouse position. + pointersInCanvas = + (this.mouseX > 0 && this.mouseX < this.width) && + (this.mouseY > 0 && this.mouseY < this.height); + + if (this._mouseWheelDeltaY !== 0) { + // zoom the camera depending on the value of _mouseWheelDeltaY. + // move away if positive, move closer if negative + deltaRadius = Math.sign(this._mouseWheelDeltaY) * sensitivityZ; + deltaRadius *= mouseZoomScaleFactor; + this._mouseWheelDeltaY = 0; + // start zoom when the mouse is wheeled within the canvas. + if (pointersInCanvas) this._renderer.executeZoom = true; + } else { + // quit zoom when you stop wheeling. + this._renderer.executeZoom = false; + } + if (this.mouseIsPressed) { + if (this.mouseButton === this.LEFT) { + deltaTheta = -sensitivityX * this.movedX / scaleFactor; + deltaPhi = sensitivityY * this.movedY / scaleFactor; + } else if (this.mouseButton === this.RIGHT) { + moveDeltaX = this.movedX; + moveDeltaY = this.movedY * cam.yScale; + } + // start rotate and move when mouse is pressed within the canvas. + if (pointersInCanvas) this._renderer.executeRotateAndMove = true; + } else { + // quit rotate and move if mouse is released. + this._renderer.executeRotateAndMove = false; + } } - } else { - /* for mouse */ - // if wheelDeltaY !== 0, zoom - // if mouseLeftButton is down, rotate - // if mouseRightButton is down, move - - // For mouse, it is calculated based on the mouse position. - pointersInCanvas = - (this.mouseX > 0 && this.mouseX < this.width) && - (this.mouseY > 0 && this.mouseY < this.height); - - if (this._mouseWheelDeltaY !== 0) { - // zoom the camera depending on the value of _mouseWheelDeltaY. - // move away if positive, move closer if negative - deltaRadius = Math.sign(this._mouseWheelDeltaY) * sensitivityZ; - deltaRadius *= mouseZoomScaleFactor; - this._mouseWheelDeltaY = 0; - // start zoom when the mouse is wheeled within the canvas. - if (pointersInCanvas) this._renderer.executeZoom = true; - } else { - // quit zoom when you stop wheeling. - this._renderer.executeZoom = false; + + // interactions + + // zoom process + if (deltaRadius !== 0 && this._renderer.executeZoom) { + // accelerate zoom velocity + this._renderer.zoomVelocity += deltaRadius; } - if (this.mouseIsPressed) { - if (this.mouseButton === this.LEFT) { - deltaTheta = -sensitivityX * this.movedX / scaleFactor; - deltaPhi = sensitivityY * this.movedY / scaleFactor; - } else if (this.mouseButton === this.RIGHT) { - moveDeltaX = this.movedX; - moveDeltaY = this.movedY * cam.yScale; + if (Math.abs(this._renderer.zoomVelocity) > 0.001) { + // if freeRotation is true, we use _orbitFree() instead of _orbit() + if (freeRotation) { + cam._orbitFree( + 0, 0, this._renderer.zoomVelocity + ); + } else { + cam._orbit( + 0, 0, this._renderer.zoomVelocity + ); + } + // In orthogonal projection, the scale does not change even if + // the distance to the gaze point is changed, so the projection matrix + // needs to be modified. + if (cam.projMatrix.mat4[15] !== 0) { + cam.projMatrix.mat4[0] *= Math.pow( + 10, -this._renderer.zoomVelocity + ); + cam.projMatrix.mat4[5] *= Math.pow( + 10, -this._renderer.zoomVelocity + ); + // modify uPMatrix + this._renderer.states.uPMatrix.mat4[0] = cam.projMatrix.mat4[0]; + this._renderer.states.uPMatrix.mat4[5] = cam.projMatrix.mat4[5]; } - // start rotate and move when mouse is pressed within the canvas. - if (pointersInCanvas) this._renderer.executeRotateAndMove = true; + // damping + this._renderer.zoomVelocity *= damping; } else { - // quit rotate and move if mouse is released. - this._renderer.executeRotateAndMove = false; + this._renderer.zoomVelocity = 0; } - } - - // interactions - - // zoom process - if (deltaRadius !== 0 && this._renderer.executeZoom) { - // accelerate zoom velocity - this._renderer.zoomVelocity += deltaRadius; - } - if (Math.abs(this._renderer.zoomVelocity) > 0.001) { - // if freeRotation is true, we use _orbitFree() instead of _orbit() - if (freeRotation) { - cam._orbitFree( - 0, 0, this._renderer.zoomVelocity + + // rotate process + if ((deltaTheta !== 0 || deltaPhi !== 0) && + this._renderer.executeRotateAndMove) { + // accelerate rotate velocity + this._renderer.rotateVelocity.add( + deltaTheta * rotateAccelerationFactor, + deltaPhi * rotateAccelerationFactor ); + } + if (this._renderer.rotateVelocity.magSq() > 0.000001) { + // if freeRotation is true, the camera always rotates freely in the direction the pointer moves + if (freeRotation) { + cam._orbitFree( + -this._renderer.rotateVelocity.x, + this._renderer.rotateVelocity.y, + 0 + ); + } else { + cam._orbit( + this._renderer.rotateVelocity.x, + this._renderer.rotateVelocity.y, + 0 + ); + } + // damping + this._renderer.rotateVelocity.mult(damping); } else { - cam._orbit( - 0, 0, this._renderer.zoomVelocity - ); + this._renderer.rotateVelocity.set(0, 0); } - // In orthogonal projection, the scale does not change even if - // the distance to the gaze point is changed, so the projection matrix - // needs to be modified. - if (cam.projMatrix.mat4[15] !== 0) { - cam.projMatrix.mat4[0] *= Math.pow( - 10, -this._renderer.zoomVelocity - ); - cam.projMatrix.mat4[5] *= Math.pow( - 10, -this._renderer.zoomVelocity + + // move process + if ((moveDeltaX !== 0 || moveDeltaY !== 0) && + this._renderer.executeRotateAndMove) { + // Normalize movement distance + const ndcX = moveDeltaX * 2/this.width; + const ndcY = -moveDeltaY * 2/this.height; + // accelerate move velocity + this._renderer.moveVelocity.add( + ndcX * moveAccelerationFactor, + ndcY * moveAccelerationFactor ); - // modify uPMatrix - this._renderer.states.uPMatrix.mat4[0] = cam.projMatrix.mat4[0]; - this._renderer.states.uPMatrix.mat4[5] = cam.projMatrix.mat4[5]; } - // damping - this._renderer.zoomVelocity *= damping; - } else { - this._renderer.zoomVelocity = 0; - } - - // rotate process - if ((deltaTheta !== 0 || deltaPhi !== 0) && - this._renderer.executeRotateAndMove) { - // accelerate rotate velocity - this._renderer.rotateVelocity.add( - deltaTheta * rotateAccelerationFactor, - deltaPhi * rotateAccelerationFactor - ); - } - if (this._renderer.rotateVelocity.magSq() > 0.000001) { - // if freeRotation is true, the camera always rotates freely in the direction the pointer moves - if (freeRotation) { - cam._orbitFree( - -this._renderer.rotateVelocity.x, - this._renderer.rotateVelocity.y, - 0 - ); - } else { - cam._orbit( - this._renderer.rotateVelocity.x, - this._renderer.rotateVelocity.y, - 0 + if (this._renderer.moveVelocity.magSq() > 0.000001) { + // Translate the camera so that the entire object moves + // perpendicular to the line of sight when the mouse is moved + // or when the centers of gravity of the two touch pointers move. + const local = cam._getLocalAxes(); + + // Calculate the z coordinate in the view coordinates of + // the center, that is, the distance to the view point + const diffX = cam.eyeX - cam.centerX; + const diffY = cam.eyeY - cam.centerY; + const diffZ = cam.eyeZ - cam.centerZ; + 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); + + // Calculate the normalized device coordinates of the center. + cv = cam.cameraMatrix.multiplyPoint(cv); + cv = this._renderer.states.uPMatrix.multiplyAndNormalizePoint(cv); + + // Move the center by this distance + // in the normalized device coordinate system. + cv.x -= this._renderer.moveVelocity.x; + cv.y -= this._renderer.moveVelocity.y; + + // Calculate the translation vector + // in the direction perpendicular to the line of sight of center. + let dx, dy; + const uP = this._renderer.states.uPMatrix.mat4; + + if (uP[15] === 0) { + dx = ((uP[8] + cv.x)/uP[0]) * viewZ; + dy = ((uP[9] + cv.y)/uP[5]) * viewZ; + } else { + dx = (cv.x - uP[12])/uP[0]; + dy = (cv.y - uP[13])/uP[5]; + } + + // translate the camera. + cam.setPosition( + cam.eyeX + dx * local.x[0] + dy * local.y[0], + cam.eyeY + dx * local.x[1] + dy * local.y[1], + cam.eyeZ + dx * local.x[2] + dy * local.y[2] ); - } - // damping - this._renderer.rotateVelocity.mult(damping); - } else { - this._renderer.rotateVelocity.set(0, 0); - } - - // move process - if ((moveDeltaX !== 0 || moveDeltaY !== 0) && - this._renderer.executeRotateAndMove) { - // Normalize movement distance - const ndcX = moveDeltaX * 2/this.width; - const ndcY = -moveDeltaY * 2/this.height; - // accelerate move velocity - this._renderer.moveVelocity.add( - ndcX * moveAccelerationFactor, - ndcY * moveAccelerationFactor - ); - } - if (this._renderer.moveVelocity.magSq() > 0.000001) { - // Translate the camera so that the entire object moves - // perpendicular to the line of sight when the mouse is moved - // or when the centers of gravity of the two touch pointers move. - const local = cam._getLocalAxes(); - - // Calculate the z coordinate in the view coordinates of - // the center, that is, the distance to the view point - const diffX = cam.eyeX - cam.centerX; - const diffY = cam.eyeY - cam.centerY; - const diffZ = cam.eyeZ - cam.centerZ; - 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); - - // Calculate the normalized device coordinates of the center. - cv = cam.cameraMatrix.multiplyPoint(cv); - cv = this._renderer.states.uPMatrix.multiplyAndNormalizePoint(cv); - - // Move the center by this distance - // in the normalized device coordinate system. - cv.x -= this._renderer.moveVelocity.x; - cv.y -= this._renderer.moveVelocity.y; - - // Calculate the translation vector - // in the direction perpendicular to the line of sight of center. - let dx, dy; - const uP = this._renderer.states.uPMatrix.mat4; - - if (uP[15] === 0) { - dx = ((uP[8] + cv.x)/uP[0]) * viewZ; - dy = ((uP[9] + cv.y)/uP[5]) * viewZ; + // damping + this._renderer.moveVelocity.mult(damping); } else { - dx = (cv.x - uP[12])/uP[0]; - dy = (cv.y - uP[13])/uP[5]; + this._renderer.moveVelocity.set(0, 0); } - // translate the camera. - cam.setPosition( - cam.eyeX + dx * local.x[0] + dy * local.y[0], - cam.eyeY + dx * local.x[1] + dy * local.y[1], - cam.eyeZ + dx * local.x[2] + dy * local.y[2] - ); - // damping - this._renderer.moveVelocity.mult(damping); - } else { - this._renderer.moveVelocity.set(0, 0); - } + return this; + }; - return this; -}; + /** + * Adds a grid and an axes icon to clarify orientation in 3D sketches. + * + * `debugMode()` adds a grid that shows where the “ground” is in a sketch. By + * default, the grid will run through the origin `(0, 0, 0)` of the sketch + * along the XZ plane. `debugMode()` also adds an axes icon that points along + * the positive x-, y-, and z-axes. Calling `debugMode()` displays the grid + * and axes icon with their default size and position. + * + * There are four ways to call `debugMode()` with optional parameters to + * customize the debugging environment. + * + * The first way to call `debugMode()` has one parameter, `mode`. If the + * system constant `GRID` is passed, as in `debugMode(GRID)`, then the grid + * will be displayed and the axes icon will be hidden. If the constant `AXES` + * is passed, as in `debugMode(AXES)`, then the axes icon will be displayed + * and the grid will be hidden. + * + * The second way to call `debugMode()` has six parameters. The first + * parameter, `mode`, selects either `GRID` or `AXES` to be displayed. The + * next five parameters, `gridSize`, `gridDivisions`, `xOff`, `yOff`, and + * `zOff` are optional. They’re numbers that set the appearance of the grid + * (`gridSize` and `gridDivisions`) and the placement of the axes icon + * (`xOff`, `yOff`, and `zOff`). For example, calling + * `debugMode(20, 5, 10, 10, 10)` sets the `gridSize` to 20 pixels, the number + * of `gridDivisions` to 5, and offsets the axes icon by 10 pixels along the + * x-, y-, and z-axes. + * + * The third way to call `debugMode()` has five parameters. The first + * parameter, `mode`, selects either `GRID` or `AXES` to be displayed. The + * next four parameters, `axesSize`, `xOff`, `yOff`, and `zOff` are optional. + * They’re numbers that set the appearance of the size of the axes icon + * (`axesSize`) and its placement (`xOff`, `yOff`, and `zOff`). + * + * The fourth way to call `debugMode()` has nine optional parameters. The + * first five parameters, `gridSize`, `gridDivisions`, `gridXOff`, `gridYOff`, + * and `gridZOff` are numbers that set the appearance of the grid. For + * example, calling `debugMode(100, 5, 0, 0, 0)` sets the `gridSize` to 100, + * the number of `gridDivisions` to 5, and sets all the offsets to 0 so that + * the grid is centered at the origin. The next four parameters, `axesSize`, + * `xOff`, `yOff`, and `zOff` are numbers that set the appearance of the size + * of the axes icon (`axesSize`) and its placement (`axesXOff`, `axesYOff`, + * and `axesZOff`). For example, calling + * `debugMode(100, 5, 0, 0, 0, 50, 10, 10, 10)` sets the `gridSize` to 100, + * the number of `gridDivisions` to 5, and sets all the offsets to 0 so that + * the grid is centered at the origin. It then sets the `axesSize` to 50 and + * offsets the icon 10 pixels along each axis. + * + * @method debugMode + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Enable debug mode. + * debugMode(); + * + * describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. + * box(20, 40); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Enable debug mode. + * // Only display the axes icon. + * debugMode(AXES); + * + * describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. + * box(20, 40); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Enable debug mode. + * // Only display the grid and customize it: + * // - size: 50 + * // - divisions: 10 + * // - offsets: 0, 20, 0 + * debugMode(GRID, 50, 10, 0, 20, 0); + * + * describe('A multicolor box on a gray background. A grid is displayed below the box.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. + * box(20, 40); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Enable debug mode. + * // Display the grid and axes icon and customize them: + * // Grid + * // ---- + * // - size: 50 + * // - divisions: 10 + * // - offsets: 0, 20, 0 + * // Axes + * // ---- + * // - size: 50 + * // - offsets: 0, 0, 0 + * debugMode(50, 10, 0, 20, 0, 50, 0, 0, 0); + * + * describe('A multicolor box on a gray background. A grid is displayed below the box. An axes icon is displayed at the center of the box.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. + * box(20, 40); + * } + * + *
+ */ + + /** + * @method debugMode + * @param {(GRID|AXES)} mode either GRID or AXES + */ + + /** + * @method debugMode + * @param {(GRID|AXES)} mode + * @param {Number} [gridSize] side length of the grid. + * @param {Number} [gridDivisions] number of divisions in the grid. + * @param {Number} [xOff] offset from origin along the x-axis. + * @param {Number} [yOff] offset from origin along the y-axis. + * @param {Number} [zOff] offset from origin along the z-axis. + */ + + /** + * @method debugMode + * @param {(GRID|AXES)} mode + * @param {Number} [axesSize] length of axes icon markers. + * @param {Number} [xOff] + * @param {Number} [yOff] + * @param {Number} [zOff] + */ + + /** + * @method debugMode + * @param {Number} [gridSize] + * @param {Number} [gridDivisions] + * @param {Number} [gridXOff] grid offset from the origin along the x-axis. + * @param {Number} [gridYOff] grid offset from the origin along the y-axis. + * @param {Number} [gridZOff] grid offset from the origin along the z-axis. + * @param {Number} [axesSize] + * @param {Number} [axesXOff] axes icon offset from the origin along the x-axis. + * @param {Number} [axesYOff] axes icon offset from the origin along the y-axis. + * @param {Number} [axesZOff] axes icon offset from the origin along the z-axis. + */ + + fn.debugMode = function(...args) { + this._assert3d('debugMode'); + p5._validateParameters('debugMode', args); + + // start by removing existing 'post' registered debug methods + for (let i = this._registeredMethods.post.length - 1; i >= 0; i--) { + // test for equality... + if ( + this._registeredMethods.post[i].toString() === this._grid().toString() || + this._registeredMethods.post[i].toString() === this._axesIcon().toString() + ) { + this._registeredMethods.post.splice(i, 1); + } + } -/** - * Adds a grid and an axes icon to clarify orientation in 3D sketches. - * - * `debugMode()` adds a grid that shows where the “ground” is in a sketch. By - * default, the grid will run through the origin `(0, 0, 0)` of the sketch - * along the XZ plane. `debugMode()` also adds an axes icon that points along - * the positive x-, y-, and z-axes. Calling `debugMode()` displays the grid - * and axes icon with their default size and position. - * - * There are four ways to call `debugMode()` with optional parameters to - * customize the debugging environment. - * - * The first way to call `debugMode()` has one parameter, `mode`. If the - * system constant `GRID` is passed, as in `debugMode(GRID)`, then the grid - * will be displayed and the axes icon will be hidden. If the constant `AXES` - * is passed, as in `debugMode(AXES)`, then the axes icon will be displayed - * and the grid will be hidden. - * - * The second way to call `debugMode()` has six parameters. The first - * parameter, `mode`, selects either `GRID` or `AXES` to be displayed. The - * next five parameters, `gridSize`, `gridDivisions`, `xOff`, `yOff`, and - * `zOff` are optional. They’re numbers that set the appearance of the grid - * (`gridSize` and `gridDivisions`) and the placement of the axes icon - * (`xOff`, `yOff`, and `zOff`). For example, calling - * `debugMode(20, 5, 10, 10, 10)` sets the `gridSize` to 20 pixels, the number - * of `gridDivisions` to 5, and offsets the axes icon by 10 pixels along the - * x-, y-, and z-axes. - * - * The third way to call `debugMode()` has five parameters. The first - * parameter, `mode`, selects either `GRID` or `AXES` to be displayed. The - * next four parameters, `axesSize`, `xOff`, `yOff`, and `zOff` are optional. - * They’re numbers that set the appearance of the size of the axes icon - * (`axesSize`) and its placement (`xOff`, `yOff`, and `zOff`). - * - * The fourth way to call `debugMode()` has nine optional parameters. The - * first five parameters, `gridSize`, `gridDivisions`, `gridXOff`, `gridYOff`, - * and `gridZOff` are numbers that set the appearance of the grid. For - * example, calling `debugMode(100, 5, 0, 0, 0)` sets the `gridSize` to 100, - * the number of `gridDivisions` to 5, and sets all the offsets to 0 so that - * the grid is centered at the origin. The next four parameters, `axesSize`, - * `xOff`, `yOff`, and `zOff` are numbers that set the appearance of the size - * of the axes icon (`axesSize`) and its placement (`axesXOff`, `axesYOff`, - * and `axesZOff`). For example, calling - * `debugMode(100, 5, 0, 0, 0, 50, 10, 10, 10)` sets the `gridSize` to 100, - * the number of `gridDivisions` to 5, and sets all the offsets to 0 so that - * the grid is centered at the origin. It then sets the `axesSize` to 50 and - * offsets the icon 10 pixels along each axis. - * - * @method debugMode - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Enable debug mode. - * debugMode(); - * - * describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. - * box(20, 40); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Enable debug mode. - * // Only display the axes icon. - * debugMode(AXES); - * - * describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. - * box(20, 40); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Enable debug mode. - * // Only display the grid and customize it: - * // - size: 50 - * // - divisions: 10 - * // - offsets: 0, 20, 0 - * debugMode(GRID, 50, 10, 0, 20, 0); - * - * describe('A multicolor box on a gray background. A grid is displayed below the box.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. - * box(20, 40); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Enable debug mode. - * // Display the grid and axes icon and customize them: - * // Grid - * // ---- - * // - size: 50 - * // - divisions: 10 - * // - offsets: 0, 20, 0 - * // Axes - * // ---- - * // - size: 50 - * // - offsets: 0, 0, 0 - * debugMode(50, 10, 0, 20, 0, 50, 0, 0, 0); - * - * describe('A multicolor box on a gray background. A grid is displayed below the box. An axes icon is displayed at the center of the box.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. - * box(20, 40); - * } - * - *
- */ + // then add new debugMode functions according to the argument list + if (args[0] === constants.GRID) { + this.registerMethod( + 'post', + this._grid(args[1], args[2], args[3], args[4], args[5]) + ); + } else if (args[0] === constants.AXES) { + this.registerMethod( + 'post', + this._axesIcon(args[1], args[2], args[3], args[4]) + ); + } else { + this.registerMethod( + 'post', + this._grid(args[0], args[1], args[2], args[3], args[4]) + ); + this.registerMethod( + 'post', + this._axesIcon(args[5], args[6], args[7], args[8]) + ); + } + }; -/** - * @method debugMode - * @param {(GRID|AXES)} mode either GRID or AXES - */ + /** + * Turns off debugMode() in a 3D sketch. + * + * @method noDebugMode + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Enable debug mode. + * debugMode(); + * + * describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box. They disappear when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the box. + * normalMaterial(); + * + * // Draw the box. box(20, 40); + * } + * + * // Disable debug mode when the user double-clicks. + * function doubleClicked() { + * noDebugMode(); + * } + * + *
+ */ + fn.noDebugMode = function() { + this._assert3d('noDebugMode'); + + // start by removing existing 'post' registered debug methods + for (let i = this._registeredMethods.post.length - 1; i >= 0; i--) { + // test for equality... + if ( + this._registeredMethods.post[i].toString() === this._grid().toString() || + this._registeredMethods.post[i].toString() === this._axesIcon().toString() + ) { + this._registeredMethods.post.splice(i, 1); + } + } + }; -/** - * @method debugMode - * @param {(GRID|AXES)} mode - * @param {Number} [gridSize] side length of the grid. - * @param {Number} [gridDivisions] number of divisions in the grid. - * @param {Number} [xOff] offset from origin along the x-axis. - * @param {Number} [yOff] offset from origin along the y-axis. - * @param {Number} [zOff] offset from origin along the z-axis. - */ + /** + * For use with debugMode + * @private + * @method _grid + * @param {Number} [size] size of grid sides + * @param {Number} [div] number of grid divisions + * @param {Number} [xOff] offset of grid center from origin in X axis + * @param {Number} [yOff] offset of grid center from origin in Y axis + * @param {Number} [zOff] offset of grid center from origin in Z axis + */ + fn._grid = function(size, numDivs, xOff, yOff, zOff) { + if (typeof size === 'undefined') { + size = this.width / 2; + } + if (typeof numDivs === 'undefined') { + // ensure at least 2 divisions + numDivs = Math.round(size / 30) < 4 ? 4 : Math.round(size / 30); + } + if (typeof xOff === 'undefined') { + xOff = 0; + } + if (typeof yOff === 'undefined') { + yOff = 0; + } + if (typeof zOff === 'undefined') { + zOff = 0; + } -/** - * @method debugMode - * @param {(GRID|AXES)} mode - * @param {Number} [axesSize] length of axes icon markers. - * @param {Number} [xOff] - * @param {Number} [yOff] - * @param {Number} [zOff] - */ + const spacing = size / numDivs; + const halfSize = size / 2; -/** - * @method debugMode - * @param {Number} [gridSize] - * @param {Number} [gridDivisions] - * @param {Number} [gridXOff] grid offset from the origin along the x-axis. - * @param {Number} [gridYOff] grid offset from the origin along the y-axis. - * @param {Number} [gridZOff] grid offset from the origin along the z-axis. - * @param {Number} [axesSize] - * @param {Number} [axesXOff] axes icon offset from the origin along the x-axis. - * @param {Number} [axesYOff] axes icon offset from the origin along the y-axis. - * @param {Number} [axesZOff] axes icon offset from the origin along the z-axis. - */ + return function() { + this.push(); + this.stroke( + this._renderer.states.curStrokeColor[0] * 255, + this._renderer.states.curStrokeColor[1] * 255, + this._renderer.states.curStrokeColor[2] * 255 + ); + this._renderer.states.uModelMatrix.reset(); + + // Lines along X axis + for (let q = 0; q <= numDivs; q++) { + this.beginShape(this.LINES); + this.vertex(-halfSize + xOff, yOff, q * spacing - halfSize + zOff); + this.vertex(+halfSize + xOff, yOff, q * spacing - halfSize + zOff); + this.endShape(); + } -p5.prototype.debugMode = function(...args) { - this._assert3d('debugMode'); - p5._validateParameters('debugMode', args); - - // start by removing existing 'post' registered debug methods - for (let i = this._registeredMethods.post.length - 1; i >= 0; i--) { - // test for equality... - if ( - this._registeredMethods.post[i].toString() === this._grid().toString() || - this._registeredMethods.post[i].toString() === this._axesIcon().toString() - ) { - this._registeredMethods.post.splice(i, 1); - } - } - - // then add new debugMode functions according to the argument list - if (args[0] === constants.GRID) { - this.registerMethod( - 'post', - this._grid(args[1], args[2], args[3], args[4], args[5]) - ); - } else if (args[0] === constants.AXES) { - this.registerMethod( - 'post', - this._axesIcon(args[1], args[2], args[3], args[4]) - ); - } else { - this.registerMethod( - 'post', - this._grid(args[0], args[1], args[2], args[3], args[4]) - ); - this.registerMethod( - 'post', - this._axesIcon(args[5], args[6], args[7], args[8]) - ); - } -}; + // Lines along Z axis + for (let i = 0; i <= numDivs; i++) { + this.beginShape(this.LINES); + this.vertex(i * spacing - halfSize + xOff, yOff, -halfSize + zOff); + this.vertex(i * spacing - halfSize + xOff, yOff, +halfSize + zOff); + this.endShape(); + } -/** - * Turns off debugMode() in a 3D sketch. - * - * @method noDebugMode - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Enable debug mode. - * debugMode(); - * - * describe('A multicolor box on a gray background. A grid and axes icon are displayed near the box. They disappear when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the box. - * normalMaterial(); - * - * // Draw the box. box(20, 40); - * } - * - * // Disable debug mode when the user double-clicks. - * function doubleClicked() { - * noDebugMode(); - * } - * - *
- */ -p5.prototype.noDebugMode = function() { - this._assert3d('noDebugMode'); - - // start by removing existing 'post' registered debug methods - for (let i = this._registeredMethods.post.length - 1; i >= 0; i--) { - // test for equality... - if ( - this._registeredMethods.post[i].toString() === this._grid().toString() || - this._registeredMethods.post[i].toString() === this._axesIcon().toString() - ) { - this._registeredMethods.post.splice(i, 1); + this.pop(); + }; + }; + + /** + * For use with debugMode + * @private + * @method _axesIcon + * @param {Number} [size] size of axes icon lines + * @param {Number} [xOff] offset of icon from origin in X axis + * @param {Number} [yOff] offset of icon from origin in Y axis + * @param {Number} [zOff] offset of icon from origin in Z axis + */ + fn._axesIcon = function(size, xOff, yOff, zOff) { + if (typeof size === 'undefined') { + size = this.width / 20 > 40 ? this.width / 20 : 40; + } + if (typeof xOff === 'undefined') { + xOff = -this.width / 4; + } + if (typeof yOff === 'undefined') { + yOff = xOff; + } + if (typeof zOff === 'undefined') { + zOff = xOff; } - } -}; -/** - * For use with debugMode - * @private - * @method _grid - * @param {Number} [size] size of grid sides - * @param {Number} [div] number of grid divisions - * @param {Number} [xOff] offset of grid center from origin in X axis - * @param {Number} [yOff] offset of grid center from origin in Y axis - * @param {Number} [zOff] offset of grid center from origin in Z axis - */ -p5.prototype._grid = function(size, numDivs, xOff, yOff, zOff) { - if (typeof size === 'undefined') { - size = this.width / 2; - } - if (typeof numDivs === 'undefined') { - // ensure at least 2 divisions - numDivs = Math.round(size / 30) < 4 ? 4 : Math.round(size / 30); - } - if (typeof xOff === 'undefined') { - xOff = 0; - } - if (typeof yOff === 'undefined') { - yOff = 0; - } - if (typeof zOff === 'undefined') { - zOff = 0; - } - - const spacing = size / numDivs; - const halfSize = size / 2; - - return function() { - this.push(); - this.stroke( - this._renderer.states.curStrokeColor[0] * 255, - this._renderer.states.curStrokeColor[1] * 255, - this._renderer.states.curStrokeColor[2] * 255 - ); - this._renderer.states.uModelMatrix.reset(); - - // Lines along X axis - for (let q = 0; q <= numDivs; q++) { + return function() { + this.push(); + this._renderer.states.uModelMatrix.reset(); + + // X axis + this.strokeWeight(2); + this.stroke(255, 0, 0); this.beginShape(this.LINES); - this.vertex(-halfSize + xOff, yOff, q * spacing - halfSize + zOff); - this.vertex(+halfSize + xOff, yOff, q * spacing - halfSize + zOff); + this.vertex(xOff, yOff, zOff); + this.vertex(xOff + size, yOff, zOff); this.endShape(); - } - - // Lines along Z axis - for (let i = 0; i <= numDivs; i++) { + // Y axis + this.stroke(0, 255, 0); this.beginShape(this.LINES); - this.vertex(i * spacing - halfSize + xOff, yOff, -halfSize + zOff); - this.vertex(i * spacing - halfSize + xOff, yOff, +halfSize + zOff); + this.vertex(xOff, yOff, zOff); + this.vertex(xOff, yOff + size, zOff); this.endShape(); - } - - this.pop(); + // Z axis + this.stroke(0, 0, 255); + this.beginShape(this.LINES); + this.vertex(xOff, yOff, zOff); + this.vertex(xOff, yOff, zOff + size); + this.endShape(); + this.pop(); + }; }; -}; +} -/** - * For use with debugMode - * @private - * @method _axesIcon - * @param {Number} [size] size of axes icon lines - * @param {Number} [xOff] offset of icon from origin in X axis - * @param {Number} [yOff] offset of icon from origin in Y axis - * @param {Number} [zOff] offset of icon from origin in Z axis - */ -p5.prototype._axesIcon = function(size, xOff, yOff, zOff) { - if (typeof size === 'undefined') { - size = this.width / 20 > 40 ? this.width / 20 : 40; - } - if (typeof xOff === 'undefined') { - xOff = -this.width / 4; - } - if (typeof yOff === 'undefined') { - yOff = xOff; - } - if (typeof zOff === 'undefined') { - zOff = xOff; - } - - return function() { - this.push(); - this._renderer.states.uModelMatrix.reset(); - - // X axis - this.strokeWeight(2); - this.stroke(255, 0, 0); - this.beginShape(this.LINES); - this.vertex(xOff, yOff, zOff); - this.vertex(xOff + size, yOff, zOff); - this.endShape(); - // Y axis - this.stroke(0, 255, 0); - this.beginShape(this.LINES); - this.vertex(xOff, yOff, zOff); - this.vertex(xOff, yOff + size, zOff); - this.endShape(); - // Z axis - this.stroke(0, 0, 255); - this.beginShape(this.LINES); - this.vertex(xOff, yOff, zOff); - this.vertex(xOff, yOff, zOff + size); - this.endShape(); - this.pop(); - }; -}; +export default interaction; -export default p5; +if(typeof p5 !== 'undefined'){ + interaction(p5, p5.prototype); +} diff --git a/src/webgl/light.js b/src/webgl/light.js index 1d17ebdc0c..4b6dda9057 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -5,1773 +5,1777 @@ * @requires core */ -import p5 from '../core/main'; - -/** - * Creates a light that shines from all directions. - * - * Ambient light does not come from one direction. Instead, 3D shapes are - * lit evenly from all sides. Ambient lights are almost always used in - * combination with other types of lights. - * - * There are three ways to call `ambientLight()` with optional parameters to - * set the light’s color. - * - * The first way to call `ambientLight()` has two parameters, `gray` and - * `alpha`. `alpha` is optional. Grayscale and alpha values between 0 and 255 - * can be passed to set the ambient light’s color, as in `ambientLight(50)` or - * `ambientLight(50, 30)`. - * - * The second way to call `ambientLight()` has one parameter, color. A - * p5.Color object, an array of color values, or a - * CSS color string, as in `ambientLight('magenta')`, can be passed to set the - * ambient light’s color. - * - * The third way to call `ambientLight()` has four parameters, `v1`, `v2`, - * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be - * passed to set the ambient light’s colors, as in `ambientLight(255, 0, 0)` - * or `ambientLight(255, 0, 0, 30)`. Color values will be interpreted using - * the current colorMode(). - * - * @method ambientLight - * @param {Number} v1 red or hue value in the current - * colorMode(). - * @param {Number} v2 green or saturation value in the current - * colorMode(). - * @param {Number} v3 blue, brightness, or lightness value in the current - * colorMode(). - * @param {Number} [alpha] alpha (transparency) value in the current - * colorMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click the canvas to turn on the light. - * - * let isLit = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn against a gray background. The sphere appears to change color when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Control the light. - * if (isLit === true) { - * // Use a grayscale value of 80. - * ambientLight(80); - * } - * - * // Draw the sphere. - * sphere(30); - * } - * - * // Turn on the ambient light when the user double-clicks. - * function doubleClicked() { - * isLit = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A faded magenta sphere drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * // Use a p5.Color object. - * let c = color('orchid'); - * ambientLight(c); - * - * // Draw the sphere. - * sphere(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A faded magenta sphere drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * // Use a CSS color string. - * ambientLight('#DA70D6'); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A faded magenta sphere drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * // Use RGB values - * ambientLight(218, 112, 214); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- */ - -/** - * @method ambientLight - * @param {Number} gray grayscale value between 0 and 255. - * @param {Number} [alpha] - * @chainable - */ - -/** - * @method ambientLight - * @param {String} value color as a CSS string. - * @chainable - */ - -/** - * @method ambientLight - * @param {Number[]} values color as an array of RGBA, HSBA, or HSLA - * values. - * @chainable - */ - -/** - * @method ambientLight - * @param {p5.Color} color color as a p5.Color object. - * @chainable - */ -p5.prototype.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; - - return this; -}; - -/** - * Sets the specular color for lights. - * - * `specularColor()` affects lights that bounce off a surface in a preferred - * direction. These lights include - * directionalLight(), - * pointLight(), and - * spotLight(). The function helps to create - * highlights on p5.Geometry objects that are - * styled with specularMaterial(). If a - * geometry does not use - * specularMaterial(), then - * `specularColor()` will have no effect. - * - * Note: `specularColor()` doesn’t affect lights that bounce in all - * directions, including ambientLight() and - * imageLight(). - * - * There are three ways to call `specularColor()` with optional parameters to - * set the specular highlight color. - * - * The first way to call `specularColor()` has two optional parameters, `gray` - * and `alpha`. Grayscale and alpha values between 0 and 255, as in - * `specularColor(50)` or `specularColor(50, 80)`, can be passed to set the - * specular highlight color. - * - * The second way to call `specularColor()` has one optional parameter, - * `color`. A p5.Color object, an array of color - * values, or a CSS color string can be passed to set the specular highlight - * color. - * - * The third way to call `specularColor()` has four optional parameters, `v1`, - * `v2`, `v3`, and `alpha`. RGBA, HSBA, or HSLA values, as in - * `specularColor(255, 0, 0, 80)`, can be passed to set the specular highlight - * color. Color values will be interpreted using the current - * colorMode(). - * - * @method specularColor - * @param {Number} v1 red or hue value in the current - * colorMode(). - * @param {Number} v2 green or saturation value in the current - * colorMode(). - * @param {Number} v3 blue, brightness, or lightness value in the current - * colorMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // No specular color. - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click the canvas to add a point light. - * - * let isLit = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn on a gray background. A spotlight starts shining when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the sphere. - * noStroke(); - * specularColor(100); - * specularMaterial(255, 255, 255); - * - * // Control the light. - * if (isLit === true) { - * // Add a white point light from the top-right. - * pointLight(255, 255, 255, 30, -20, 40); - * } - * - * // Draw the sphere. - * sphere(30); - * } - * - * // Turn on the point light when the user double-clicks. - * function doubleClicked() { - * isLit = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a specular highlight. - * // Use a p5.Color object. - * let c = color('dodgerblue'); - * specularColor(c); - * - * // Add a white point light from the top-right. - * pointLight(255, 255, 255, 30, -20, 40); - * - * // Style the sphere. - * noStroke(); - * - * // Add a white specular material. - * specularMaterial(255, 255, 255); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a specular highlight. - * // Use a CSS color string. - * specularColor('#1E90FF'); - * - * // Add a white point light from the top-right. - * pointLight(255, 255, 255, 30, -20, 40); - * - * // Style the sphere. - * noStroke(); - * - * // Add a white specular material. - * specularMaterial(255, 255, 255); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a specular highlight. - * // Use RGB values. - * specularColor(30, 144, 255); - * - * // Add a white point light from the top-right. - * pointLight(255, 255, 255, 30, -20, 40); - * - * // Style the sphere. - * noStroke(); - * - * // Add a white specular material. - * specularMaterial(255, 255, 255); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- */ - -/** - * @method specularColor - * @param {Number} gray grayscale value between 0 and 255. - * @chainable - */ - -/** - * @method specularColor - * @param {String} value color as a CSS string. - * @chainable - */ - -/** - * @method specularColor - * @param {Number[]} values color as an array of RGBA, HSBA, or HSLA - * values. - * @chainable - */ - -/** - * @method specularColor - * @param {p5.Color} color color as a p5.Color object. - * @chainable - */ -p5.prototype.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] - ]; - - return this; -}; - -/** - * Creates a light that shines in one direction. - * - * Directional lights don’t shine from a specific point. They’re like a sun - * that shines from somewhere offscreen. The light’s direction is set using - * three `(x, y, z)` values between -1 and 1. For example, setting a light’s - * direction as `(1, 0, 0)` will light p5.Geometry - * objects from the left since the light faces directly to the right. - * - * There are four ways to call `directionalLight()` with parameters to set the - * light’s color and direction. - * - * The first way to call `directionalLight()` has six parameters. The first - * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the - * current colorMode(). The last three - * parameters, `x`, `y`, and `z`, set the light’s direction. For example, - * `directionalLight(255, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light - * that shines to the right `(1, 0, 0)`. - * - * The second way to call `directionalLight()` has four parameters. The first - * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the - * current colorMode(). The last parameter, - * `direction` sets the light’s direction using a - * p5.Geometry object. For example, - * `directionalLight(255, 0, 0, lightDir)` creates a red `(255, 0, 0)` light - * that shines in the direction the `lightDir` vector points. - * - * The third way to call `directionalLight()` has four parameters. The first - * parameter, `color`, sets the light’s color using a - * p5.Color object or an array of color values. The - * last three parameters, `x`, `y`, and `z`, set the light’s direction. For - * example, `directionalLight(myColor, 1, 0, 0)` creates a light that shines - * to the right `(1, 0, 0)` with the color value of `myColor`. - * - * The fourth way to call `directionalLight()` has two parameters. The first - * parameter, `color`, sets the light’s color using a - * p5.Color object or an array of color values. The - * second parameter, `direction`, sets the light’s direction using a - * p5.Color object. For example, - * `directionalLight(myColor, lightDir)` creates a light that shines in the - * direction the `lightDir` vector points with the color value of `myColor`. - * - * @method directionalLight - * @param {Number} v1 red or hue value in the current - * colorMode(). - * @param {Number} v2 green or saturation value in the current - * colorMode(). - * @param {Number} v3 blue, brightness, or lightness value in the current - * colorMode(). - * @param {Number} x x-component of the light's direction between -1 and 1. - * @param {Number} y y-component of the light's direction between -1 and 1. - * @param {Number} z z-component of the light's direction between -1 and 1. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click to turn on the directional light. - * - * let isLit = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Control the light. - * if (isLit === true) { - * // Add a red directional light from above. - * // Use RGB values and XYZ directions. - * directionalLight(255, 0, 0, 0, 1, 0); - * } - * - * // Style the sphere. - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a red directional light from above. - * // Use a p5.Color object and XYZ directions. - * let c = color(255, 0, 0); - * directionalLight(c, 0, 1, 0); - * - * // Style the sphere. - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a red directional light from above. - * // Use a p5.Color object and a p5.Vector object. - * let c = color(255, 0, 0); - * let lightDir = createVector(0, 1, 0); - * directionalLight(c, lightDir); - * - * // Style the sphere. - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- */ - -/** - * @method directionalLight - * @param {Number} v1 - * @param {Number} v2 - * @param {Number} v3 - * @param {p5.Vector} direction direction of the light as a - * p5.Vector object. - * @chainable - */ - -/** - * @method directionalLight - * @param {p5.Color|Number[]|String} color color as a p5.Color object, - * an array of color values, or as a CSS string. - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @chainable - */ - -/** - * @method directionalLight - * @param {p5.Color|Number[]|String} color - * @param {p5.Vector} direction - * @chainable - */ -p5.prototype.directionalLight = function (v1, v2, v3, x, y, z) { - this._assert3d('directionalLight'); - 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; - - return this; -}; - -/** - * Creates a light that shines from a point in all directions. - * - * Point lights are like light bulbs that shine in all directions. They can be - * placed at different positions to achieve different lighting effects. A - * maximum of 5 point lights can be active at once. - * - * There are four ways to call `pointLight()` with parameters to set the - * light’s color and position. - * - * The first way to call `pointLight()` has six parameters. The first three - * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current - * colorMode(). The last three parameters, `x`, - * `y`, and `z`, set the light’s position. For example, - * `pointLight(255, 0, 0, 50, 0, 0)` creates a red `(255, 0, 0)` light that - * shines from the coordinates `(50, 0, 0)`. - * - * The second way to call `pointLight()` has four parameters. The first three - * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current - * colorMode(). The last parameter, position sets - * the light’s position using a p5.Vector object. - * For example, `pointLight(255, 0, 0, lightPos)` creates a red `(255, 0, 0)` - * light that shines from the position set by the `lightPos` vector. - * - * The third way to call `pointLight()` has four parameters. The first - * parameter, `color`, sets the light’s color using a - * p5.Color object or an array of color values. The - * last three parameters, `x`, `y`, and `z`, set the light’s position. For - * example, `directionalLight(myColor, 50, 0, 0)` creates a light that shines - * from the coordinates `(50, 0, 0)` with the color value of `myColor`. - * - * The fourth way to call `pointLight()` has two parameters. The first - * parameter, `color`, sets the light’s color using a - * p5.Color object or an array of color values. The - * second parameter, `position`, sets the light’s position using a - * p5.Vector object. For example, - * `directionalLight(myColor, lightPos)` creates a light that shines from the - * position set by the `lightPos` vector with the color value of `myColor`. - * - * @method pointLight - * @param {Number} v1 red or hue value in the current - * colorMode(). - * @param {Number} v2 green or saturation value in the current - * colorMode(). - * @param {Number} v3 blue, brightness, or lightness value in the current - * colorMode(). - * @param {Number} x x-coordinate of the light. - * @param {Number} y y-coordinate of the light. - * @param {Number} z z-coordinate of the light. - * @chainable - * - * @example - * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click to turn on the point light. - * - * let isLit = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Control the light. - * if (isLit === true) { - * // Add a red point light from above. - * // Use RGB values and XYZ coordinates. - * pointLight(255, 0, 0, 0, -150, 0); - * } - * - * // Style the sphere. - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - * // Turn on the point light when the user double-clicks. - * function doubleClicked() { - * isLit = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a red point light from above. - * // Use a p5.Color object and XYZ directions. - * let c = color(255, 0, 0); - * pointLight(c, 0, -150, 0); - * - * // Style the sphere. - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a red point light from above. - * // Use a p5.Color object and a p5.Vector object. - * let c = color(255, 0, 0); - * let lightPos = createVector(0, -150, 0); - * pointLight(c, lightPos); - * - * // Style the sphere. - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('Four spheres arranged in a square and drawn on a gray background. The spheres appear bright red toward the center of the square. The color gets darker toward the corners of the square.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a red point light that points to the center of the scene. - * // Use a p5.Color object and a p5.Vector object. - * let c = color(255, 0, 0); - * let lightPos = createVector(0, 0, 65); - * pointLight(c, lightPos); - * - * // Style the spheres. - * noStroke(); - * - * // Draw a sphere up and to the left. - * push(); - * translate(-25, -25, 25); - * sphere(10); - * pop(); - * - * // Draw a box up and to the right. - * push(); - * translate(25, -25, 25); - * sphere(10); - * pop(); - * - * // Draw a sphere down and to the left. - * push(); - * translate(-25, 25, 25); - * sphere(10); - * pop(); - * - * // Draw a box down and to the right. - * push(); - * translate(25, 25, 25); - * sphere(10); - * pop(); - * } - * - *
- */ - -/** - * @method pointLight - * @param {Number} v1 - * @param {Number} v2 - * @param {Number} v3 - * @param {p5.Vector} position position of the light as a - * p5.Vector object. - * @chainable - */ - -/** - * @method pointLight - * @param {p5.Color|Number[]|String} color color as a p5.Color object, - * an array of color values, or a CSS string. - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @chainable - */ - -/** - * @method pointLight - * @param {p5.Color|Number[]|String} color - * @param {p5.Vector} position - * @chainable - */ -p5.prototype.pointLight = function (v1, v2, v3, x, y, z) { - this._assert3d('pointLight'); - 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; - - return this; -}; - -/** - * Creates an ambient light from an image. - * - * `imageLight()` simulates a light shining from all directions. The effect is - * like placing the sketch at the center of a giant sphere that uses the image - * as its texture. The image's diffuse light will be affected by - * fill() and the specular reflections will be - * affected by specularMaterial() and - * shininess(). - * - * The parameter, `img`, is the p5.Image object to - * use as the light source. - * - * @method imageLight - * @param {p5.image} img image to use as the light source. - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let img; - * - * // Load an image and create a p5.Image object. - * function preload() { - * img = loadImage('assets/outdoor_spheremap.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape.'); - * } - * - * function draw() { - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the image as a panorama (360˚ background). - * panorama(img); - * - * // Add a soft ambient light. - * ambientLight(50); - * - * // Add light from the image. - * imageLight(img); - * - * // Style the sphere. - * specularMaterial(20); - * shininess(100); - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- */ -p5.prototype.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; -}; - -/** - * Creates an immersive 3D background. - * - * `panorama()` transforms images containing 360˚ content, such as maps or - * HDRIs, into immersive 3D backgrounds that surround a sketch. Exploring the - * space requires changing the camera's perspective with functions such as - * orbitControl() or - * camera(). - * - * @method panorama - * @param {p5.Image} img 360˚ image to use as the background. - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let img; - * - * // Load an image and create a p5.Image object. - * function preload() { - * img = loadImage('assets/outdoor_spheremap.jpg'); - * } - * - * function setup() { - * createCanvas(100 ,100 ,WEBGL); - * - * describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape. The full landscape is viewable in 3D as the user drags the mouse.'); - * } - * - * function draw() { - * // Add the panorama. - * panorama(img); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Use the image as a light source. - * imageLight(img); - * - * // Style the sphere. - * noStroke(); - * specularMaterial(50); - * shininess(200); - * metalness(100); - * - * // Draw the sphere. - * sphere(30); - * } - * - *
- */ -p5.prototype.panorama = function (img) { - this.filter(this._renderer._getSphereMapping(img)); -}; - -/** - * Places an ambient and directional light in the scene. - * The lights are set to ambientLight(128, 128, 128) and - * directionalLight(128, 128, 128, 0, 0, -1). - * - * Note: lights need to be called (whether directly or indirectly) - * within draw() to remain persistent in a looping program. - * Placing them in setup() will cause them to only have an effect - * the first time through the loop. - * - * @method lights - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click to turn on the lights. - * - * let isLit = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white box drawn against a gray background. The quality of the light changes when the user double-clicks.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Control the lights. - * if (isLit === true) { - * lights(); - * } - * - * // Draw the box. - * box(); - * } - * - * // Turn on the lights when the user double-clicks. - * function doubleClicked() { - * isLit = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white box drawn against a gray background.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * ambientLight(128, 128, 128); - * directionalLight(128, 128, 128, 0, 0, -1); - * - * // Draw the box. - * box(); - * } - * - *
- */ -p5.prototype.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); - return this; -}; - -/** - * Sets the falloff rate for pointLight() - * and spotLight(). - * - * A light’s falloff describes the intensity of its beam at a distance. For - * example, a lantern has a slow falloff, a flashlight has a medium falloff, - * and a laser pointer has a sharp falloff. - * - * `lightFalloff()` has three parameters, `constant`, `linear`, and - * `quadratic`. They’re numbers used to calculate falloff at a distance, `d`, - * as follows: - * - * `falloff = 1 / (constant + d * linear + (d * d) * quadratic)` - * - * Note: `constant`, `linear`, and `quadratic` should always be set to values - * greater than 0. - * - * @method lightFalloff - * @param {Number} constant constant value for calculating falloff. - * @param {Number} linear linear value for calculating falloff. - * @param {Number} quadratic quadratic value for calculating falloff. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click to change the falloff rate. - * - * let useFalloff = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A sphere drawn against a gray background. The intensity of the light changes when the user double-clicks.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Set the light falloff. - * if (useFalloff === true) { - * lightFalloff(2, 0, 0); - * } - * - * // Add a white point light from the front. - * pointLight(255, 255, 255, 0, 0, 100); - * - * // Style the sphere. - * noStroke(); - * - * // Draw the sphere. - * sphere(30); - * } - * - * // Change the falloff value when the user double-clicks. - * function doubleClicked() { - * useFalloff = true; - * } - * - *
- */ -p5.prototype.lightFalloff = function ( - constantAttenuation, - linearAttenuation, - quadraticAttenuation -) { - 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.' +function light(p5, fn){ + /** + * Creates a light that shines from all directions. + * + * Ambient light does not come from one direction. Instead, 3D shapes are + * lit evenly from all sides. Ambient lights are almost always used in + * combination with other types of lights. + * + * There are three ways to call `ambientLight()` with optional parameters to + * set the light’s color. + * + * The first way to call `ambientLight()` has two parameters, `gray` and + * `alpha`. `alpha` is optional. Grayscale and alpha values between 0 and 255 + * can be passed to set the ambient light’s color, as in `ambientLight(50)` or + * `ambientLight(50, 30)`. + * + * The second way to call `ambientLight()` has one parameter, color. A + * p5.Color object, an array of color values, or a + * CSS color string, as in `ambientLight('magenta')`, can be passed to set the + * ambient light’s color. + * + * The third way to call `ambientLight()` has four parameters, `v1`, `v2`, + * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be + * passed to set the ambient light’s colors, as in `ambientLight(255, 0, 0)` + * or `ambientLight(255, 0, 0, 30)`. Color values will be interpreted using + * the current colorMode(). + * + * @method ambientLight + * @param {Number} v1 red or hue value in the current + * colorMode(). + * @param {Number} v2 green or saturation value in the current + * colorMode(). + * @param {Number} v3 blue, brightness, or lightness value in the current + * colorMode(). + * @param {Number} [alpha] alpha (transparency) value in the current + * colorMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click the canvas to turn on the light. + * + * let isLit = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn against a gray background. The sphere appears to change color when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Control the light. + * if (isLit === true) { + * // Use a grayscale value of 80. + * ambientLight(80); + * } + * + * // Draw the sphere. + * sphere(30); + * } + * + * // Turn on the ambient light when the user double-clicks. + * function doubleClicked() { + * isLit = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A faded magenta sphere drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * // Use a p5.Color object. + * let c = color('orchid'); + * ambientLight(c); + * + * // Draw the sphere. + * sphere(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A faded magenta sphere drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * // Use a CSS color string. + * ambientLight('#DA70D6'); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A faded magenta sphere drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * // Use RGB values + * ambientLight(218, 112, 214); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ */ + + /** + * @method ambientLight + * @param {Number} gray grayscale value between 0 and 255. + * @param {Number} [alpha] + * @chainable + */ + + /** + * @method ambientLight + * @param {String} value color as a CSS string. + * @chainable + */ + + /** + * @method ambientLight + * @param {Number[]} values color as an array of RGBA, HSBA, or HSLA + * values. + * @chainable + */ + + /** + * @method ambientLight + * @param {p5.Color} color color as a p5.Color object. + * @chainable + */ + 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] ); - } - if (linearAttenuation < 0) { - linearAttenuation = 0; - console.warn( - 'Value of linear argument in lightFalloff() should be never be negative. Set to 0.' + this._renderer.states._enableLighting = true; + + return this; + }; + + /** + * Sets the specular color for lights. + * + * `specularColor()` affects lights that bounce off a surface in a preferred + * direction. These lights include + * directionalLight(), + * pointLight(), and + * spotLight(). The function helps to create + * highlights on p5.Geometry objects that are + * styled with specularMaterial(). If a + * geometry does not use + * specularMaterial(), then + * `specularColor()` will have no effect. + * + * Note: `specularColor()` doesn’t affect lights that bounce in all + * directions, including ambientLight() and + * imageLight(). + * + * There are three ways to call `specularColor()` with optional parameters to + * set the specular highlight color. + * + * The first way to call `specularColor()` has two optional parameters, `gray` + * and `alpha`. Grayscale and alpha values between 0 and 255, as in + * `specularColor(50)` or `specularColor(50, 80)`, can be passed to set the + * specular highlight color. + * + * The second way to call `specularColor()` has one optional parameter, + * `color`. A p5.Color object, an array of color + * values, or a CSS color string can be passed to set the specular highlight + * color. + * + * The third way to call `specularColor()` has four optional parameters, `v1`, + * `v2`, `v3`, and `alpha`. RGBA, HSBA, or HSLA values, as in + * `specularColor(255, 0, 0, 80)`, can be passed to set the specular highlight + * color. Color values will be interpreted using the current + * colorMode(). + * + * @method specularColor + * @param {Number} v1 red or hue value in the current + * colorMode(). + * @param {Number} v2 green or saturation value in the current + * colorMode(). + * @param {Number} v3 blue, brightness, or lightness value in the current + * colorMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // No specular color. + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click the canvas to add a point light. + * + * let isLit = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn on a gray background. A spotlight starts shining when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the sphere. + * noStroke(); + * specularColor(100); + * specularMaterial(255, 255, 255); + * + * // Control the light. + * if (isLit === true) { + * // Add a white point light from the top-right. + * pointLight(255, 255, 255, 30, -20, 40); + * } + * + * // Draw the sphere. + * sphere(30); + * } + * + * // Turn on the point light when the user double-clicks. + * function doubleClicked() { + * isLit = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a specular highlight. + * // Use a p5.Color object. + * let c = color('dodgerblue'); + * specularColor(c); + * + * // Add a white point light from the top-right. + * pointLight(255, 255, 255, 30, -20, 40); + * + * // Style the sphere. + * noStroke(); + * + * // Add a white specular material. + * specularMaterial(255, 255, 255); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a specular highlight. + * // Use a CSS color string. + * specularColor('#1E90FF'); + * + * // Add a white point light from the top-right. + * pointLight(255, 255, 255, 30, -20, 40); + * + * // Style the sphere. + * noStroke(); + * + * // Add a white specular material. + * specularMaterial(255, 255, 255); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A black sphere drawn on a gray background. An area on the surface of the sphere is highlighted in blue.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a specular highlight. + * // Use RGB values. + * specularColor(30, 144, 255); + * + * // Add a white point light from the top-right. + * pointLight(255, 255, 255, 30, -20, 40); + * + * // Style the sphere. + * noStroke(); + * + * // Add a white specular material. + * specularMaterial(255, 255, 255); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ */ + + /** + * @method specularColor + * @param {Number} gray grayscale value between 0 and 255. + * @chainable + */ + + /** + * @method specularColor + * @param {String} value color as a CSS string. + * @chainable + */ + + /** + * @method specularColor + * @param {Number[]} values color as an array of RGBA, HSBA, or HSLA + * values. + * @chainable + */ + + /** + * @method specularColor + * @param {p5.Color} color color as a p5.Color object. + * @chainable + */ + 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] + ]; + + return this; + }; + + /** + * Creates a light that shines in one direction. + * + * Directional lights don’t shine from a specific point. They’re like a sun + * that shines from somewhere offscreen. The light’s direction is set using + * three `(x, y, z)` values between -1 and 1. For example, setting a light’s + * direction as `(1, 0, 0)` will light p5.Geometry + * objects from the left since the light faces directly to the right. + * + * There are four ways to call `directionalLight()` with parameters to set the + * light’s color and direction. + * + * The first way to call `directionalLight()` has six parameters. The first + * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the + * current colorMode(). The last three + * parameters, `x`, `y`, and `z`, set the light’s direction. For example, + * `directionalLight(255, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light + * that shines to the right `(1, 0, 0)`. + * + * The second way to call `directionalLight()` has four parameters. The first + * three parameters, `v1`, `v2`, and `v3`, set the light’s color using the + * current colorMode(). The last parameter, + * `direction` sets the light’s direction using a + * p5.Geometry object. For example, + * `directionalLight(255, 0, 0, lightDir)` creates a red `(255, 0, 0)` light + * that shines in the direction the `lightDir` vector points. + * + * The third way to call `directionalLight()` has four parameters. The first + * parameter, `color`, sets the light’s color using a + * p5.Color object or an array of color values. The + * last three parameters, `x`, `y`, and `z`, set the light’s direction. For + * example, `directionalLight(myColor, 1, 0, 0)` creates a light that shines + * to the right `(1, 0, 0)` with the color value of `myColor`. + * + * The fourth way to call `directionalLight()` has two parameters. The first + * parameter, `color`, sets the light’s color using a + * p5.Color object or an array of color values. The + * second parameter, `direction`, sets the light’s direction using a + * p5.Color object. For example, + * `directionalLight(myColor, lightDir)` creates a light that shines in the + * direction the `lightDir` vector points with the color value of `myColor`. + * + * @method directionalLight + * @param {Number} v1 red or hue value in the current + * colorMode(). + * @param {Number} v2 green or saturation value in the current + * colorMode(). + * @param {Number} v3 blue, brightness, or lightness value in the current + * colorMode(). + * @param {Number} x x-component of the light's direction between -1 and 1. + * @param {Number} y y-component of the light's direction between -1 and 1. + * @param {Number} z z-component of the light's direction between -1 and 1. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click to turn on the directional light. + * + * let isLit = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Control the light. + * if (isLit === true) { + * // Add a red directional light from above. + * // Use RGB values and XYZ directions. + * directionalLight(255, 0, 0, 0, 1, 0); + * } + * + * // Style the sphere. + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a red directional light from above. + * // Use a p5.Color object and XYZ directions. + * let c = color(255, 0, 0); + * directionalLight(c, 0, 1, 0); + * + * // Style the sphere. + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a red directional light from above. + * // Use a p5.Color object and a p5.Vector object. + * let c = color(255, 0, 0); + * let lightDir = createVector(0, 1, 0); + * directionalLight(c, lightDir); + * + * // Style the sphere. + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ */ + + /** + * @method directionalLight + * @param {Number} v1 + * @param {Number} v2 + * @param {Number} v3 + * @param {p5.Vector} direction direction of the light as a + * p5.Vector object. + * @chainable + */ + + /** + * @method directionalLight + * @param {p5.Color|Number[]|String} color color as a p5.Color object, + * an array of color values, or as a CSS string. + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @chainable + */ + + /** + * @method directionalLight + * @param {p5.Color|Number[]|String} color + * @param {p5.Vector} direction + * @chainable + */ + fn.directionalLight = function (v1, v2, v3, x, y, z) { + this._assert3d('directionalLight'); + 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 ); - } - if (quadraticAttenuation < 0) { - quadraticAttenuation = 0; - console.warn( - 'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.' + this._renderer.states._enableLighting = true; + + return this; + }; + + /** + * Creates a light that shines from a point in all directions. + * + * Point lights are like light bulbs that shine in all directions. They can be + * placed at different positions to achieve different lighting effects. A + * maximum of 5 point lights can be active at once. + * + * There are four ways to call `pointLight()` with parameters to set the + * light’s color and position. + * + * The first way to call `pointLight()` has six parameters. The first three + * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current + * colorMode(). The last three parameters, `x`, + * `y`, and `z`, set the light’s position. For example, + * `pointLight(255, 0, 0, 50, 0, 0)` creates a red `(255, 0, 0)` light that + * shines from the coordinates `(50, 0, 0)`. + * + * The second way to call `pointLight()` has four parameters. The first three + * parameters, `v1`, `v2`, and `v3`, set the light’s color using the current + * colorMode(). The last parameter, position sets + * the light’s position using a p5.Vector object. + * For example, `pointLight(255, 0, 0, lightPos)` creates a red `(255, 0, 0)` + * light that shines from the position set by the `lightPos` vector. + * + * The third way to call `pointLight()` has four parameters. The first + * parameter, `color`, sets the light’s color using a + * p5.Color object or an array of color values. The + * last three parameters, `x`, `y`, and `z`, set the light’s position. For + * example, `directionalLight(myColor, 50, 0, 0)` creates a light that shines + * from the coordinates `(50, 0, 0)` with the color value of `myColor`. + * + * The fourth way to call `pointLight()` has two parameters. The first + * parameter, `color`, sets the light’s color using a + * p5.Color object or an array of color values. The + * second parameter, `position`, sets the light’s position using a + * p5.Vector object. For example, + * `directionalLight(myColor, lightPos)` creates a light that shines from the + * position set by the `lightPos` vector with the color value of `myColor`. + * + * @method pointLight + * @param {Number} v1 red or hue value in the current + * colorMode(). + * @param {Number} v2 green or saturation value in the current + * colorMode(). + * @param {Number} v3 blue, brightness, or lightness value in the current + * colorMode(). + * @param {Number} x x-coordinate of the light. + * @param {Number} y y-coordinate of the light. + * @param {Number} z z-coordinate of the light. + * @chainable + * + * @example + * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click to turn on the point light. + * + * let isLit = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn on a gray background. A red light starts shining from above when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Control the light. + * if (isLit === true) { + * // Add a red point light from above. + * // Use RGB values and XYZ coordinates. + * pointLight(255, 0, 0, 0, -150, 0); + * } + * + * // Style the sphere. + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + * // Turn on the point light when the user double-clicks. + * function doubleClicked() { + * isLit = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a red point light from above. + * // Use a p5.Color object and XYZ directions. + * let c = color(255, 0, 0); + * pointLight(c, 0, -150, 0); + * + * // Style the sphere. + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn on a gray background. The top of the sphere appears bright red. The color gets darker toward the bottom.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a red point light from above. + * // Use a p5.Color object and a p5.Vector object. + * let c = color(255, 0, 0); + * let lightPos = createVector(0, -150, 0); + * pointLight(c, lightPos); + * + * // Style the sphere. + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('Four spheres arranged in a square and drawn on a gray background. The spheres appear bright red toward the center of the square. The color gets darker toward the corners of the square.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Add a red point light that points to the center of the scene. + * // Use a p5.Color object and a p5.Vector object. + * let c = color(255, 0, 0); + * let lightPos = createVector(0, 0, 65); + * pointLight(c, lightPos); + * + * // Style the spheres. + * noStroke(); + * + * // Draw a sphere up and to the left. + * push(); + * translate(-25, -25, 25); + * sphere(10); + * pop(); + * + * // Draw a box up and to the right. + * push(); + * translate(25, -25, 25); + * sphere(10); + * pop(); + * + * // Draw a sphere down and to the left. + * push(); + * translate(-25, 25, 25); + * sphere(10); + * pop(); + * + * // Draw a box down and to the right. + * push(); + * translate(25, 25, 25); + * sphere(10); + * pop(); + * } + * + *
+ */ + + /** + * @method pointLight + * @param {Number} v1 + * @param {Number} v2 + * @param {Number} v3 + * @param {p5.Vector} position position of the light as a + * p5.Vector object. + * @chainable + */ + + /** + * @method pointLight + * @param {p5.Color|Number[]|String} color color as a p5.Color object, + * an array of color values, or a CSS string. + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @chainable + */ + + /** + * @method pointLight + * @param {p5.Color|Number[]|String} color + * @param {p5.Vector} position + * @chainable + */ + fn.pointLight = function (v1, v2, v3, x, y, z) { + this._assert3d('pointLight'); + 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 ); - } - if ( - constantAttenuation === 0 && - (linearAttenuation === 0 && quadraticAttenuation === 0) + this._renderer.states._enableLighting = true; + + return this; + }; + + /** + * Creates an ambient light from an image. + * + * `imageLight()` simulates a light shining from all directions. The effect is + * like placing the sketch at the center of a giant sphere that uses the image + * as its texture. The image's diffuse light will be affected by + * fill() and the specular reflections will be + * affected by specularMaterial() and + * shininess(). + * + * The parameter, `img`, is the p5.Image object to + * use as the light source. + * + * @method imageLight + * @param {p5.image} img image to use as the light source. + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let img; + * + * // Load an image and create a p5.Image object. + * function preload() { + * img = loadImage('assets/outdoor_spheremap.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape.'); + * } + * + * function draw() { + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the image as a panorama (360˚ background). + * panorama(img); + * + * // Add a soft ambient light. + * ambientLight(50); + * + * // Add light from the image. + * imageLight(img); + * + * // Style the sphere. + * specularMaterial(20); + * shininess(100); + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ */ + 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; + }; + + /** + * Creates an immersive 3D background. + * + * `panorama()` transforms images containing 360˚ content, such as maps or + * HDRIs, into immersive 3D backgrounds that surround a sketch. Exploring the + * space requires changing the camera's perspective with functions such as + * orbitControl() or + * camera(). + * + * @method panorama + * @param {p5.Image} img 360˚ image to use as the background. + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let img; + * + * // Load an image and create a p5.Image object. + * function preload() { + * img = loadImage('assets/outdoor_spheremap.jpg'); + * } + * + * function setup() { + * createCanvas(100 ,100 ,WEBGL); + * + * describe('A sphere floating above a landscape. The surface of the sphere reflects the landscape. The full landscape is viewable in 3D as the user drags the mouse.'); + * } + * + * function draw() { + * // Add the panorama. + * panorama(img); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Use the image as a light source. + * imageLight(img); + * + * // Style the sphere. + * noStroke(); + * specularMaterial(50); + * shininess(200); + * metalness(100); + * + * // Draw the sphere. + * sphere(30); + * } + * + *
+ */ + fn.panorama = function (img) { + this.filter(this._renderer._getSphereMapping(img)); + }; + + /** + * Places an ambient and directional light in the scene. + * The lights are set to ambientLight(128, 128, 128) and + * directionalLight(128, 128, 128, 0, 0, -1). + * + * Note: lights need to be called (whether directly or indirectly) + * within draw() to remain persistent in a looping program. + * Placing them in setup() will cause them to only have an effect + * the first time through the loop. + * + * @method lights + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click to turn on the lights. + * + * let isLit = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white box drawn against a gray background. The quality of the light changes when the user double-clicks.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Control the lights. + * if (isLit === true) { + * lights(); + * } + * + * // Draw the box. + * box(); + * } + * + * // Turn on the lights when the user double-clicks. + * function doubleClicked() { + * isLit = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white box drawn against a gray background.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * ambientLight(128, 128, 128); + * directionalLight(128, 128, 128, 0, 0, -1); + * + * // Draw the box. + * box(); + * } + * + *
+ */ + 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); + return this; + }; + + /** + * Sets the falloff rate for pointLight() + * and spotLight(). + * + * A light’s falloff describes the intensity of its beam at a distance. For + * example, a lantern has a slow falloff, a flashlight has a medium falloff, + * and a laser pointer has a sharp falloff. + * + * `lightFalloff()` has three parameters, `constant`, `linear`, and + * `quadratic`. They’re numbers used to calculate falloff at a distance, `d`, + * as follows: + * + * `falloff = 1 / (constant + d * linear + (d * d) * quadratic)` + * + * Note: `constant`, `linear`, and `quadratic` should always be set to values + * greater than 0. + * + * @method lightFalloff + * @param {Number} constant constant value for calculating falloff. + * @param {Number} linear linear value for calculating falloff. + * @param {Number} quadratic quadratic value for calculating falloff. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click to change the falloff rate. + * + * let useFalloff = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A sphere drawn against a gray background. The intensity of the light changes when the user double-clicks.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Set the light falloff. + * if (useFalloff === true) { + * lightFalloff(2, 0, 0); + * } + * + * // Add a white point light from the front. + * pointLight(255, 255, 255, 0, 0, 100); + * + * // Style the sphere. + * noStroke(); + * + * // Draw the sphere. + * sphere(30); + * } + * + * // Change the falloff value when the user double-clicks. + * function doubleClicked() { + * useFalloff = true; + * } + * + *
+ */ + fn.lightFalloff = function ( + constantAttenuation, + linearAttenuation, + quadraticAttenuation ) { - constantAttenuation = 1; - console.warn( - 'Either one of the three arguments in lightFalloff() should be greater than zero. Set constant argument to 1.' - ); - } + this._assert3d('lightFalloff'); + p5._validateParameters('lightFalloff', arguments); - this._renderer.states.constantAttenuation = constantAttenuation; - this._renderer.states.linearAttenuation = linearAttenuation; - this._renderer.states.quadraticAttenuation = quadraticAttenuation; + if (constantAttenuation < 0) { + constantAttenuation = 0; + console.warn( + 'Value of constant argument in lightFalloff() should be never be negative. Set to 0.' + ); + } - return this; -}; + if (linearAttenuation < 0) { + linearAttenuation = 0; + console.warn( + 'Value of linear argument in lightFalloff() should be never be negative. Set to 0.' + ); + } -/** - * Creates a light that shines from a point in one direction. - * - * Spot lights are like flashlights that shine in one direction creating a - * cone of light. The shape of the cone can be controlled using the angle and - * concentration parameters. A maximum of 5 spot lights can be active at once. - * - * There are eight ways to call `spotLight()` with parameters to set the - * light’s color, position, direction. For example, - * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light - * at the origin `(0, 0, 0)` that points to the right `(1, 0, 0)`. - * - * The `angle` parameter is optional. It sets the radius of the light cone. - * For example, `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16)` creates a - * red `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right - * `(1, 0, 0)` with an angle of `PI / 16` radians. By default, `angle` is - * `PI / 3` radians. - * - * The `concentration` parameter is also optional. It focuses the light - * towards the center of the light cone. For example, - * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16, 50)` creates a red - * `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right - * `(1, 0, 0)` with an angle of `PI / 16` radians at concentration of 50. By - * default, `concentration` is 100. - * - * @method spotLight - * @param {Number} v1 red or hue value in the current - * colorMode(). - * @param {Number} v2 green or saturation value in the current - * colorMode(). - * @param {Number} v3 blue, brightness, or lightness value in the current - * colorMode(). - * @param {Number} x x-coordinate of the light. - * @param {Number} y y-coordinate of the light. - * @param {Number} z z-coordinate of the light. - * @param {Number} rx x-component of light direction between -1 and 1. - * @param {Number} ry y-component of light direction between -1 and 1. - * @param {Number} rz z-component of light direction between -1 and 1. - * @param {Number} [angle] angle of the light cone. Defaults to `PI / 3`. - * @param {Number} [concentration] concentration of the light. Defaults to 100. - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click to adjust the spotlight. - * - * let isLit = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Control the spotlight. - * if (isLit === true) { - * // Add a red spot light that shines into the screen. - * // Set its angle to PI / 32 radians. - * spotLight(255, 0, 0, 0, 0, 100, 0, 0, -1, PI / 32); - * } - * - * // Draw the sphere. - * sphere(30); - * } - * - * // Turn on the spotlight when the user double-clicks. - * function doubleClicked() { - * isLit = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click to adjust the spotlight. - * - * let isLit = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Control the spotlight. - * if (isLit === true) { - * // Add a red spot light that shines into the screen. - * // Set its angle to PI / 3 radians (default). - * // Set its concentration to 1000. - * let c = color(255, 0, 0); - * let position = createVector(0, 0, 100); - * let direction = createVector(0, 0, -1); - * spotLight(c, position, direction, PI / 3, 1000); - * } - * - * // Draw the sphere. - * sphere(30); - * } - * - * // Turn on the spotlight when the user double-clicks. - * function doubleClicked() { - * isLit = true; - * } - * - *
- */ -/** - * @method spotLight - * @param {p5.Color|Number[]|String} color color as a p5.Color object, - * an array of color values, or a CSS string. - * @param {p5.Vector} position position of the light as a p5.Vector object. - * @param {p5.Vector} direction direction of light as a p5.Vector object. - * @param {Number} [angle] - * @param {Number} [concentration] - */ -/** - * @method spotLight - * @param {Number} v1 - * @param {Number} v2 - * @param {Number} v3 - * @param {p5.Vector} position - * @param {p5.Vector} direction - * @param {Number} [angle] - * @param {Number} [concentration] - */ -/** - * @method spotLight - * @param {p5.Color|Number[]|String} color - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @param {p5.Vector} direction - * @param {Number} [angle] - * @param {Number} [concentration] - */ -/** - * @method spotLight - * @param {p5.Color|Number[]|String} color - * @param {p5.Vector} position - * @param {Number} rx - * @param {Number} ry - * @param {Number} rz - * @param {Number} [angle] - * @param {Number} [concentration] - */ -/** - * @method spotLight - * @param {Number} v1 - * @param {Number} v2 - * @param {Number} v3 - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @param {p5.Vector} direction - * @param {Number} [angle] - * @param {Number} [concentration] - */ -/** - * @method spotLight - * @param {Number} v1 - * @param {Number} v2 - * @param {Number} v3 - * @param {p5.Vector} position - * @param {Number} rx - * @param {Number} ry - * @param {Number} rz - * @param {Number} [angle] - * @param {Number} [concentration] - */ -/** - * @method spotLight - * @param {p5.Color|Number[]|String} color - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @param {Number} rx - * @param {Number} ry - * @param {Number} rz - * @param {Number} [angle] - * @param {Number} [concentration] - */ -p5.prototype.spotLight = function ( - v1, - v2, - v3, - x, - y, - z, - nx, - ny, - nz, - angle, - concentration -) { - this._assert3d('spotLight'); - p5._validateParameters('spotLight', arguments); - - let color, position, direction; - const length = arguments.length; - - switch (length) { - case 11: - case 10: - color = this.color(v1, v2, v3); - position = new p5.Vector(x, y, z); - direction = new p5.Vector(nx, ny, nz); - break; + if (quadraticAttenuation < 0) { + quadraticAttenuation = 0; + console.warn( + 'Value of quadratic argument in lightFalloff() should be never be negative. Set to 0.' + ); + } - case 9: - if (v1 instanceof p5.Color) { - color = v1; - position = new p5.Vector(v2, v3, x); - direction = new p5.Vector(y, z, nx); - angle = ny; - concentration = nz; - } else if (x instanceof p5.Vector) { - color = this.color(v1, v2, v3); - position = x; - direction = new p5.Vector(y, z, nx); - angle = ny; - concentration = nz; - } else if (nx instanceof p5.Vector) { - color = this.color(v1, v2, v3); - position = new p5.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); - } - break; + 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; + + return this; + }; + + /** + * Creates a light that shines from a point in one direction. + * + * Spot lights are like flashlights that shine in one direction creating a + * cone of light. The shape of the cone can be controlled using the angle and + * concentration parameters. A maximum of 5 spot lights can be active at once. + * + * There are eight ways to call `spotLight()` with parameters to set the + * light’s color, position, direction. For example, + * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0)` creates a red `(255, 0, 0)` light + * at the origin `(0, 0, 0)` that points to the right `(1, 0, 0)`. + * + * The `angle` parameter is optional. It sets the radius of the light cone. + * For example, `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16)` creates a + * red `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right + * `(1, 0, 0)` with an angle of `PI / 16` radians. By default, `angle` is + * `PI / 3` radians. + * + * The `concentration` parameter is also optional. It focuses the light + * towards the center of the light cone. For example, + * `spotLight(255, 0, 0, 0, 0, 0, 1, 0, 0, PI / 16, 50)` creates a red + * `(255, 0, 0)` light at the origin `(0, 0, 0)` that points to the right + * `(1, 0, 0)` with an angle of `PI / 16` radians at concentration of 50. By + * default, `concentration` is 100. + * + * @method spotLight + * @param {Number} v1 red or hue value in the current + * colorMode(). + * @param {Number} v2 green or saturation value in the current + * colorMode(). + * @param {Number} v3 blue, brightness, or lightness value in the current + * colorMode(). + * @param {Number} x x-coordinate of the light. + * @param {Number} y y-coordinate of the light. + * @param {Number} z z-coordinate of the light. + * @param {Number} rx x-component of light direction between -1 and 1. + * @param {Number} ry y-component of light direction between -1 and 1. + * @param {Number} rz z-component of light direction between -1 and 1. + * @param {Number} [angle] angle of the light cone. Defaults to `PI / 3`. + * @param {Number} [concentration] concentration of the light. Defaults to 100. + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click to adjust the spotlight. + * + * let isLit = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Control the spotlight. + * if (isLit === true) { + * // Add a red spot light that shines into the screen. + * // Set its angle to PI / 32 radians. + * spotLight(255, 0, 0, 0, 0, 100, 0, 0, -1, PI / 32); + * } + * + * // Draw the sphere. + * sphere(30); + * } + * + * // Turn on the spotlight when the user double-clicks. + * function doubleClicked() { + * isLit = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click to adjust the spotlight. + * + * let isLit = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white sphere drawn on a gray background. A red spotlight starts shining when the user double-clicks.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Control the spotlight. + * if (isLit === true) { + * // Add a red spot light that shines into the screen. + * // Set its angle to PI / 3 radians (default). + * // Set its concentration to 1000. + * let c = color(255, 0, 0); + * let position = createVector(0, 0, 100); + * let direction = createVector(0, 0, -1); + * spotLight(c, position, direction, PI / 3, 1000); + * } + * + * // Draw the sphere. + * sphere(30); + * } + * + * // Turn on the spotlight when the user double-clicks. + * function doubleClicked() { + * isLit = true; + * } + * + *
+ */ + /** + * @method spotLight + * @param {p5.Color|Number[]|String} color color as a p5.Color object, + * an array of color values, or a CSS string. + * @param {p5.Vector} position position of the light as a p5.Vector object. + * @param {p5.Vector} direction direction of light as a p5.Vector object. + * @param {Number} [angle] + * @param {Number} [concentration] + */ + /** + * @method spotLight + * @param {Number} v1 + * @param {Number} v2 + * @param {Number} v3 + * @param {p5.Vector} position + * @param {p5.Vector} direction + * @param {Number} [angle] + * @param {Number} [concentration] + */ + /** + * @method spotLight + * @param {p5.Color|Number[]|String} color + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @param {p5.Vector} direction + * @param {Number} [angle] + * @param {Number} [concentration] + */ + /** + * @method spotLight + * @param {p5.Color|Number[]|String} color + * @param {p5.Vector} position + * @param {Number} rx + * @param {Number} ry + * @param {Number} rz + * @param {Number} [angle] + * @param {Number} [concentration] + */ + /** + * @method spotLight + * @param {Number} v1 + * @param {Number} v2 + * @param {Number} v3 + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @param {p5.Vector} direction + * @param {Number} [angle] + * @param {Number} [concentration] + */ + /** + * @method spotLight + * @param {Number} v1 + * @param {Number} v2 + * @param {Number} v3 + * @param {p5.Vector} position + * @param {Number} rx + * @param {Number} ry + * @param {Number} rz + * @param {Number} [angle] + * @param {Number} [concentration] + */ + /** + * @method spotLight + * @param {p5.Color|Number[]|String} color + * @param {Number} x + * @param {Number} y + * @param {Number} z + * @param {Number} rx + * @param {Number} ry + * @param {Number} rz + * @param {Number} [angle] + * @param {Number} [concentration] + */ + fn.spotLight = function ( + v1, + v2, + v3, + x, + y, + z, + nx, + ny, + nz, + angle, + concentration + ) { + this._assert3d('spotLight'); + p5._validateParameters('spotLight', arguments); - case 8: - if (v1 instanceof p5.Color) { - color = v1; - position = new p5.Vector(v2, v3, x); - direction = new p5.Vector(y, z, nx); - angle = ny; - } else if (x instanceof p5.Vector) { - color = this.color(v1, v2, v3); - position = x; - direction = new p5.Vector(y, z, nx); - angle = ny; - } else { - color = this.color(v1, v2, v3); - position = new p5.Vector(x, y, z); - direction = nx; - angle = ny; - } - break; + let color, position, direction; + const length = arguments.length; - case 7: - if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { - color = v1; - position = v2; - direction = new p5.Vector(v3, x, y); - angle = z; - concentration = nx; - } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { - color = v1; - position = new p5.Vector(v2, v3, x); - direction = y; - angle = z; - concentration = nx; - } else if (x instanceof p5.Vector && y instanceof p5.Vector) { - color = this.color(v1, v2, v3); - position = x; - direction = y; - angle = z; - 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) { - color = this.color(v1, v2, v3); - position = x; - direction = new p5.Vector(y, z, nx); - } else { + switch (length) { + case 11: + case 10: color = this.color(v1, v2, v3); position = new p5.Vector(x, y, z); - direction = nx; - } - break; - - case 6: - if (x instanceof p5.Vector && y instanceof p5.Vector) { - color = this.color(v1, v2, v3); - position = x; - direction = y; - angle = z; - } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { - color = v1; - position = new p5.Vector(v2, v3, x); - direction = y; - angle = z; - } else if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { - color = v1; - position = v2; - direction = new p5.Vector(v3, x, y); - angle = z; - } - break; - - case 5: - if ( - v1 instanceof p5.Color && - v2 instanceof p5.Vector && - v3 instanceof p5.Vector - ) { + direction = new p5.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); + angle = ny; + concentration = nz; + } else if (x instanceof p5.Vector) { + color = this.color(v1, v2, v3); + position = x; + direction = new p5.Vector(y, z, nx); + angle = ny; + concentration = nz; + } else if (nx instanceof p5.Vector) { + color = this.color(v1, v2, v3); + position = new p5.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); + } + break; + + case 8: + if (v1 instanceof p5.Color) { + color = v1; + position = new p5.Vector(v2, v3, x); + direction = new p5.Vector(y, z, nx); + angle = ny; + } else if (x instanceof p5.Vector) { + color = this.color(v1, v2, v3); + position = x; + direction = new p5.Vector(y, z, nx); + angle = ny; + } else { + color = this.color(v1, v2, v3); + position = new p5.Vector(x, y, z); + direction = nx; + angle = ny; + } + break; + + case 7: + if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { + color = v1; + position = v2; + direction = new p5.Vector(v3, x, y); + angle = z; + concentration = nx; + } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { + color = v1; + position = new p5.Vector(v2, v3, x); + direction = y; + angle = z; + concentration = nx; + } else if (x instanceof p5.Vector && y instanceof p5.Vector) { + color = this.color(v1, v2, v3); + position = x; + direction = y; + angle = z; + 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) { + color = this.color(v1, v2, v3); + position = x; + direction = new p5.Vector(y, z, nx); + } else { + color = this.color(v1, v2, v3); + position = new p5.Vector(x, y, z); + direction = nx; + } + break; + + case 6: + if (x instanceof p5.Vector && y instanceof p5.Vector) { + color = this.color(v1, v2, v3); + position = x; + direction = y; + angle = z; + } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { + color = v1; + position = new p5.Vector(v2, v3, x); + direction = y; + angle = z; + } else if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { + color = v1; + position = v2; + direction = new p5.Vector(v3, x, y); + angle = z; + } + break; + + case 5: + if ( + v1 instanceof p5.Color && + v2 instanceof p5.Vector && + v3 instanceof p5.Vector + ) { + color = v1; + position = v2; + direction = v3; + angle = x; + concentration = y; + } else if (x instanceof p5.Vector && y instanceof p5.Vector) { + color = this.color(v1, v2, v3); + position = x; + direction = y; + } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { + color = v1; + position = new p5.Vector(v2, v3, x); + direction = y; + } else if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { + color = v1; + position = v2; + direction = new p5.Vector(v3, x, y); + } + break; + + case 4: color = v1; position = v2; direction = v3; angle = x; - concentration = y; - } else if (x instanceof p5.Vector && y instanceof p5.Vector) { - color = this.color(v1, v2, v3); - position = x; - direction = y; - } else if (v1 instanceof p5.Color && y instanceof p5.Vector) { - color = v1; - position = new p5.Vector(v2, v3, x); - direction = y; - } else if (v1 instanceof p5.Color && v2 instanceof p5.Vector) { + break; + + case 3: color = v1; position = v2; - direction = new p5.Vector(v3, x, y); - } - break; - - case 4: - color = v1; - position = v2; - direction = v3; - angle = x; - break; - - case 3: - color = v1; - position = v2; - direction = v3; - break; - - default: + direction = v3; + break; + + default: + console.warn( + `Sorry, input for spotlight() is not in prescribed format. Too ${ + length < 3 ? 'few' : 'many' + } arguments were provided` + ); + return this; + } + this._renderer.states.spotLightDiffuseColors = [ + color._array[0], + color._array[1], + color._array[2] + ]; + + this._renderer.states.spotLightSpecularColors = [ + ...this._renderer.states.specularColors + ]; + + this._renderer.states.spotLightPositions = [position.x, position.y, position.z]; + direction.normalize(); + this._renderer.states.spotLightDirections = [ + direction.x, + direction.y, + direction.z + ]; + + if (angle === undefined) { + angle = Math.PI / 3; + } + + if (concentration !== undefined && concentration < 1) { + concentration = 1; console.warn( - `Sorry, input for spotlight() is not in prescribed format. Too ${ - length < 3 ? 'few' : 'many' - } arguments were provided` + 'Value of concentration needs to be greater than 1. Setting it to 1' ); - return this; - } - this._renderer.states.spotLightDiffuseColors = [ - color._array[0], - color._array[1], - color._array[2] - ]; - - this._renderer.states.spotLightSpecularColors = [ - ...this._renderer.states.specularColors - ]; - - this._renderer.states.spotLightPositions = [position.x, position.y, position.z]; - direction.normalize(); - this._renderer.states.spotLightDirections = [ - direction.x, - direction.y, - direction.z - ]; - - if (angle === undefined) { - angle = Math.PI / 3; - } - - if (concentration !== undefined && concentration < 1) { - concentration = 1; - console.warn( - 'Value of concentration needs to be greater than 1. Setting it to 1' - ); - } else if (concentration === undefined) { - 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; - - 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(); - * } - * - *
- */ -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; - - return this; -}; - -export default p5; + } else if (concentration === undefined) { + 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; + + 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.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; + + return this; + }; +} + +export default light; + +if(typeof p5 !== 'undefined'){ + light(p5, p5.prototype); +} diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 414c62315b..9eae577b57 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -6,470 +6,433 @@ * @requires p5.Geometry */ -import p5 from '../core/main'; -import './p5.Geometry'; - -/** - * Loads a 3D model to create a - * p5.Geometry object. - * - * `loadModel()` can load 3D models from OBJ and STL files. Once the model is - * loaded, it can be displayed with the - * model() function, as in `model(shape)`. - * - * There are three ways to call `loadModel()` with optional parameters to help - * process the model. - * - * The first parameter, `path`, is always a `String` with the path to the - * file. Paths to local files should be relative, as in - * `loadModel('assets/model.obj')`. URLs such as - * `'https://example.com/model.obj'` may be blocked due to browser security. - * - * The first way to call `loadModel()` has three optional parameters after the - * file path. The first optional parameter, `successCallback`, is a function - * to call once the model loads. For example, - * `loadModel('assets/model.obj', handleModel)` will call the `handleModel()` - * function once the model loads. The second optional parameter, - * `failureCallback`, is a function to call if the model fails to load. For - * example, `loadModel('assets/model.obj', handleModel, handleFailure)` will - * call the `handleFailure()` function if an error occurs while loading. The - * third optional parameter, `fileType`, is the model’s file extension as a - * string. For example, - * `loadModel('assets/model', handleModel, handleFailure, '.obj')` will try to - * load the file model as a `.obj` file. - * - * The second way to call `loadModel()` has four optional parameters after the - * file path. The first optional parameter is a `Boolean` value. If `true` is - * passed, as in `loadModel('assets/model.obj', true)`, then the model will be - * resized to ensure it fits the canvas. The next three parameters are - * `successCallback`, `failureCallback`, and `fileType` as described above. - * - * The third way to call `loadModel()` has one optional parameter after the - * file path. The optional parameter, `options`, is an `Object` with options, - * as in `loadModel('assets/model.obj', options)`. The `options` object can - * have the following properties: - * - * ```js - * let options = { - * // Enables standardized size scaling during loading if set to true. - * normalize: true, - * - * // Function to call once the model loads. - * successCallback: handleModel, - * - * // Function to call if an error occurs while loading. - * failureCallback: handleError, - * - * // Model's file extension. - * fileType: '.stl', - * - * // Flips the U texture coordinates of the model. - * flipU: false, - * - * // Flips the V texture coordinates of the model. - * flipV: false - * }; - * - * // Pass the options object to loadModel(). - * loadModel('assets/model.obj', options); - * ``` - * - * Models can take time to load. Calling `loadModel()` in - * preload() ensures models load before they're - * used in setup() or draw(). - * - * Note: There’s no support for colored STL files. STL files with color will - * be rendered without color. - * - * @method loadModel - * @param {String} path path of the model to be loaded. - * @param {Boolean} normalize if `true`, scale the model to fit the canvas. - * @param {function(p5.Geometry)} [successCallback] function to call once the model is loaded. Will be passed - * the p5.Geometry object. - * @param {function(Event)} [failureCallback] function to call if the model fails to load. Will be passed an `Error` event object. - * @param {String} [fileType] model’s file extension. Either `'.obj'` or `'.stl'`. - * @return {p5.Geometry} the p5.Geometry object - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * // Load the file and create a p5.Geometry object. - * function preload() { - * shape = loadModel('assets/teapot.obj'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white teapot drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the shape. - * model(shape); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * // Load the file and create a p5.Geometry object. - * // Normalize the geometry's size to fit the canvas. - * function preload() { - * shape = loadModel('assets/teapot.obj', true); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white teapot drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the shape. - * model(shape); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * // Load the file and create a p5.Geometry object. - * function preload() { - * loadModel('assets/teapot.obj', true, handleModel); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white teapot drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the shape. - * model(shape); - * } - * - * // Set the shape variable and log the geometry's - * // ID to the console. - * function handleModel(data) { - * shape = data; - * console.log(shape.gid); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * // Load the file and create a p5.Geometry object. - * function preload() { - * loadModel('assets/wrong.obj', true, handleModel, handleError); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white teapot drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the shape. - * model(shape); - * } - * - * // Set the shape variable and print the geometry's - * // ID to the console. - * function handleModel(data) { - * shape = data; - * console.log(shape.gid); - * } - * - * // Print an error message if the file doesn't load. - * function handleError(error) { - * console.error('Oops!', error); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * // Load the file and create a p5.Geometry object. - * function preload() { - * loadModel('assets/teapot.obj', true, handleModel, handleError, '.obj'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white teapot drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the shape. - * model(shape); - * } - * - * // Set the shape variable and print the geometry's - * // ID to the console. - * function handleModel(data) { - * shape = data; - * console.log(shape.gid); - * } - * - * // Print an error message if the file doesn't load. - * function handleError(error) { - * console.error('Oops!', error); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * let options = { - * normalize: true, - * successCallback: handleModel, - * failureCallback: handleError, - * fileType: '.obj' - * }; - * - * // Load the file and create a p5.Geometry object. - * function preload() { - * loadModel('assets/teapot.obj', options); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white teapot drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the shape. - * model(shape); - * } - * - * // Set the shape variable and print the geometry's - * // ID to the console. - * function handleModel(data) { - * shape = data; - * console.log(shape.gid); - * } - * - * // Print an error message if the file doesn't load. - * function handleError(error) { - * console.error('Oops!', error); - * } - * - *
- */ -/** - * @method loadModel - * @param {String} path - * @param {function(p5.Geometry)} [successCallback] - * @param {function(Event)} [failureCallback] - * @param {String} [fileType] - * @return {p5.Geometry} new p5.Geometry object. - */ -/** - * @method loadModel - * @param {String} path - * @param {Object} [options] loading options. - * @param {function(p5.Geometry)} [options.successCallback] - * @param {function(Event)} [options.failureCallback] - * @param {String} [options.fileType] - * @param {Boolean} [options.normalize] - * @param {Boolean} [options.flipU] - * @param {Boolean} [options.flipV] - * @return {p5.Geometry} new p5.Geometry object. - */ -p5.prototype.loadModel = async function (path, options) { - p5._validateParameters('loadModel', arguments); - let normalize = false; - let successCallback; - let failureCallback; - let flipU = false; - let flipV = false; - let fileType = path.slice(-4); - if (options && typeof options === 'object') { - normalize = options.normalize || false; - successCallback = options.successCallback; - failureCallback = options.failureCallback; - fileType = options.fileType || fileType; - flipU = options.flipU || false; - flipV = options.flipV || false; - } else if (typeof options === 'boolean') { - normalize = options; - successCallback = arguments[2]; - failureCallback = arguments[3]; - if (typeof arguments[4] !== 'undefined') { - fileType = arguments[4]; - } - } else { - successCallback = typeof arguments[1] === 'function' ? arguments[1] : undefined; - failureCallback = arguments[2]; - if (typeof arguments[3] !== 'undefined') { - fileType = arguments[3]; +function loading(p5, fn){ + /** + * Loads a 3D model to create a + * p5.Geometry object. + * + * `loadModel()` can load 3D models from OBJ and STL files. Once the model is + * loaded, it can be displayed with the + * model() function, as in `model(shape)`. + * + * There are three ways to call `loadModel()` with optional parameters to help + * process the model. + * + * The first parameter, `path`, is always a `String` with the path to the + * file. Paths to local files should be relative, as in + * `loadModel('assets/model.obj')`. URLs such as + * `'https://example.com/model.obj'` may be blocked due to browser security. + * + * The first way to call `loadModel()` has three optional parameters after the + * file path. The first optional parameter, `successCallback`, is a function + * to call once the model loads. For example, + * `loadModel('assets/model.obj', handleModel)` will call the `handleModel()` + * function once the model loads. The second optional parameter, + * `failureCallback`, is a function to call if the model fails to load. For + * example, `loadModel('assets/model.obj', handleModel, handleFailure)` will + * call the `handleFailure()` function if an error occurs while loading. The + * third optional parameter, `fileType`, is the model’s file extension as a + * string. For example, + * `loadModel('assets/model', handleModel, handleFailure, '.obj')` will try to + * load the file model as a `.obj` file. + * + * The second way to call `loadModel()` has four optional parameters after the + * file path. The first optional parameter is a `Boolean` value. If `true` is + * passed, as in `loadModel('assets/model.obj', true)`, then the model will be + * resized to ensure it fits the canvas. The next three parameters are + * `successCallback`, `failureCallback`, and `fileType` as described above. + * + * The third way to call `loadModel()` has one optional parameter after the + * file path. The optional parameter, `options`, is an `Object` with options, + * as in `loadModel('assets/model.obj', options)`. The `options` object can + * have the following properties: + * + * ```js + * let options = { + * // Enables standardized size scaling during loading if set to true. + * normalize: true, + * + * // Function to call once the model loads. + * successCallback: handleModel, + * + * // Function to call if an error occurs while loading. + * failureCallback: handleError, + * + * // Model's file extension. + * fileType: '.stl', + * + * // Flips the U texture coordinates of the model. + * flipU: false, + * + * // Flips the V texture coordinates of the model. + * flipV: false + * }; + * + * // Pass the options object to loadModel(). + * loadModel('assets/model.obj', options); + * ``` + * + * Models can take time to load. Calling `loadModel()` in + * preload() ensures models load before they're + * used in setup() or draw(). + * + * Note: There’s no support for colored STL files. STL files with color will + * be rendered without color. + * + * @method loadModel + * @param {String} path path of the model to be loaded. + * @param {Boolean} normalize if `true`, scale the model to fit the canvas. + * @param {function(p5.Geometry)} [successCallback] function to call once the model is loaded. Will be passed + * the p5.Geometry object. + * @param {function(Event)} [failureCallback] function to call if the model fails to load. Will be passed an `Error` event object. + * @param {String} [fileType] model’s file extension. Either `'.obj'` or `'.stl'`. + * @return {p5.Geometry} the p5.Geometry object + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * // Load the file and create a p5.Geometry object. + * function preload() { + * shape = loadModel('assets/teapot.obj'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white teapot drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the shape. + * model(shape); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * // Load the file and create a p5.Geometry object. + * // Normalize the geometry's size to fit the canvas. + * function preload() { + * shape = loadModel('assets/teapot.obj', true); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white teapot drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the shape. + * model(shape); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * // Load the file and create a p5.Geometry object. + * function preload() { + * loadModel('assets/teapot.obj', true, handleModel); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white teapot drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the shape. + * model(shape); + * } + * + * // Set the shape variable and log the geometry's + * // ID to the console. + * function handleModel(data) { + * shape = data; + * console.log(shape.gid); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * // Load the file and create a p5.Geometry object. + * function preload() { + * loadModel('assets/wrong.obj', true, handleModel, handleError); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white teapot drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the shape. + * model(shape); + * } + * + * // Set the shape variable and print the geometry's + * // ID to the console. + * function handleModel(data) { + * shape = data; + * console.log(shape.gid); + * } + * + * // Print an error message if the file doesn't load. + * function handleError(error) { + * console.error('Oops!', error); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * // Load the file and create a p5.Geometry object. + * function preload() { + * loadModel('assets/teapot.obj', true, handleModel, handleError, '.obj'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white teapot drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the shape. + * model(shape); + * } + * + * // Set the shape variable and print the geometry's + * // ID to the console. + * function handleModel(data) { + * shape = data; + * console.log(shape.gid); + * } + * + * // Print an error message if the file doesn't load. + * function handleError(error) { + * console.error('Oops!', error); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * let options = { + * normalize: true, + * successCallback: handleModel, + * failureCallback: handleError, + * fileType: '.obj' + * }; + * + * // Load the file and create a p5.Geometry object. + * function preload() { + * loadModel('assets/teapot.obj', options); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white teapot drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the shape. + * model(shape); + * } + * + * // Set the shape variable and print the geometry's + * // ID to the console. + * function handleModel(data) { + * shape = data; + * console.log(shape.gid); + * } + * + * // Print an error message if the file doesn't load. + * function handleError(error) { + * console.error('Oops!', error); + * } + * + *
+ */ + /** + * @method loadModel + * @param {String} path + * @param {function(p5.Geometry)} [successCallback] + * @param {function(Event)} [failureCallback] + * @param {String} [fileType] + * @return {p5.Geometry} new p5.Geometry object. + */ + /** + * @method loadModel + * @param {String} path + * @param {Object} [options] loading options. + * @param {function(p5.Geometry)} [options.successCallback] + * @param {function(Event)} [options.failureCallback] + * @param {String} [options.fileType] + * @param {Boolean} [options.normalize] + * @param {Boolean} [options.flipU] + * @param {Boolean} [options.flipV] + * @return {p5.Geometry} new p5.Geometry object. + */ + fn.loadModel = async function (path, options) { + p5._validateParameters('loadModel', arguments); + let normalize = false; + let successCallback; + let failureCallback; + let flipU = false; + let flipV = false; + let fileType = path.slice(-4); + if (options && typeof options === 'object') { + normalize = options.normalize || false; + successCallback = options.successCallback; + failureCallback = options.failureCallback; + fileType = options.fileType || fileType; + flipU = options.flipU || false; + flipV = options.flipV || false; + } else if (typeof options === 'boolean') { + normalize = options; + successCallback = arguments[2]; + failureCallback = arguments[3]; + if (typeof arguments[4] !== 'undefined') { + fileType = arguments[4]; + } + } else { + successCallback = typeof arguments[1] === 'function' ? arguments[1] : undefined; + failureCallback = arguments[2]; + if (typeof arguments[3] !== 'undefined') { + fileType = arguments[3]; + } } - } - const model = new p5.Geometry(); - model.gid = `${path}|${normalize}`; - const self = this; - - async function getMaterials(lines) { - const parsedMaterialPromises = []; - - for (let i = 0; i < lines.length; i++) { - const mtllibMatch = lines[i].match(/^mtllib (.+)/); - if (mtllibMatch) { - let mtlPath = ''; - const mtlFilename = mtllibMatch[1]; - const objPathParts = path.split('/'); - if (objPathParts.length > 1) { - objPathParts.pop(); - const objFolderPath = objPathParts.join('/'); - mtlPath = objFolderPath + '/' + mtlFilename; - } else { - mtlPath = mtlFilename; - } - parsedMaterialPromises.push( - fileExists(mtlPath).then(exists => { - if (exists) { - return parseMtl(self, mtlPath); - } else { - console.warn(`MTL file not found or error in parsing; proceeding without materials: ${mtlPath}`); - return {}; + const model = new p5.Geometry(); + model.gid = `${path}|${normalize}`; + const self = this; + + async function getMaterials(lines) { + const parsedMaterialPromises = []; + + for (let i = 0; i < lines.length; i++) { + const mtllibMatch = lines[i].match(/^mtllib (.+)/); + if (mtllibMatch) { + let mtlPath = ''; + const mtlFilename = mtllibMatch[1]; + const objPathParts = path.split('/'); + if (objPathParts.length > 1) { + objPathParts.pop(); + const objFolderPath = objPathParts.join('/'); + mtlPath = objFolderPath + '/' + mtlFilename; + } else { + mtlPath = mtlFilename; + } + parsedMaterialPromises.push( + fileExists(mtlPath).then(exists => { + if (exists) { + return parseMtl(self, mtlPath); + } else { + console.warn(`MTL file not found or error in parsing; proceeding without materials: ${mtlPath}`); + return {}; - } - }).catch(error => { - console.warn(`Error loading MTL file: ${mtlPath}`, error); - return {}; - }) - ); + } + }).catch(error => { + console.warn(`Error loading MTL file: ${mtlPath}`, error); + return {}; + }) + ); + } + } + try { + const parsedMaterials = await Promise.all(parsedMaterialPromises); + const materials = Object.assign({}, ...parsedMaterials); + return materials; + } catch (error) { + return {}; } } - try { - const parsedMaterials = await Promise.all(parsedMaterialPromises); - const materials = Object.assign({}, ...parsedMaterials); - return materials; - } catch (error) { - return {}; - } - } - async function fileExists(url) { - try { - const response = await fetch(url, { method: 'HEAD' }); - return response.ok; - } catch (error) { - return false; + async function fileExists(url) { + try { + const response = await fetch(url, { method: 'HEAD' }); + return response.ok; + } catch (error) { + return false; + } } - } - if (fileType.match(/\.stl$/i)) { - await new Promise(resolve => this.httpDo( - path, - 'GET', - 'arrayBuffer', - arrayBuffer => { - parseSTL(model, arrayBuffer); - - if (normalize) { - model.normalize(); - } + if (fileType.match(/\.stl$/i)) { + await new Promise(resolve => this.httpDo( + path, + 'GET', + 'arrayBuffer', + arrayBuffer => { + parseSTL(model, arrayBuffer); - if (flipU) { - model.flipU(); - } - - if (flipV) { - model.flipV(); - } - - resolve(); - if (typeof successCallback === 'function') { - successCallback(model); - } - }, - failureCallback - )); - } else if (fileType.match(/\.obj$/i)) { - await new Promise(resolve => this.loadStrings( - path, - async lines => { - try { - const parsedMaterials = await getMaterials(lines); - - parseObj(model, lines, parsedMaterials); - - } catch (error) { - if (failureCallback) { - failureCallback(error); - } else { - p5._friendlyError('Error during parsing: ' + error.message); - } - return; - } - finally { if (normalize) { model.normalize(); } + if (flipU) { model.flipU(); } + if (flipV) { model.flipV(); } @@ -478,655 +441,695 @@ p5.prototype.loadModel = async function (path, options) { if (typeof successCallback === 'function') { successCallback(model); } - } - }, - failureCallback - )); - } else { - p5._friendlyFileLoadError(3, path); - if (failureCallback) { - failureCallback(); + }, + failureCallback + )); + } else if (fileType.match(/\.obj$/i)) { + await new Promise(resolve => this.loadStrings( + path, + async lines => { + try { + const parsedMaterials = await getMaterials(lines); + + parseObj(model, lines, parsedMaterials); + + } catch (error) { + if (failureCallback) { + failureCallback(error); + } else { + p5._friendlyError('Error during parsing: ' + error.message); + } + return; + } + finally { + if (normalize) { + model.normalize(); + } + if (flipU) { + model.flipU(); + } + if (flipV) { + model.flipV(); + } + + resolve(); + if (typeof successCallback === 'function') { + successCallback(model); + } + } + }, + failureCallback + )); } else { - p5._friendlyError( - 'Sorry, the file type is invalid. Only OBJ and STL files are supported.' - ); + p5._friendlyFileLoadError(3, path); + if (failureCallback) { + failureCallback(); + } else { + p5._friendlyError( + 'Sorry, the file type is invalid. Only OBJ and STL files are supported.' + ); + } } - } - return model; -}; + return model; + }; -function parseMtl(p5, mtlPath) { - return new Promise((resolve, reject) => { - let currentMaterial = null; - let materials = {}; - p5.loadStrings( - mtlPath, - lines => { - for (let line = 0; line < lines.length; ++line) { - const tokens = lines[line].trim().split(/\s+/); - if (tokens[0] === 'newmtl') { - const materialName = tokens[1]; - currentMaterial = materialName; - materials[currentMaterial] = {}; - } else if (tokens[0] === 'Kd') { - //Diffuse color - materials[currentMaterial].diffuseColor = [ - parseFloat(tokens[1]), - parseFloat(tokens[2]), - parseFloat(tokens[3]) - ]; - } else if (tokens[0] === 'Ka') { - //Ambient Color - materials[currentMaterial].ambientColor = [ - parseFloat(tokens[1]), - parseFloat(tokens[2]), - parseFloat(tokens[3]) - ]; - } else if (tokens[0] === 'Ks') { - //Specular color - materials[currentMaterial].specularColor = [ - parseFloat(tokens[1]), - parseFloat(tokens[2]), - parseFloat(tokens[3]) - ]; - - } else if (tokens[0] === 'map_Kd') { - //Texture path - materials[currentMaterial].texturePath = tokens[1]; + function parseMtl(p5, mtlPath) { + return new Promise((resolve, reject) => { + let currentMaterial = null; + let materials = {}; + p5.loadStrings( + mtlPath, + lines => { + for (let line = 0; line < lines.length; ++line) { + const tokens = lines[line].trim().split(/\s+/); + if (tokens[0] === 'newmtl') { + const materialName = tokens[1]; + currentMaterial = materialName; + materials[currentMaterial] = {}; + } else if (tokens[0] === 'Kd') { + //Diffuse color + materials[currentMaterial].diffuseColor = [ + parseFloat(tokens[1]), + parseFloat(tokens[2]), + parseFloat(tokens[3]) + ]; + } else if (tokens[0] === 'Ka') { + //Ambient Color + materials[currentMaterial].ambientColor = [ + parseFloat(tokens[1]), + parseFloat(tokens[2]), + parseFloat(tokens[3]) + ]; + } else if (tokens[0] === 'Ks') { + //Specular color + materials[currentMaterial].specularColor = [ + parseFloat(tokens[1]), + parseFloat(tokens[2]), + parseFloat(tokens[3]) + ]; + + } else if (tokens[0] === 'map_Kd') { + //Texture path + materials[currentMaterial].texturePath = tokens[1]; + } } - } - resolve(materials); - }, reject - ); - }); -} - -/** - * Parse OBJ lines into model. For reference, this is what a simple model of a - * square might look like: - * - * v -0.5 -0.5 0.5 - * v -0.5 -0.5 -0.5 - * v -0.5 0.5 -0.5 - * v -0.5 0.5 0.5 - * - * f 4 3 2 1 - */ -function parseObj(model, lines, materials = {}) { - // OBJ allows a face to specify an index for a vertex (in the above example), - // but it also allows you to specify a custom combination of vertex, UV - // coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with - // UV coordinate 4 and vertex normal 3". In WebGL, every vertex with different - // parameters must be a different vertex, so loadedVerts is used to - // temporarily store the parsed vertices, normals, etc., and indexedVerts is - // used to map a specific combination (keyed on, for example, the string - // "3/4/3"), to the actual index of the newly created vertex in the final - // object. - const loadedVerts = { - v: [], - vt: [], - vn: [] - }; + resolve(materials); + }, reject + ); + }); + } + /** + * Parse OBJ lines into model. For reference, this is what a simple model of a + * square might look like: + * + * v -0.5 -0.5 0.5 + * v -0.5 -0.5 -0.5 + * v -0.5 0.5 -0.5 + * v -0.5 0.5 0.5 + * + * f 4 3 2 1 + */ + function parseObj(model, lines, materials = {}) { + // OBJ allows a face to specify an index for a vertex (in the above example), + // but it also allows you to specify a custom combination of vertex, UV + // coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with + // UV coordinate 4 and vertex normal 3". In WebGL, every vertex with different + // parameters must be a different vertex, so loadedVerts is used to + // temporarily store the parsed vertices, normals, etc., and indexedVerts is + // used to map a specific combination (keyed on, for example, the string + // "3/4/3"), to the actual index of the newly created vertex in the final + // object. + const loadedVerts = { + v: [], + vt: [], + vn: [] + }; + + + // Map from source index → Map of material → destination index + const usedVerts = {}; // Track colored vertices + let currentMaterial = null; + const coloredVerts = new Set(); //unique vertices with color + let hasColoredVertices = false; + let hasColorlessVertices = false; + for (let line = 0; line < lines.length; ++line) { + // Each line is a separate object (vertex, face, vertex normal, etc) + // For each line, split it into tokens on whitespace. The first token + // describes the type. + const tokens = lines[line].trim().split(/\b\s+/); + + if (tokens.length > 0) { + if (tokens[0] === 'usemtl') { + // Switch to a new material + currentMaterial = tokens[1]; + } else if (tokens[0] === 'v' || tokens[0] === 'vn') { + // Check if this line describes a vertex or vertex normal. + // It will have three numeric parameters. + const vertex = new p5.Vector( + parseFloat(tokens[1]), + parseFloat(tokens[2]), + parseFloat(tokens[3]) + ); + loadedVerts[tokens[0]].push(vertex); + } else if (tokens[0] === 'vt') { + // Check if this line describes a texture coordinate. + // It will have two numeric parameters U and V (W is omitted). + // Because of WebGL texture coordinates rendering behaviour, the V + // coordinate is inversed. + const texVertex = [parseFloat(tokens[1]), 1 - parseFloat(tokens[2])]; + loadedVerts[tokens[0]].push(texVertex); + } else if (tokens[0] === 'f') { + // Check if this line describes a face. + // OBJ faces can have more than three points. Triangulate points. + for (let tri = 3; tri < tokens.length; ++tri) { + const face = []; + const vertexTokens = [1, tri - 1, tri]; + + for (let tokenInd = 0; tokenInd < vertexTokens.length; ++tokenInd) { + // Now, convert the given token into an index + const vertString = tokens[vertexTokens[tokenInd]]; + let vertParts = vertString.split('/'); + + // TODO: Faces can technically use negative numbers to refer to the + // previous nth vertex. I haven't seen this used in practice, but + // it might be good to implement this in the future. + + for (let i = 0; i < vertParts.length; i++) { + vertParts[i] = parseInt(vertParts[i]) - 1; + } - // Map from source index → Map of material → destination index - const usedVerts = {}; // Track colored vertices - let currentMaterial = null; - const coloredVerts = new Set(); //unique vertices with color - let hasColoredVertices = false; - let hasColorlessVertices = false; - for (let line = 0; line < lines.length; ++line) { - // Each line is a separate object (vertex, face, vertex normal, etc) - // For each line, split it into tokens on whitespace. The first token - // describes the type. - const tokens = lines[line].trim().split(/\b\s+/); - - if (tokens.length > 0) { - if (tokens[0] === 'usemtl') { - // Switch to a new material - currentMaterial = tokens[1]; - } else if (tokens[0] === 'v' || tokens[0] === 'vn') { - // Check if this line describes a vertex or vertex normal. - // It will have three numeric parameters. - const vertex = new p5.Vector( - parseFloat(tokens[1]), - parseFloat(tokens[2]), - parseFloat(tokens[3]) - ); - loadedVerts[tokens[0]].push(vertex); - } else if (tokens[0] === 'vt') { - // Check if this line describes a texture coordinate. - // It will have two numeric parameters U and V (W is omitted). - // Because of WebGL texture coordinates rendering behaviour, the V - // coordinate is inversed. - const texVertex = [parseFloat(tokens[1]), 1 - parseFloat(tokens[2])]; - loadedVerts[tokens[0]].push(texVertex); - } else if (tokens[0] === 'f') { - // Check if this line describes a face. - // OBJ faces can have more than three points. Triangulate points. - for (let tri = 3; tri < tokens.length; ++tri) { - const face = []; - const vertexTokens = [1, tri - 1, tri]; - - for (let tokenInd = 0; tokenInd < vertexTokens.length; ++tokenInd) { - // Now, convert the given token into an index - const vertString = tokens[vertexTokens[tokenInd]]; - let vertParts = vertString.split('/'); - - // TODO: Faces can technically use negative numbers to refer to the - // previous nth vertex. I haven't seen this used in practice, but - // it might be good to implement this in the future. - - for (let i = 0; i < vertParts.length; i++) { - vertParts[i] = parseInt(vertParts[i]) - 1; - } + if (!usedVerts[vertString]) { + usedVerts[vertString] = {}; + } - if (!usedVerts[vertString]) { - usedVerts[vertString] = {}; + if (usedVerts[vertString][currentMaterial] === undefined) { + const vertIndex = model.vertices.length; + model.vertices.push(loadedVerts.v[vertParts[0]].copy()); + model.uvs.push(loadedVerts.vt[vertParts[1]] ? + loadedVerts.vt[vertParts[1]].slice() : [0, 0]); + model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ? + loadedVerts.vn[vertParts[2]].copy() : new p5.Vector()); + + usedVerts[vertString][currentMaterial] = vertIndex; + face.push(vertIndex); + if (currentMaterial + && materials[currentMaterial] + && materials[currentMaterial].diffuseColor) { + // Mark this vertex as colored + coloredVerts.add(loadedVerts.v[vertParts[0]]); //since a set would only push unique values + } + } else { + face.push(usedVerts[vertString][currentMaterial]); + } } - if (usedVerts[vertString][currentMaterial] === undefined) { - const vertIndex = model.vertices.length; - model.vertices.push(loadedVerts.v[vertParts[0]].copy()); - model.uvs.push(loadedVerts.vt[vertParts[1]] ? - loadedVerts.vt[vertParts[1]].slice() : [0, 0]); - model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ? - loadedVerts.vn[vertParts[2]].copy() : new p5.Vector()); - - usedVerts[vertString][currentMaterial] = vertIndex; - face.push(vertIndex); + if ( + face[0] !== face[1] && + face[0] !== face[2] && + face[1] !== face[2] + ) { + model.faces.push(face); + //same material for all vertices in a particular face if (currentMaterial && materials[currentMaterial] && materials[currentMaterial].diffuseColor) { - // Mark this vertex as colored - coloredVerts.add(loadedVerts.v[vertParts[0]]); //since a set would only push unique values + hasColoredVertices = true; + //flag to track color or no color model + hasColoredVertices = true; + const materialDiffuseColor = + materials[currentMaterial].diffuseColor; + for (let i = 0; i < face.length; i++) { + model.vertexColors.push(materialDiffuseColor[0]); + model.vertexColors.push(materialDiffuseColor[1]); + model.vertexColors.push(materialDiffuseColor[2]); + model.vertexColors.push(1); + } + } else { + hasColorlessVertices = true; } - } else { - face.push(usedVerts[vertString][currentMaterial]); - } - } - - if ( - face[0] !== face[1] && - face[0] !== face[2] && - face[1] !== face[2] - ) { - model.faces.push(face); - //same material for all vertices in a particular face - if (currentMaterial - && materials[currentMaterial] - && materials[currentMaterial].diffuseColor) { - hasColoredVertices = true; - //flag to track color or no color model - hasColoredVertices = true; - const materialDiffuseColor = - materials[currentMaterial].diffuseColor; - for (let i = 0; i < face.length; i++) { - model.vertexColors.push(materialDiffuseColor[0]); - model.vertexColors.push(materialDiffuseColor[1]); - model.vertexColors.push(materialDiffuseColor[2]); - model.vertexColors.push(1); - } - } else { - hasColorlessVertices = true; } } } } } + // If the model doesn't have normals, compute the normals + if (model.vertexNormals.length === 0) { + model.computeNormals(); + } + if (hasColoredVertices === hasColorlessVertices) { + // If both are true or both are false, throw an error because the model is inconsistent + throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.'); + } + return model; } - // If the model doesn't have normals, compute the normals - if (model.vertexNormals.length === 0) { - model.computeNormals(); - } - if (hasColoredVertices === hasColorlessVertices) { - // If both are true or both are false, throw an error because the model is inconsistent - throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.'); - } - return model; -} -/** - * STL files can be of two types, ASCII and Binary, - * - * We need to convert the arrayBuffer to an array of strings, - * to parse it as an ASCII file. - */ -function parseSTL(model, buffer) { - if (isBinary(buffer)) { - parseBinarySTL(model, buffer); - } else { - const reader = new DataView(buffer); + /** + * STL files can be of two types, ASCII and Binary, + * + * We need to convert the arrayBuffer to an array of strings, + * to parse it as an ASCII file. + */ + function parseSTL(model, buffer) { + if (isBinary(buffer)) { + parseBinarySTL(model, buffer); + } else { + const reader = new DataView(buffer); - if (!('TextDecoder' in window)) { - console.warn( - 'Sorry, ASCII STL loading only works in browsers that support TextDecoder (https://caniuse.com/#feat=textencoder)' - ); - return model; - } + if (!('TextDecoder' in window)) { + console.warn( + 'Sorry, ASCII STL loading only works in browsers that support TextDecoder (https://caniuse.com/#feat=textencoder)' + ); + return model; + } - const decoder = new TextDecoder('utf-8'); - const lines = decoder.decode(reader); - const lineArray = lines.split('\n'); - parseASCIISTL(model, lineArray); + const decoder = new TextDecoder('utf-8'); + const lines = decoder.decode(reader); + const lineArray = lines.split('\n'); + parseASCIISTL(model, lineArray); + } + return model; } - return model; -} -/** - * This function checks if the file is in ASCII format or in Binary format - * - * It is done by searching keyword `solid` at the start of the file. - * - * An ASCII STL data must begin with `solid` as the first six bytes. - * However, ASCII STLs lacking the SPACE after the `d` are known to be - * plentiful. So, check the first 5 bytes for `solid`. - * - * Several encodings, such as UTF-8, precede the text with up to 5 bytes: - * https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding - * Search for `solid` to start anywhere after those prefixes. - */ -function isBinary(data) { - const reader = new DataView(data); - - // US-ASCII ordinal values for `s`, `o`, `l`, `i`, `d` - const solid = [115, 111, 108, 105, 100]; - for (let off = 0; off < 5; off++) { - // If "solid" text is matched to the current offset, declare it to be an ASCII STL. - if (matchDataViewAt(solid, reader, off)) return false; + /** + * This function checks if the file is in ASCII format or in Binary format + * + * It is done by searching keyword `solid` at the start of the file. + * + * An ASCII STL data must begin with `solid` as the first six bytes. + * However, ASCII STLs lacking the SPACE after the `d` are known to be + * plentiful. So, check the first 5 bytes for `solid`. + * + * Several encodings, such as UTF-8, precede the text with up to 5 bytes: + * https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding + * Search for `solid` to start anywhere after those prefixes. + */ + function isBinary(data) { + const reader = new DataView(data); + + // US-ASCII ordinal values for `s`, `o`, `l`, `i`, `d` + const solid = [115, 111, 108, 105, 100]; + for (let off = 0; off < 5; off++) { + // If "solid" text is matched to the current offset, declare it to be an ASCII STL. + if (matchDataViewAt(solid, reader, off)) return false; + } + + // Couldn't find "solid" text at the beginning; it is binary STL. + return true; } - // Couldn't find "solid" text at the beginning; it is binary STL. - return true; -} + /** + * This function matches the `query` at the provided `offset` + */ + function matchDataViewAt(query, reader, offset) { + // Check if each byte in query matches the corresponding byte from the current offset + for (let i = 0, il = query.length; i < il; i++) { + if (query[i] !== reader.getUint8(offset + i, false)) return false; + } -/** - * This function matches the `query` at the provided `offset` - */ -function matchDataViewAt(query, reader, offset) { - // Check if each byte in query matches the corresponding byte from the current offset - for (let i = 0, il = query.length; i < il; i++) { - if (query[i] !== reader.getUint8(offset + i, false)) return false; + return true; } - return true; -} + /** + * This function parses the Binary STL files. + * https://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL + * + * Currently there is no support for the colors provided in STL files. + */ + function parseBinarySTL(model, buffer) { + const reader = new DataView(buffer); -/** - * This function parses the Binary STL files. - * https://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL - * - * Currently there is no support for the colors provided in STL files. - */ -function parseBinarySTL(model, buffer) { - const reader = new DataView(buffer); - - // Number of faces is present following the header - const faces = reader.getUint32(80, true); - let r, - g, - b, - hasColors = false, - colors; - let defaultR, defaultG, defaultB; - - // Binary files contain 80-byte header, which is generally ignored. - for (let index = 0; index < 80 - 10; index++) { - // Check for `COLOR=` - if ( - reader.getUint32(index, false) === 0x434f4c4f /*COLO*/ && - reader.getUint8(index + 4) === 0x52 /*'R'*/ && - reader.getUint8(index + 5) === 0x3d /*'='*/ - ) { - hasColors = true; - colors = []; - - defaultR = reader.getUint8(index + 6) / 255; - defaultG = reader.getUint8(index + 7) / 255; - defaultB = reader.getUint8(index + 8) / 255; - // To be used when color support is added - // alpha = reader.getUint8(index + 9) / 255; + // Number of faces is present following the header + const faces = reader.getUint32(80, true); + let r, + g, + b, + hasColors = false, + colors; + let defaultR, defaultG, defaultB; + + // Binary files contain 80-byte header, which is generally ignored. + for (let index = 0; index < 80 - 10; index++) { + // Check for `COLOR=` + if ( + reader.getUint32(index, false) === 0x434f4c4f /*COLO*/ && + reader.getUint8(index + 4) === 0x52 /*'R'*/ && + reader.getUint8(index + 5) === 0x3d /*'='*/ + ) { + hasColors = true; + colors = []; + + defaultR = reader.getUint8(index + 6) / 255; + defaultG = reader.getUint8(index + 7) / 255; + defaultB = reader.getUint8(index + 8) / 255; + // To be used when color support is added + // alpha = reader.getUint8(index + 9) / 255; + } } - } - const dataOffset = 84; - const faceLength = 12 * 4 + 2; + const dataOffset = 84; + const faceLength = 12 * 4 + 2; - // Iterate the faces - for (let face = 0; face < faces; face++) { - const start = dataOffset + face * faceLength; - const normalX = reader.getFloat32(start, true); - const normalY = reader.getFloat32(start + 4, true); - const normalZ = reader.getFloat32(start + 8, true); + // Iterate the faces + for (let face = 0; face < faces; face++) { + const start = dataOffset + face * faceLength; + const normalX = reader.getFloat32(start, true); + const normalY = reader.getFloat32(start + 4, true); + const normalZ = reader.getFloat32(start + 8, true); - if (hasColors) { - const packedColor = reader.getUint16(start + 48, true); + if (hasColors) { + const packedColor = reader.getUint16(start + 48, true); - if ((packedColor & 0x8000) === 0) { - // facet has its own unique color - r = (packedColor & 0x1f) / 31; - g = ((packedColor >> 5) & 0x1f) / 31; - b = ((packedColor >> 10) & 0x1f) / 31; - } else { - r = defaultR; - g = defaultG; - b = defaultB; + if ((packedColor & 0x8000) === 0) { + // facet has its own unique color + r = (packedColor & 0x1f) / 31; + g = ((packedColor >> 5) & 0x1f) / 31; + b = ((packedColor >> 10) & 0x1f) / 31; + } else { + r = defaultR; + g = defaultG; + b = defaultB; + } } - } - const newNormal = new p5.Vector(normalX, normalY, normalZ); + const newNormal = new p5.Vector(normalX, normalY, normalZ); - for (let i = 1; i <= 3; i++) { - const vertexstart = start + i * 12; + for (let i = 1; i <= 3; i++) { + const vertexstart = start + i * 12; - const newVertex = new p5.Vector( - reader.getFloat32(vertexstart, true), - reader.getFloat32(vertexstart + 4, true), - reader.getFloat32(vertexstart + 8, true) - ); + const newVertex = new p5.Vector( + reader.getFloat32(vertexstart, true), + reader.getFloat32(vertexstart + 4, true), + reader.getFloat32(vertexstart + 8, true) + ); - model.vertices.push(newVertex); - model.vertexNormals.push(newNormal); + model.vertices.push(newVertex); + model.vertexNormals.push(newNormal); - if (hasColors) { - colors.push(r, g, b); + if (hasColors) { + colors.push(r, g, b); + } } - } - model.faces.push([3 * face, 3 * face + 1, 3 * face + 2]); - model.uvs.push([0, 0], [0, 0], [0, 0]); - } - if (hasColors) { - // add support for colors here. + model.faces.push([3 * face, 3 * face + 1, 3 * face + 2]); + model.uvs.push([0, 0], [0, 0], [0, 0]); + } + if (hasColors) { + // add support for colors here. + } + return model; } - return model; -} -/** - * ASCII STL file starts with `solid 'nameOfFile'` - * Then contain the normal of the face, starting with `facet normal` - * Next contain a keyword indicating the start of face vertex, `outer loop` - * Next comes the three vertex, starting with `vertex x y z` - * Vertices ends with `endloop` - * Face ends with `endfacet` - * Next face starts with `facet normal` - * The end of the file is indicated by `endsolid` - */ -function parseASCIISTL(model, lines) { - let state = ''; - let curVertexIndex = []; - let newNormal, newVertex; - - for (let iterator = 0; iterator < lines.length; ++iterator) { - const line = lines[iterator].trim(); - const parts = line.split(' '); - - for (let partsiterator = 0; partsiterator < parts.length; ++partsiterator) { - if (parts[partsiterator] === '') { - // Ignoring multiple whitespaces - parts.splice(partsiterator, 1); + /** + * ASCII STL file starts with `solid 'nameOfFile'` + * Then contain the normal of the face, starting with `facet normal` + * Next contain a keyword indicating the start of face vertex, `outer loop` + * Next comes the three vertex, starting with `vertex x y z` + * Vertices ends with `endloop` + * Face ends with `endfacet` + * Next face starts with `facet normal` + * The end of the file is indicated by `endsolid` + */ + function parseASCIISTL(model, lines) { + let state = ''; + let curVertexIndex = []; + let newNormal, newVertex; + + for (let iterator = 0; iterator < lines.length; ++iterator) { + const line = lines[iterator].trim(); + const parts = line.split(' '); + + for (let partsiterator = 0; partsiterator < parts.length; ++partsiterator) { + if (parts[partsiterator] === '') { + // Ignoring multiple whitespaces + parts.splice(partsiterator, 1); + } } - } - if (parts.length === 0) { - // Remove newline - continue; - } + if (parts.length === 0) { + // Remove newline + continue; + } - switch (state) { - case '': // First run - if (parts[0] !== 'solid') { - // Invalid state - console.error(line); - console.error(`Invalid state "${parts[0]}", should be "solid"`); - return; - } else { - state = 'solid'; - } - break; - - case 'solid': // First face - if (parts[0] !== 'facet' || parts[1] !== 'normal') { - // Invalid state - console.error(line); - console.error( - `Invalid state "${parts[0]}", should be "facet normal"` - ); - return; - } else { - // Push normal for first face - newNormal = new p5.Vector( - parseFloat(parts[2]), - parseFloat(parts[3]), - parseFloat(parts[4]) - ); - model.vertexNormals.push(newNormal, newNormal, newNormal); - state = 'facet normal'; - } - break; - - case 'facet normal': // After normal is defined - if (parts[0] !== 'outer' || parts[1] !== 'loop') { - // Invalid State - console.error(line); - console.error(`Invalid state "${parts[0]}", should be "outer loop"`); - return; - } else { - // Next should be vertices - state = 'vertex'; - } - break; - - case 'vertex': - if (parts[0] === 'vertex') { - //Vertex of triangle - newVertex = new p5.Vector( - parseFloat(parts[1]), - parseFloat(parts[2]), - parseFloat(parts[3]) - ); - model.vertices.push(newVertex); - model.uvs.push([0, 0]); - curVertexIndex.push(model.vertices.indexOf(newVertex)); - } else if (parts[0] === 'endloop') { - // End of vertices - model.faces.push(curVertexIndex); - curVertexIndex = []; - state = 'endloop'; - } else { - // Invalid State - console.error(line); - console.error( - `Invalid state "${parts[0]}", should be "vertex" or "endloop"` - ); - return; - } - break; - - case 'endloop': - if (parts[0] !== 'endfacet') { - // End of face - console.error(line); - console.error(`Invalid state "${parts[0]}", should be "endfacet"`); - return; - } else { - state = 'endfacet'; - } - break; - - case 'endfacet': - if (parts[0] === 'endsolid') { - // End of solid - } else if (parts[0] === 'facet' && parts[1] === 'normal') { - // Next face - newNormal = new p5.Vector( - parseFloat(parts[2]), - parseFloat(parts[3]), - parseFloat(parts[4]) - ); - model.vertexNormals.push(newNormal, newNormal, newNormal); - state = 'facet normal'; - } else { - // Invalid State - console.error(line); - console.error( - `Invalid state "${parts[0] - }", should be "endsolid" or "facet normal"` - ); - return; - } - break; + switch (state) { + case '': // First run + if (parts[0] !== 'solid') { + // Invalid state + console.error(line); + console.error(`Invalid state "${parts[0]}", should be "solid"`); + return; + } else { + state = 'solid'; + } + break; + + case 'solid': // First face + if (parts[0] !== 'facet' || parts[1] !== 'normal') { + // Invalid state + console.error(line); + console.error( + `Invalid state "${parts[0]}", should be "facet normal"` + ); + return; + } else { + // Push normal for first face + newNormal = new p5.Vector( + parseFloat(parts[2]), + parseFloat(parts[3]), + parseFloat(parts[4]) + ); + model.vertexNormals.push(newNormal, newNormal, newNormal); + state = 'facet normal'; + } + break; + + case 'facet normal': // After normal is defined + if (parts[0] !== 'outer' || parts[1] !== 'loop') { + // Invalid State + console.error(line); + console.error(`Invalid state "${parts[0]}", should be "outer loop"`); + return; + } else { + // Next should be vertices + state = 'vertex'; + } + break; + + case 'vertex': + if (parts[0] === 'vertex') { + //Vertex of triangle + newVertex = new p5.Vector( + parseFloat(parts[1]), + parseFloat(parts[2]), + parseFloat(parts[3]) + ); + model.vertices.push(newVertex); + model.uvs.push([0, 0]); + curVertexIndex.push(model.vertices.indexOf(newVertex)); + } else if (parts[0] === 'endloop') { + // End of vertices + model.faces.push(curVertexIndex); + curVertexIndex = []; + state = 'endloop'; + } else { + // Invalid State + console.error(line); + console.error( + `Invalid state "${parts[0]}", should be "vertex" or "endloop"` + ); + return; + } + break; + + case 'endloop': + if (parts[0] !== 'endfacet') { + // End of face + console.error(line); + console.error(`Invalid state "${parts[0]}", should be "endfacet"`); + return; + } else { + state = 'endfacet'; + } + break; + + case 'endfacet': + if (parts[0] === 'endsolid') { + // End of solid + } else if (parts[0] === 'facet' && parts[1] === 'normal') { + // Next face + newNormal = new p5.Vector( + parseFloat(parts[2]), + parseFloat(parts[3]), + parseFloat(parts[4]) + ); + model.vertexNormals.push(newNormal, newNormal, newNormal); + state = 'facet normal'; + } else { + // Invalid State + console.error(line); + console.error( + `Invalid state "${parts[0] + }", should be "endsolid" or "facet normal"` + ); + return; + } + break; - default: - console.error(`Invalid state "${state}"`); - break; + default: + console.error(`Invalid state "${state}"`); + break; + } } + return model; } - return model; -} -/** - * Draws a p5.Geometry object to the canvas. - * - * The parameter, `model`, is the - * p5.Geometry object to draw. - * p5.Geometry objects can be built with - * buildGeometry(), or - * beginGeometry() and - * endGeometry(). They can also be loaded from - * a file with loadGeometry(). - * - * Note: `model()` can only be used in WebGL mode. - * - * @method model - * @param {p5.Geometry} model 3D shape to be drawn. - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Geometry object. - * shape = buildGeometry(createShape); - * - * describe('A white cone drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the p5.Geometry object. - * model(shape); - * } - * - * // Create p5.Geometry object from a single cone. - * function createShape() { - * cone(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Geometry object. - * shape = buildGeometry(createArrow); - * - * describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the arrows. - * noStroke(); - * - * // Draw the p5.Geometry object. - * model(shape); - * - * // Translate and rotate the coordinate system. - * translate(30, 0, 0); - * rotateZ(frameCount * 0.01); - * - * // Draw the p5.Geometry object again. - * model(shape); - * } - * - * function createArrow() { - * // Add shapes to the p5.Geometry object. - * push(); - * rotateX(PI); - * cone(10); - * translate(0, -10, 0); - * cylinder(3, 20); - * pop(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let shape; - * - * // Load the file and create a p5.Geometry object. - * function preload() { - * shape = loadModel('assets/octahedron.obj'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white octahedron drawn against a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the shape. - * model(shape); - * } - * - *
- */ -p5.prototype.model = function (model) { - this._assert3d('model'); - p5._validateParameters('model', arguments); - if (model.vertices.length > 0) { - if (!this._renderer.geometryInHash(model.gid)) { - - if (model.edges.length === 0) { - model._makeTriangleEdges(); + /** + * Draws a p5.Geometry object to the canvas. + * + * The parameter, `model`, is the + * p5.Geometry object to draw. + * p5.Geometry objects can be built with + * buildGeometry(), or + * beginGeometry() and + * endGeometry(). They can also be loaded from + * a file with loadGeometry(). + * + * Note: `model()` can only be used in WebGL mode. + * + * @method model + * @param {p5.Geometry} model 3D shape to be drawn. + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Geometry object. + * shape = buildGeometry(createShape); + * + * describe('A white cone drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the p5.Geometry object. + * model(shape); + * } + * + * // Create p5.Geometry object from a single cone. + * function createShape() { + * cone(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Geometry object. + * shape = buildGeometry(createArrow); + * + * describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the arrows. + * noStroke(); + * + * // Draw the p5.Geometry object. + * model(shape); + * + * // Translate and rotate the coordinate system. + * translate(30, 0, 0); + * rotateZ(frameCount * 0.01); + * + * // Draw the p5.Geometry object again. + * model(shape); + * } + * + * function createArrow() { + * // Add shapes to the p5.Geometry object. + * push(); + * rotateX(PI); + * cone(10); + * translate(0, -10, 0); + * cylinder(3, 20); + * pop(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let shape; + * + * // Load the file and create a p5.Geometry object. + * function preload() { + * shape = loadModel('assets/octahedron.obj'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white octahedron drawn against a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the shape. + * model(shape); + * } + * + *
+ */ + fn.model = function (model) { + this._assert3d('model'); + p5._validateParameters('model', arguments); + if (model.vertices.length > 0) { + if (!this._renderer.geometryInHash(model.gid)) { + + if (model.edges.length === 0) { + model._makeTriangleEdges(); + } + + model._edgesToVertices(); + this._renderer.createBuffers(model.gid, model); } - model._edgesToVertices(); - this._renderer.createBuffers(model.gid, model); + this._renderer.drawBuffers(model.gid); } + }; +} - this._renderer.drawBuffers(model.gid); - } -}; +export default loading; -export default p5; +if(typeof p5 !== 'undefined'){ + loading(p5, p5.prototype); +} diff --git a/src/webgl/material.js b/src/webgl/material.js index 6379ca1931..b1283d2510 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -5,3306 +5,3310 @@ * @requires core */ -import p5 from '../core/main'; import * as constants from '../core/constants'; -import './p5.Texture'; -/** - * Loads vertex and fragment shaders to create a - * p5.Shader object. - * - * Shaders are programs that run on the graphics processing unit (GPU). They - * can process many pixels 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. - * - * Once the p5.Shader object is created, it can be - * used with the shader() function, as in - * `shader(myShader)`. 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. - * - * `loadShader()` loads the vertex and fragment shaders from their `.vert` and - * `.frag` files. For example, calling - * `loadShader('assets/shader.vert', 'assets/shader.frag')` loads both - * required shaders and returns a p5.Shader object. - * - * The third parameter, `successCallback`, is optional. If a function is - * passed, it will be called once the shader has loaded. The callback function - * can use the new p5.Shader object as its - * parameter. - * - * The fourth parameter, `failureCallback`, is also optional. If a function is - * passed, it will be called if the shader fails to load. The callback - * function can use the event error as its parameter. - * - * Shaders can take time to load. Calling `loadShader()` in - * preload() ensures shaders load before they're - * used in setup() or draw(). - * - * Note: Shaders can only be used in WebGL mode. - * - * @method loadShader - * @param {String} vertFilename path of the vertex shader to be loaded. - * @param {String} fragFilename path of the fragment shader to be loaded. - * @param {Function} [successCallback] function to call once the shader is loaded. Can be passed the - * p5.Shader object. - * @param {Function} [failureCallback] function to call if the shader fails to load. Can be passed an - * `Error` event object. - * @return {p5.Shader} new shader created from the vertex and fragment shader files. - * - * @example - *
- * - * // 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); - * - * // Compile and apply the p5.Shader object. - * shader(mandelbrot); - * - * // Set the shader uniform p to an array. - * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); - * - * // Set the shader uniform r to the value 1.5. - * mandelbrot.setUniform('r', 1.5); - * - * // Add a quad as a display surface for the shader. - * quad(-1, -1, 1, -1, 1, 1, -1, 1); - * - * describe('A black fractal image on a magenta background.'); - * } - * - *
- * - *
- * - * // 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.prototype.loadShader = function ( - vertFilename, - fragFilename, - successCallback, - failureCallback -) { - p5._validateParameters('loadShader', arguments); - if (!failureCallback) { - failureCallback = console.error; - } +function material(p5, fn){ + /** + * Loads vertex and fragment shaders to create a + * p5.Shader object. + * + * Shaders are programs that run on the graphics processing unit (GPU). They + * can process many pixels 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. + * + * Once the p5.Shader object is created, it can be + * used with the shader() function, as in + * `shader(myShader)`. 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. + * + * `loadShader()` loads the vertex and fragment shaders from their `.vert` and + * `.frag` files. For example, calling + * `loadShader('assets/shader.vert', 'assets/shader.frag')` loads both + * required shaders and returns a p5.Shader object. + * + * The third parameter, `successCallback`, is optional. If a function is + * passed, it will be called once the shader has loaded. The callback function + * can use the new p5.Shader object as its + * parameter. + * + * The fourth parameter, `failureCallback`, is also optional. If a function is + * passed, it will be called if the shader fails to load. The callback + * function can use the event error as its parameter. + * + * Shaders can take time to load. Calling `loadShader()` in + * preload() ensures shaders load before they're + * used in setup() or draw(). + * + * Note: Shaders can only be used in WebGL mode. + * + * @method loadShader + * @param {String} vertFilename path of the vertex shader to be loaded. + * @param {String} fragFilename path of the fragment shader to be loaded. + * @param {Function} [successCallback] function to call once the shader is loaded. Can be passed the + * p5.Shader object. + * @param {Function} [failureCallback] function to call if the shader fails to load. Can be passed an + * `Error` event object. + * @return {p5.Shader} new shader created from the vertex and fragment shader files. + * + * @example + *
+ * + * // 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); + * + * // Compile and apply the p5.Shader object. + * shader(mandelbrot); + * + * // Set the shader uniform p to an array. + * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); + * + * // Set the shader uniform r to the value 1.5. + * mandelbrot.setUniform('r', 1.5); + * + * // Add a quad as a display surface for the shader. + * quad(-1, -1, 1, -1, 1, 1, -1, 1); + * + * describe('A black fractal image on a magenta background.'); + * } + * + *
+ * + *
+ * + * // 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); + * } + * + *
+ */ + fn.loadShader = function ( + vertFilename, + fragFilename, + successCallback, + failureCallback + ) { + p5._validateParameters('loadShader', arguments); + if (!failureCallback) { + failureCallback = console.error; + } - const loadedShader = new p5.Shader(); + const loadedShader = new p5.Shader(); - const self = this; - let loadedFrag = false; - let loadedVert = false; + const self = this; + let loadedFrag = false; + let loadedVert = false; - const onLoad = () => { - self._decrementPreload(); - if (successCallback) { - successCallback(loadedShader); - } - }; - - this.loadStrings( - vertFilename, - result => { - loadedShader._vertSrc = result.join('\n'); - loadedVert = true; - if (loadedFrag) { - onLoad(); + const onLoad = () => { + self._decrementPreload(); + if (successCallback) { + successCallback(loadedShader); } - }, - failureCallback - ); + }; - this.loadStrings( - fragFilename, - result => { - loadedShader._fragSrc = result.join('\n'); - loadedFrag = true; - if (loadedVert) { - onLoad(); - } - }, - failureCallback - ); + this.loadStrings( + vertFilename, + result => { + loadedShader._vertSrc = result.join('\n'); + loadedVert = true; + if (loadedFrag) { + onLoad(); + } + }, + failureCallback + ); - return loadedShader; -}; + this.loadStrings( + fragFilename, + result => { + loadedShader._fragSrc = result.join('\n'); + loadedFrag = true; + if (loadedVert) { + onLoad(); + } + }, + failureCallback + ); -/** - * Creates a new p5.Shader object. - * - * Shaders are programs that run on the graphics processing unit (GPU). They - * can process many pixels 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. - * - * Once the p5.Shader object is created, it can be - * used with the shader() function, as in - * `shader(myShader)`. A shader program consists of two parts, 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. - * - * The first parameter, `vertSrc`, sets the vertex shader. It’s a string that - * contains the vertex shader program written in GLSL. - * - * The second parameter, `fragSrc`, sets the fragment shader. It’s a string - * that contains the fragment shader program written in GLSL. - * - * A shader can optionally describe *hooks,* which are functions in GLSL that - * users may choose to provide to customize the behavior of the shader using the - * `modify()` method of `p5.Shader`. These are added by - * describing the hooks in a third parameter, `options`, and referencing the hooks in - * your `vertSrc` or `fragSrc`. Hooks for the vertex or fragment shader are described under - * the `vertex` and `fragment` keys of `options`. Each one is 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_`. If you want to check if the default - * hook has been replaced, maybe to avoid extra overhead, you can check if the - * same name prefixed by `AUGMENTED_HOOK_` has been defined: - * - * ```glsl - * void main() { - * // In most cases, just calling the hook is fine: - * HOOK_beforeVertex(); - * - * // Alternatively, for more efficiency: - * #ifdef AUGMENTED_HOOK_beforeVertex - * HOOK_beforeVertex(); - * #endif - * - * // Add the rest of your shader code here! - * } - * ``` - * - * Note: Only filter shaders can be used in 2D mode. All shaders can be used - * in WebGL mode. - * - * @method createShader - * @param {String} vertSrc source code for the vertex shader. - * @param {String} fragSrc source code for the fragment shader. - * @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. - * @returns {p5.Shader} new shader object created from the - * vertex and fragment shaders. - * - * @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 shaderProgram = createShader(vertSrc, fragSrc); - * - * // Compile and apply the p5.Shader object. - * shader(shaderProgram); - * - * // 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. - * - * // 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 - * ); - * } - * `; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * let 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]); - * - * // Set the shader uniform r to 0.005. - * // r is the size of the image in Mandelbrot-space. - * mandelbrot.setUniform('r', 0.005); - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a plane as a drawing surface. - * plane(100, 100); - * - * describe('A black fractal image on a magenta background.'); - * } - * - *
- * - *
- * - * // 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); - * - * // 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); - * } - * - *
- * - *
- * - * // A shader with hooks. - * let myShader; - * - * // A shader with modified hooks. - * let modifiedShader; - * - * // 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; - * - * void main() { - * vec4 positionVec4 = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - * } - * `; - * - * // Create a fragment shader that uses a hook. - * let fragSrc = ` - * precision highp float; - * void main() { - * // Let users override the color - * gl_FragColor = HOOK_getColor(vec4(1., 0., 0., 1.)); - * } - * `; - * - * function setup() { - * createCanvas(50, 50, WEBGL); - * - * // Create a shader with hooks - * myShader = createShader(vertSrc, fragSrc, { - * fragment: { - * 'vec4 getColor': '(vec4 color) { return color; }' - * } - * }); - * - * // Make a version of the shader with a hook overridden - * modifiedShader = myShader.modify({ - * 'vec4 getColor': `(vec4 color) { - * return vec4(0., 0., 1., 1.); - * }` - * }); - * } - * - * function draw() { - * noStroke(); - * - * push(); - * shader(myShader); - * translate(-width/3, 0); - * sphere(10); - * pop(); - * - * push(); - * shader(modifiedShader); - * translate(width/3, 0); - * sphere(10); - * pop(); - * } - * - *
- */ -p5.prototype.createShader = function (vertSrc, fragSrc, options) { - p5._validateParameters('createShader', arguments); - return new p5.Shader(this._renderer, vertSrc, fragSrc, options); -}; + return loadedShader; + }; -/** - * Creates a p5.Shader object to be used with the - * filter() function. - * - * `createFilterShader()` works like - * createShader() but has a default vertex - * shader included. `createFilterShader()` is intended to be used along with - * filter() for filtering the contents of a canvas. - * A filter shader will be applied to the whole canvas instead of just - * p5.Geometry objects. - * - * The parameter, `fragSrc`, sets the fragment shader. It’s a string that - * contains the fragment shader program written in - * GLSL. - * - * The p5.Shader object that's created has some - * uniforms that can be set: - * - `sampler2D tex0`, which contains the canvas contents as a texture. - * - `vec2 canvasSize`, which is the width and height of the canvas, not including pixel density. - * - `vec2 texelSize`, which is the size of a physical pixel including pixel density. This is calculated as `1.0 / (width * density)` for the pixel width and `1.0 / (height * density)` for the pixel height. - * - * The p5.Shader that's created also provides - * `varying vec2 vTexCoord`, a coordinate with values between 0 and 1. - * `vTexCoord` describes where on the canvas the pixel will be drawn. - * - * For more info about filters and shaders, see Adam Ferriss' repo of shader examples - * or the Introduction to Shaders tutorial. - * - * @method createFilterShader - * @param {String} fragSrc source code for the fragment shader. - * @returns {p5.Shader} new shader object created from the fragment shader. - * - * @example - *
- * - * function setup() { - * let fragSrc = `precision highp float; - * void main() { - * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); - * }`; - * - * createCanvas(100, 100, WEBGL); - * let s = createFilterShader(fragSrc); - * filter(s); - * describe('a yellow canvas'); - * } - * - *
- * - *
- * - * let img, s; - * function preload() { - * img = loadImage('assets/bricks.jpg'); - * } - * function setup() { - * let fragSrc = `precision highp float; - * - * // x,y coordinates, given from the vertex shader - * varying vec2 vTexCoord; - * - * // the canvas contents, given from filter() - * uniform sampler2D tex0; - * // other useful information from the canvas - * uniform vec2 texelSize; - * uniform vec2 canvasSize; - * // a custom variable from this sketch - * uniform float darkness; - * - * void main() { - * // get the color at current pixel - * vec4 color = texture2D(tex0, vTexCoord); - * // set the output color - * color.b = 1.0; - * color *= darkness; - * gl_FragColor = vec4(color.rgb, 1.0); - * }`; - * - * createCanvas(100, 100, WEBGL); - * s = createFilterShader(fragSrc); - * } - * function draw() { - * image(img, -50, -50); - * s.setUniform('darkness', 0.5); - * filter(s); - * describe('a image of bricks tinted dark blue'); - * } - * - *
- */ -p5.prototype.createFilterShader = function (fragSrc) { - p5._validateParameters('createFilterShader', arguments); - let defaultVertV1 = ` - uniform mat4 uModelViewMatrix; - uniform mat4 uProjectionMatrix; + /** + * Creates a new p5.Shader object. + * + * Shaders are programs that run on the graphics processing unit (GPU). They + * can process many pixels 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. + * + * Once the p5.Shader object is created, it can be + * used with the shader() function, as in + * `shader(myShader)`. A shader program consists of two parts, 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. + * + * The first parameter, `vertSrc`, sets the vertex shader. It’s a string that + * contains the vertex shader program written in GLSL. + * + * The second parameter, `fragSrc`, sets the fragment shader. It’s a string + * that contains the fragment shader program written in GLSL. + * + * A shader can optionally describe *hooks,* which are functions in GLSL that + * users may choose to provide to customize the behavior of the shader using the + * `modify()` method of `p5.Shader`. These are added by + * describing the hooks in a third parameter, `options`, and referencing the hooks in + * your `vertSrc` or `fragSrc`. Hooks for the vertex or fragment shader are described under + * the `vertex` and `fragment` keys of `options`. Each one is 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_`. If you want to check if the default + * hook has been replaced, maybe to avoid extra overhead, you can check if the + * same name prefixed by `AUGMENTED_HOOK_` has been defined: + * + * ```glsl + * void main() { + * // In most cases, just calling the hook is fine: + * HOOK_beforeVertex(); + * + * // Alternatively, for more efficiency: + * #ifdef AUGMENTED_HOOK_beforeVertex + * HOOK_beforeVertex(); + * #endif + * + * // Add the rest of your shader code here! + * } + * ``` + * + * Note: Only filter shaders can be used in 2D mode. All shaders can be used + * in WebGL mode. + * + * @method createShader + * @param {String} vertSrc source code for the vertex shader. + * @param {String} fragSrc source code for the fragment shader. + * @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. + * @returns {p5.Shader} new shader object created from the + * vertex and fragment shaders. + * + * @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 shaderProgram = createShader(vertSrc, fragSrc); + * + * // Compile and apply the p5.Shader object. + * shader(shaderProgram); + * + * // 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. + * + * // 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 + * ); + * } + * `; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * let 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]); + * + * // Set the shader uniform r to 0.005. + * // r is the size of the image in Mandelbrot-space. + * mandelbrot.setUniform('r', 0.005); + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a plane as a drawing surface. + * plane(100, 100); + * + * describe('A black fractal image on a magenta background.'); + * } + * + *
+ * + *
+ * + * // 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); + * + * // 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); + * } + * + *
+ * + *
+ * + * // A shader with hooks. + * let myShader; + * + * // A shader with modified hooks. + * let modifiedShader; + * + * // 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; + * + * void main() { + * vec4 positionVec4 = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * // Create a fragment shader that uses a hook. + * let fragSrc = ` + * precision highp float; + * void main() { + * // Let users override the color + * gl_FragColor = HOOK_getColor(vec4(1., 0., 0., 1.)); + * } + * `; + * + * function setup() { + * createCanvas(50, 50, WEBGL); + * + * // Create a shader with hooks + * myShader = createShader(vertSrc, fragSrc, { + * fragment: { + * 'vec4 getColor': '(vec4 color) { return color; }' + * } + * }); + * + * // Make a version of the shader with a hook overridden + * modifiedShader = myShader.modify({ + * 'vec4 getColor': `(vec4 color) { + * return vec4(0., 0., 1., 1.); + * }` + * }); + * } + * + * function draw() { + * noStroke(); + * + * push(); + * shader(myShader); + * translate(-width/3, 0); + * sphere(10); + * pop(); + * + * push(); + * shader(modifiedShader); + * translate(width/3, 0); + * sphere(10); + * pop(); + * } + * + *
+ */ + fn.createShader = function (vertSrc, fragSrc, options) { + p5._validateParameters('createShader', arguments); + return new p5.Shader(this._renderer, vertSrc, fragSrc, options); + }; - attribute vec3 aPosition; - // texcoords only come from p5 to vertex shader - // so pass texcoords on to the fragment shader in a varying variable - attribute vec2 aTexCoord; - varying vec2 vTexCoord; + /** + * Creates a p5.Shader object to be used with the + * filter() function. + * + * `createFilterShader()` works like + * createShader() but has a default vertex + * shader included. `createFilterShader()` is intended to be used along with + * filter() for filtering the contents of a canvas. + * A filter shader will be applied to the whole canvas instead of just + * p5.Geometry objects. + * + * The parameter, `fragSrc`, sets the fragment shader. It’s a string that + * contains the fragment shader program written in + * GLSL. + * + * The p5.Shader object that's created has some + * uniforms that can be set: + * - `sampler2D tex0`, which contains the canvas contents as a texture. + * - `vec2 canvasSize`, which is the width and height of the canvas, not including pixel density. + * - `vec2 texelSize`, which is the size of a physical pixel including pixel density. This is calculated as `1.0 / (width * density)` for the pixel width and `1.0 / (height * density)` for the pixel height. + * + * The p5.Shader that's created also provides + * `varying vec2 vTexCoord`, a coordinate with values between 0 and 1. + * `vTexCoord` describes where on the canvas the pixel will be drawn. + * + * For more info about filters and shaders, see Adam Ferriss' repo of shader examples + * or the Introduction to Shaders tutorial. + * + * @method createFilterShader + * @param {String} fragSrc source code for the fragment shader. + * @returns {p5.Shader} new shader object created from the fragment shader. + * + * @example + *
+ * + * function setup() { + * let fragSrc = `precision highp float; + * void main() { + * gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0); + * }`; + * + * createCanvas(100, 100, WEBGL); + * let s = createFilterShader(fragSrc); + * filter(s); + * describe('a yellow canvas'); + * } + * + *
+ * + *
+ * + * let img, s; + * function preload() { + * img = loadImage('assets/bricks.jpg'); + * } + * function setup() { + * let fragSrc = `precision highp float; + * + * // x,y coordinates, given from the vertex shader + * varying vec2 vTexCoord; + * + * // the canvas contents, given from filter() + * uniform sampler2D tex0; + * // other useful information from the canvas + * uniform vec2 texelSize; + * uniform vec2 canvasSize; + * // a custom variable from this sketch + * uniform float darkness; + * + * void main() { + * // get the color at current pixel + * vec4 color = texture2D(tex0, vTexCoord); + * // set the output color + * color.b = 1.0; + * color *= darkness; + * gl_FragColor = vec4(color.rgb, 1.0); + * }`; + * + * createCanvas(100, 100, WEBGL); + * s = createFilterShader(fragSrc); + * } + * function draw() { + * image(img, -50, -50); + * s.setUniform('darkness', 0.5); + * filter(s); + * describe('a image of bricks tinted dark blue'); + * } + * + *
+ */ + fn.createFilterShader = function (fragSrc) { + p5._validateParameters('createFilterShader', arguments); + let defaultVertV1 = ` + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; - void main() { - // transferring texcoords for the frag shader - vTexCoord = aTexCoord; + attribute vec3 aPosition; + // texcoords only come from p5 to vertex shader + // so pass texcoords on to the fragment shader in a varying variable + attribute vec2 aTexCoord; + varying vec2 vTexCoord; - // copy position with a fourth coordinate for projection (1.0 is normal) - vec4 positionVec4 = vec4(aPosition, 1.0); + void main() { + // transferring texcoords for the frag shader + vTexCoord = aTexCoord; - // project to 3D space - gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - } - `; - let defaultVertV2 = `#version 300 es - uniform mat4 uModelViewMatrix; - uniform mat4 uProjectionMatrix; + // copy position with a fourth coordinate for projection (1.0 is normal) + vec4 positionVec4 = vec4(aPosition, 1.0); - in vec3 aPosition; - in vec2 aTexCoord; - out vec2 vTexCoord; + // project to 3D space + gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + } + `; + let defaultVertV2 = `#version 300 es + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; - void main() { - // transferring texcoords for the frag shader - vTexCoord = aTexCoord; + in vec3 aPosition; + in vec2 aTexCoord; + out vec2 vTexCoord; - // copy position with a fourth coordinate for projection (1.0 is normal) - vec4 positionVec4 = vec4(aPosition, 1.0); + void main() { + // transferring texcoords for the frag shader + vTexCoord = aTexCoord; - // project to 3D space - gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; - } - `; - let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; - const shader = new p5.Shader(this._renderer, vertSrc, fragSrc); - if (this._renderer.GL) { - shader.ensureCompiledOnContext(this); - } else { - shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); - } - return shader; -}; + // copy position with a fourth coordinate for projection (1.0 is normal) + vec4 positionVec4 = vec4(aPosition, 1.0); -/** - * Sets the p5.Shader object to apply while drawing. - * - * Shaders are programs that run on the graphics processing unit (GPU). They - * 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. - * p5.Shader objects can be created using the - * createShader() and - * loadShader() functions. - * - * The parameter, `s`, is the p5.Shader object to - * apply. For example, calling `shader(myShader)` applies `myShader` to - * process each pixel on the canvas. The shader will be used for: - * - Fills when a texture is enabled if it includes a uniform `sampler2D`. - * - Fills when lights are enabled if it includes the attribute `aNormal`, or if it has any of the following uniforms: `uUseLighting`, `uAmbientLightCount`, `uDirectionalLightCount`, `uPointLightCount`, `uAmbientColor`, `uDirectionalDiffuseColors`, `uDirectionalSpecularColors`, `uPointLightLocation`, `uPointLightDiffuseColors`, `uPointLightSpecularColors`, `uLightingDirection`, or `uSpecular`. - * - Fills whenever there are no lights or textures. - * - Strokes if it includes the uniform `uStrokeWeight`. - * - * The source code from a p5.Shader object's - * fragment and vertex shaders will be compiled the first time it's passed to - * `shader()`. See - * MDN - * for more information about compiling shaders. - * - * Calling resetShader() restores a sketch’s - * default shaders. - * - * Note: Shaders can only be used in WebGL mode. - * - * @method shader - * @chainable - * @param {p5.Shader} s p5.Shader object - * to apply. - * - * @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 shaderProgram = createShader(vertSrc, fragSrc); - * - * // Apply the p5.Shader object. - * shader(shaderProgram); - * - * // 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); - * } - * - *
- * - *
- * - * // Note: A "uniform" is a global variable within a shader program. - * - * let redGreen; - * let orangeBlue; - * let showRedGreen = false; - * - * // Load the shader and create two separate p5.Shader objects. - * function preload() { - * redGreen = loadShader('assets/shader.vert', 'assets/shader-gradient.frag'); - * orangeBlue = loadShader('assets/shader.vert', 'assets/shader-gradient.frag'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Initialize the redGreen shader. - * shader(redGreen); - * - * // Set the redGreen shader's center and background color. - * redGreen.setUniform('colorCenter', [1.0, 0.0, 0.0]); - * redGreen.setUniform('colorBackground', [0.0, 1.0, 0.0]); - * - * // Initialize the orangeBlue shader. - * shader(orangeBlue); - * - * // Set the orangeBlue shader's center and background color. - * orangeBlue.setUniform('colorCenter', [1.0, 0.5, 0.0]); - * orangeBlue.setUniform('colorBackground', [0.226, 0.0, 0.615]); - * - * describe( - * 'The scene toggles between two circular gradients when the user double-clicks. An orange and blue gradient vertically, and red and green gradient moves horizontally.' - * ); - * } - * - * function draw() { - * // Update the offset values for each shader. - * // Move orangeBlue vertically. - * // Move redGreen horizontally. - * orangeBlue.setUniform('offset', [0, sin(frameCount * 0.01) + 1]); - * redGreen.setUniform('offset', [sin(frameCount * 0.01), 1]); - * - * if (showRedGreen === true) { - * shader(redGreen); - * } else { - * shader(orangeBlue); - * } - * - * // Style the drawing surface. - * noStroke(); - * - * // Add a quad as a drawing surface. - * quad(-1, -1, 1, -1, 1, 1, -1, 1); - * } - * - * // Toggle between shaders when the user double-clicks. - * function doubleClicked() { - * showRedGreen = !showRedGreen; - * } - * - *
- */ -p5.prototype.shader = function (s) { - this._assert3d('shader'); - p5._validateParameters('shader', arguments); + // project to 3D space + gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + } + `; + let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; + const shader = new p5.Shader(this._renderer, vertSrc, fragSrc); + if (this._renderer.GL) { + shader.ensureCompiledOnContext(this); + } else { + shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); + } + return shader; + }; - s.ensureCompiledOnContext(this); + /** + * Sets the p5.Shader object to apply while drawing. + * + * Shaders are programs that run on the graphics processing unit (GPU). They + * 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. + * p5.Shader objects can be created using the + * createShader() and + * loadShader() functions. + * + * The parameter, `s`, is the p5.Shader object to + * apply. For example, calling `shader(myShader)` applies `myShader` to + * process each pixel on the canvas. The shader will be used for: + * - Fills when a texture is enabled if it includes a uniform `sampler2D`. + * - Fills when lights are enabled if it includes the attribute `aNormal`, or if it has any of the following uniforms: `uUseLighting`, `uAmbientLightCount`, `uDirectionalLightCount`, `uPointLightCount`, `uAmbientColor`, `uDirectionalDiffuseColors`, `uDirectionalSpecularColors`, `uPointLightLocation`, `uPointLightDiffuseColors`, `uPointLightSpecularColors`, `uLightingDirection`, or `uSpecular`. + * - Fills whenever there are no lights or textures. + * - Strokes if it includes the uniform `uStrokeWeight`. + * + * The source code from a p5.Shader object's + * fragment and vertex shaders will be compiled the first time it's passed to + * `shader()`. See + * MDN + * for more information about compiling shaders. + * + * Calling resetShader() restores a sketch’s + * default shaders. + * + * Note: Shaders can only be used in WebGL mode. + * + * @method shader + * @chainable + * @param {p5.Shader} s p5.Shader object + * to apply. + * + * @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 shaderProgram = createShader(vertSrc, fragSrc); + * + * // Apply the p5.Shader object. + * shader(shaderProgram); + * + * // 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); + * } + * + *
+ * + *
+ * + * // Note: A "uniform" is a global variable within a shader program. + * + * let redGreen; + * let orangeBlue; + * let showRedGreen = false; + * + * // Load the shader and create two separate p5.Shader objects. + * function preload() { + * redGreen = loadShader('assets/shader.vert', 'assets/shader-gradient.frag'); + * orangeBlue = loadShader('assets/shader.vert', 'assets/shader-gradient.frag'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Initialize the redGreen shader. + * shader(redGreen); + * + * // Set the redGreen shader's center and background color. + * redGreen.setUniform('colorCenter', [1.0, 0.0, 0.0]); + * redGreen.setUniform('colorBackground', [0.0, 1.0, 0.0]); + * + * // Initialize the orangeBlue shader. + * shader(orangeBlue); + * + * // Set the orangeBlue shader's center and background color. + * orangeBlue.setUniform('colorCenter', [1.0, 0.5, 0.0]); + * orangeBlue.setUniform('colorBackground', [0.226, 0.0, 0.615]); + * + * describe( + * 'The scene toggles between two circular gradients when the user double-clicks. An orange and blue gradient vertically, and red and green gradient moves horizontally.' + * ); + * } + * + * function draw() { + * // Update the offset values for each shader. + * // Move orangeBlue vertically. + * // Move redGreen horizontally. + * orangeBlue.setUniform('offset', [0, sin(frameCount * 0.01) + 1]); + * redGreen.setUniform('offset', [sin(frameCount * 0.01), 1]); + * + * if (showRedGreen === true) { + * shader(redGreen); + * } else { + * shader(orangeBlue); + * } + * + * // Style the drawing surface. + * noStroke(); + * + * // Add a quad as a drawing surface. + * quad(-1, -1, 1, -1, 1, 1, -1, 1); + * } + * + * // Toggle between shaders when the user double-clicks. + * function doubleClicked() { + * showRedGreen = !showRedGreen; + * } + * + *
+ */ + fn.shader = function (s) { + this._assert3d('shader'); + p5._validateParameters('shader', arguments); - if (s.isStrokeShader()) { - this._renderer.states.userStrokeShader = s; - } else { - this._renderer.states.userFillShader = s; - this._renderer.states._useNormalMaterial = false; - } + s.ensureCompiledOnContext(this); - s.setDefaultUniforms(); + if (s.isStrokeShader()) { + this._renderer.states.userStrokeShader = s; + } else { + this._renderer.states.userFillShader = s; + this._renderer.states._useNormalMaterial = false; + } - return this; -}; + s.setDefaultUniforms(); -/** - * Get the default shader used with lights, materials, - * and textures. - * - * You can call `baseMaterialShader().modify()` - * and change any of the following hooks: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
HookDescription
- * - * `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. - * - *
- * - * `vec3 getLocalNormal` - * - * - * - * Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. - * - *
- * - * `vec3 getWorldNormal` - * - * - * - * Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. - * - *
- * - * `vec2 getUV` - * - * - * - * Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. - * - *
- * - * `vec4 getVertexColor` - * - * - * - * Update the color of each vertex. It takes in a `vec4 color` 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. - * - *
- * - * `Inputs getPixelInputs` - * - * - * - * Update the per-pixel inputs of the material. It takes in an `Inputs` struct, which includes: - * - `vec3 normal`, the direction pointing out of the surface - * - `vec2 texCoord`, a vector where `x` and `y` are between 0 and 1 describing the spot on a texture the pixel is mapped to, as a fraction of the texture size - * - `vec3 ambientLight`, the ambient light color on the vertex - * - `vec4 color`, the base material color of the pixel - * - `vec3 ambientMaterial`, the color of the pixel when affected by ambient light - * - `vec3 specularMaterial`, the color of the pixel when reflecting specular highlights - * - `vec3 emissiveMaterial`, the light color emitted by the pixel - * - `float shininess`, a number representing how sharp specular reflections should be, from 1 to infinity - * - `float metalness`, a number representing how mirrorlike the material should be, between 0 and 1 - * The struct can be modified and returned. - *
- * - * `vec4 combineColors` - * - * - * - * Take in a `ColorComponents` struct containing all the different components of light, and combining them into - * a single final color. The struct contains: - * - `vec3 baseColor`, the base color of the pixel - * - `float opacity`, the opacity between 0 and 1 that it should be drawn at - * - `vec3 ambientColor`, the color of the pixel when affected by ambient light - * - `vec3 specularColor`, the color of the pixel when affected by specular reflections - * - `vec3 diffuse`, the amount of diffused light hitting the pixel - * - `vec3 ambient`, the amount of ambient light hitting the pixel - * - `vec3 specular`, the amount of specular reflection hitting the pixel - * - `vec3 emissive`, the amount of light emitted by the pixel - * - *
- * - * `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. - * - *
- * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. - * - * Call `baseMaterialShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @method baseMaterialShader - * @beta - * @returns {p5.Shader} The material shader - * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * 'vec3 getWorldPosition': `(vec3 pos) { - * pos.y += 20.0 * 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({ - * declarations: 'vec3 myNormal;', - * 'Inputs getPixelInputs': `(Inputs inputs) { - * myNormal = inputs.normal; - * return inputs; - * }`, - * 'vec4 getFinalColor': `(vec4 color) { - * return mix( - * vec4(1.0, 1.0, 1.0, 1.0), - * color, - * abs(dot(myNormal, vec3(0.0, 0.0, 1.0))) - * ); - * }` - * }); - * } - * - * function draw() { - * background(255); - * rotateY(millis() * 0.001); - * shader(myShader); - * lights(); - * noStroke(); - * fill('red'); - * torus(30); - * } - * - *
- * - * @example - *
- * - * let myShader; - * let environment; - * - * function preload() { - * environment = loadImage('assets/outdoor_spheremap.jpg'); - * } - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * 'Inputs getPixelInputs': `(Inputs inputs) { - * float factor = - * sin( - * inputs.texCoord.x * ${TWO_PI} + - * inputs.texCoord.y * ${TWO_PI} - * ) * 0.4 + 0.5; - * inputs.shininess = mix(1., 100., factor); - * inputs.metalness = factor; - * return inputs; - * }` - * }); - * } - * - * function draw() { - * panorama(environment); - * ambientLight(100); - * imageLight(environment); - * rotateY(millis() * 0.001); - * shader(myShader); - * noStroke(); - * fill(255); - * specularMaterial(150); - * sphere(50); - * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseMaterialShader().modify({ - * 'Inputs getPixelInputs': `(Inputs inputs) { - * vec3 newNormal = inputs.normal; - * // Simple bump mapping: adjust the normal based on position - * newNormal.x += 0.2 * sin( - * sin( - * inputs.texCoord.y * ${TWO_PI} * 10.0 + - * inputs.texCoord.x * ${TWO_PI} * 25.0 - * ) - * ); - * newNormal.y += 0.2 * sin( - * sin( - * inputs.texCoord.x * ${TWO_PI} * 10.0 + - * inputs.texCoord.y * ${TWO_PI} * 25.0 - * ) - * ); - * inputs.normal = normalize(newNormal); - * return inputs; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * ambientLight(150); - * pointLight( - * 255, 255, 255, - * 100*cos(frameCount*0.04), -50, 100*sin(frameCount*0.04) - * ); - * noStroke(); - * fill('red'); - * shininess(200); - * specularMaterial(255); - * sphere(50); - * } - * - *
- */ -p5.prototype.baseMaterialShader = function() { - this._assert3d('baseMaterialShader'); - return this._renderer.baseMaterialShader(); -}; + return this; + }; -/** - * Get the shader used by `normalMaterial()`. - * - * You can call `baseNormalShader().modify()` - * and change any of the following hooks: - * - * Hook | Description - * -----|------------ - * `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. - * `vec3 getLocalNormal` | Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. - * `vec3 getWorldNormal` | Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. - * `vec2 getUV` | Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. - * `vec4 getVertexColor` | Update the color of each vertex. It takes in a `vec4 color` 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. - * `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. - * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. - * - * Call `baseNormalShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @method baseNormalShader - * @beta - * @returns {p5.Shader} The `normalMaterial` shader - * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseNormalShader().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); - * noStroke(); - * sphere(50); - * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseNormalShader().modify({ - * 'vec3 getWorldNormal': '(vec3 normal) { return abs(normal); }', - * 'vec4 getFinalColor': `(vec4 color) { - * // Map the r, g, and b values of the old normal to new colors - * // instead of just red, green, and blue: - * vec3 newColor = - * color.r * vec3(89.0, 240.0, 232.0) / 255.0 + - * color.g * vec3(240.0, 237.0, 89.0) / 255.0 + - * color.b * vec3(205.0, 55.0, 222.0) / 255.0; - * newColor = newColor / (color.r + color.g + color.b); - * return vec4(newColor, 1.0) * color.a; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * noStroke(); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.015); - * box(100); - * } - * - *
- */ -p5.prototype.baseNormalShader = function() { - this._assert3d('baseNormalShader'); - return this._renderer.baseNormalShader(); -}; + /** + * Get the default shader used with lights, materials, + * and textures. + * + * You can call `baseMaterialShader().modify()` + * and change any of the following hooks: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
HookDescription
+ * + * `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. + * + *
+ * + * `vec3 getLocalNormal` + * + * + * + * Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. + * + *
+ * + * `vec3 getWorldNormal` + * + * + * + * Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. + * + *
+ * + * `vec2 getUV` + * + * + * + * Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. + * + *
+ * + * `vec4 getVertexColor` + * + * + * + * Update the color of each vertex. It takes in a `vec4 color` 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. + * + *
+ * + * `Inputs getPixelInputs` + * + * + * + * Update the per-pixel inputs of the material. It takes in an `Inputs` struct, which includes: + * - `vec3 normal`, the direction pointing out of the surface + * - `vec2 texCoord`, a vector where `x` and `y` are between 0 and 1 describing the spot on a texture the pixel is mapped to, as a fraction of the texture size + * - `vec3 ambientLight`, the ambient light color on the vertex + * - `vec4 color`, the base material color of the pixel + * - `vec3 ambientMaterial`, the color of the pixel when affected by ambient light + * - `vec3 specularMaterial`, the color of the pixel when reflecting specular highlights + * - `vec3 emissiveMaterial`, the light color emitted by the pixel + * - `float shininess`, a number representing how sharp specular reflections should be, from 1 to infinity + * - `float metalness`, a number representing how mirrorlike the material should be, between 0 and 1 + * The struct can be modified and returned. + *
+ * + * `vec4 combineColors` + * + * + * + * Take in a `ColorComponents` struct containing all the different components of light, and combining them into + * a single final color. The struct contains: + * - `vec3 baseColor`, the base color of the pixel + * - `float opacity`, the opacity between 0 and 1 that it should be drawn at + * - `vec3 ambientColor`, the color of the pixel when affected by ambient light + * - `vec3 specularColor`, the color of the pixel when affected by specular reflections + * - `vec3 diffuse`, the amount of diffused light hitting the pixel + * - `vec3 ambient`, the amount of ambient light hitting the pixel + * - `vec3 specular`, the amount of specular reflection hitting the pixel + * - `vec3 emissive`, the amount of light emitted by the pixel + * + *
+ * + * `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. + * + *
+ * + * Most of the time, you will need to write your hooks in GLSL ES version 300. If you + * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * + * Call `baseMaterialShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method baseMaterialShader + * @beta + * @returns {p5.Shader} The material shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * 'vec3 getWorldPosition': `(vec3 pos) { + * pos.y += 20.0 * 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({ + * declarations: 'vec3 myNormal;', + * 'Inputs getPixelInputs': `(Inputs inputs) { + * myNormal = inputs.normal; + * return inputs; + * }`, + * 'vec4 getFinalColor': `(vec4 color) { + * return mix( + * vec4(1.0, 1.0, 1.0, 1.0), + * color, + * abs(dot(myNormal, vec3(0.0, 0.0, 1.0))) + * ); + * }` + * }); + * } + * + * function draw() { + * background(255); + * rotateY(millis() * 0.001); + * shader(myShader); + * lights(); + * noStroke(); + * fill('red'); + * torus(30); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * let environment; + * + * function preload() { + * environment = loadImage('assets/outdoor_spheremap.jpg'); + * } + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * 'Inputs getPixelInputs': `(Inputs inputs) { + * float factor = + * sin( + * inputs.texCoord.x * ${TWO_PI} + + * inputs.texCoord.y * ${TWO_PI} + * ) * 0.4 + 0.5; + * inputs.shininess = mix(1., 100., factor); + * inputs.metalness = factor; + * return inputs; + * }` + * }); + * } + * + * function draw() { + * panorama(environment); + * ambientLight(100); + * imageLight(environment); + * rotateY(millis() * 0.001); + * shader(myShader); + * noStroke(); + * fill(255); + * specularMaterial(150); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseMaterialShader().modify({ + * 'Inputs getPixelInputs': `(Inputs inputs) { + * vec3 newNormal = inputs.normal; + * // Simple bump mapping: adjust the normal based on position + * newNormal.x += 0.2 * sin( + * sin( + * inputs.texCoord.y * ${TWO_PI} * 10.0 + + * inputs.texCoord.x * ${TWO_PI} * 25.0 + * ) + * ); + * newNormal.y += 0.2 * sin( + * sin( + * inputs.texCoord.x * ${TWO_PI} * 10.0 + + * inputs.texCoord.y * ${TWO_PI} * 25.0 + * ) + * ); + * inputs.normal = normalize(newNormal); + * return inputs; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * ambientLight(150); + * pointLight( + * 255, 255, 255, + * 100*cos(frameCount*0.04), -50, 100*sin(frameCount*0.04) + * ); + * noStroke(); + * fill('red'); + * shininess(200); + * specularMaterial(255); + * sphere(50); + * } + * + *
+ */ + fn.baseMaterialShader = function() { + this._assert3d('baseMaterialShader'); + return this._renderer.baseMaterialShader(); + }; -/** - * Get the shader used when no lights or materials are applied. - * - * You can call `baseColorShader().modify()` - * and change any of the following hooks: - * - * Hook | Description - * -------|------------- - * `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. - * `vec3 getLocalNormal` | Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. - * `vec3 getWorldNormal` | Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. - * `vec2 getUV` | Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. - * `vec4 getVertexColor` | Update the color of each vertex. It takes in a `vec4 color` 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. - * `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. - * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. - * - * Call `baseColorShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @method baseColorShader - * @beta - * @returns {p5.Shader} The color shader - * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseColorShader().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); - * noStroke(); - * fill('red'); - * circle(0, 0, 50); - * } - * - *
- */ -p5.prototype.baseColorShader = function() { - this._assert3d('baseColorShader'); - return this._renderer.baseColorShader(); -}; + /** + * Get the shader used by `normalMaterial()`. + * + * You can call `baseNormalShader().modify()` + * and change any of the following hooks: + * + * Hook | Description + * -----|------------ + * `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. + * `vec3 getLocalNormal` | Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec3 getWorldNormal` | Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec2 getUV` | Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. + * `vec4 getVertexColor` | Update the color of each vertex. It takes in a `vec4 color` 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. + * `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. + * + * Most of the time, you will need to write your hooks in GLSL ES version 300. If you + * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * + * Call `baseNormalShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method baseNormalShader + * @beta + * @returns {p5.Shader} The `normalMaterial` shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseNormalShader().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); + * noStroke(); + * sphere(50); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseNormalShader().modify({ + * 'vec3 getWorldNormal': '(vec3 normal) { return abs(normal); }', + * 'vec4 getFinalColor': `(vec4 color) { + * // Map the r, g, and b values of the old normal to new colors + * // instead of just red, green, and blue: + * vec3 newColor = + * color.r * vec3(89.0, 240.0, 232.0) / 255.0 + + * color.g * vec3(240.0, 237.0, 89.0) / 255.0 + + * color.b * vec3(205.0, 55.0, 222.0) / 255.0; + * newColor = newColor / (color.r + color.g + color.b); + * return vec4(newColor, 1.0) * color.a; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * noStroke(); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.015); + * box(100); + * } + * + *
+ */ + fn.baseNormalShader = function() { + this._assert3d('baseNormalShader'); + return this._renderer.baseNormalShader(); + }; -/** - * Get the shader used when drawing the strokes of shapes. - * - * You can call `baseStrokeShader().modify()` - * and change any of the following hooks: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
HookDescription
- * - * `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 getStrokeWeight` - * - * - * - * Update the stroke weight. It takes in `float weight` and pust return a modified version. - * - *
- * - * `vec2 getLineCenter` - * - * - * - * Update the center of the line. It takes in `vec2 center` and must return a modified version. - * - *
- * - * `vec2 getLinePosition` - * - * - * - * Update the position of each vertex on the edge of the line. It takes in `vec2 position` and must return a modified version. - * - *
- * - * `vec4 getVertexColor` - * - * - * - * Update the color of each vertex. It takes in a `vec4 color` 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. - * - *
- * - * `Inputs getPixelInputs` - * - * - * - * Update the inputs to the shader. It takes in a struct `Inputs inputs`, which includes: - * - `vec4 color`, the color of the stroke - * - `vec2 tangent`, the direction of the stroke in screen space - * - `vec2 center`, the coordinate of the center of the stroke in screen space p5.js pixels - * - `vec2 position`, the coordinate of the current pixel in screen space p5.js pixels - * - `float strokeWeight`, the thickness of the stroke in p5.js pixels - * - *
- * - * `bool shouldDiscard` - * - * - * - * Caps and joins are made by discarded pixels in the fragment shader to carve away unwanted areas. 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. - * - *
- * - * Most of the time, you will need to write your hooks in GLSL ES version 300. If you - * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. - * - * Call `baseStrokeShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @method baseStrokeShader - * @beta - * @returns {p5.Shader} The stroke shader - * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseStrokeShader().modify({ - * 'Inputs getPixelInputs': `(Inputs inputs) { - * float opacity = 1.0 - smoothstep( - * 0.0, - * 15.0, - * length(inputs.position - inputs.center) - * ); - * inputs.color *= opacity; - * return inputs; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * strokeWeight(30); - * line( - * -width/3, - * sin(millis()*0.001) * height/4, - * width/3, - * sin(millis()*0.001 + 1) * height/4 - * ); - * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseStrokeShader().modify({ - * uniforms: { - * 'float time': () => millis() - * }, - * declarations: 'vec3 myPosition;', - * 'vec3 getWorldPosition': `(vec3 pos) { - * myPosition = pos; - * return pos; - * }`, - * 'float getStrokeWeight': `(float w) { - * // Add a somewhat random offset to the weight - * // that varies based on position and time - * float scale = 0.8 + 0.2*sin(10.0 * sin( - * floor(time/250.) + - * myPosition.x*0.01 + - * myPosition.y*0.01 - * )); - * return w * scale; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * myShader.setUniform('time', millis()); - * strokeWeight(10); - * beginShape(); - * for (let i = 0; i <= 50; i++) { - * let r = map(i, 0, 50, 0, width/3); - * let x = r*cos(i*0.2); - * let y = r*sin(i*0.2); - * vertex(x, y); - * } - * endShape(); - * } - * - *
- * - * @example - *
- * - * let myShader; - * - * function setup() { - * createCanvas(200, 200, WEBGL); - * myShader = baseStrokeShader().modify({ - * 'float random': `(vec2 p) { - * vec3 p3 = fract(vec3(p.xyx) * .1031); - * p3 += dot(p3, p3.yzx + 33.33); - * return fract((p3.x + p3.y) * p3.z); - * }`, - * 'Inputs getPixelInputs': `(Inputs inputs) { - * // Replace alpha in the color with dithering by - * // randomly setting pixel colors to 0 based on opacity - * float a = inputs.color.a; - * inputs.color.a = 1.0; - * inputs.color *= random(inputs.position.xy) > a ? 0.0 : 1.0; - * return inputs; - * }` - * }); - * } - * - * function draw() { - * background(255); - * shader(myShader); - * strokeWeight(10); - * beginShape(); - * for (let i = 0; i <= 50; i++) { - * stroke( - * 0, - * 255 - * * map(i, 0, 20, 0, 1, true) - * * map(i, 30, 50, 1, 0, true) - * ); - * vertex( - * map(i, 0, 50, -1, 1) * width/3, - * 50 * sin(i/10 + frameCount/100) - * ); - * } - * endShape(); - * } - * - *
- */ -p5.prototype.baseStrokeShader = function() { - this._assert3d('baseStrokeShader'); - return this._renderer.baseStrokeShader(); -}; + /** + * Get the shader used when no lights or materials are applied. + * + * You can call `baseColorShader().modify()` + * and change any of the following hooks: + * + * Hook | Description + * -------|------------- + * `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. + * `vec3 getLocalNormal` | Update the normal before transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec3 getWorldNormal` | Update the normal after transforms are applied. It takes in `vec3 normal` and must return a modified version. + * `vec2 getUV` | Update the texture coordinates. It takes in `vec2 uv` and must return a modified version. + * `vec4 getVertexColor` | Update the color of each vertex. It takes in a `vec4 color` 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. + * `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. + * + * Most of the time, you will need to write your hooks in GLSL ES version 300. If you + * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * + * Call `baseColorShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method baseColorShader + * @beta + * @returns {p5.Shader} The color shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseColorShader().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); + * noStroke(); + * fill('red'); + * circle(0, 0, 50); + * } + * + *
+ */ + fn.baseColorShader = function() { + this._assert3d('baseColorShader'); + return this._renderer.baseColorShader(); + }; -/** - * Restores the default shaders. - * - * `resetShader()` deactivates any shaders previously applied by - * shader(). - * - * Note: Shaders can only be used in WebGL mode. - * - * @method resetShader - * @chainable - * - * @example - *
- * - * // Create a string with the vertex shader program. - * // The vertex shader is called for each vertex. - * let vertSrc = ` - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * uniform mat4 uProjectionMatrix; - * uniform mat4 uModelViewMatrix; - * varying vec2 vTexCoord; - * - * void main() { - * vTexCoord = aTexCoord; - * vec4 position = vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * uModelViewMatrix * position; - * } - * `; - * - * // 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 myShader; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Shader object. - * myShader = createShader(vertSrc, fragSrc); - * - * describe( - * 'Two rotating cubes on a gray background. The left one has a blue-purple gradient on each face. The right one is red.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Draw a box using the p5.Shader. - * // shader() sets the active shader to myShader. - * shader(myShader); - * push(); - * translate(-25, 0, 0); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(width / 4); - * pop(); - * - * // Draw a box using the default fill shader. - * // resetShader() restores the default fill shader. - * resetShader(); - * fill(255, 0, 0); - * push(); - * translate(25, 0, 0); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(width / 4); - * pop(); - * } - * - *
- */ -p5.prototype.resetShader = function () { - this._renderer.states.userFillShader = this._renderer.states.userStrokeShader = null; - return this; -}; + /** + * Get the shader used when drawing the strokes of shapes. + * + * You can call `baseStrokeShader().modify()` + * and change any of the following hooks: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
HookDescription
+ * + * `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 getStrokeWeight` + * + * + * + * Update the stroke weight. It takes in `float weight` and pust return a modified version. + * + *
+ * + * `vec2 getLineCenter` + * + * + * + * Update the center of the line. It takes in `vec2 center` and must return a modified version. + * + *
+ * + * `vec2 getLinePosition` + * + * + * + * Update the position of each vertex on the edge of the line. It takes in `vec2 position` and must return a modified version. + * + *
+ * + * `vec4 getVertexColor` + * + * + * + * Update the color of each vertex. It takes in a `vec4 color` 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. + * + *
+ * + * `Inputs getPixelInputs` + * + * + * + * Update the inputs to the shader. It takes in a struct `Inputs inputs`, which includes: + * - `vec4 color`, the color of the stroke + * - `vec2 tangent`, the direction of the stroke in screen space + * - `vec2 center`, the coordinate of the center of the stroke in screen space p5.js pixels + * - `vec2 position`, the coordinate of the current pixel in screen space p5.js pixels + * - `float strokeWeight`, the thickness of the stroke in p5.js pixels + * + *
+ * + * `bool shouldDiscard` + * + * + * + * Caps and joins are made by discarded pixels in the fragment shader to carve away unwanted areas. 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. + * + *
+ * + * Most of the time, you will need to write your hooks in GLSL ES version 300. If you + * are using WebGL 1 instead of 2, write your hooks in GLSL ES 100 instead. + * + * Call `baseStrokeShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @method baseStrokeShader + * @beta + * @returns {p5.Shader} The stroke shader + * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseStrokeShader().modify({ + * 'Inputs getPixelInputs': `(Inputs inputs) { + * float opacity = 1.0 - smoothstep( + * 0.0, + * 15.0, + * length(inputs.position - inputs.center) + * ); + * inputs.color *= opacity; + * return inputs; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * strokeWeight(30); + * line( + * -width/3, + * sin(millis()*0.001) * height/4, + * width/3, + * sin(millis()*0.001 + 1) * height/4 + * ); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseStrokeShader().modify({ + * uniforms: { + * 'float time': () => millis() + * }, + * declarations: 'vec3 myPosition;', + * 'vec3 getWorldPosition': `(vec3 pos) { + * myPosition = pos; + * return pos; + * }`, + * 'float getStrokeWeight': `(float w) { + * // Add a somewhat random offset to the weight + * // that varies based on position and time + * float scale = 0.8 + 0.2*sin(10.0 * sin( + * floor(time/250.) + + * myPosition.x*0.01 + + * myPosition.y*0.01 + * )); + * return w * scale; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * myShader.setUniform('time', millis()); + * strokeWeight(10); + * beginShape(); + * for (let i = 0; i <= 50; i++) { + * let r = map(i, 0, 50, 0, width/3); + * let x = r*cos(i*0.2); + * let y = r*sin(i*0.2); + * vertex(x, y); + * } + * endShape(); + * } + * + *
+ * + * @example + *
+ * + * let myShader; + * + * function setup() { + * createCanvas(200, 200, WEBGL); + * myShader = baseStrokeShader().modify({ + * 'float random': `(vec2 p) { + * vec3 p3 = fract(vec3(p.xyx) * .1031); + * p3 += dot(p3, p3.yzx + 33.33); + * return fract((p3.x + p3.y) * p3.z); + * }`, + * 'Inputs getPixelInputs': `(Inputs inputs) { + * // Replace alpha in the color with dithering by + * // randomly setting pixel colors to 0 based on opacity + * float a = inputs.color.a; + * inputs.color.a = 1.0; + * inputs.color *= random(inputs.position.xy) > a ? 0.0 : 1.0; + * return inputs; + * }` + * }); + * } + * + * function draw() { + * background(255); + * shader(myShader); + * strokeWeight(10); + * beginShape(); + * for (let i = 0; i <= 50; i++) { + * stroke( + * 0, + * 255 + * * map(i, 0, 20, 0, 1, true) + * * map(i, 30, 50, 1, 0, true) + * ); + * vertex( + * map(i, 0, 50, -1, 1) * width/3, + * 50 * sin(i/10 + frameCount/100) + * ); + * } + * endShape(); + * } + * + *
+ */ + fn.baseStrokeShader = function() { + this._assert3d('baseStrokeShader'); + return this._renderer.baseStrokeShader(); + }; -/** - * Sets the texture that will be used on shapes. - * - * A texture is like a skin that wraps around a shape. `texture()` works with - * built-in shapes, such as square() and - * sphere(), and custom shapes created with - * functions such as buildGeometry(). To - * texture a geometry created with beginShape(), - * uv coordinates must be passed to each - * vertex() call. - * - * The parameter, `tex`, is the texture to apply. `texture()` can use a range - * of sources including images, videos, and offscreen renderers such as - * p5.Graphics and - * p5.Framebuffer objects. - * - * To texture a geometry created with beginShape(), - * you will need to specify uv coordinates in vertex(). - * - * Note: `texture()` can only be used in WebGL mode. - * - * @method texture - * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex media to use as the texture. - * @chainable - * - * @example - *
- * - * let img; - * - * // Load an image and create a p5.Image object. - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A spinning cube with an image of a ceiling on each face.'); - * } - * - * function draw() { - * background(0); - * - * // Rotate around the x-, y-, and z-axes. - * rotateZ(frameCount * 0.01); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * - * // Apply the image as a texture. - * texture(img); - * - * // Draw the box. - * box(50); - * } - * - *
- * - *
- * - * let pg; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Graphics object. - * pg = createGraphics(100, 100); - * - * // Draw a circle to the p5.Graphics object. - * pg.background(200); - * pg.circle(50, 50, 30); - * - * describe('A spinning cube with circle at the center of each face.'); - * } - * - * function draw() { - * background(0); - * - * // Rotate around the x-, y-, and z-axes. - * rotateZ(frameCount * 0.01); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * - * // Apply the p5.Graphics object as a texture. - * texture(pg); - * - * // Draw the box. - * box(50); - * } - * - *
- * - *
- * - * let vid; - * - * // Load a video and create a p5.MediaElement object. - * function preload() { - * vid = createVideo('assets/fingers.mov'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Hide the video. - * vid.hide(); - * - * // Set the video to loop. - * vid.loop(); - * - * describe('A rectangle with video as texture'); - * } - * - * function draw() { - * background(0); - * - * // Rotate around the y-axis. - * rotateY(frameCount * 0.01); - * - * // Apply the video as a texture. - * texture(vid); - * - * // Draw the rectangle. - * rect(-40, -40, 80, 80); - * } - * - *
- * - *
- * - * let vid; - * - * // Load a video and create a p5.MediaElement object. - * function preload() { - * vid = createVideo('assets/fingers.mov'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Hide the video. - * vid.hide(); - * - * // Set the video to loop. - * vid.loop(); - * - * describe('A rectangle with video as texture'); - * } - * - * function draw() { - * background(0); - * - * // Rotate around the y-axis. - * rotateY(frameCount * 0.01); - * - * // Set the texture mode. - * textureMode(NORMAL); - * - * // Apply the video as a texture. - * texture(vid); - * - * // Draw a custom shape using uv coordinates. - * beginShape(); - * vertex(-40, -40, 0, 0); - * vertex(40, -40, 1, 0); - * vertex(40, 40, 1, 1); - * vertex(-40, 40, 0, 1); - * endShape(); - * } - * - *
- */ -p5.prototype.texture = function (tex) { - this._assert3d('texture'); - p5._validateParameters('texture', arguments); - if (tex.gifProperties) { - tex._animateGif(this); - } + /** + * Restores the default shaders. + * + * `resetShader()` deactivates any shaders previously applied by + * shader(). + * + * Note: Shaders can only be used in WebGL mode. + * + * @method resetShader + * @chainable + * + * @example + *
+ * + * // Create a string with the vertex shader program. + * // The vertex shader is called for each vertex. + * let vertSrc = ` + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * uniform mat4 uProjectionMatrix; + * uniform mat4 uModelViewMatrix; + * varying vec2 vTexCoord; + * + * void main() { + * vTexCoord = aTexCoord; + * vec4 position = vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * uModelViewMatrix * position; + * } + * `; + * + * // 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 myShader; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Shader object. + * myShader = createShader(vertSrc, fragSrc); + * + * describe( + * 'Two rotating cubes on a gray background. The left one has a blue-purple gradient on each face. The right one is red.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Draw a box using the p5.Shader. + * // shader() sets the active shader to myShader. + * shader(myShader); + * push(); + * translate(-25, 0, 0); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(width / 4); + * pop(); + * + * // Draw a box using the default fill shader. + * // resetShader() restores the default fill shader. + * resetShader(); + * fill(255, 0, 0); + * push(); + * translate(25, 0, 0); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(width / 4); + * pop(); + * } + * + *
+ */ + fn.resetShader = function () { + this._renderer.states.userFillShader = this._renderer.states.userStrokeShader = null; + return this; + }; - this._renderer.states.drawMode = constants.TEXTURE; - this._renderer.states._useNormalMaterial = false; - this._renderer.states._tex = tex; - this._renderer.states.doFill = true; + /** + * Sets the texture that will be used on shapes. + * + * A texture is like a skin that wraps around a shape. `texture()` works with + * built-in shapes, such as square() and + * sphere(), and custom shapes created with + * functions such as buildGeometry(). To + * texture a geometry created with beginShape(), + * uv coordinates must be passed to each + * vertex() call. + * + * The parameter, `tex`, is the texture to apply. `texture()` can use a range + * of sources including images, videos, and offscreen renderers such as + * p5.Graphics and + * p5.Framebuffer objects. + * + * To texture a geometry created with beginShape(), + * you will need to specify uv coordinates in vertex(). + * + * Note: `texture()` can only be used in WebGL mode. + * + * @method texture + * @param {p5.Image|p5.MediaElement|p5.Graphics|p5.Texture|p5.Framebuffer|p5.FramebufferTexture} tex media to use as the texture. + * @chainable + * + * @example + *
+ * + * let img; + * + * // Load an image and create a p5.Image object. + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A spinning cube with an image of a ceiling on each face.'); + * } + * + * function draw() { + * background(0); + * + * // Rotate around the x-, y-, and z-axes. + * rotateZ(frameCount * 0.01); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * + * // Apply the image as a texture. + * texture(img); + * + * // Draw the box. + * box(50); + * } + * + *
+ * + *
+ * + * let pg; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Graphics object. + * pg = createGraphics(100, 100); + * + * // Draw a circle to the p5.Graphics object. + * pg.background(200); + * pg.circle(50, 50, 30); + * + * describe('A spinning cube with circle at the center of each face.'); + * } + * + * function draw() { + * background(0); + * + * // Rotate around the x-, y-, and z-axes. + * rotateZ(frameCount * 0.01); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * + * // Apply the p5.Graphics object as a texture. + * texture(pg); + * + * // Draw the box. + * box(50); + * } + * + *
+ * + *
+ * + * let vid; + * + * // Load a video and create a p5.MediaElement object. + * function preload() { + * vid = createVideo('assets/fingers.mov'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Hide the video. + * vid.hide(); + * + * // Set the video to loop. + * vid.loop(); + * + * describe('A rectangle with video as texture'); + * } + * + * function draw() { + * background(0); + * + * // Rotate around the y-axis. + * rotateY(frameCount * 0.01); + * + * // Apply the video as a texture. + * texture(vid); + * + * // Draw the rectangle. + * rect(-40, -40, 80, 80); + * } + * + *
+ * + *
+ * + * let vid; + * + * // Load a video and create a p5.MediaElement object. + * function preload() { + * vid = createVideo('assets/fingers.mov'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Hide the video. + * vid.hide(); + * + * // Set the video to loop. + * vid.loop(); + * + * describe('A rectangle with video as texture'); + * } + * + * function draw() { + * background(0); + * + * // Rotate around the y-axis. + * rotateY(frameCount * 0.01); + * + * // Set the texture mode. + * textureMode(NORMAL); + * + * // Apply the video as a texture. + * texture(vid); + * + * // Draw a custom shape using uv coordinates. + * beginShape(); + * vertex(-40, -40, 0, 0); + * vertex(40, -40, 1, 0); + * vertex(40, 40, 1, 1); + * vertex(-40, 40, 0, 1); + * endShape(); + * } + * + *
+ */ + fn.texture = function (tex) { + this._assert3d('texture'); + p5._validateParameters('texture', arguments); + if (tex.gifProperties) { + tex._animateGif(this); + } - return this; -}; + this._renderer.states.drawMode = constants.TEXTURE; + this._renderer.states._useNormalMaterial = false; + this._renderer.states._tex = tex; + this._renderer.states.doFill = true; -/** - * Changes the coordinate system used for textures when they’re applied to - * custom shapes. - * - * In order for texture() to work, a shape needs a - * way to map the points on its surface to the pixels in an image. Built-in - * shapes such as rect() and - * box() already have these texture mappings based on - * their vertices. Custom shapes created with - * vertex() require texture mappings to be passed as - * uv coordinates. - * - * Each call to vertex() must include 5 arguments, - * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)` - * to the pixel at coordinates `(u, v)` within an image. For example, the - * corners of a rectangular image are mapped to the corners of a rectangle by default: - * - * - * // Apply the image as a texture. - * texture(img); - * - * // Draw the rectangle. - * rect(0, 0, 30, 50); - * - * - * If the image in the code snippet above has dimensions of 300 x 500 pixels, - * the same result could be achieved as follows: - * - * - * // Apply the image as a texture. - * texture(img); - * - * // Draw the rectangle. - * beginShape(); - * - * // Top-left. - * // u: 0, v: 0 - * vertex(0, 0, 0, 0, 0); - * - * // Top-right. - * // u: 300, v: 0 - * vertex(30, 0, 0, 300, 0); - * - * // Bottom-right. - * // u: 300, v: 500 - * vertex(30, 50, 0, 300, 500); - * - * // Bottom-left. - * // u: 0, v: 500 - * vertex(0, 50, 0, 0, 500); - * - * endShape(); - * - * - * `textureMode()` changes the coordinate system for uv coordinates. - * - * The parameter, `mode`, accepts two possible constants. If `NORMAL` is - * passed, as in `textureMode(NORMAL)`, then the texture’s uv coordinates can - * be provided in the range 0 to 1 instead of the image’s dimensions. This can - * be helpful for using the same code for multiple images of different sizes. - * For example, the code snippet above could be rewritten as follows: - * - * - * // Set the texture mode to use normalized coordinates. - * textureMode(NORMAL); - * - * // Apply the image as a texture. - * texture(img); - * - * // Draw the rectangle. - * beginShape(); - * - * // Top-left. - * // u: 0, v: 0 - * vertex(0, 0, 0, 0, 0); - * - * // Top-right. - * // u: 1, v: 0 - * vertex(30, 0, 0, 1, 0); - * - * // Bottom-right. - * // u: 1, v: 1 - * vertex(30, 50, 0, 1, 1); - * - * // Bottom-left. - * // u: 0, v: 1 - * vertex(0, 50, 0, 0, 1); - * - * endShape(); - * - * - * By default, `mode` is `IMAGE`, which scales uv coordinates to the - * dimensions of the image. Calling `textureMode(IMAGE)` applies the default. - * - * Note: `textureMode()` can only be used in WebGL mode. - * - * @method textureMode - * @param {(IMAGE|NORMAL)} mode either IMAGE or NORMAL. - * - * @example - *
- * - * let img; - * - * // Load an image and create a p5.Image object. - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('An image of a ceiling against a black background.'); - * } - * - * function draw() { - * background(0); - * - * // Apply the image as a texture. - * texture(img); - * - * // Draw the custom shape. - * // Use the image's width and height as uv coordinates. - * beginShape(); - * vertex(-30, -30, 0, 0); - * vertex(30, -30, img.width, 0); - * vertex(30, 30, img.width, img.height); - * vertex(-30, 30, 0, img.height); - * endShape(); - * } - * - *
- * - *
- * - * let img; - * - * // Load an image and create a p5.Image object. - * function preload() { - * img = loadImage('assets/laDefense.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('An image of a ceiling against a black background.'); - * } - * - * function draw() { - * background(0); - * - * // Set the texture mode. - * textureMode(NORMAL); - * - * // Apply the image as a texture. - * texture(img); - * - * // Draw the custom shape. - * // Use normalized uv coordinates. - * beginShape(); - * vertex(-30, -30, 0, 0); - * vertex(30, -30, 1, 0); - * vertex(30, 30, 1, 1); - * vertex(-30, 30, 0, 1); - * endShape(); - * } - * - *
- */ -p5.prototype.textureMode = function (mode) { - if (mode !== constants.IMAGE && mode !== constants.NORMAL) { - console.warn( - `You tried to set ${mode} textureMode only supports IMAGE & NORMAL ` - ); - } else { - this._renderer.textureMode = mode; - } -}; + return this; + }; -/** - * Changes the way textures behave when a shape’s uv coordinates go beyond the - * texture. - * - * In order for texture() to work, a shape needs a - * way to map the points on its surface to the pixels in an image. Built-in - * shapes such as rect() and - * box() already have these texture mappings based on - * their vertices. Custom shapes created with - * vertex() require texture mappings to be passed as - * uv coordinates. - * - * Each call to vertex() must include 5 arguments, - * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)` - * to the pixel at coordinates `(u, v)` within an image. For example, the - * corners of a rectangular image are mapped to the corners of a rectangle by default: - * - * ```js - * // Apply the image as a texture. - * texture(img); - * - * // Draw the rectangle. - * rect(0, 0, 30, 50); - * ``` - * - * If the image in the code snippet above has dimensions of 300 x 500 pixels, - * the same result could be achieved as follows: - * - * ```js - * // Apply the image as a texture. - * texture(img); - * - * // Draw the rectangle. - * beginShape(); - * - * // Top-left. - * // u: 0, v: 0 - * vertex(0, 0, 0, 0, 0); - * - * // Top-right. - * // u: 300, v: 0 - * vertex(30, 0, 0, 300, 0); - * - * // Bottom-right. - * // u: 300, v: 500 - * vertex(30, 50, 0, 300, 500); - * - * // Bottom-left. - * // u: 0, v: 500 - * vertex(0, 50, 0, 0, 500); - * - * endShape(); - * ``` - * - * `textureWrap()` controls how textures behave when their uv's go beyond the - * texture. Doing so can produce interesting visual effects such as tiling. - * For example, the custom shape above could have u-coordinates are greater - * than the image’s width: - * - * ```js - * // Apply the image as a texture. - * texture(img); - * - * // Draw the rectangle. - * beginShape(); - * vertex(0, 0, 0, 0, 0); - * - * // Top-right. - * // u: 600 - * vertex(30, 0, 0, 600, 0); - * - * // Bottom-right. - * // u: 600 - * vertex(30, 50, 0, 600, 500); - * - * vertex(0, 50, 0, 0, 500); - * endShape(); - * ``` - * - * The u-coordinates of 600 are greater than the texture image’s width of 300. - * This creates interesting possibilities. - * - * The first parameter, `wrapX`, accepts three possible constants. If `CLAMP` - * is passed, as in `textureWrap(CLAMP)`, the pixels at the edge of the - * texture will extend to the shape’s edges. If `REPEAT` is passed, as in - * `textureWrap(REPEAT)`, the texture will tile repeatedly until reaching the - * shape’s edges. If `MIRROR` is passed, as in `textureWrap(MIRROR)`, the - * texture will tile repeatedly until reaching the shape’s edges, flipping - * its orientation between tiles. By default, textures `CLAMP`. - * - * The second parameter, `wrapY`, is optional. It accepts the same three - * constants, `CLAMP`, `REPEAT`, and `MIRROR`. If one of these constants is - * passed, as in `textureWRAP(MIRROR, REPEAT)`, then the texture will `MIRROR` - * horizontally and `REPEAT` vertically. By default, `wrapY` will be set to - * the same value as `wrapX`. - * - * Note: `textureWrap()` can only be used in WebGL mode. - * - * @method textureWrap - * @param {(CLAMP|REPEAT|MIRROR)} wrapX either CLAMP, REPEAT, or MIRROR - * @param {(CLAMP|REPEAT|MIRROR)} [wrapY=wrapX] either CLAMP, REPEAT, or MIRROR - * - * @example - *
- * - * let img; - * - * function preload() { - * img = loadImage('assets/rockies128.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'An image of a landscape occupies the top-left corner of a square. Its edge colors smear to cover the other thre quarters of the square.' - * ); - * } - * - * function draw() { - * background(0); - * - * // Set the texture mode. - * textureMode(NORMAL); - * - * // Set the texture wrapping. - * // Note: CLAMP is the default mode. - * textureWrap(CLAMP); - * - * // Apply the image as a texture. - * texture(img); - * - * // Style the shape. - * noStroke(); - * - * // Draw the shape. - * // Use uv coordinates > 1. - * beginShape(); - * vertex(-30, -30, 0, 0, 0); - * vertex(30, -30, 0, 2, 0); - * vertex(30, 30, 0, 2, 2); - * vertex(-30, 30, 0, 0, 2); - * endShape(); - * } - * - *
- * - *
- * - * let img; - * - * function preload() { - * img = loadImage('assets/rockies128.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('Four identical images of a landscape arranged in a grid.'); - * } - * - * function draw() { - * background(0); - * - * // Set the texture mode. - * textureMode(NORMAL); - * - * // Set the texture wrapping. - * textureWrap(REPEAT); - * - * // Apply the image as a texture. - * texture(img); - * - * // Style the shape. - * noStroke(); - * - * // Draw the shape. - * // Use uv coordinates > 1. - * beginShape(); - * vertex(-30, -30, 0, 0, 0); - * vertex(30, -30, 0, 2, 0); - * vertex(30, 30, 0, 2, 2); - * vertex(-30, 30, 0, 0, 2); - * endShape(); - * } - * - *
- * - *
- * - * let img; - * - * function preload() { - * img = loadImage('assets/rockies128.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'Four identical images of a landscape arranged in a grid. The images are reflected horizontally and vertically, creating a kaleidoscope effect.' - * ); - * } - * - * function draw() { - * background(0); - * - * // Set the texture mode. - * textureMode(NORMAL); - * - * // Set the texture wrapping. - * textureWrap(MIRROR); - * - * // Apply the image as a texture. - * texture(img); - * - * // Style the shape. - * noStroke(); - * - * // Draw the shape. - * // Use uv coordinates > 1. - * beginShape(); - * vertex(-30, -30, 0, 0, 0); - * vertex(30, -30, 0, 2, 0); - * vertex(30, 30, 0, 2, 2); - * vertex(-30, 30, 0, 0, 2); - * endShape(); - * } - * - *
- * - *
- * - * let img; - * - * function preload() { - * img = loadImage('assets/rockies128.jpg'); - * } - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'Four identical images of a landscape arranged in a grid. The top row and bottom row are reflections of each other.' - * ); - * } - * - * function draw() { - * background(0); - * - * // Set the texture mode. - * textureMode(NORMAL); - * - * // Set the texture wrapping. - * textureWrap(REPEAT, MIRROR); - * - * // Apply the image as a texture. - * texture(img); - * - * // Style the shape. - * noStroke(); - * - * // Draw the shape. - * // Use uv coordinates > 1. - * beginShape(); - * vertex(-30, -30, 0, 0, 0); - * vertex(30, -30, 0, 2, 0); - * vertex(30, 30, 0, 2, 2); - * vertex(-30, 30, 0, 0, 2); - * endShape(); - * } - * - *
- */ -p5.prototype.textureWrap = function (wrapX, wrapY = wrapX) { - this._renderer.textureWrapX = wrapX; - this._renderer.textureWrapY = wrapY; + /** + * Changes the coordinate system used for textures when they’re applied to + * custom shapes. + * + * In order for texture() to work, a shape needs a + * way to map the points on its surface to the pixels in an image. Built-in + * shapes such as rect() and + * box() already have these texture mappings based on + * their vertices. Custom shapes created with + * vertex() require texture mappings to be passed as + * uv coordinates. + * + * Each call to vertex() must include 5 arguments, + * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)` + * to the pixel at coordinates `(u, v)` within an image. For example, the + * corners of a rectangular image are mapped to the corners of a rectangle by default: + * + * + * // Apply the image as a texture. + * texture(img); + * + * // Draw the rectangle. + * rect(0, 0, 30, 50); + * + * + * If the image in the code snippet above has dimensions of 300 x 500 pixels, + * the same result could be achieved as follows: + * + * + * // Apply the image as a texture. + * texture(img); + * + * // Draw the rectangle. + * beginShape(); + * + * // Top-left. + * // u: 0, v: 0 + * vertex(0, 0, 0, 0, 0); + * + * // Top-right. + * // u: 300, v: 0 + * vertex(30, 0, 0, 300, 0); + * + * // Bottom-right. + * // u: 300, v: 500 + * vertex(30, 50, 0, 300, 500); + * + * // Bottom-left. + * // u: 0, v: 500 + * vertex(0, 50, 0, 0, 500); + * + * endShape(); + * + * + * `textureMode()` changes the coordinate system for uv coordinates. + * + * The parameter, `mode`, accepts two possible constants. If `NORMAL` is + * passed, as in `textureMode(NORMAL)`, then the texture’s uv coordinates can + * be provided in the range 0 to 1 instead of the image’s dimensions. This can + * be helpful for using the same code for multiple images of different sizes. + * For example, the code snippet above could be rewritten as follows: + * + * + * // Set the texture mode to use normalized coordinates. + * textureMode(NORMAL); + * + * // Apply the image as a texture. + * texture(img); + * + * // Draw the rectangle. + * beginShape(); + * + * // Top-left. + * // u: 0, v: 0 + * vertex(0, 0, 0, 0, 0); + * + * // Top-right. + * // u: 1, v: 0 + * vertex(30, 0, 0, 1, 0); + * + * // Bottom-right. + * // u: 1, v: 1 + * vertex(30, 50, 0, 1, 1); + * + * // Bottom-left. + * // u: 0, v: 1 + * vertex(0, 50, 0, 0, 1); + * + * endShape(); + * + * + * By default, `mode` is `IMAGE`, which scales uv coordinates to the + * dimensions of the image. Calling `textureMode(IMAGE)` applies the default. + * + * Note: `textureMode()` can only be used in WebGL mode. + * + * @method textureMode + * @param {(IMAGE|NORMAL)} mode either IMAGE or NORMAL. + * + * @example + *
+ * + * let img; + * + * // Load an image and create a p5.Image object. + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('An image of a ceiling against a black background.'); + * } + * + * function draw() { + * background(0); + * + * // Apply the image as a texture. + * texture(img); + * + * // Draw the custom shape. + * // Use the image's width and height as uv coordinates. + * beginShape(); + * vertex(-30, -30, 0, 0); + * vertex(30, -30, img.width, 0); + * vertex(30, 30, img.width, img.height); + * vertex(-30, 30, 0, img.height); + * endShape(); + * } + * + *
+ * + *
+ * + * let img; + * + * // Load an image and create a p5.Image object. + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('An image of a ceiling against a black background.'); + * } + * + * function draw() { + * background(0); + * + * // Set the texture mode. + * textureMode(NORMAL); + * + * // Apply the image as a texture. + * texture(img); + * + * // Draw the custom shape. + * // Use normalized uv coordinates. + * beginShape(); + * vertex(-30, -30, 0, 0); + * vertex(30, -30, 1, 0); + * vertex(30, 30, 1, 1); + * vertex(-30, 30, 0, 1); + * endShape(); + * } + * + *
+ */ + fn.textureMode = function (mode) { + if (mode !== constants.IMAGE && mode !== constants.NORMAL) { + console.warn( + `You tried to set ${mode} textureMode only supports IMAGE & NORMAL ` + ); + } else { + this._renderer.textureMode = mode; + } + }; - for (const texture of this._renderer.textures.values()) { - texture.setWrapMode(wrapX, wrapY); - } -}; + /** + * Changes the way textures behave when a shape’s uv coordinates go beyond the + * texture. + * + * In order for texture() to work, a shape needs a + * way to map the points on its surface to the pixels in an image. Built-in + * shapes such as rect() and + * box() already have these texture mappings based on + * their vertices. Custom shapes created with + * vertex() require texture mappings to be passed as + * uv coordinates. + * + * Each call to vertex() must include 5 arguments, + * as in `vertex(x, y, z, u, v)`, to map the vertex at coordinates `(x, y, z)` + * to the pixel at coordinates `(u, v)` within an image. For example, the + * corners of a rectangular image are mapped to the corners of a rectangle by default: + * + * ```js + * // Apply the image as a texture. + * texture(img); + * + * // Draw the rectangle. + * rect(0, 0, 30, 50); + * ``` + * + * If the image in the code snippet above has dimensions of 300 x 500 pixels, + * the same result could be achieved as follows: + * + * ```js + * // Apply the image as a texture. + * texture(img); + * + * // Draw the rectangle. + * beginShape(); + * + * // Top-left. + * // u: 0, v: 0 + * vertex(0, 0, 0, 0, 0); + * + * // Top-right. + * // u: 300, v: 0 + * vertex(30, 0, 0, 300, 0); + * + * // Bottom-right. + * // u: 300, v: 500 + * vertex(30, 50, 0, 300, 500); + * + * // Bottom-left. + * // u: 0, v: 500 + * vertex(0, 50, 0, 0, 500); + * + * endShape(); + * ``` + * + * `textureWrap()` controls how textures behave when their uv's go beyond the + * texture. Doing so can produce interesting visual effects such as tiling. + * For example, the custom shape above could have u-coordinates are greater + * than the image’s width: + * + * ```js + * // Apply the image as a texture. + * texture(img); + * + * // Draw the rectangle. + * beginShape(); + * vertex(0, 0, 0, 0, 0); + * + * // Top-right. + * // u: 600 + * vertex(30, 0, 0, 600, 0); + * + * // Bottom-right. + * // u: 600 + * vertex(30, 50, 0, 600, 500); + * + * vertex(0, 50, 0, 0, 500); + * endShape(); + * ``` + * + * The u-coordinates of 600 are greater than the texture image’s width of 300. + * This creates interesting possibilities. + * + * The first parameter, `wrapX`, accepts three possible constants. If `CLAMP` + * is passed, as in `textureWrap(CLAMP)`, the pixels at the edge of the + * texture will extend to the shape’s edges. If `REPEAT` is passed, as in + * `textureWrap(REPEAT)`, the texture will tile repeatedly until reaching the + * shape’s edges. If `MIRROR` is passed, as in `textureWrap(MIRROR)`, the + * texture will tile repeatedly until reaching the shape’s edges, flipping + * its orientation between tiles. By default, textures `CLAMP`. + * + * The second parameter, `wrapY`, is optional. It accepts the same three + * constants, `CLAMP`, `REPEAT`, and `MIRROR`. If one of these constants is + * passed, as in `textureWRAP(MIRROR, REPEAT)`, then the texture will `MIRROR` + * horizontally and `REPEAT` vertically. By default, `wrapY` will be set to + * the same value as `wrapX`. + * + * Note: `textureWrap()` can only be used in WebGL mode. + * + * @method textureWrap + * @param {(CLAMP|REPEAT|MIRROR)} wrapX either CLAMP, REPEAT, or MIRROR + * @param {(CLAMP|REPEAT|MIRROR)} [wrapY=wrapX] either CLAMP, REPEAT, or MIRROR + * + * @example + *
+ * + * let img; + * + * function preload() { + * img = loadImage('assets/rockies128.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'An image of a landscape occupies the top-left corner of a square. Its edge colors smear to cover the other thre quarters of the square.' + * ); + * } + * + * function draw() { + * background(0); + * + * // Set the texture mode. + * textureMode(NORMAL); + * + * // Set the texture wrapping. + * // Note: CLAMP is the default mode. + * textureWrap(CLAMP); + * + * // Apply the image as a texture. + * texture(img); + * + * // Style the shape. + * noStroke(); + * + * // Draw the shape. + * // Use uv coordinates > 1. + * beginShape(); + * vertex(-30, -30, 0, 0, 0); + * vertex(30, -30, 0, 2, 0); + * vertex(30, 30, 0, 2, 2); + * vertex(-30, 30, 0, 0, 2); + * endShape(); + * } + * + *
+ * + *
+ * + * let img; + * + * function preload() { + * img = loadImage('assets/rockies128.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('Four identical images of a landscape arranged in a grid.'); + * } + * + * function draw() { + * background(0); + * + * // Set the texture mode. + * textureMode(NORMAL); + * + * // Set the texture wrapping. + * textureWrap(REPEAT); + * + * // Apply the image as a texture. + * texture(img); + * + * // Style the shape. + * noStroke(); + * + * // Draw the shape. + * // Use uv coordinates > 1. + * beginShape(); + * vertex(-30, -30, 0, 0, 0); + * vertex(30, -30, 0, 2, 0); + * vertex(30, 30, 0, 2, 2); + * vertex(-30, 30, 0, 0, 2); + * endShape(); + * } + * + *
+ * + *
+ * + * let img; + * + * function preload() { + * img = loadImage('assets/rockies128.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'Four identical images of a landscape arranged in a grid. The images are reflected horizontally and vertically, creating a kaleidoscope effect.' + * ); + * } + * + * function draw() { + * background(0); + * + * // Set the texture mode. + * textureMode(NORMAL); + * + * // Set the texture wrapping. + * textureWrap(MIRROR); + * + * // Apply the image as a texture. + * texture(img); + * + * // Style the shape. + * noStroke(); + * + * // Draw the shape. + * // Use uv coordinates > 1. + * beginShape(); + * vertex(-30, -30, 0, 0, 0); + * vertex(30, -30, 0, 2, 0); + * vertex(30, 30, 0, 2, 2); + * vertex(-30, 30, 0, 0, 2); + * endShape(); + * } + * + *
+ * + *
+ * + * let img; + * + * function preload() { + * img = loadImage('assets/rockies128.jpg'); + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'Four identical images of a landscape arranged in a grid. The top row and bottom row are reflections of each other.' + * ); + * } + * + * function draw() { + * background(0); + * + * // Set the texture mode. + * textureMode(NORMAL); + * + * // Set the texture wrapping. + * textureWrap(REPEAT, MIRROR); + * + * // Apply the image as a texture. + * texture(img); + * + * // Style the shape. + * noStroke(); + * + * // Draw the shape. + * // Use uv coordinates > 1. + * beginShape(); + * vertex(-30, -30, 0, 0, 0); + * vertex(30, -30, 0, 2, 0); + * vertex(30, 30, 0, 2, 2); + * vertex(-30, 30, 0, 0, 2); + * endShape(); + * } + * + *
+ */ + fn.textureWrap = function (wrapX, wrapY = wrapX) { + this._renderer.textureWrapX = wrapX; + this._renderer.textureWrapY = wrapY; -/** - * Sets the current material as a normal material. - * - * A normal material sets surfaces facing the x-axis to red, those facing the - * y-axis to green, and those facing the z-axis to blue. Normal material isn't - * affected by light. It’s often used as a placeholder material when debugging. - * - * Note: `normalMaterial()` can only be used in WebGL mode. - * - * @method normalMaterial - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A multicolor torus drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Style the torus. - * normalMaterial(); - * - * // Draw the torus. - * torus(30); - * } - * - *
- */ -p5.prototype.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(); - return this; -}; + for (const texture of this._renderer.textures.values()) { + texture.setWrapMode(wrapX, wrapY); + } + }; -/** - * Sets the ambient color of shapes’ surface material. - * - * The `ambientMaterial()` color sets the components of the - * ambientLight() color that shapes will - * reflect. For example, calling `ambientMaterial(255, 255, 0)` would cause a - * shape to reflect red and green light, but not blue light. - * - * `ambientMaterial()` can be called three ways with different parameters to - * set the material’s color. - * - * The first way to call `ambientMaterial()` has one parameter, `gray`. - * Grayscale values between 0 and 255, as in `ambientMaterial(50)`, can be - * passed to set the material’s color. Higher grayscale values make shapes - * appear brighter. - * - * The second way to call `ambientMaterial()` has one parameter, `color`. A - * p5.Color object, an array of color values, or a - * CSS color string, as in `ambientMaterial('magenta')`, can be passed to set - * the material’s color. - * - * The third way to call `ambientMaterial()` has three parameters, `v1`, `v2`, - * and `v3`. RGB, HSB, or HSL values, as in `ambientMaterial(255, 0, 0)`, can - * be passed to set the material’s colors. Color values will be interpreted - * using the current colorMode(). - * - * Note: `ambientMaterial()` can only be used in WebGL mode. - * - * @method ambientMaterial - * @param {Number} v1 red or hue value in the current - * colorMode(). - * @param {Number} v2 green or saturation value in the - * current colorMode(). - * @param {Number} v3 blue, brightness, or lightness value in the - * current colorMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A magenta cube drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a magenta ambient light. - * ambientLight(255, 0, 255); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A purple cube drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a magenta ambient light. - * ambientLight(255, 0, 255); - * - * // Add a dark gray ambient material. - * ambientMaterial(150); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A red cube drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a magenta ambient light. - * ambientLight(255, 0, 255); - * - * // Add a yellow ambient material using RGB values. - * ambientMaterial(255, 255, 0); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A red cube drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a magenta ambient light. - * ambientLight(255, 0, 255); - * - * // Add a yellow ambient material using a p5.Color object. - * let c = color(255, 255, 0); - * ambientMaterial(c); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A red cube drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a magenta ambient light. - * ambientLight(255, 0, 255); - * - * // Add a yellow ambient material using a color string. - * ambientMaterial('yellow'); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A yellow cube drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a white ambient light. - * ambientLight(255, 255, 255); - * - * // Add a yellow ambient material using a color string. - * ambientMaterial('yellow'); - * - * // Draw the box. - * box(); - * } - * - *
- */ + /** + * Sets the current material as a normal material. + * + * A normal material sets surfaces facing the x-axis to red, those facing the + * y-axis to green, and those facing the z-axis to blue. Normal material isn't + * affected by light. It’s often used as a placeholder material when debugging. + * + * Note: `normalMaterial()` can only be used in WebGL mode. + * + * @method normalMaterial + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A multicolor torus drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Style the torus. + * normalMaterial(); + * + * // Draw the torus. + * torus(30); + * } + * + *
+ */ + 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(); + return this; + }; -/** - * @method ambientMaterial - * @param {Number} gray grayscale value between 0 (black) and 255 (white). - * @chainable - */ + /** + * Sets the ambient color of shapes’ surface material. + * + * The `ambientMaterial()` color sets the components of the + * ambientLight() color that shapes will + * reflect. For example, calling `ambientMaterial(255, 255, 0)` would cause a + * shape to reflect red and green light, but not blue light. + * + * `ambientMaterial()` can be called three ways with different parameters to + * set the material’s color. + * + * The first way to call `ambientMaterial()` has one parameter, `gray`. + * Grayscale values between 0 and 255, as in `ambientMaterial(50)`, can be + * passed to set the material’s color. Higher grayscale values make shapes + * appear brighter. + * + * The second way to call `ambientMaterial()` has one parameter, `color`. A + * p5.Color object, an array of color values, or a + * CSS color string, as in `ambientMaterial('magenta')`, can be passed to set + * the material’s color. + * + * The third way to call `ambientMaterial()` has three parameters, `v1`, `v2`, + * and `v3`. RGB, HSB, or HSL values, as in `ambientMaterial(255, 0, 0)`, can + * be passed to set the material’s colors. Color values will be interpreted + * using the current colorMode(). + * + * Note: `ambientMaterial()` can only be used in WebGL mode. + * + * @method ambientMaterial + * @param {Number} v1 red or hue value in the current + * colorMode(). + * @param {Number} v2 green or saturation value in the + * current colorMode(). + * @param {Number} v3 blue, brightness, or lightness value in the + * current colorMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A magenta cube drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a magenta ambient light. + * ambientLight(255, 0, 255); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A purple cube drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a magenta ambient light. + * ambientLight(255, 0, 255); + * + * // Add a dark gray ambient material. + * ambientMaterial(150); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A red cube drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a magenta ambient light. + * ambientLight(255, 0, 255); + * + * // Add a yellow ambient material using RGB values. + * ambientMaterial(255, 255, 0); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A red cube drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a magenta ambient light. + * ambientLight(255, 0, 255); + * + * // Add a yellow ambient material using a p5.Color object. + * let c = color(255, 255, 0); + * ambientMaterial(c); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A red cube drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a magenta ambient light. + * ambientLight(255, 0, 255); + * + * // Add a yellow ambient material using a color string. + * ambientMaterial('yellow'); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A yellow cube drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a white ambient light. + * ambientLight(255, 255, 255); + * + * // Add a yellow ambient material using a color string. + * ambientMaterial('yellow'); + * + * // Draw the box. + * box(); + * } + * + *
+ */ -/** - * @method ambientMaterial - * @param {p5.Color|Number[]|String} color - * color as a p5.Color object, - * an array of color values, or a CSS string. - * @chainable - */ -p5.prototype.ambientMaterial = function (v1, v2, v3) { - this._assert3d('ambientMaterial'); - p5._validateParameters('ambientMaterial', arguments); + /** + * @method ambientMaterial + * @param {Number} gray grayscale value between 0 (black) and 255 (white). + * @chainable + */ - const color = p5.prototype.color.apply(this, arguments); - this._renderer.states._hasSetAmbient = true; - this._renderer.states.curAmbientColor = color._array; - this._renderer.states._useNormalMaterial = false; - this._renderer.states._enableLighting = true; - this._renderer.states.doFill = true; - return this; -}; + /** + * @method ambientMaterial + * @param {p5.Color|Number[]|String} color + * color as a p5.Color object, + * an array of color values, or a CSS string. + * @chainable + */ + fn.ambientMaterial = function (v1, v2, v3) { + this._assert3d('ambientMaterial'); + p5._validateParameters('ambientMaterial', arguments); -/** - * Sets the emissive color of shapes’ surface material. - * - * The `emissiveMaterial()` color sets a color shapes display at full - * strength, regardless of lighting. This can give the appearance that a shape - * is glowing. However, emissive materials don’t actually emit light that - * can affect surrounding objects. - * - * `emissiveMaterial()` can be called three ways with different parameters to - * set the material’s color. - * - * The first way to call `emissiveMaterial()` has one parameter, `gray`. - * Grayscale values between 0 and 255, as in `emissiveMaterial(50)`, can be - * passed to set the material’s color. Higher grayscale values make shapes - * appear brighter. - * - * The second way to call `emissiveMaterial()` has one parameter, `color`. A - * p5.Color object, an array of color values, or a - * CSS color string, as in `emissiveMaterial('magenta')`, can be passed to set - * the material’s color. - * - * The third way to call `emissiveMaterial()` has four parameters, `v1`, `v2`, - * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be - * passed to set the material’s colors, as in `emissiveMaterial(255, 0, 0)` or - * `emissiveMaterial(255, 0, 0, 30)`. Color values will be interpreted using - * the current colorMode(). - * - * Note: `emissiveMaterial()` can only be used in WebGL mode. - * - * @method emissiveMaterial - * @param {Number} v1 red or hue value in the current - * colorMode(). - * @param {Number} v2 green or saturation value in the - * current colorMode(). - * @param {Number} v3 blue, brightness, or lightness value in the - * current colorMode(). - * @param {Number} [alpha] alpha value in the current - * colorMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A red cube drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a white ambient light. - * ambientLight(255, 255, 255); - * - * // Add a red emissive material using RGB values. - * emissiveMaterial(255, 0, 0); - * - * // Draw the box. - * box(); - * } - * - *
- */ + const color = fn.color.apply(this, arguments); + this._renderer.states._hasSetAmbient = true; + this._renderer.states.curAmbientColor = color._array; + this._renderer.states._useNormalMaterial = false; + this._renderer.states._enableLighting = true; + this._renderer.states.doFill = true; + return this; + }; -/** - * @method emissiveMaterial - * @param {Number} gray grayscale value between 0 (black) and 255 (white). - * @chainable - */ + /** + * Sets the emissive color of shapes’ surface material. + * + * The `emissiveMaterial()` color sets a color shapes display at full + * strength, regardless of lighting. This can give the appearance that a shape + * is glowing. However, emissive materials don’t actually emit light that + * can affect surrounding objects. + * + * `emissiveMaterial()` can be called three ways with different parameters to + * set the material’s color. + * + * The first way to call `emissiveMaterial()` has one parameter, `gray`. + * Grayscale values between 0 and 255, as in `emissiveMaterial(50)`, can be + * passed to set the material’s color. Higher grayscale values make shapes + * appear brighter. + * + * The second way to call `emissiveMaterial()` has one parameter, `color`. A + * p5.Color object, an array of color values, or a + * CSS color string, as in `emissiveMaterial('magenta')`, can be passed to set + * the material’s color. + * + * The third way to call `emissiveMaterial()` has four parameters, `v1`, `v2`, + * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be + * passed to set the material’s colors, as in `emissiveMaterial(255, 0, 0)` or + * `emissiveMaterial(255, 0, 0, 30)`. Color values will be interpreted using + * the current colorMode(). + * + * Note: `emissiveMaterial()` can only be used in WebGL mode. + * + * @method emissiveMaterial + * @param {Number} v1 red or hue value in the current + * colorMode(). + * @param {Number} v2 green or saturation value in the + * current colorMode(). + * @param {Number} v3 blue, brightness, or lightness value in the + * current colorMode(). + * @param {Number} [alpha] alpha value in the current + * colorMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A red cube drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a white ambient light. + * ambientLight(255, 255, 255); + * + * // Add a red emissive material using RGB values. + * emissiveMaterial(255, 0, 0); + * + * // Draw the box. + * box(); + * } + * + *
+ */ -/** - * @method emissiveMaterial - * @param {p5.Color|Number[]|String} color - * color as a p5.Color object, - * an array of color values, or a CSS string. - * @chainable - */ -p5.prototype.emissiveMaterial = function (v1, v2, v3, a) { - this._assert3d('emissiveMaterial'); - p5._validateParameters('emissiveMaterial', arguments); + /** + * @method emissiveMaterial + * @param {Number} gray grayscale value between 0 (black) and 255 (white). + * @chainable + */ - const color = p5.prototype.color.apply(this, arguments); - this._renderer.states.curEmissiveColor = color._array; - this._renderer.states._useEmissiveMaterial = true; - this._renderer.states._useNormalMaterial = false; - this._renderer.states._enableLighting = true; + /** + * @method emissiveMaterial + * @param {p5.Color|Number[]|String} color + * color as a p5.Color object, + * an array of color values, or a CSS string. + * @chainable + */ + fn.emissiveMaterial = function (v1, v2, v3, a) { + this._assert3d('emissiveMaterial'); + p5._validateParameters('emissiveMaterial', arguments); - return this; -}; + const color = fn.color.apply(this, arguments); + this._renderer.states.curEmissiveColor = color._array; + this._renderer.states._useEmissiveMaterial = true; + this._renderer.states._useNormalMaterial = false; + this._renderer.states._enableLighting = true; -/** - * Sets the specular color of shapes’ surface material. - * - * The `specularMaterial()` color sets the components of light color that - * glossy coats on shapes will reflect. For example, calling - * `specularMaterial(255, 255, 0)` would cause a shape to reflect red and - * green light, but not blue light. - * - * Unlike ambientMaterial(), - * `specularMaterial()` will reflect the full color of light sources including - * directionalLight(), - * pointLight(), - * and spotLight(). This is what gives it shapes - * their "shiny" appearance. The material’s shininess can be controlled by the - * shininess() function. - * - * `specularMaterial()` can be called three ways with different parameters to - * set the material’s color. - * - * The first way to call `specularMaterial()` has one parameter, `gray`. - * Grayscale values between 0 and 255, as in `specularMaterial(50)`, can be - * passed to set the material’s color. Higher grayscale values make shapes - * appear brighter. - * - * The second way to call `specularMaterial()` has one parameter, `color`. A - * p5.Color> object, an array of color values, or a CSS - * color string, as in `specularMaterial('magenta')`, can be passed to set the - * material’s color. - * - * The third way to call `specularMaterial()` has four parameters, `v1`, `v2`, - * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be - * passed to set the material’s colors, as in `specularMaterial(255, 0, 0)` or - * `specularMaterial(255, 0, 0, 30)`. Color values will be interpreted using - * the current colorMode(). - * - * @method specularMaterial - * @param {Number} gray grayscale value between 0 (black) and 255 (white). - * @param {Number} [alpha] alpha value in the current current - * colorMode(). - * @chainable - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click the canvas to apply a specular material. - * - * let isGlossy = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A red torus drawn on a gray background. It becomes glossy when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a white point light at the top-right. - * pointLight(255, 255, 255, 30, -40, 30); - * - * // Add a glossy coat if the user has double-clicked. - * if (isGlossy === true) { - * specularMaterial(255); - * shininess(50); - * } - * - * // Style the torus. - * noStroke(); - * fill(255, 0, 0); - * - * // Draw the torus. - * torus(30); - * } - * - * // Make the torus glossy when the user double-clicks. - * function doubleClicked() { - * isGlossy = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click the canvas to apply a specular material. - * - * let isGlossy = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a white point light at the top-right. - * pointLight(255, 255, 255, 30, -40, 30); - * - * // Add a glossy green coat if the user has double-clicked. - * if (isGlossy === true) { - * specularMaterial(0, 255, 0); - * shininess(50); - * } - * - * // Style the torus. - * noStroke(); - * fill(255, 0, 0); - * - * // Draw the torus. - * torus(30); - * } - * - * // Make the torus glossy when the user double-clicks. - * function doubleClicked() { - * isGlossy = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click the canvas to apply a specular material. - * - * let isGlossy = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a white point light at the top-right. - * pointLight(255, 255, 255, 30, -40, 30); - * - * // Add a glossy green coat if the user has double-clicked. - * if (isGlossy === true) { - * // Create a p5.Color object. - * let c = color('green'); - * specularMaterial(c); - * shininess(50); - * } - * - * // Style the torus. - * noStroke(); - * fill(255, 0, 0); - * - * // Draw the torus. - * torus(30); - * } - * - * // Make the torus glossy when the user double-clicks. - * function doubleClicked() { - * isGlossy = true; - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * // Double-click the canvas to apply a specular material. - * - * let isGlossy = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on a white point light at the top-right. - * pointLight(255, 255, 255, 30, -40, 30); - * - * // Add a glossy green coat if the user has double-clicked. - * if (isGlossy === true) { - * specularMaterial('#00FF00'); - * shininess(50); - * } - * - * // Style the torus. - * noStroke(); - * fill(255, 0, 0); - * - * // Draw the torus. - * torus(30); - * } - * - * // Make the torus glossy when the user double-clicks. - * function doubleClicked() { - * isGlossy = true; - * } - * - *
- */ + return this; + }; -/** - * @method specularMaterial - * @param {Number} v1 red or hue value in - * the current colorMode(). - * @param {Number} v2 green or saturation value - * in the current colorMode(). - * @param {Number} v3 blue, brightness, or lightness value - * in the current colorMode(). - * @param {Number} [alpha] - * @chainable - */ + /** + * Sets the specular color of shapes’ surface material. + * + * The `specularMaterial()` color sets the components of light color that + * glossy coats on shapes will reflect. For example, calling + * `specularMaterial(255, 255, 0)` would cause a shape to reflect red and + * green light, but not blue light. + * + * Unlike ambientMaterial(), + * `specularMaterial()` will reflect the full color of light sources including + * directionalLight(), + * pointLight(), + * and spotLight(). This is what gives it shapes + * their "shiny" appearance. The material’s shininess can be controlled by the + * shininess() function. + * + * `specularMaterial()` can be called three ways with different parameters to + * set the material’s color. + * + * The first way to call `specularMaterial()` has one parameter, `gray`. + * Grayscale values between 0 and 255, as in `specularMaterial(50)`, can be + * passed to set the material’s color. Higher grayscale values make shapes + * appear brighter. + * + * The second way to call `specularMaterial()` has one parameter, `color`. A + * p5.Color> object, an array of color values, or a CSS + * color string, as in `specularMaterial('magenta')`, can be passed to set the + * material’s color. + * + * The third way to call `specularMaterial()` has four parameters, `v1`, `v2`, + * `v3`, and `alpha`. `alpha` is optional. RGBA, HSBA, or HSLA values can be + * passed to set the material’s colors, as in `specularMaterial(255, 0, 0)` or + * `specularMaterial(255, 0, 0, 30)`. Color values will be interpreted using + * the current colorMode(). + * + * @method specularMaterial + * @param {Number} gray grayscale value between 0 (black) and 255 (white). + * @param {Number} [alpha] alpha value in the current current + * colorMode(). + * @chainable + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click the canvas to apply a specular material. + * + * let isGlossy = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A red torus drawn on a gray background. It becomes glossy when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a white point light at the top-right. + * pointLight(255, 255, 255, 30, -40, 30); + * + * // Add a glossy coat if the user has double-clicked. + * if (isGlossy === true) { + * specularMaterial(255); + * shininess(50); + * } + * + * // Style the torus. + * noStroke(); + * fill(255, 0, 0); + * + * // Draw the torus. + * torus(30); + * } + * + * // Make the torus glossy when the user double-clicks. + * function doubleClicked() { + * isGlossy = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click the canvas to apply a specular material. + * + * let isGlossy = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a white point light at the top-right. + * pointLight(255, 255, 255, 30, -40, 30); + * + * // Add a glossy green coat if the user has double-clicked. + * if (isGlossy === true) { + * specularMaterial(0, 255, 0); + * shininess(50); + * } + * + * // Style the torus. + * noStroke(); + * fill(255, 0, 0); + * + * // Draw the torus. + * torus(30); + * } + * + * // Make the torus glossy when the user double-clicks. + * function doubleClicked() { + * isGlossy = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click the canvas to apply a specular material. + * + * let isGlossy = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a white point light at the top-right. + * pointLight(255, 255, 255, 30, -40, 30); + * + * // Add a glossy green coat if the user has double-clicked. + * if (isGlossy === true) { + * // Create a p5.Color object. + * let c = color('green'); + * specularMaterial(c); + * shininess(50); + * } + * + * // Style the torus. + * noStroke(); + * fill(255, 0, 0); + * + * // Draw the torus. + * torus(30); + * } + * + * // Make the torus glossy when the user double-clicks. + * function doubleClicked() { + * isGlossy = true; + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * // Double-click the canvas to apply a specular material. + * + * let isGlossy = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'A red torus drawn on a gray background. It becomes glossy and reflects green light when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on a white point light at the top-right. + * pointLight(255, 255, 255, 30, -40, 30); + * + * // Add a glossy green coat if the user has double-clicked. + * if (isGlossy === true) { + * specularMaterial('#00FF00'); + * shininess(50); + * } + * + * // Style the torus. + * noStroke(); + * fill(255, 0, 0); + * + * // Draw the torus. + * torus(30); + * } + * + * // Make the torus glossy when the user double-clicks. + * function doubleClicked() { + * isGlossy = true; + * } + * + *
+ */ -/** - * @method specularMaterial - * @param {p5.Color|Number[]|String} color - * color as a p5.Color object, - * an array of color values, or a CSS string. - * @chainable - */ -p5.prototype.specularMaterial = function (v1, v2, v3, alpha) { - this._assert3d('specularMaterial'); - p5._validateParameters('specularMaterial', arguments); + /** + * @method specularMaterial + * @param {Number} v1 red or hue value in + * the current colorMode(). + * @param {Number} v2 green or saturation value + * in the current colorMode(). + * @param {Number} v3 blue, brightness, or lightness value + * in the current colorMode(). + * @param {Number} [alpha] + * @chainable + */ - const color = p5.prototype.color.apply(this, arguments); - this._renderer.states.curSpecularColor = color._array; - this._renderer.states._useSpecularMaterial = true; - this._renderer.states._useNormalMaterial = false; - this._renderer.states._enableLighting = true; + /** + * @method specularMaterial + * @param {p5.Color|Number[]|String} color + * color as a p5.Color object, + * an array of color values, or a CSS string. + * @chainable + */ + fn.specularMaterial = function (v1, v2, v3, alpha) { + this._assert3d('specularMaterial'); + p5._validateParameters('specularMaterial', arguments); - return this; -}; + const color = fn.color.apply(this, arguments); + this._renderer.states.curSpecularColor = color._array; + this._renderer.states._useSpecularMaterial = true; + this._renderer.states._useNormalMaterial = false; + this._renderer.states._enableLighting = true; -/** - * Sets the amount of gloss ("shininess") of a - * specularMaterial(). - * - * Shiny materials focus reflected light more than dull materials. - * `shininess()` affects the way materials reflect light sources including - * directionalLight(), - * pointLight(), - * and spotLight(). - * - * The parameter, `shine`, is a number that sets the amount of shininess. - * `shine` must be greater than 1, which is its default value. - * - * @method shininess - * @param {Number} shine amount of shine. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'Two red spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is shinier than the left sphere.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Turn on a red ambient light. - * ambientLight(255, 0, 0); - * - * // Get the mouse's coordinates. - * let mx = mouseX - 50; - * let my = mouseY - 50; - * - * // Turn on a white point light that follows the mouse. - * pointLight(255, 255, 255, mx, my, 50); - * - * // Style the sphere. - * noStroke(); - * - * // Add a specular material with a grayscale value. - * specularMaterial(255); - * - * // Draw the left sphere with low shininess. - * translate(-25, 0, 0); - * shininess(10); - * sphere(20); - * - * // Draw the right sphere with high shininess. - * translate(50, 0, 0); - * shininess(100); - * sphere(20); - * } - * - *
- */ -p5.prototype.shininess = function (shine) { - this._assert3d('shininess'); - p5._validateParameters('shininess', arguments); + return this; + }; - if (shine < 1) { - shine = 1; - } - this._renderer.states._useShininess = shine; - return this; -}; + /** + * Sets the amount of gloss ("shininess") of a + * specularMaterial(). + * + * Shiny materials focus reflected light more than dull materials. + * `shininess()` affects the way materials reflect light sources including + * directionalLight(), + * pointLight(), + * and spotLight(). + * + * The parameter, `shine`, is a number that sets the amount of shininess. + * `shine` must be greater than 1, which is its default value. + * + * @method shininess + * @param {Number} shine amount of shine. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'Two red spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is shinier than the left sphere.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Turn on a red ambient light. + * ambientLight(255, 0, 0); + * + * // Get the mouse's coordinates. + * let mx = mouseX - 50; + * let my = mouseY - 50; + * + * // Turn on a white point light that follows the mouse. + * pointLight(255, 255, 255, mx, my, 50); + * + * // Style the sphere. + * noStroke(); + * + * // Add a specular material with a grayscale value. + * specularMaterial(255); + * + * // Draw the left sphere with low shininess. + * translate(-25, 0, 0); + * shininess(10); + * sphere(20); + * + * // Draw the right sphere with high shininess. + * translate(50, 0, 0); + * shininess(100); + * sphere(20); + * } + * + *
+ */ + fn.shininess = function (shine) { + this._assert3d('shininess'); + p5._validateParameters('shininess', arguments); -/** - * Sets the amount of "metalness" of a - * specularMaterial(). - * - * `metalness()` can make materials appear more metallic. It affects the way - * materials reflect light sources including - * affects the way materials reflect light sources including - * directionalLight(), - * pointLight(), - * spotLight(), and - * imageLight(). - * - * The parameter, `metallic`, is a number that sets the amount of metalness. - * `metallic` must be greater than 1, which is its default value. Higher - * values, such as `metalness(100)`, make specular materials appear more - * metallic. - * - * @method metalness - * @param {Number} metallic amount of metalness. - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'Two blue spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is more metallic than the left sphere.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Turn on an ambient light. - * ambientLight(200); - * - * // Get the mouse's coordinates. - * let mx = mouseX - 50; - * let my = mouseY - 50; - * - * // Turn on a white point light that follows the mouse. - * pointLight(255, 255, 255, mx, my, 50); - * - * // Style the spheres. - * noStroke(); - * fill(30, 30, 255); - * specularMaterial(255); - * shininess(20); - * - * // Draw the left sphere with low metalness. - * translate(-25, 0, 0); - * metalness(1); - * sphere(20); - * - * // Draw the right sphere with high metalness. - * translate(50, 0, 0); - * metalness(50); - * sphere(20); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let img; - * - * function preload() { - * img = loadImage('assets/outdoor_spheremap.jpg'); - * } - * - * function setup() { - * createCanvas(100 ,100 ,WEBGL); - * - * describe( - * 'Two spheres floating above a landscape. The surface of the spheres reflect the landscape. The right sphere is more reflective than the left sphere.' - * ); - * } - * - * function draw() { - * // Add the panorama. - * panorama(img); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Use the image as a light source. - * imageLight(img); - * - * // Style the spheres. - * noStroke(); - * specularMaterial(50); - * shininess(200); - * - * // Draw the left sphere with low metalness. - * translate(-25, 0, 0); - * metalness(1); - * sphere(20); - * - * // Draw the right sphere with high metalness. - * translate(50, 0, 0); - * metalness(50); - * sphere(20); - * } - * - *
- */ -p5.prototype.metalness = function (metallic) { - this._assert3d('metalness'); - const metalMix = 1 - Math.exp(-metallic / 100); - this._renderer.states._useMetalness = metalMix; - return this; -}; + if (shine < 1) { + shine = 1; + } + this._renderer.states._useShininess = shine; + return this; + }; -/** - * @private blends colors according to color components. - * If alpha value is less than 1, or non-standard blendMode - * we need to enable blending on our gl context. - * @param {Number[]} color The currently set color, with values in 0-1 range - * @param {Boolean} [hasTransparency] Whether the shape being drawn has other - * transparency internally, e.g. via vertex colors - * @return {Number[]} Normalized numbers array - */ -p5.RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { - const gl = this.GL; + /** + * Sets the amount of "metalness" of a + * specularMaterial(). + * + * `metalness()` can make materials appear more metallic. It affects the way + * materials reflect light sources including + * affects the way materials reflect light sources including + * directionalLight(), + * pointLight(), + * spotLight(), and + * imageLight(). + * + * The parameter, `metallic`, is a number that sets the amount of metalness. + * `metallic` must be greater than 1, which is its default value. Higher + * values, such as `metalness(100)`, make specular materials appear more + * metallic. + * + * @method metalness + * @param {Number} metallic amount of metalness. + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'Two blue spheres drawn on a gray background. White light reflects from their surfaces as the mouse moves. The right sphere is more metallic than the left sphere.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Turn on an ambient light. + * ambientLight(200); + * + * // Get the mouse's coordinates. + * let mx = mouseX - 50; + * let my = mouseY - 50; + * + * // Turn on a white point light that follows the mouse. + * pointLight(255, 255, 255, mx, my, 50); + * + * // Style the spheres. + * noStroke(); + * fill(30, 30, 255); + * specularMaterial(255); + * shininess(20); + * + * // Draw the left sphere with low metalness. + * translate(-25, 0, 0); + * metalness(1); + * sphere(20); + * + * // Draw the right sphere with high metalness. + * translate(50, 0, 0); + * metalness(50); + * sphere(20); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let img; + * + * function preload() { + * img = loadImage('assets/outdoor_spheremap.jpg'); + * } + * + * function setup() { + * createCanvas(100 ,100 ,WEBGL); + * + * describe( + * 'Two spheres floating above a landscape. The surface of the spheres reflect the landscape. The right sphere is more reflective than the left sphere.' + * ); + * } + * + * function draw() { + * // Add the panorama. + * panorama(img); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Use the image as a light source. + * imageLight(img); + * + * // Style the spheres. + * noStroke(); + * specularMaterial(50); + * shininess(200); + * + * // Draw the left sphere with low metalness. + * translate(-25, 0, 0); + * metalness(1); + * sphere(20); + * + * // Draw the right sphere with high metalness. + * translate(50, 0, 0); + * metalness(50); + * sphere(20); + * } + * + *
+ */ + fn.metalness = function (metallic) { + this._assert3d('metalness'); + const metalMix = 1 - Math.exp(-metallic / 100); + this._renderer.states._useMetalness = metalMix; + return this; + }; - const isTexture = this.states.drawMode === constants.TEXTURE; - const doBlend = - hasTransparency || - this.states.userFillShader || - this.states.userStrokeShader || - this.states.userPointShader || - isTexture || - this.states.curBlendMode !== constants.BLEND || - colors[colors.length - 1] < 1.0 || - this._isErasing; + /** + * @private blends colors according to color components. + * If alpha value is less than 1, or non-standard blendMode + * we need to enable blending on our gl context. + * @param {Number[]} color The currently set color, with values in 0-1 range + * @param {Boolean} [hasTransparency] Whether the shape being drawn has other + * transparency internally, e.g. via vertex colors + * @return {Number[]} Normalized numbers array + */ + p5.RendererGL.prototype._applyColorBlend = function (colors, hasTransparency) { + const gl = this.GL; - if (doBlend !== this._isBlending) { - if ( - doBlend || - (this.states.curBlendMode !== constants.BLEND && - this.states.curBlendMode !== constants.ADD) - ) { - gl.enable(gl.BLEND); - } else { - gl.disable(gl.BLEND); - } - gl.depthMask(true); - this._isBlending = doBlend; - } - this._applyBlendMode(); - return colors; -}; + const isTexture = this.states.drawMode === constants.TEXTURE; + const doBlend = + hasTransparency || + this.states.userFillShader || + this.states.userStrokeShader || + this.states.userPointShader || + isTexture || + this.states.curBlendMode !== constants.BLEND || + colors[colors.length - 1] < 1.0 || + this._isErasing; -/** - * @private sets blending in gl context to curBlendMode - * @param {Number[]} color [description] - * @return {Number[]} Normalized numbers array - */ -p5.RendererGL.prototype._applyBlendMode = function () { - if (this._cachedBlendMode === this.states.curBlendMode) { - return; - } - const gl = this.GL; - switch (this.states.curBlendMode) { - case constants.BLEND: - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - break; - case constants.ADD: - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.ONE, gl.ONE); - break; - case constants.REMOVE: - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.ZERO, gl.ONE_MINUS_SRC_ALPHA); - break; - case constants.MULTIPLY: - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA); - break; - case constants.SCREEN: - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR); - break; - case constants.EXCLUSION: - gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); - gl.blendFuncSeparate( - gl.ONE_MINUS_DST_COLOR, - gl.ONE_MINUS_SRC_COLOR, - gl.ONE, - gl.ONE - ); - break; - case constants.REPLACE: - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.ONE, gl.ZERO); - break; - case constants.SUBTRACT: - gl.blendEquationSeparate(gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_ADD); - gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - break; - case constants.DARKEST: - if (this.blendExt) { - gl.blendEquationSeparate( - this.blendExt.MIN || this.blendExt.MIN_EXT, - gl.FUNC_ADD - ); - gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); + if (doBlend !== this._isBlending) { + if ( + doBlend || + (this.states.curBlendMode !== constants.BLEND && + this.states.curBlendMode !== constants.ADD) + ) { + gl.enable(gl.BLEND); } else { - console.warn( - 'blendMode(DARKEST) does not work in your browser in WEBGL mode.' - ); + gl.disable(gl.BLEND); } - break; - case constants.LIGHTEST: - if (this.blendExt) { - gl.blendEquationSeparate( - this.blendExt.MAX || this.blendExt.MAX_EXT, - gl.FUNC_ADD + gl.depthMask(true); + this._isBlending = doBlend; + } + this._applyBlendMode(); + return colors; + }; + + /** + * @private sets blending in gl context to curBlendMode + * @param {Number[]} color [description] + * @return {Number[]} Normalized numbers array + */ + p5.RendererGL.prototype._applyBlendMode = function () { + if (this._cachedBlendMode === this.states.curBlendMode) { + return; + } + const gl = this.GL; + switch (this.states.curBlendMode) { + case constants.BLEND: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + break; + case constants.ADD: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE); + break; + case constants.REMOVE: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ZERO, gl.ONE_MINUS_SRC_ALPHA); + break; + case constants.MULTIPLY: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA); + break; + case constants.SCREEN: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR); + break; + case constants.EXCLUSION: + gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); + gl.blendFuncSeparate( + gl.ONE_MINUS_DST_COLOR, + gl.ONE_MINUS_SRC_COLOR, + gl.ONE, + gl.ONE ); - gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); - } else { - console.warn( - 'blendMode(LIGHTEST) does not work in your browser in WEBGL mode.' + break; + case constants.REPLACE: + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ZERO); + break; + case constants.SUBTRACT: + gl.blendEquationSeparate(gl.FUNC_REVERSE_SUBTRACT, gl.FUNC_ADD); + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + break; + case constants.DARKEST: + if (this.blendExt) { + gl.blendEquationSeparate( + this.blendExt.MIN || this.blendExt.MIN_EXT, + gl.FUNC_ADD + ); + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); + } else { + console.warn( + 'blendMode(DARKEST) does not work in your browser in WEBGL mode.' + ); + } + break; + case constants.LIGHTEST: + if (this.blendExt) { + gl.blendEquationSeparate( + this.blendExt.MAX || this.blendExt.MAX_EXT, + gl.FUNC_ADD + ); + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); + } else { + console.warn( + 'blendMode(LIGHTEST) does not work in your browser in WEBGL mode.' + ); + } + break; + default: + console.error( + 'Oops! Somehow RendererGL set curBlendMode to an unsupported mode.' ); - } - break; - default: - console.error( - 'Oops! Somehow RendererGL set curBlendMode to an unsupported mode.' - ); - break; - } - if (!this._isErasing) { - this._cachedBlendMode = this.states.curBlendMode; - } -}; + break; + } + if (!this._isErasing) { + this._cachedBlendMode = this.states.curBlendMode; + } + }; +} + +export default material; -export default p5; +if(typeof p5 !== 'undefined'){ + loading(p5, p5.prototype); +} diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 8e446bde83..586971a5f7 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -4,3936 +4,3940 @@ * @requires core */ -import p5 from '../core/main'; - -//////////////////////////////////////////////////////////////////////////////// -// p5.Prototype Methods -//////////////////////////////////////////////////////////////////////////////// - -/** - * Sets the position and orientation of the current camera in a 3D sketch. - * - * `camera()` allows objects to be viewed from different angles. It has nine - * parameters that are all optional. - * - * The first three parameters, `x`, `y`, and `z`, are the coordinates of the - * camera’s position. For example, calling `camera(0, 0, 0)` places the camera - * at the origin `(0, 0, 0)`. By default, the camera is placed at - * `(0, 0, 800)`. - * - * The next three parameters, `centerX`, `centerY`, and `centerZ` are the - * coordinates of the point where the camera faces. For example, calling - * `camera(0, 0, 0, 10, 20, 30)` places the camera at the origin `(0, 0, 0)` - * and points it at `(10, 20, 30)`. By default, the camera points at the - * origin `(0, 0, 0)`. - * - * The last three parameters, `upX`, `upY`, and `upZ` are the components of - * the "up" vector. The "up" vector orients the camera’s y-axis. For example, - * calling `camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the - * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector - * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up" - * vector is `(0, 1, 0)`. - * - * Note: `camera()` can only be used in WebGL mode. - * - * @method camera - * @for p5 - * @param {Number} [x] x-coordinate of the camera. Defaults to 0. - * @param {Number} [y] y-coordinate of the camera. Defaults to 0. - * @param {Number} [z] z-coordinate of the camera. Defaults to 800. - * @param {Number} [centerX] x-coordinate of the point the camera faces. Defaults to 0. - * @param {Number} [centerY] y-coordinate of the point the camera faces. Defaults to 0. - * @param {Number} [centerZ] z-coordinate of the point the camera faces. Defaults to 0. - * @param {Number} [upX] x-component of the camera’s "up" vector. Defaults to 0. - * @param {Number} [upY] y-component of the camera’s "up" vector. Defaults to 1. - * @param {Number} [upZ] z-component of the camera’s "up" vector. Defaults to 0. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Move the camera to the top-right. - * camera(200, -400, 800); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube apperas to sway left and right on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Calculate the camera's x-coordinate. - * let x = 400 * cos(frameCount * 0.01); - * - * // Orbit the camera around the box. - * camera(x, -400, 800); - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Adjust the range sliders to change the camera's position. - * - * let xSlider; - * let ySlider; - * let zSlider; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create slider objects to set the camera's coordinates. - * xSlider = createSlider(-400, 400, 400); - * xSlider.position(0, 100); - * xSlider.size(100); - * ySlider = createSlider(-400, 400, -200); - * ySlider.position(0, 120); - * ySlider.size(100); - * zSlider = createSlider(0, 1600, 800); - * zSlider.position(0, 140); - * zSlider.size(100); - * - * describe( - * 'A white cube drawn against a gray background. Three range sliders appear beneath the image. The camera position changes when the user moves the sliders.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Get the camera's coordinates from the sliders. - * let x = xSlider.value(); - * let y = ySlider.value(); - * let z = zSlider.value(); - * - * // Move the camera. - * camera(x, y, z); - * - * // Draw the box. - * box(); - * } - * - *
- */ -p5.prototype.camera = function (...args) { - this._assert3d('camera'); - p5._validateParameters('camera', args); - this._renderer.states.curCamera.camera(...args); - return this; -}; - -/** - * Sets a perspective projection for the current camera in a 3D sketch. - * - * In a perspective projection, shapes that are further from the camera appear - * smaller than shapes that are near the camera. This technique, called - * foreshortening, creates realistic 3D scenes. It’s applied by default in - * WebGL mode. - * - * `perspective()` changes the camera’s perspective by changing its viewing - * frustum. The frustum is the volume of space that’s visible to the camera. - * Its shape is a pyramid with its top cut off. The camera is placed where - * the top of the pyramid should be and views everything between the frustum’s - * top (near) plane and its bottom (far) plane. - * - * The first parameter, `fovy`, is the camera’s vertical field of view. It’s - * an angle that describes how tall or narrow a view the camera has. For - * example, calling `perspective(0.5)` sets the camera’s vertical field of - * view to 0.5 radians. By default, `fovy` is calculated based on the sketch’s - * height and the camera’s default z-coordinate, which is 800. The formula for - * the default `fovy` is `2 * atan(height / 2 / 800)`. - * - * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number - * that describes the ratio of the top plane’s width to its height. For - * example, calling `perspective(0.5, 1.5)` sets the camera’s field of view to - * 0.5 radians and aspect ratio to 1.5, which would make shapes appear thinner - * on a square canvas. By default, aspect is set to `width / height`. - * - * The third parameter, `near`, is the distance from the camera to the near - * plane. For example, calling `perspective(0.5, 1.5, 100)` sets the camera’s - * field of view to 0.5 radians, its aspect ratio to 1.5, and places the near - * plane 100 pixels from the camera. Any shapes drawn less than 100 pixels - * from the camera won’t be visible. By default, near is set to `0.1 * 800`, - * which is 1/10th the default distance between the camera and the origin. - * - * The fourth parameter, `far`, is the distance from the camera to the far - * plane. For example, calling `perspective(0.5, 1.5, 100, 10000)` sets the - * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, places the - * near plane 100 pixels from the camera, and places the far plane 10,000 - * pixels from the camera. Any shapes drawn more than 10,000 pixels from the - * camera won’t be visible. By default, far is set to `10 * 800`, which is 10 - * times the default distance between the camera and the origin. - * - * Note: `perspective()` can only be used in WebGL mode. - * - * @method perspective - * @for p5 - * @param {Number} [fovy] camera frustum vertical field of view. Defaults to - * `2 * atan(height / 2 / 800)`. - * @param {Number} [aspect] camera frustum aspect ratio. Defaults to - * `width / height`. - * @param {Number} [near] distance from the camera to the near clipping plane. - * Defaults to `0.1 * 800`. - * @param {Number} [far] distance from the camera to the far clipping plane. - * Defaults to `10 * 800`. - * @chainable - * - * @example - *
- * - * // Double-click to squeeze the box. - * - * let isSqueezed = false; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white rectangular prism on a gray background. The box appears to become thinner when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Place the camera at the top-right. - * camera(400, -400, 800); - * - * if (isSqueezed === true) { - * // Set fovy to 0.2. - * // Set aspect to 1.5. - * perspective(0.2, 1.5); - * } - * - * // Draw the box. - * box(); - * } - * - * // Change the camera's perspective when the user double-clicks. - * function doubleClicked() { - * isSqueezed = true; - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white rectangular prism on a gray background. The prism moves away from the camera until it disappears.'); - * } - * - * function draw() { - * background(200); - * - * // Place the camera at the top-right. - * camera(400, -400, 800); - * - * // Set fovy to 0.2. - * // Set aspect to 1.5. - * // Set near to 600. - * // Set far to 1200. - * perspective(0.2, 1.5, 600, 1200); - * - * // Move the origin away from the camera. - * let x = -frameCount; - * let y = frameCount; - * let z = -2 * frameCount; - * translate(x, y, z); - * - * // Draw the box. - * box(); - * } - * - *
- */ -p5.prototype.perspective = function (...args) { - this._assert3d('perspective'); - p5._validateParameters('perspective', args); - this._renderer.states.curCamera.perspective(...args); - return this; -}; - - -/** - * Enables or disables perspective for lines in 3D sketches. - * - * In WebGL mode, lines can be drawn with a thinner stroke when they’re - * further from the camera. Doing so gives them a more realistic appearance. - * - * By default, lines are drawn differently based on the type of perspective - * being used: - * - `perspective()` and `frustum()` simulate a realistic perspective. In - * these modes, stroke weight is affected by the line’s distance from the - * camera. Doing so results in a more natural appearance. `perspective()` is - * the default mode for 3D sketches. - * - `ortho()` doesn’t simulate a realistic perspective. In this mode, stroke - * weights are consistent regardless of the line’s distance from the camera. - * Doing so results in a more predictable and consistent appearance. - * - * `linePerspective()` can override the default line drawing mode. - * - * The parameter, `enable`, is optional. It’s a `Boolean` value that sets the - * way lines are drawn. If `true` is passed, as in `linePerspective(true)`, - * then lines will appear thinner when they are further from the camera. If - * `false` is passed, as in `linePerspective(false)`, then lines will have - * consistent stroke weights regardless of their distance from the camera. By - * default, `linePerspective()` is enabled. - * - * Calling `linePerspective()` without passing an argument returns `true` if - * it's enabled and `false` if not. - * - * Note: `linePerspective()` can only be used in WebGL mode. - * - * @method linePerspective - * @for p5 - * @param {Boolean} enable whether to enable line perspective. - * - * @example - *
- * - * // Double-click the canvas to toggle the line perspective. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'A white cube with black edges on a gray background. Its edges toggle between thick and thin when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Translate the origin toward the camera. - * translate(-10, 10, 600); - * - * // Rotate the coordinate system. - * rotateY(-0.1); - * rotateX(-0.1); - * - * // Draw the row of boxes. - * for (let i = 0; i < 6; i += 1) { - * translate(0, 0, -40); - * box(10); - * } - * } - * - * // Toggle the line perspective when the user double-clicks. - * function doubleClicked() { - * let isEnabled = linePerspective(); - * linePerspective(!isEnabled); - * } - * - *
- * - *
- * - * // Double-click the canvas to toggle the line perspective. - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe( - * 'A row of cubes with black edges on a gray background. Their edges toggle between thick and thin when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Use an orthographic projection. - * ortho(); - * - * // Translate the origin toward the camera. - * translate(-10, 10, 600); - * - * // Rotate the coordinate system. - * rotateY(-0.1); - * rotateX(-0.1); - * - * // Draw the row of boxes. - * for (let i = 0; i < 6; i += 1) { - * translate(0, 0, -40); - * box(10); - * } - * } - * - * // Toggle the line perspective when the user double-clicks. - * function doubleClicked() { - * let isEnabled = linePerspective(); - * linePerspective(!isEnabled); - * } - * - *
- */ -/** - * @method linePerspective - * @return {Boolean} whether line perspective is enabled. - */ - -p5.prototype.linePerspective = function (enable) { - p5._validateParameters('linePerspective', arguments); - if (!(this._renderer instanceof p5.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; - } -}; - - -/** - * Sets an orthographic projection for the current camera in a 3D sketch. - * - * In an orthographic projection, shapes with the same size always appear the - * same size, regardless of whether they are near or far from the camera. - * - * `ortho()` changes the camera’s perspective by changing its viewing frustum - * from a truncated pyramid to a rectangular prism. The camera is placed in - * front of the frustum and views everything between the frustum’s near plane - * and its far plane. `ortho()` has six optional parameters to define the - * frustum. - * - * The first four parameters, `left`, `right`, `bottom`, and `top`, set the - * coordinates of the frustum’s sides, bottom, and top. For example, calling - * `ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide and - * 400 pixels tall. By default, these coordinates are set based on the - * sketch’s width and height, as in - * `ortho(-width / 2, width / 2, -height / 2, height / 2)`. - * - * The last two parameters, `near` and `far`, set the distance of the - * frustum’s near and far plane from the camera. For example, calling - * `ortho(-100, 100, 200, 200, 50, 1000)` creates a frustum that’s 200 pixels - * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000 - * pixels from the camera. By default, `near` and `far` are set to 0 and - * `max(width, height) + 800`, respectively. - * - * Note: `ortho()` can only be used in WebGL mode. - * - * @method ortho - * @for p5 - * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 2`. - * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 2`. - * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`. - * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 2`. - * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to 0. - * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A row of tiny, white cubes on a gray background. All the cubes appear the same size.'); - * } - * - * function draw() { - * background(200); - * - * // Apply an orthographic projection. - * ortho(); - * - * // Translate the origin toward the camera. - * translate(-10, 10, 600); - * - * // Rotate the coordinate system. - * rotateY(-0.1); - * rotateX(-0.1); - * - * // Draw the row of boxes. - * for (let i = 0; i < 6; i += 1) { - * translate(0, 0, -40); - * box(10); - * } - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Apply an orthographic projection. - * // Center the frustum. - * // Set its width and height to 20. - * // Place its near plane 300 pixels from the camera. - * // Place its far plane 350 pixels from the camera. - * ortho(-10, 10, -10, 10, 300, 350); - * - * // Translate the origin toward the camera. - * translate(-10, 10, 600); - * - * // Rotate the coordinate system. - * rotateY(-0.1); - * rotateX(-0.1); - * - * // Draw the row of boxes. - * for (let i = 0; i < 6; i += 1) { - * translate(0, 0, -40); - * box(10); - * } - * } - * - *
- */ -p5.prototype.ortho = function (...args) { - this._assert3d('ortho'); - p5._validateParameters('ortho', args); - this._renderer.states.curCamera.ortho(...args); - return this; -}; +function camera(p5, fn){ + //////////////////////////////////////////////////////////////////////////////// + // p5.Prototype Methods + //////////////////////////////////////////////////////////////////////////////// -/** - * Sets the frustum of the current camera in a 3D sketch. - * - * In a frustum projection, shapes that are further from the camera appear - * smaller than shapes that are near the camera. This technique, called - * foreshortening, creates realistic 3D scenes. - * - * `frustum()` changes the default camera’s perspective by changing its - * viewing frustum. The frustum is the volume of space that’s visible to the - * camera. The frustum’s shape is a pyramid with its top cut off. The camera - * is placed where the top of the pyramid should be and points towards the - * base of the pyramid. It views everything within the frustum. - * - * The first four parameters, `left`, `right`, `bottom`, and `top`, set the - * coordinates of the frustum’s sides, bottom, and top. For example, calling - * `frustum(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide - * and 400 pixels tall. By default, these coordinates are set based on the - * sketch’s width and height, as in - * `ortho(-width / 20, width / 20, height / 20, -height / 20)`. - * - * The last two parameters, `near` and `far`, set the distance of the - * frustum’s near and far plane from the camera. For example, calling - * `ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s 200 pixels - * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000 - * pixels from the camera. By default, near is set to `0.1 * 800`, which is - * 1/10th the default distance between the camera and the origin. `far` is set - * to `10 * 800`, which is 10 times the default distance between the camera - * and the origin. - * - * Note: `frustum()` can only be used in WebGL mode. - * - * @method frustum - * @for p5 - * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 20`. - * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 20`. - * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`. - * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 20`. - * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`. - * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `10 * 800`. - * @chainable - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * describe('A row of white cubes on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Apply the default frustum projection. - * frustum(); - * - * // Translate the origin toward the camera. - * translate(-10, 10, 600); - * - * // Rotate the coordinate system. - * rotateY(-0.1); - * rotateX(-0.1); - * - * // Draw the row of boxes. - * for (let i = 0; i < 6; i += 1) { - * translate(0, 0, -40); - * box(10); - * } - * } - * - *
- * - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * describe('A white cube on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Adjust the frustum. - * // Center it. - * // Set its width and height to 20 pixels. - * // Place its near plane 300 pixels from the camera. - * // Place its far plane 350 pixels from the camera. - * frustum(-10, 10, -10, 10, 300, 350); - * - * // Translate the origin toward the camera. - * translate(-10, 10, 600); - * - * // Rotate the coordinate system. - * rotateY(-0.1); - * rotateX(-0.1); - * - * // Draw the row of boxes. - * for (let i = 0; i < 6; i += 1) { - * translate(0, 0, -40); - * box(10); - * } - * } - * - *
- */ -p5.prototype.frustum = function (...args) { - this._assert3d('frustum'); - p5._validateParameters('frustum', args); - this._renderer.states.curCamera.frustum(...args); - return this; -}; + /** + * Sets the position and orientation of the current camera in a 3D sketch. + * + * `camera()` allows objects to be viewed from different angles. It has nine + * parameters that are all optional. + * + * The first three parameters, `x`, `y`, and `z`, are the coordinates of the + * camera’s position. For example, calling `camera(0, 0, 0)` places the camera + * at the origin `(0, 0, 0)`. By default, the camera is placed at + * `(0, 0, 800)`. + * + * The next three parameters, `centerX`, `centerY`, and `centerZ` are the + * coordinates of the point where the camera faces. For example, calling + * `camera(0, 0, 0, 10, 20, 30)` places the camera at the origin `(0, 0, 0)` + * and points it at `(10, 20, 30)`. By default, the camera points at the + * origin `(0, 0, 0)`. + * + * The last three parameters, `upX`, `upY`, and `upZ` are the components of + * the "up" vector. The "up" vector orients the camera’s y-axis. For example, + * calling `camera(0, 0, 0, 10, 20, 30, 0, -1, 0)` places the camera at the + * origin `(0, 0, 0)`, points it at `(10, 20, 30)`, and sets the "up" vector + * to `(0, -1, 0)` which is like holding it upside-down. By default, the "up" + * vector is `(0, 1, 0)`. + * + * Note: `camera()` can only be used in WebGL mode. + * + * @method camera + * @for p5 + * @param {Number} [x] x-coordinate of the camera. Defaults to 0. + * @param {Number} [y] y-coordinate of the camera. Defaults to 0. + * @param {Number} [z] z-coordinate of the camera. Defaults to 800. + * @param {Number} [centerX] x-coordinate of the point the camera faces. Defaults to 0. + * @param {Number} [centerY] y-coordinate of the point the camera faces. Defaults to 0. + * @param {Number} [centerZ] z-coordinate of the point the camera faces. Defaults to 0. + * @param {Number} [upX] x-component of the camera’s "up" vector. Defaults to 0. + * @param {Number} [upY] y-component of the camera’s "up" vector. Defaults to 1. + * @param {Number} [upZ] z-component of the camera’s "up" vector. Defaults to 0. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Move the camera to the top-right. + * camera(200, -400, 800); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube apperas to sway left and right on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Calculate the camera's x-coordinate. + * let x = 400 * cos(frameCount * 0.01); + * + * // Orbit the camera around the box. + * camera(x, -400, 800); + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Adjust the range sliders to change the camera's position. + * + * let xSlider; + * let ySlider; + * let zSlider; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create slider objects to set the camera's coordinates. + * xSlider = createSlider(-400, 400, 400); + * xSlider.position(0, 100); + * xSlider.size(100); + * ySlider = createSlider(-400, 400, -200); + * ySlider.position(0, 120); + * ySlider.size(100); + * zSlider = createSlider(0, 1600, 800); + * zSlider.position(0, 140); + * zSlider.size(100); + * + * describe( + * 'A white cube drawn against a gray background. Three range sliders appear beneath the image. The camera position changes when the user moves the sliders.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Get the camera's coordinates from the sliders. + * let x = xSlider.value(); + * let y = ySlider.value(); + * let z = zSlider.value(); + * + * // Move the camera. + * camera(x, y, z); + * + * // Draw the box. + * box(); + * } + * + *
+ */ + fn.camera = function (...args) { + this._assert3d('camera'); + p5._validateParameters('camera', args); + this._renderer.states.curCamera.camera(...args); + return this; + }; -//////////////////////////////////////////////////////////////////////////////// -// p5.Camera -//////////////////////////////////////////////////////////////////////////////// + /** + * Sets a perspective projection for the current camera in a 3D sketch. + * + * In a perspective projection, shapes that are further from the camera appear + * smaller than shapes that are near the camera. This technique, called + * foreshortening, creates realistic 3D scenes. It’s applied by default in + * WebGL mode. + * + * `perspective()` changes the camera’s perspective by changing its viewing + * frustum. The frustum is the volume of space that’s visible to the camera. + * Its shape is a pyramid with its top cut off. The camera is placed where + * the top of the pyramid should be and views everything between the frustum’s + * top (near) plane and its bottom (far) plane. + * + * The first parameter, `fovy`, is the camera’s vertical field of view. It’s + * an angle that describes how tall or narrow a view the camera has. For + * example, calling `perspective(0.5)` sets the camera’s vertical field of + * view to 0.5 radians. By default, `fovy` is calculated based on the sketch’s + * height and the camera’s default z-coordinate, which is 800. The formula for + * the default `fovy` is `2 * atan(height / 2 / 800)`. + * + * The second parameter, `aspect`, is the camera’s aspect ratio. It’s a number + * that describes the ratio of the top plane’s width to its height. For + * example, calling `perspective(0.5, 1.5)` sets the camera’s field of view to + * 0.5 radians and aspect ratio to 1.5, which would make shapes appear thinner + * on a square canvas. By default, aspect is set to `width / height`. + * + * The third parameter, `near`, is the distance from the camera to the near + * plane. For example, calling `perspective(0.5, 1.5, 100)` sets the camera’s + * field of view to 0.5 radians, its aspect ratio to 1.5, and places the near + * plane 100 pixels from the camera. Any shapes drawn less than 100 pixels + * from the camera won’t be visible. By default, near is set to `0.1 * 800`, + * which is 1/10th the default distance between the camera and the origin. + * + * The fourth parameter, `far`, is the distance from the camera to the far + * plane. For example, calling `perspective(0.5, 1.5, 100, 10000)` sets the + * camera’s field of view to 0.5 radians, its aspect ratio to 1.5, places the + * near plane 100 pixels from the camera, and places the far plane 10,000 + * pixels from the camera. Any shapes drawn more than 10,000 pixels from the + * camera won’t be visible. By default, far is set to `10 * 800`, which is 10 + * times the default distance between the camera and the origin. + * + * Note: `perspective()` can only be used in WebGL mode. + * + * @method perspective + * @for p5 + * @param {Number} [fovy] camera frustum vertical field of view. Defaults to + * `2 * atan(height / 2 / 800)`. + * @param {Number} [aspect] camera frustum aspect ratio. Defaults to + * `width / height`. + * @param {Number} [near] distance from the camera to the near clipping plane. + * Defaults to `0.1 * 800`. + * @param {Number} [far] distance from the camera to the far clipping plane. + * Defaults to `10 * 800`. + * @chainable + * + * @example + *
+ * + * // Double-click to squeeze the box. + * + * let isSqueezed = false; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white rectangular prism on a gray background. The box appears to become thinner when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Place the camera at the top-right. + * camera(400, -400, 800); + * + * if (isSqueezed === true) { + * // Set fovy to 0.2. + * // Set aspect to 1.5. + * perspective(0.2, 1.5); + * } + * + * // Draw the box. + * box(); + * } + * + * // Change the camera's perspective when the user double-clicks. + * function doubleClicked() { + * isSqueezed = true; + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white rectangular prism on a gray background. The prism moves away from the camera until it disappears.'); + * } + * + * function draw() { + * background(200); + * + * // Place the camera at the top-right. + * camera(400, -400, 800); + * + * // Set fovy to 0.2. + * // Set aspect to 1.5. + * // Set near to 600. + * // Set far to 1200. + * perspective(0.2, 1.5, 600, 1200); + * + * // Move the origin away from the camera. + * let x = -frameCount; + * let y = frameCount; + * let z = -2 * frameCount; + * translate(x, y, z); + * + * // Draw the box. + * box(); + * } + * + *
+ */ + fn.perspective = function (...args) { + this._assert3d('perspective'); + p5._validateParameters('perspective', args); + this._renderer.states.curCamera.perspective(...args); + return this; + }; -/** - * Creates a new p5.Camera object and sets it - * as the current (active) camera. - * - * The new camera is initialized with a default position `(0, 0, 800)` and a - * default perspective projection. Its properties can be controlled with - * p5.Camera methods such as - * `myCamera.lookAt(0, 0, 0)`. - * - * Note: Every 3D sketch starts with a default camera initialized. - * This camera can be controlled with the functions - * camera(), - * perspective(), - * ortho(), and - * frustum() if it's the only camera in the scene. - * - * Note: `createCamera()` can only be used in WebGL mode. - * - * @method createCamera - * @return {p5.Camera} the new camera. - * @for p5 - * - * @example - *
- * - * // Double-click to toggle between cameras. - * - * let cam1; - * let cam2; - * let usingCam1 = true; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the first camera. - * // Keep its default settings. - * cam1 = createCamera(); - * - * // Create the second camera. - * // Place it at the top-left. - * // Point it at the origin. - * cam2 = createCamera(); - * cam2.setPosition(400, -400, 800); - * cam2.lookAt(0, 0, 0); - * - * // Set the current camera to cam1. - * setCamera(cam1); - * - * describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'); - * } - * - * function draw() { - * background(200); - * - * // Draw the box. - * box(); - * } - * - * // Toggle the current camera when the user double-clicks. - * function doubleClicked() { - * if (usingCam1 === true) { - * setCamera(cam2); - * usingCam1 = false; - * } else { - * setCamera(cam1); - * usingCam1 = true; - * } - * } - * - *
- */ -p5.prototype.createCamera = function () { - this._assert3d('createCamera'); - // compute default camera settings, then set a default camera - const _cam = new p5.Camera(this._renderer); - _cam._computeCameraDefaultSettings(); - _cam._setDefaultCamera(); + /** + * Enables or disables perspective for lines in 3D sketches. + * + * In WebGL mode, lines can be drawn with a thinner stroke when they’re + * further from the camera. Doing so gives them a more realistic appearance. + * + * By default, lines are drawn differently based on the type of perspective + * being used: + * - `perspective()` and `frustum()` simulate a realistic perspective. In + * these modes, stroke weight is affected by the line’s distance from the + * camera. Doing so results in a more natural appearance. `perspective()` is + * the default mode for 3D sketches. + * - `ortho()` doesn’t simulate a realistic perspective. In this mode, stroke + * weights are consistent regardless of the line’s distance from the camera. + * Doing so results in a more predictable and consistent appearance. + * + * `linePerspective()` can override the default line drawing mode. + * + * The parameter, `enable`, is optional. It’s a `Boolean` value that sets the + * way lines are drawn. If `true` is passed, as in `linePerspective(true)`, + * then lines will appear thinner when they are further from the camera. If + * `false` is passed, as in `linePerspective(false)`, then lines will have + * consistent stroke weights regardless of their distance from the camera. By + * default, `linePerspective()` is enabled. + * + * Calling `linePerspective()` without passing an argument returns `true` if + * it's enabled and `false` if not. + * + * Note: `linePerspective()` can only be used in WebGL mode. + * + * @method linePerspective + * @for p5 + * @param {Boolean} enable whether to enable line perspective. + * + * @example + *
+ * + * // Double-click the canvas to toggle the line perspective. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'A white cube with black edges on a gray background. Its edges toggle between thick and thin when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Translate the origin toward the camera. + * translate(-10, 10, 600); + * + * // Rotate the coordinate system. + * rotateY(-0.1); + * rotateX(-0.1); + * + * // Draw the row of boxes. + * for (let i = 0; i < 6; i += 1) { + * translate(0, 0, -40); + * box(10); + * } + * } + * + * // Toggle the line perspective when the user double-clicks. + * function doubleClicked() { + * let isEnabled = linePerspective(); + * linePerspective(!isEnabled); + * } + * + *
+ * + *
+ * + * // Double-click the canvas to toggle the line perspective. + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe( + * 'A row of cubes with black edges on a gray background. Their edges toggle between thick and thin when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Use an orthographic projection. + * ortho(); + * + * // Translate the origin toward the camera. + * translate(-10, 10, 600); + * + * // Rotate the coordinate system. + * rotateY(-0.1); + * rotateX(-0.1); + * + * // Draw the row of boxes. + * for (let i = 0; i < 6; i += 1) { + * translate(0, 0, -40); + * box(10); + * } + * } + * + * // Toggle the line perspective when the user double-clicks. + * function doubleClicked() { + * let isEnabled = linePerspective(); + * linePerspective(!isEnabled); + * } + * + *
+ */ + /** + * @method linePerspective + * @return {Boolean} whether line perspective is enabled. + */ + + fn.linePerspective = function (enable) { + p5._validateParameters('linePerspective', arguments); + if (!(this._renderer instanceof p5.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; + } + }; - return _cam; -}; -/** - * A class to describe a camera for viewing a 3D sketch. - * - * Each `p5.Camera` object represents a camera that views a section of 3D - * space. It stores information about the camera’s position, orientation, and - * projection. - * - * In WebGL mode, the default camera is a `p5.Camera` object that can be - * controlled with the camera(), - * perspective(), - * ortho(), and - * frustum() functions. Additional cameras can be - * created with createCamera() and activated - * with setCamera(). - * - * Note: `p5.Camera`’s methods operate in two coordinate systems: - * - The “world” coordinate system describes positions in terms of their - * relationship to the origin along the x-, y-, and z-axes. For example, - * calling `myCamera.setPosition()` places the camera in 3D space using - * "world" coordinates. - * - The "local" coordinate system describes positions from the camera's point - * of view: left-right, up-down, and forward-backward. For example, calling - * `myCamera.move()` moves the camera along its own axes. - * - * @class p5.Camera - * @param {rendererGL} rendererGL instance of WebGL renderer - * - * @example - *
- * - * let cam; - * let delta = 0.001; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Camera object. - * cam = createCamera(); - * - * // Place the camera at the top-center. - * cam.setPosition(0, -400, 800); - * - * // Point the camera at the origin. - * cam.lookAt(0, 0, 0); - * - * describe( - * 'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Turn the camera left and right, called "panning". - * cam.pan(delta); - * - * // Switch directions every 120 frames. - * if (frameCount % 120 === 0) { - * delta *= -1; - * } - * - * // Draw the box. - * box(); - * } - * - *
- * - *
- * - * // Double-click to toggle between cameras. - * - * let cam1; - * let cam2; - * let isDefaultCamera = true; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the first camera. - * // Keep its default settings. - * cam1 = createCamera(); - * - * // Create the second camera. - * // Place it at the top-left. - * // Point it at the origin. - * cam2 = createCamera(); - * cam2.setPosition(400, -400, 800); - * cam2.lookAt(0, 0, 0); - * - * // Set the current camera to cam1. - * setCamera(cam1); - * - * describe( - * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.' - * ); - * } - * - * function draw() { - * background(200); - * - * // Draw the box. - * box(); - * } - * - * // Toggle the current camera when the user double-clicks. - * function doubleClicked() { - * if (isDefaultCamera === true) { - * setCamera(cam2); - * isDefaultCamera = false; - * } else { - * setCamera(cam1); - * isDefaultCamera = true; - * } - * } - * - *
- */ -p5.Camera = 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); - * } - * - *
- */ + * Sets an orthographic projection for the current camera in a 3D sketch. + * + * In an orthographic projection, shapes with the same size always appear the + * same size, regardless of whether they are near or far from the camera. + * + * `ortho()` changes the camera’s perspective by changing its viewing frustum + * from a truncated pyramid to a rectangular prism. The camera is placed in + * front of the frustum and views everything between the frustum’s near plane + * and its far plane. `ortho()` has six optional parameters to define the + * frustum. + * + * The first four parameters, `left`, `right`, `bottom`, and `top`, set the + * coordinates of the frustum’s sides, bottom, and top. For example, calling + * `ortho(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide and + * 400 pixels tall. By default, these coordinates are set based on the + * sketch’s width and height, as in + * `ortho(-width / 2, width / 2, -height / 2, height / 2)`. + * + * The last two parameters, `near` and `far`, set the distance of the + * frustum’s near and far plane from the camera. For example, calling + * `ortho(-100, 100, 200, 200, 50, 1000)` creates a frustum that’s 200 pixels + * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000 + * pixels from the camera. By default, `near` and `far` are set to 0 and + * `max(width, height) + 800`, respectively. + * + * Note: `ortho()` can only be used in WebGL mode. + * + * @method ortho + * @for p5 + * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 2`. + * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 2`. + * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 2`. + * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 2`. + * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to 0. + * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `max(width, height) + 800`. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A row of tiny, white cubes on a gray background. All the cubes appear the same size.'); + * } + * + * function draw() { + * background(200); + * + * // Apply an orthographic projection. + * ortho(); + * + * // Translate the origin toward the camera. + * translate(-10, 10, 600); + * + * // Rotate the coordinate system. + * rotateY(-0.1); + * rotateX(-0.1); + * + * // Draw the row of boxes. + * for (let i = 0; i < 6; i += 1) { + * translate(0, 0, -40); + * box(10); + * } + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Apply an orthographic projection. + * // Center the frustum. + * // Set its width and height to 20. + * // Place its near plane 300 pixels from the camera. + * // Place its far plane 350 pixels from the camera. + * ortho(-10, 10, -10, 10, 300, 350); + * + * // Translate the origin toward the camera. + * translate(-10, 10, 600); + * + * // Rotate the coordinate system. + * rotateY(-0.1); + * rotateX(-0.1); + * + * // Draw the row of boxes. + * for (let i = 0; i < 6; i += 1) { + * translate(0, 0, -40); + * box(10); + * } + * } + * + *
+ */ + fn.ortho = function (...args) { + this._assert3d('ortho'); + p5._validateParameters('ortho', args); + this._renderer.states.curCamera.ortho(...args); + return this; + }; /** - * 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); - * } - * - *
- */ + * Sets the frustum of the current camera in a 3D sketch. + * + * In a frustum projection, shapes that are further from the camera appear + * smaller than shapes that are near the camera. This technique, called + * foreshortening, creates realistic 3D scenes. + * + * `frustum()` changes the default camera’s perspective by changing its + * viewing frustum. The frustum is the volume of space that’s visible to the + * camera. The frustum’s shape is a pyramid with its top cut off. The camera + * is placed where the top of the pyramid should be and points towards the + * base of the pyramid. It views everything within the frustum. + * + * The first four parameters, `left`, `right`, `bottom`, and `top`, set the + * coordinates of the frustum’s sides, bottom, and top. For example, calling + * `frustum(-100, 100, 200, -200)` creates a frustum that’s 200 pixels wide + * and 400 pixels tall. By default, these coordinates are set based on the + * sketch’s width and height, as in + * `ortho(-width / 20, width / 20, height / 20, -height / 20)`. + * + * The last two parameters, `near` and `far`, set the distance of the + * frustum’s near and far plane from the camera. For example, calling + * `ortho(-100, 100, 200, -200, 50, 1000)` creates a frustum that’s 200 pixels + * wide, 400 pixels tall, starts 50 pixels from the camera, and ends 1,000 + * pixels from the camera. By default, near is set to `0.1 * 800`, which is + * 1/10th the default distance between the camera and the origin. `far` is set + * to `10 * 800`, which is 10 times the default distance between the camera + * and the origin. + * + * Note: `frustum()` can only be used in WebGL mode. + * + * @method frustum + * @for p5 + * @param {Number} [left] x-coordinate of the frustum’s left plane. Defaults to `-width / 20`. + * @param {Number} [right] x-coordinate of the frustum’s right plane. Defaults to `width / 20`. + * @param {Number} [bottom] y-coordinate of the frustum’s bottom plane. Defaults to `height / 20`. + * @param {Number} [top] y-coordinate of the frustum’s top plane. Defaults to `-height / 20`. + * @param {Number} [near] z-coordinate of the frustum’s near plane. Defaults to `0.1 * 800`. + * @param {Number} [far] z-coordinate of the frustum’s far plane. Defaults to `10 * 800`. + * @chainable + * + * @example + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * describe('A row of white cubes on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Apply the default frustum projection. + * frustum(); + * + * // Translate the origin toward the camera. + * translate(-10, 10, 600); + * + * // Rotate the coordinate system. + * rotateY(-0.1); + * rotateX(-0.1); + * + * // Draw the row of boxes. + * for (let i = 0; i < 6; i += 1) { + * translate(0, 0, -40); + * box(10); + * } + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(100, 100, WEBGL); + * describe('A white cube on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Adjust the frustum. + * // Center it. + * // Set its width and height to 20 pixels. + * // Place its near plane 300 pixels from the camera. + * // Place its far plane 350 pixels from the camera. + * frustum(-10, 10, -10, 10, 300, 350); + * + * // Translate the origin toward the camera. + * translate(-10, 10, 600); + * + * // Rotate the coordinate system. + * rotateY(-0.1); + * rotateX(-0.1); + * + * // Draw the row of boxes. + * for (let i = 0; i < 6; i += 1) { + * translate(0, 0, -40); + * box(10); + * } + * } + * + *
+ */ + fn.frustum = function (...args) { + this._assert3d('frustum'); + p5._validateParameters('frustum', args); + this._renderer.states.curCamera.frustum(...args); + return this; + }; - /** - * 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); - * } - * - *
- */ + //////////////////////////////////////////////////////////////////////////////// + // p5.Camera + //////////////////////////////////////////////////////////////////////////////// /** - * 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); - * } - * - *
- */ + * Creates a new p5.Camera object and sets it + * as the current (active) camera. + * + * The new camera is initialized with a default position `(0, 0, 800)` and a + * default perspective projection. Its properties can be controlled with + * p5.Camera methods such as + * `myCamera.lookAt(0, 0, 0)`. + * + * Note: Every 3D sketch starts with a default camera initialized. + * This camera can be controlled with the functions + * camera(), + * perspective(), + * ortho(), and + * frustum() if it's the only camera in the scene. + * + * Note: `createCamera()` can only be used in WebGL mode. + * + * @method createCamera + * @return {p5.Camera} the new camera. + * @for p5 + * + * @example + *
+ * + * // Double-click to toggle between cameras. + * + * let cam1; + * let cam2; + * let usingCam1 = true; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the first camera. + * // Keep its default settings. + * cam1 = createCamera(); + * + * // Create the second camera. + * // Place it at the top-left. + * // Point it at the origin. + * cam2 = createCamera(); + * cam2.setPosition(400, -400, 800); + * cam2.lookAt(0, 0, 0); + * + * // Set the current camera to cam1. + * setCamera(cam1); + * + * describe('A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.'); + * } + * + * function draw() { + * background(200); + * + * // Draw the box. + * box(); + * } + * + * // Toggle the current camera when the user double-clicks. + * function doubleClicked() { + * if (usingCam1 === true) { + * setCamera(cam2); + * usingCam1 = false; + * } else { + * setCamera(cam1); + * usingCam1 = true; + * } + * } + * + *
+ */ + fn.createCamera = function () { + this._assert3d('createCamera'); + + // compute default camera settings, then set a default camera + const _cam = new p5.Camera(this._renderer); + _cam._computeCameraDefaultSettings(); + _cam._setDefaultCamera(); - /** - * 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); - * } - * - *
- */ + return _cam; + }; /** - * 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); - * } - * - *
- */ + * A class to describe a camera for viewing a 3D sketch. + * + * Each `p5.Camera` object represents a camera that views a section of 3D + * space. It stores information about the camera’s position, orientation, and + * projection. + * + * In WebGL mode, the default camera is a `p5.Camera` object that can be + * controlled with the camera(), + * perspective(), + * ortho(), and + * frustum() functions. Additional cameras can be + * created with createCamera() and activated + * with setCamera(). + * + * Note: `p5.Camera`’s methods operate in two coordinate systems: + * - The “world” coordinate system describes positions in terms of their + * relationship to the origin along the x-, y-, and z-axes. For example, + * calling `myCamera.setPosition()` places the camera in 3D space using + * "world" coordinates. + * - The "local" coordinate system describes positions from the camera's point + * of view: left-right, up-down, and forward-backward. For example, calling + * `myCamera.move()` moves the camera along its own axes. + * + * @class p5.Camera + * @param {rendererGL} rendererGL instance of WebGL renderer + * + * @example + *
+ * + * let cam; + * let delta = 0.001; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Camera object. + * cam = createCamera(); + * + * // Place the camera at the top-center. + * cam.setPosition(0, -400, 800); + * + * // Point the camera at the origin. + * cam.lookAt(0, 0, 0); + * + * describe( + * 'A white cube on a gray background. The cube goes in and out of view as the camera pans left and right.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Turn the camera left and right, called "panning". + * cam.pan(delta); + * + * // Switch directions every 120 frames. + * if (frameCount % 120 === 0) { + * delta *= -1; + * } + * + * // Draw the box. + * box(); + * } + * + *
+ * + *
+ * + * // Double-click to toggle between cameras. + * + * let cam1; + * let cam2; + * let isDefaultCamera = true; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the first camera. + * // Keep its default settings. + * cam1 = createCamera(); + * + * // Create the second camera. + * // Place it at the top-left. + * // Point it at the origin. + * cam2 = createCamera(); + * cam2.setPosition(400, -400, 800); + * cam2.lookAt(0, 0, 0); + * + * // Set the current camera to cam1. + * setCamera(cam1); + * + * describe( + * 'A white cube on a gray background. The camera toggles between frontal and aerial views when the user double-clicks.' + * ); + * } + * + * function draw() { + * background(200); + * + * // Draw the box. + * box(); + * } + * + * // Toggle the current camera when the user double-clicks. + * function doubleClicked() { + * if (isDefaultCamera === true) { + * setCamera(cam2); + * isDefaultCamera = false; + * } else { + * setCamera(cam1); + * isDefaultCamera = true; + * } + * } + * + *
+ */ + p5.Camera = 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; + } - /** - * 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); - * } - * - *
- */ + if (near <= 0.0001) { + near = 0.01; + console.log( + 'Avoid perspective near plane values close to or below 0. ' + + 'Setting value to 0.01.' + ); + } - /** - * 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); - * } - * - *
- */ + if (far < near) { + console.log( + 'Perspective far plane value is less than near plane value. ' + + 'Nothing will be shown.' + ); + } - /** - * 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); - * } - * - *
- */ + this.aspectRatio = aspect; + this.cameraNear = near; + this.cameraFar = far; - //////////////////////////////////////////////////////////////////////////////// - // Camera Projection Methods - //////////////////////////////////////////////////////////////////////////////// + this.projMatrix = p5.Matrix.identity(); - /** - * 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; + 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); + } } - if (typeof near === 'undefined') { - near = this.defaultCameraNear; + + /** + * 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'; } - if (typeof far === 'undefined') { - far = this.defaultCameraFar; + /** + * 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'; } - if (near <= 0.0001) { - near = 0.01; - console.log( - 'Avoid perspective near plane values close to or below 0. ' + - 'Setting value to 0.01.' + //////////////////////////////////////////////////////////////////////////////// + // 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 ); } - if (far < near) { - console.log( - 'Perspective far plane value is less than near plane value. ' + - 'Nothing will be shown.' + /** + * 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 ); } - 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); + /** + * 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]); } - } - /** - * 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); + /** + * 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]); } - 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); + + /** + * 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 + ); } - this.cameraType = 'custom'; - } + //////////////////////////////////////////////////////////////////////////////// + // 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; + } - //////////////////////////////////////////////////////////////////////////////// - // Camera Orientation Methods - //////////////////////////////////////////////////////////////////////////////// + this.eyeX = eyeX; + this.eyeY = eyeY; + this.eyeZ = eyeZ; - /** - * 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 - ); - } + if (typeof centerX !== 'undefined') { + this.centerX = centerX; + this.centerY = centerY; + this.centerZ = centerZ; + } - /** - * 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 - ); - } + if (typeof upX !== 'undefined') { + this.upX = upX; + this.upY = upY; + this.upZ = upZ; + } - /** - * 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]); - } + const local = this._getLocalAxes(); - /** - * 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]); - } + // 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 */ - /** - * 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 - ); - } + const tx = -eyeX; + const ty = -eyeY; + const tz = -eyeZ; - //////////////////////////////////////////////////////////////////////////////// - // Camera Position Methods - //////////////////////////////////////////////////////////////////////////////// + this.cameraMatrix.translate([tx, ty, tz]); - /** - * 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; + if (this._isActive()) { + this._renderer.states.uViewMatrix.set(this.cameraMatrix); + } + return this; } - this.eyeX = eyeX; - this.eyeY = eyeY; - this.eyeZ = eyeZ; - - if (typeof centerX !== 'undefined') { - this.centerX = centerX; - this.centerY = centerY; - this.centerZ = centerZ; - } + /** + * 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]; - if (typeof upX !== 'undefined') { - this.upX = upX; - this.upY = upY; - this.upZ = upZ; + 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 + ); } - 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]); + /** + * 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; - if (this._isActive()) { - this._renderer.states.uViewMatrix.set(this.cameraMatrix); + this.camera( + x, + y, + z, + this.centerX + diffX, + this.centerY + diffY, + this.centerZ + diffZ, + this.upX, + this.upY, + this.upZ + ); } - 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, 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]; + } - /** - * 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 - ); - } + this.cameraMatrix = cam.cameraMatrix.copy(); + this.projMatrix = cam.projMatrix.copy(); - /** - * 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]; + 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; + } - this.cameraMatrix = cam.cameraMatrix.copy(); - this.projMatrix = cam.projMatrix.copy(); + // 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(); + } + } - 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; - } + // 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)); + } - // 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(); + // 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; } - } - // 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)); - } + // 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; + } - // 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. + // 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. - newFront.set(p5.Vector.lerp(front0, front1, amt)).normalize(); + lerpedRotMat.multiplyVec3(front0, newFront); 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(); + lerpedRotMat.multiplyVec3(up0, newUp); - // set the camera + // 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 ); - 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; + //////////////////////////////////////////////////////////////////////////////// + // 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; } - // 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(); + //detect if user didn't set the camera + //then call this function below + _setDefaultCamera() { 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; + 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; - return _cam; - } + this.perspective(); + this.camera(); - clone() { - return this.copy(); - } + this.cameraType = 'default'; + } - /** - * 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; + _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(); + } } - // 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; + /** + * 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; } - const ymag = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); - if (ymag !== 0) { - y0 /= ymag; - y1 /= ymag; - y2 /= ymag; + clone() { + return this.copy(); } - return { - x: [x0, x1, x2], - y: [y0, y1, y2], - z: [z0, z1, z2] - }; - } + /** + * 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; + } - /** - * 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 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; + } - // 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; + return { + x: [x0, x1, x2], + y: [y0, y1, y2], + z: [z0, z1, z2] + }; } - // 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)); + /** + * 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; + } - front.set(up).add(vertical).add(side); + // 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; + } - this.eyeX = camRadius * front.x + this.centerX; - this.eyeY = camRadius * front.y + this.centerY; - this.eyeZ = camRadius * front.z + this.centerZ; + // 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)); - // update camera - this.camera( - this.eyeX, this.eyeY, this.eyeZ, - this.centerX, this.centerY, this.centerZ, - this.upX, this.upY, this.upZ - ); - } + front.set(up).add(vertical).add(side); - /** - * 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; - } + this.eyeX = camRadius * front.x + this.centerX; + this.eyeY = camRadius * front.y + this.centerY; + this.eyeZ = camRadius * front.z + this.centerZ; - // 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 - ); - } + // update camera + this.camera( + this.eyeX, this.eyeY, this.eyeZ, + this.centerX, this.centerY, this.centerZ, + this.upX, this.upY, this.upZ + ); + } - /** - * Returns true if camera is currently attached to renderer. - * @private - */ - _isActive() { - return this === this._renderer.states.curCamera; - } -}; + /** + * 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; + } -/** - * 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; - * } - * } - * - *
- */ -p5.prototype.setCamera = function (cam) { - this._renderer.states.curCamera = cam; + // 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 + ); + } - // 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); -}; + /** + * Returns true if camera is currently attached to renderer. + * @private + */ + _isActive() { + return this === this._renderer.states.curCamera; + } + }; -export default p5.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; + + // 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); + }; +} + +export default camera; + +if(typeof p5 !== 'undefined'){ + camera(p5, p5.prototype); +} diff --git a/src/webgl/p5.DataArray.js b/src/webgl/p5.DataArray.js index 9ab0c2eaf2..c0aebdacec 100644 --- a/src/webgl/p5.DataArray.js +++ b/src/webgl/p5.DataArray.js @@ -1,110 +1,114 @@ -import p5 from '../core/main'; - -/** - * An internal class to store data that will be sent to a p5.RenderBuffer. - * Those need to eventually go into a Float32Array, so this class provides a - * variable-length array container backed by a Float32Array so that it can be - * sent to the GPU without allocating a new array each frame. - * - * Like a C++ vector, its fixed-length Float32Array backing its contents will - * double in size when it goes over its capacity. - * - * @example - *
- * - * // Initialize storage with a capacity of 4 - * const storage = new DataArray(4); - * console.log(storage.data.length); // 4 - * console.log(storage.length); // 0 - * console.log(storage.dataArray()); // Empty Float32Array - * - * storage.push(1, 2, 3, 4, 5, 6); - * console.log(storage.data.length); // 8 - * console.log(storage.length); // 6 - * console.log(storage.dataArray()); // Float32Array{1, 2, 3, 4, 5, 6} - * - *
- */ -p5.DataArray = class DataArray { - constructor(initialLength = 128) { - this.length = 0; - this.data = new Float32Array(initialLength); - this.initialLength = initialLength; - } - +function dataArray(p5, fn){ /** - * Returns a Float32Array window sized to the exact length of the data + * An internal class to store data that will be sent to a p5.RenderBuffer. + * Those need to eventually go into a Float32Array, so this class provides a + * variable-length array container backed by a Float32Array so that it can be + * sent to the GPU without allocating a new array each frame. + * + * Like a C++ vector, its fixed-length Float32Array backing its contents will + * double in size when it goes over its capacity. + * + * @example + *
+ * + * // Initialize storage with a capacity of 4 + * const storage = new DataArray(4); + * console.log(storage.data.length); // 4 + * console.log(storage.length); // 0 + * console.log(storage.dataArray()); // Empty Float32Array + * + * storage.push(1, 2, 3, 4, 5, 6); + * console.log(storage.data.length); // 8 + * console.log(storage.length); // 6 + * console.log(storage.dataArray()); // Float32Array{1, 2, 3, 4, 5, 6} + * + *
*/ - dataArray() { - return this.subArray(0, this.length); - } + p5.DataArray = class DataArray { + constructor(initialLength = 128) { + this.length = 0; + this.data = new Float32Array(initialLength); + this.initialLength = initialLength; + } - /** - * A "soft" clear, which keeps the underlying storage size the same, but - * empties the contents of its dataArray() - */ - clear() { - this.length = 0; - } + /** + * Returns a Float32Array window sized to the exact length of the data + */ + dataArray() { + return this.subArray(0, this.length); + } - /** - * 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 "soft" clear, which keeps the underlying storage size the same, but + * empties the contents of its dataArray() + */ + clear() { + this.length = 0; } - } - /** - * A full reset, which allocates a new underlying Float32Array at its initial - * length - */ - reset() { - this.clear(); - this.data = new Float32Array(this.initialLength); - } + /** + * 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; + } + } - /** - * 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; - } + /** + * A full reset, which allocates a new underlying Float32Array at its initial + * length + */ + reset() { + this.clear(); + this.data = new Float32Array(this.initialLength); + } - /** - * 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)); - } + /** + * 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 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)); - } + /** + * 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)); + } - /** - * 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; + /** + * 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)); } - } -}; -export default p5.DataArray; + /** + * 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; + } + } + }; +} + +export default dataArray; + +if(typeof p5 !== 'undefined'){ + dataArray(p5, p5.prototype); +} diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index e5a4b5477a..c9eb6bba62 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -3,1275 +3,1741 @@ * @requires constants */ -import p5 from '../core/main'; import * as constants from '../core/constants'; import { checkWebGLCapabilities } from './p5.Texture'; import { readPixelsWebGL, readPixelWebGL } from './p5.RendererGL'; -/** - * 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; - } - - _computeCameraDefaultSettings() { - super._computeCameraDefaultSettings(); - this.defaultAspectRatio = this.fbo.width / this.fbo.height; - this.defaultCameraFOV = - 2 * Math.atan(this.fbo.height / 2 / this.defaultEyeZ); - } -}; - -/** - * 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 width() { - return this.framebuffer.width * this.framebuffer.density; - } - - get height() { - return this.framebuffer.height * this.framebuffer.density; - } - - rawTexture() { - return this.framebuffer[this.property]; - } -}; - -/** - * 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 = 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; +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; } - 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.'); + _computeCameraDefaultSettings() { + super._computeCameraDefaultSettings(); + this.defaultAspectRatio = this.fbo.width / this.fbo.height; + this.defaultCameraFOV = + 2 * Math.atan(this.fbo.height / 2 / this.defaultEyeZ); } - 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'); - } + /** + * 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; } - this._recreateTextures(); + get width() { + return this.framebuffer.width * this.framebuffer.density; + } - const prevCam = this.target._renderer.states.curCamera; - this.defaultCamera = this.createCamera(); - this.filterCamera = this.createCamera(); - this.target._renderer.states.curCamera = prevCam; + get height() { + return this.framebuffer.height * this.framebuffer.density; + } - this.draw(() => this.target.clear()); - } + rawTexture() { + return this.framebuffer[this.property]; + } + }; /** - * 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); + * A class to describe a high-performance drawing surface for textures. * - * // 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); + * 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. * - * // 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` 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. */ - 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(); - } + p5.Framebuffer = class Framebuffer { + constructor(target, settings = {}) { + this.target = target; + this.target._renderer.framebuffers.add(this); - /** - * 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; - } - } + this._isClipApplied = false; - /** - * 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(); - } - } + this.pixels = []; - /** - * 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.format = settings.format || constants.UNSIGNED_BYTE; + this.channels = settings.channels || ( + target._renderer._pInst._glAttributes.alpha + ? constants.RGBA + : constants.RGB ); - this.useDepth = false; - } + 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 ( - 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 (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); - 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; - } + 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'); + } + } - 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; + 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.clear()); } - 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; + + /** + * 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) { + 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(); } - 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; + + /** + * 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; + } } - 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; + /** + * 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(); + } } - } - /** - * Creates new textures and renderbuffers given the current size of the - * framebuffer. - * - * @private - */ - _recreateTextures() { - const gl = this.gl; + /** + * 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; + } - this._updateSize(); + 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; + } - const prevBoundTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); - const prevBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + 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 colorTexture = gl.createTexture(); - if (!colorTexture) { - throw new Error('Unable to create color texture'); + 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; + } } - 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'); + + /** + * Creates new textures and renderbuffers given the current size of the + * framebuffer. + * + * @private + */ + _recreateTextures() { + const gl = this.gl; + + this._updateSize(); + + 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 depthFormat = this._glDepthFormat(); - gl.bindTexture(gl.TEXTURE_2D, depthTexture); + gl.bindTexture(gl.TEXTURE_2D, colorTexture); + const colorFormat = this._glColorFormat(); gl.texImage2D( gl.TEXTURE_2D, 0, - depthFormat.internalFormat, + colorFormat.internalFormat, this.width * this.density, this.height * this.density, 0, - depthFormat.format, - depthFormat.type, + colorFormat.format, + colorFormat.type, null ); - + this.colorTexture = colorTexture; + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, - this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, + gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, - depthTexture, + colorTexture, 0 ); - this.depthTexture = depthTexture; - } - - // 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, - Math.min(this.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) - ), - colorFormat.internalFormat, - this.width * this.density, - this.height * this.density - ); 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(); - this.depthRenderbuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthRenderbuffer); + gl.bindTexture(gl.TEXTURE_2D, depthTexture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + depthFormat.internalFormat, + this.width * this.density, + this.height * this.density, + 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; + } + + // 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, Math.min(this.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) ), - depthFormat.internalFormat, + colorFormat.internalFormat, this.width * this.density, this.height * this.density ); - } - gl.bindFramebuffer(gl.FRAMEBUFFER, this.aaFramebuffer); - gl.framebufferRenderbuffer( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.RENDERBUFFER, - this.colorRenderbuffer - ); - if (this.useDepth) { + 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.framebufferRenderbuffer( gl.FRAMEBUFFER, - this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, + gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, - this.depthRenderbuffer + 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.target._renderer.textures.set(this.depth, this.depthP5Texture); } - } - if (this.useDepth) { - this.depth = new p5.FramebufferTexture(this, 'depthTexture'); - const depthFilter = gl.NEAREST; - this.depthP5Texture = new p5.Texture( + this.color = new p5.FramebufferTexture(this, 'colorTexture'); + const filter = this.textureFiltering === constants.LINEAR + ? gl.LINEAR + : gl.NEAREST; + this.colorP5Texture = new p5.Texture( this.target._renderer, - this.depth, + this.color, { - minFilter: depthFilter, - magFilter: depthFilter + minFilter: filter, + magFilter: filter } ); - this.target._renderer.textures.set(this.depth, this.depthP5Texture); + this.target._renderer.textures.set(this.color, this.colorP5Texture); + + gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture); + gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer); } - this.color = new p5.FramebufferTexture(this, 'colorTexture'); - const filter = this.textureFiltering === constants.LINEAR - ? gl.LINEAR - : gl.NEAREST; - this.colorP5Texture = new p5.Texture( - this.target._renderer, - this.color, - { - minFilter: filter, - magFilter: filter - } - ); - this.target._renderer.textures.set(this.color, this.colorP5Texture); + /** + * 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; - gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture); - gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer); - } + 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.) - * - * 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; + } - if (this.channels === constants.RGBA) { - format = gl.RGBA; - } else { - format = gl.RGB; - } + 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.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; + return { internalFormat, format, type }; } - 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.) + * + * 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; - /** - * 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) { + 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.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; + if (this.useStencil) { + format = gl.DEPTH_STENCIL; } else { - type = gl.getExtension('WEBGL_depth_texture').UNSIGNED_INT_24_8_WEBGL; + format = gl.DEPTH_COMPONENT; } - } else { - if (this.depthFormat === constants.FLOAT) { - type = gl.FLOAT; + + 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 { - type = gl.UNSIGNED_INT; + internalFormat = gl.DEPTH_COMPONENT; } - } - 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; + /** + * 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(); } - } else if (this.target.webglVersion === constants.WEBGL2) { - if (this.depthFormat === constants.FLOAT) { - internalFormat = gl.DEPTH_COMPONENT32F; - } else { - internalFormat = gl.DEPTH_COMPONENT24; + } + + /** + * 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(); } - } else { - internalFormat = gl.DEPTH_COMPONENT; } - return { internalFormat, format, type }; - } + /** + * 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); - /** - * 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(); + this._recreateTextures(); + this.defaultCamera._resize(); } - } - /** - * 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(); + /** + * 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 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(); - } - - /** - * 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; - } - /** - * 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()); + /** + * 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()); - this.target._renderer.textures.delete(texture); - } + this.target._renderer.textures.delete(texture); + } - /** - * 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); + /** + * 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); } - if (this.depthRenderbuffer) { - gl.deleteRenderbuffer(this.depthRenderbuffer); + + /** + * 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(); } - if (this.colorRenderbuffer) { - gl.deleteRenderbuffer(this.colorRenderbuffer); + + /** + * 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; + } } - this.target._renderer.framebuffers.delete(this); - } - /** - * 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(); + /** + * 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 + ); } - 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(); - } - /** - * 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 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 + ); + } + } } - } - /** - * 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() { + 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."); + } + 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 + ); + } + this.target._renderer._applyStencilTestIfClipping(); + } - /** - * Ensures that the framebuffer is ready to be read by other framebuffers. - * - * @private - */ - _beforeEnd() { - if (this.antialias) { + /** + * 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; - 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] + 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); + } + } + + /** + * 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); + } + + return readPixelWebGL( + this.gl, + this.framebuffer, + x * this.density, + y * this.density, + colorFormat.format, + colorFormat.type ); } - 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 + + 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; + } + + /** + * 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.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(); + } 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); + } + } } - } + }; /** - * Stops drawing shapes to the framebuffer. + * An object that stores the framebuffer's color data. * - * 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. + * Each framebuffer uses a + * WebGLTexture + * object internally to store its color data. The `myBuffer.color` property + * makes it possible to pass this data directly to other functions. For + * example, calling `texture(myBuffer.color)` or + * `myShader.setUniform('colorTexture', myBuffer.color)` may be helpful for + * advanced use cases. + * + * Note: By default, a framebuffer's y-coordinates are flipped compared to + * images and videos. It's easy to flip a framebuffer's y-coordinates as + * needed when applying it as a texture. For example, calling + * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer. + * + * @property {p5.FramebufferTexture} color + * @for p5.Framebuffer * * @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); * + * // Create a p5.Framebuffer object. + * let myBuffer = createFramebuffer(); + * * // Start drawing to the p5.Framebuffer object. * myBuffer.begin(); * - * background(50); - * rotateY(frameCount * 0.01); - * normalMaterial(); - * torus(30); + * triangle(-25, 25, 0, -25, 25, 25); * * // 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); - * } + * // Use the p5.Framebuffer object's WebGLTexture. + * texture(myBuffer.color); + * + * // Style the plane. + * noStroke(); + * + * // Draw the plane. + * plane(myBuffer.width, myBuffer.height); + * + * describe('A white triangle on a gray background.'); * } * *
*/ - end() { - 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."); - } - 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 - ); - } - this.target._renderer._applyStencilTestIfClipping(); - } /** - * Draws to the framebuffer by calling a function that contains drawing - * instructions. + * An object that stores the framebuffer's dpeth data. * - * 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: + * Each framebuffer uses a + * WebGLTexture + * object internally to store its depth data. The `myBuffer.depth` property + * makes it possible to pass this data directly to other functions. For + * example, calling `texture(myBuffer.depth)` or + * `myShader.setUniform('depthTexture', myBuffer.depth)` may be helpful for + * advanced use cases. * - * ```js - * myBuffer.begin(); - * myFunction(); - * myBuffer.end(); - * ``` + * Note: By default, a framebuffer's y-coordinates are flipped compared to + * images and videos. It's easy to flip a framebuffer's y-coordinates as + * needed when applying it as a texture. For example, calling + * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer. * - * @param {Function} callback function that draws to the framebuffer. + * @property {p5.FramebufferTexture} depth + * @for p5.Framebuffer * * @example *
* - * // Click the canvas to display the framebuffer. + * // 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; + * attribute vec3 aPosition; + * attribute vec2 aTexCoord; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * varying vec2 vTexCoord; + * + * void main() { + * vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); + * gl_Position = uProjectionMatrix * viewModelPosition; + * vTexCoord = aTexCoord; + * } + * `; + * + * // Create a string with the fragment shader program. + * // The fragment shader is called for each pixel. + * let fragSrc = ` + * precision highp float; + * varying vec2 vTexCoord; + * uniform sampler2D depth; + * + * void main() { + * // Get the pixel's depth value. + * float depthVal = texture2D(depth, vTexCoord).r; + * + * // Set the pixel's color based on its depth. + * gl_FragColor = mix( + * vec4(0., 0., 0., 1.), + * vec4(1., 0., 1., 1.), + * depthVal); + * } + * `; * * let myBuffer; + * let myShader; * * function setup() { * createCanvas(100, 100, WEBGL); @@ -1279,240 +1745,52 @@ p5.Framebuffer = class Framebuffer { * // 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); + * // Create a p5.Shader object. + * myShader = createShader(vertSrc, fragSrc); * - * // Display the p5.Framebuffer object while - * // the user presses the mouse. - * if (mouseIsPressed === true) { - * image(myBuffer, -50, -50); - * } - * } + * // Compile and apply the shader. + * shader(myShader); * - * // Draw a rotating, multicolor torus. - * function bagel() { - * background(50); - * rotateY(frameCount * 0.01); - * normalMaterial(); - * torus(30); + * describe('The shadow of a box rotates slowly against a magenta background.'); * } - * - *
- */ - 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; - * } + * function draw() { + * // Draw to the p5.Framebuffer object. + * myBuffer.begin(); + * background(255); + * rotateX(frameCount * 0.01); + * box(20, 20, 80); + * myBuffer.end(); * - * // Update the pixels array. - * myBuffer.updatePixels(); + * // Set the shader's depth uniform using + * // the framebuffer's depth texture. + * myShader.setUniform('depth', myBuffer.depth); * - * // Draw the p5.Framebuffer object to the canvas. - * image(myBuffer, -50, -50); + * // Style the plane. + * noStroke(); * - * describe('A pink rectangle above a gray rectangle.'); + * // Draw the plane. + * plane(myBuffer.width, myBuffer.height); * } * *
*/ - 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); - } - } /** - * 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. + * An array containing the color of each pixel in the framebuffer. * - * 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); - } - - return readPixelWebGL( - this.gl, - this.framebuffer, - x * this.density, - y * this.density, - colorFormat.format, - colorFormat.type - ); - } - - 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; - } - - /** - * Updates the framebuffer with the RGBA values in the - * pixels array. + * myBuffer.loadPixels() must be + * called before accessing the `myBuffer.pixels` array. + * myBuffer.updatePixels() + * must be called after any changes are made. * - * `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(). + * Note: Updating pixels via this property is slower than drawing to the + * framebuffer directly. Consider using a + * p5.Shader object instead of looping over + * `myBuffer.pixels`. * - * @method updatePixels + * @property {Number[]} pixels + * @for p5.Framebuffer * * @example *
@@ -1551,283 +1829,10 @@ p5.Framebuffer = class Framebuffer { * *
*/ - 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.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(); - } 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); - } - } - } -}; +} -/** - * An object that stores the framebuffer's color data. - * - * Each framebuffer uses a - * WebGLTexture - * object internally to store its color data. The `myBuffer.color` property - * makes it possible to pass this data directly to other functions. For - * example, calling `texture(myBuffer.color)` or - * `myShader.setUniform('colorTexture', myBuffer.color)` may be helpful for - * advanced use cases. - * - * Note: By default, a framebuffer's y-coordinates are flipped compared to - * images and videos. It's easy to flip a framebuffer's y-coordinates as - * needed when applying it as a texture. For example, calling - * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer. - * - * @property {p5.FramebufferTexture} color - * @for p5.Framebuffer - * - * @example - *
- * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * background(200); - * - * // Create a p5.Framebuffer object. - * let myBuffer = createFramebuffer(); - * - * // Start drawing to the p5.Framebuffer object. - * myBuffer.begin(); - * - * triangle(-25, 25, 0, -25, 25, 25); - * - * // Stop drawing to the p5.Framebuffer object. - * myBuffer.end(); - * - * // Use the p5.Framebuffer object's WebGLTexture. - * texture(myBuffer.color); - * - * // Style the plane. - * noStroke(); - * - * // Draw the plane. - * plane(myBuffer.width, myBuffer.height); - * - * describe('A white triangle on a gray background.'); - * } - * - *
- */ - -/** - * An object that stores the framebuffer's dpeth data. - * - * Each framebuffer uses a - * WebGLTexture - * object internally to store its depth data. The `myBuffer.depth` property - * makes it possible to pass this data directly to other functions. For - * example, calling `texture(myBuffer.depth)` or - * `myShader.setUniform('depthTexture', myBuffer.depth)` may be helpful for - * advanced use cases. - * - * Note: By default, a framebuffer's y-coordinates are flipped compared to - * images and videos. It's easy to flip a framebuffer's y-coordinates as - * needed when applying it as a texture. For example, calling - * `plane(myBuffer.width, -myBuffer.height)` will flip the framebuffer. - * - * @property {p5.FramebufferTexture} depth - * @for p5.Framebuffer - * - * @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; - * attribute vec3 aPosition; - * attribute vec2 aTexCoord; - * uniform mat4 uModelViewMatrix; - * uniform mat4 uProjectionMatrix; - * varying vec2 vTexCoord; - * - * void main() { - * vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); - * gl_Position = uProjectionMatrix * viewModelPosition; - * vTexCoord = aTexCoord; - * } - * `; - * - * // Create a string with the fragment shader program. - * // The fragment shader is called for each pixel. - * let fragSrc = ` - * precision highp float; - * varying vec2 vTexCoord; - * uniform sampler2D depth; - * - * void main() { - * // Get the pixel's depth value. - * float depthVal = texture2D(depth, vTexCoord).r; - * - * // Set the pixel's color based on its depth. - * gl_FragColor = mix( - * vec4(0., 0., 0., 1.), - * vec4(1., 0., 1., 1.), - * depthVal); - * } - * `; - * - * let myBuffer; - * let myShader; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Framebuffer object. - * myBuffer = createFramebuffer(); - * - * // Create a p5.Shader object. - * myShader = createShader(vertSrc, fragSrc); - * - * // Compile and apply the shader. - * shader(myShader); - * - * describe('The shadow of a box rotates slowly against a magenta background.'); - * } - * - * function draw() { - * // Draw to the p5.Framebuffer object. - * myBuffer.begin(); - * background(255); - * rotateX(frameCount * 0.01); - * box(20, 20, 80); - * myBuffer.end(); - * - * // Set the shader's depth uniform using - * // the framebuffer's depth texture. - * myShader.setUniform('depth', myBuffer.depth); - * - * // Style the plane. - * noStroke(); - * - * // Draw the plane. - * plane(myBuffer.width, myBuffer.height); - * } - * - *
- */ - -/** - * An array containing the color of each pixel in the framebuffer. - * - * myBuffer.loadPixels() must be - * called before accessing the `myBuffer.pixels` array. - * myBuffer.updatePixels() - * must be called after any changes are made. - * - * Note: Updating pixels via this property is slower than drawing to the - * framebuffer directly. Consider using a - * p5.Shader object instead of looping over - * `myBuffer.pixels`. - * - * @property {Number[]} pixels - * @for p5.Framebuffer - * - * @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.'); - * } - * - *
- */ +export default framebuffer; -export default p5.Framebuffer; +if(typeof p5 !== 'undefined'){ + framebuffer(p5, p5.prototype); +} diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 12689acc86..6cd433015d 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -8,1158 +8,2215 @@ //some of the functions are adjusted from Three.js(http://threejs.org) -import p5 from '../core/main'; import * as constants from '../core/constants'; -/** - * A class to describe a 3D shape. - * - * Each `p5.Geometry` object represents a 3D shape as a set of connected - * points called *vertices*. All 3D shapes are made by connecting vertices to - * form triangles that are stitched together. Each triangular patch on the - * geometry's surface is called a *face*. The geometry stores information - * about its vertices and faces for use with effects such as lighting and - * texture mapping. - * - * The first parameter, `detailX`, is optional. If a number is passed, as in - * `new p5.Geometry(24)`, it sets the number of triangle subdivisions to use - * along the geometry's x-axis. By default, `detailX` is 1. - * - * The second parameter, `detailY`, is also optional. If a number is passed, - * as in `new p5.Geometry(24, 16)`, it sets the number of triangle - * subdivisions to use along the geometry's y-axis. By default, `detailX` is - * 1. - * - * The third parameter, `callback`, is also optional. If a function is passed, - * as in `new p5.Geometry(24, 16, createShape)`, it will be called once to add - * vertices to the new 3D shape. - * - * @class p5.Geometry - * @param {Integer} [detailX] number of vertices along the x-axis. - * @param {Integer} [detailY] number of vertices along the y-axis. - * @param {function} [callback] function to call once the geometry is created. - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Geometry object. - * myGeometry = new p5.Geometry(); - * - * // Create p5.Vector objects to position the vertices. - * let v0 = createVector(-40, 0, 0); - * let v1 = createVector(0, -40, 0); - * let v2 = createVector(40, 0, 0); - * - * // Add the vertices to the p5.Geometry object's vertices array. - * myGeometry.vertices.push(v0, v1, v2); - * - * describe('A white triangle drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the p5.Geometry object. - * model(myGeometry); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Geometry object using a callback function. - * myGeometry = new p5.Geometry(1, 1, createShape); - * - * describe('A white triangle drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the p5.Geometry object. - * model(myGeometry); - * } - * - * function createShape() { - * // Create p5.Vector objects to position the vertices. - * let v0 = createVector(-40, 0, 0); - * let v1 = createVector(0, -40, 0); - * let v2 = createVector(40, 0, 0); - * - * // "this" refers to the p5.Geometry object being created. - * - * // Add the vertices to the p5.Geometry object's vertices array. - * this.vertices.push(v0, v1, v2); - * - * // Add an array to list which vertices belong to the face. - * // Vertices are listed in clockwise "winding" order from - * // left to top to right. - * this.faces.push([0, 1, 2]); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Geometry object using a callback function. - * myGeometry = new p5.Geometry(1, 1, createShape); - * - * describe('A white triangle drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the p5.Geometry object. - * model(myGeometry); - * } - * - * function createShape() { - * // Create p5.Vector objects to position the vertices. - * let v0 = createVector(-40, 0, 0); - * let v1 = createVector(0, -40, 0); - * let v2 = createVector(40, 0, 0); - * - * // "this" refers to the p5.Geometry object being created. - * - * // Add the vertices to the p5.Geometry object's vertices array. - * this.vertices.push(v0, v1, v2); - * - * // Add an array to list which vertices belong to the face. - * // Vertices are listed in clockwise "winding" order from - * // left to top to right. - * this.faces.push([0, 1, 2]); - * - * // Compute the surface normals to help with lighting. - * this.computeNormals(); - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * // Adapted from Paul Wheeler's wonderful p5.Geometry tutorial. - * // https://www.paulwheeler.us/articles/custom-3d-geometry-in-p5js/ - * // CC-BY-SA 4.0 - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create the p5.Geometry object. - * // Set detailX to 48 and detailY to 2. - * // >>> try changing them. - * myGeometry = new p5.Geometry(48, 2, createShape); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the p5.Geometry object. - * strokeWeight(0.2); - * - * // Draw the p5.Geometry object. - * model(myGeometry); - * } - * - * function createShape() { - * // "this" refers to the p5.Geometry object being created. - * - * // Define the Möbius strip with a few parameters. - * let spread = 0.1; - * let radius = 30; - * let stripWidth = 15; - * let xInterval = 4 * PI / this.detailX; - * let yOffset = -stripWidth / 2; - * let yInterval = stripWidth / this.detailY; - * - * for (let j = 0; j <= this.detailY; j += 1) { - * // Calculate the "vertical" point along the strip. - * let v = yOffset + yInterval * j; - * - * for (let i = 0; i <= this.detailX; i += 1) { - * // Calculate the angle of rotation around the strip. - * let u = i * xInterval; - * - * // Calculate the coordinates of the vertex. - * let x = (radius + v * cos(u / 2)) * cos(u) - sin(u / 2) * 2 * spread; - * let y = (radius + v * cos(u / 2)) * sin(u); - * if (u < TWO_PI) { - * y += sin(u) * spread; - * } else { - * y -= sin(u) * spread; - * } - * let z = v * sin(u / 2) + sin(u / 4) * 4 * spread; - * - * // Create a p5.Vector object to position the vertex. - * let vert = createVector(x, y, z); - * - * // Add the vertex to the p5.Geometry object's vertices array. - * this.vertices.push(vert); - * } - * } - * - * // Compute the faces array. - * this.computeFaces(); - * - * // Compute the surface normals to help with lighting. - * this.computeNormals(); - * } - * - *
- */ -p5.Geometry = class Geometry { - constructor(detailX, detailY, callback) { - this.vertices = []; - this.boundingBoxCache = null; +function geometry(p5, fn){ + /** + * A class to describe a 3D shape. + * + * Each `p5.Geometry` object represents a 3D shape as a set of connected + * points called *vertices*. All 3D shapes are made by connecting vertices to + * form triangles that are stitched together. Each triangular patch on the + * geometry's surface is called a *face*. The geometry stores information + * about its vertices and faces for use with effects such as lighting and + * texture mapping. + * + * The first parameter, `detailX`, is optional. If a number is passed, as in + * `new p5.Geometry(24)`, it sets the number of triangle subdivisions to use + * along the geometry's x-axis. By default, `detailX` is 1. + * + * The second parameter, `detailY`, is also optional. If a number is passed, + * as in `new p5.Geometry(24, 16)`, it sets the number of triangle + * subdivisions to use along the geometry's y-axis. By default, `detailX` is + * 1. + * + * The third parameter, `callback`, is also optional. If a function is passed, + * as in `new p5.Geometry(24, 16, createShape)`, it will be called once to add + * vertices to the new 3D shape. + * + * @class p5.Geometry + * @param {Integer} [detailX] number of vertices along the x-axis. + * @param {Integer} [detailY] number of vertices along the y-axis. + * @param {function} [callback] function to call once the geometry is created. + * + * @example + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let myGeometry; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Geometry object. + * myGeometry = new p5.Geometry(); + * + * // Create p5.Vector objects to position the vertices. + * let v0 = createVector(-40, 0, 0); + * let v1 = createVector(0, -40, 0); + * let v2 = createVector(40, 0, 0); + * + * // Add the vertices to the p5.Geometry object's vertices array. + * myGeometry.vertices.push(v0, v1, v2); + * + * describe('A white triangle drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the p5.Geometry object. + * model(myGeometry); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let myGeometry; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Geometry object using a callback function. + * myGeometry = new p5.Geometry(1, 1, createShape); + * + * describe('A white triangle drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the p5.Geometry object. + * model(myGeometry); + * } + * + * function createShape() { + * // Create p5.Vector objects to position the vertices. + * let v0 = createVector(-40, 0, 0); + * let v1 = createVector(0, -40, 0); + * let v2 = createVector(40, 0, 0); + * + * // "this" refers to the p5.Geometry object being created. + * + * // Add the vertices to the p5.Geometry object's vertices array. + * this.vertices.push(v0, v1, v2); + * + * // Add an array to list which vertices belong to the face. + * // Vertices are listed in clockwise "winding" order from + * // left to top to right. + * this.faces.push([0, 1, 2]); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * let myGeometry; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Geometry object using a callback function. + * myGeometry = new p5.Geometry(1, 1, createShape); + * + * describe('A white triangle drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the p5.Geometry object. + * model(myGeometry); + * } + * + * function createShape() { + * // Create p5.Vector objects to position the vertices. + * let v0 = createVector(-40, 0, 0); + * let v1 = createVector(0, -40, 0); + * let v2 = createVector(40, 0, 0); + * + * // "this" refers to the p5.Geometry object being created. + * + * // Add the vertices to the p5.Geometry object's vertices array. + * this.vertices.push(v0, v1, v2); + * + * // Add an array to list which vertices belong to the face. + * // Vertices are listed in clockwise "winding" order from + * // left to top to right. + * this.faces.push([0, 1, 2]); + * + * // Compute the surface normals to help with lighting. + * this.computeNormals(); + * } + * + *
+ * + *
+ * + * // Click and drag the mouse to view the scene from different angles. + * + * // Adapted from Paul Wheeler's wonderful p5.Geometry tutorial. + * // https://www.paulwheeler.us/articles/custom-3d-geometry-in-p5js/ + * // CC-BY-SA 4.0 + * + * let myGeometry; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Create the p5.Geometry object. + * // Set detailX to 48 and detailY to 2. + * // >>> try changing them. + * myGeometry = new p5.Geometry(48, 2, createShape); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); + * + * // Style the p5.Geometry object. + * strokeWeight(0.2); + * + * // Draw the p5.Geometry object. + * model(myGeometry); + * } + * + * function createShape() { + * // "this" refers to the p5.Geometry object being created. + * + * // Define the Möbius strip with a few parameters. + * let spread = 0.1; + * let radius = 30; + * let stripWidth = 15; + * let xInterval = 4 * PI / this.detailX; + * let yOffset = -stripWidth / 2; + * let yInterval = stripWidth / this.detailY; + * + * for (let j = 0; j <= this.detailY; j += 1) { + * // Calculate the "vertical" point along the strip. + * let v = yOffset + yInterval * j; + * + * for (let i = 0; i <= this.detailX; i += 1) { + * // Calculate the angle of rotation around the strip. + * let u = i * xInterval; + * + * // Calculate the coordinates of the vertex. + * let x = (radius + v * cos(u / 2)) * cos(u) - sin(u / 2) * 2 * spread; + * let y = (radius + v * cos(u / 2)) * sin(u); + * if (u < TWO_PI) { + * y += sin(u) * spread; + * } else { + * y -= sin(u) * spread; + * } + * let z = v * sin(u / 2) + sin(u / 4) * 4 * spread; + * + * // Create a p5.Vector object to position the vertex. + * let vert = createVector(x, y, z); + * + * // Add the vertex to the p5.Geometry object's vertices array. + * this.vertices.push(vert); + * } + * } + * + * // Compute the faces array. + * this.computeFaces(); + * + * // Compute the surface normals to help with lighting. + * this.computeNormals(); + * } + * + *
+ */ + p5.Geometry = 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; + * + * 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. + * + * `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. + * + * 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. + * + * 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); + } + } - //an array containing every vertex for stroke drawing - this.lineVertices = new p5.DataArray(); + // 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]; + } + }); - // 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(); + // update the deduplicated vertices + this.vertices = vertices = uniqueVertices; + } - // 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(); + // initialize the vertexNormals array with empty vectors + vertexNormals.length = 0; + for (iv = 0; iv < vertices.length; ++iv) { + vertexNormals.push(new p5.Vector()); + } - this.vertexNormals = []; + // 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); - this.faces = []; + // all three vertices get the normal added + for (let fv = 0; fv < 3; ++fv) { + const vertexIndex = face[fv]; + vertexNormals[vertexIndex].add(faceNormal); + } + }); - this.uvs = []; - // a 2D array containing edge connectivity pattern for create line vertices - //based on faces for most objects; - this.edges = []; - this.vertexColors = []; + // normalize the normals + for (iv = 0; iv < vertices.length; ++iv) { + vertexNormals[iv].normalize(); + } - // One color per vertex representing the stroke color at that vertex - this.vertexStrokeColors = []; + return this; + } - this.userVertexProperties = {}; + /** + * 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; + } - // 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 = {}; + /** + * 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); - this._hasFillTransparency = undefined; - this._hasStrokeTransparency = undefined; + for (let i = 0; i < this.detailX; i++) { + this.vertexNormals[i] = sum; + } - if (callback instanceof Function) { - callback.call(this); + //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; } - } - /** - * 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 + /** + * 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; } - 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); + /** + * 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; + } + } - 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); + 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 + }); + } + } - 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; + if (dirOK) { + lastValidDir = dir; } } + for (const { point, dir, color } of potentialCaps.values()) { + this._addCap(point, dir, color); + } + return this; } - 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; + + /** + * 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; } - return this._hasStrokeTransparency; - } - /** - * Removes the geometry’s internal colors. + /** + * 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. * - * `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. + * Note: `myGeometry.normalize()` only works when called in the + * setup() function. + * + * @chainable * * @example *
* + * let myGeometry; + * * function setup() { * createCanvas(100, 100, WEBGL); * - * background(200); - * - * // Create a p5.Geometry object. - * // Set its internal color to red. + * // Create a very small torus. * beginGeometry(); - * fill(255, 0, 0); - * plane(20); - * let myGeometry = endGeometry(); - * - * // Style the shape. - * noStroke(); - * - * // Draw the p5.Geometry object (center). - * model(myGeometry); + * torus(1, 0.25); + * myGeometry = endGeometry(); * - * // Translate the origin to the bottom-right. - * translate(25, 25, 0); + * // Normalize the torus so its vertices fill + * // the range [-100, 100]. + * myGeometry.normalize(); * - * // Try to fill the geometry with green. - * fill(0, 255, 0); + * describe('A white torus rotates slowly against a dark gray background.'); + * } * - * // Draw the geometry again (bottom-right). - * model(myGeometry); + * function draw() { + * background(50); * - * // Clear the geometry's colors. - * myGeometry.clearColors(); + * // Turn on the lights. + * lights(); * - * // Fill the geometry with blue. - * fill(0, 0, 255); + * // Rotate around the y-axis. + * rotateY(frameCount * 0.01); * - * // Translate the origin up. - * translate(0, -50, 0); + * // Style the torus. + * noStroke(); * - * // Draw the geometry again (top-right). + * // Draw the torus. * 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; - } + 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); + } - /** - * 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. + 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 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. + * 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. * - * @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; + * 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(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(); - * } + * 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; + * }` * }); * - * saveBtn = createButton('Save .obj'); - * saveBtn.mousePressed(() => myModel.saveObj()); + * // 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 few spheres rotating in space'); + * describe('A rough pink sphere rotating on a blue background.'); * } * * function draw() { - * background(0); + * // Set some styles and lighting + * background('lightblue'); * noStroke(); - * lights(); - * rotateX(millis() * 0.001); - * rotateY(millis() * 0.002); - * model(myModel); + * + * 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. */ - 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`; + 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(); } } - // 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; + _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()'); } - faceStr += '/'; - if (this.vertexNormals.length > 0) { - faceStr += index + 1; + 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; } - }); - objStr += faceStr + '\n'; - }); - - const blob = new Blob([objStr], { type: 'text/plain' }); - p5.prototype.downloadFile(blob, fileName , 'obj'); - - } + }; + this[prop.getSrcName()] = []; + return this.userVertexProperties[propertyName]; + } + }; /** - * 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. + * An array with the geometry's vertices. * - * The exported .stl file will include the faces, vertices, and normals of the `p5.Geometry`. + * 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. * - * 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. + * @property vertices + * @for p5.Geometry + * @name vertices * - * @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; + * // Click and drag the mouse to view the scene from different angles. + * + * let myGeometry; + * * 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(); - * } - * }); + * createCanvas(100, 100, WEBGL); + * + * // Create a p5.Geometry object. + * myGeometry = new p5.Geometry(); + * + * // Create p5.Vector objects to position the vertices. + * let v0 = createVector(-40, 0, 0); + * let v1 = createVector(0, -40, 0); + * let v2 = createVector(40, 0, 0); + * + * // Add the vertices to the p5.Geometry object's vertices array. + * myGeometry.vertices.push(v0, v1, v2); + * + * describe('A white triangle drawn on a gray background.'); + * } + * + * function draw() { + * background(200); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Draw the p5.Geometry object. + * model(myGeometry); + * } + * + *
+ * + *
+ * + * // 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(30, 15, 10, 8); + * myGeometry = endGeometry(); + * + * describe('A white torus rotates slowly against a dark gray background. Red spheres mark its vertices.'); + * } + * + * function draw() { + * background(50); + * + * // Enable orbiting with the mouse. + * orbitControl(); + * + * // Turn on the lights. + * lights(); * - * saveBtn1 = createButton('Save .stl'); - * saveBtn1.mousePressed(function() { - * myModel.saveStl(); - * }); - * saveBtn2 = createButton('Save binary .stl'); - * saveBtn2.mousePressed(function() { - * myModel.saveStl('model.stl', { binary: true }); - * }); + * // Rotate the coordinate system. + * rotateY(frameCount * 0.01); * - * describe('A few spheres rotating in space'); - * } + * // Style the p5.Geometry object. + * fill(255); + * stroke(0); * - * function draw() { - * background(0); + * // Display the p5.Geometry object. + * model(myGeometry); + * + * // Style the vertices. + * fill(255, 0, 0); * noStroke(); - * lights(); - * rotateX(millis() * 0.001); - * rotateY(millis() * 0.002); - * model(myModel); + * + * // Iterate over the vertices array. + * for (let v of myGeometry.vertices) { + * // Draw a sphere to mark the vertex. + * push(); + * translate(v); + * sphere(2.5); + * pop(); + * } * } * *
*/ - 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' }); - p5.prototype.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. + * An array with the vectors that are normal to the geometry's vertices. * * A face's orientation is defined by its *normal vector* which points out * of the face and is normal (perpendicular) to the surface. Calling - * `myGeometry.computeNormals()` first calculates each face's normal vector. - * Then it calculates the normal vector for each vertex by averaging the - * normal vectors of the faces surrounding the vertex. The vertex normals - * are stored as p5.Vector objects in the - * myGeometry.vertexNormals array. - * - * 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 + * `myGeometry.computeNormals()` first calculates each face's normal + * vector. Then it calculates the normal vector for each vertex by + * averaging the normal vectors of the faces surrounding the vertex. The + * vertex normals are stored as p5.Vector + * objects in the `myGeometry.vertexNormals` array. + * + * @property vertexNormals + * @name vertexNormals + * @for p5.Geometry * * @example *
@@ -1173,14 +2230,14 @@ p5.Geometry = class Geometry { * * // Create a p5.Geometry object. * beginGeometry(); - * torus(); + * torus(30, 15, 10, 8); * 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." + * 'A white torus rotates against a dark gray background. Red lines extend outward from its vertices.' * ); * } * @@ -1194,12 +2251,12 @@ p5.Geometry = class Geometry { * lights(); * * // Rotate the coordinate system. - * rotateX(1); + * rotateY(frameCount * 0.01); * - * // Style the helix. + * // Style the p5.Geometry object. * stroke(0); * - * // Display the helix. + * // Display the p5.Geometry object. * model(myGeometry); * * // Style the normal vectors. @@ -1215,7 +2272,7 @@ p5.Geometry = class Geometry { * let n = myGeometry.vertexNormals[i]; * * // Calculate a point along the vertex normal. - * let p = p5.Vector.mult(n, 5); + * let p = p5.Vector.mult(n, 8); * * // Draw the vertex normal as a red line. * push(); @@ -1236,7 +2293,7 @@ p5.Geometry = class Geometry { * function setup() { * createCanvas(100, 100, WEBGL); * - * // Create a p5.Geometry object using a callback function. + * // Create a p5.Geometry object. * myGeometry = new p5.Geometry(); * * // Create p5.Vector objects to position the vertices. @@ -1270,12 +2327,45 @@ p5.Geometry = class Geometry { * noStroke(); * fill(255, 0, 0); * - * // Draw the p5.Geometry object. + * // Display the p5.Geometry object. * model(myGeometry); * } * *
+ */ + + /** + * An array that lists which of the geometry's vertices form each of its + * faces. + * + * All 3D shapes are made by connecting sets of points called *vertices*. A + * geometry's surface is formed by connecting vertices to form triangles + * that are stitched together. Each triangular patch on the geometry's + * surface is called a *face*. + * + * The geometry's vertices are stored as + * p5.Vector objects in the + * myGeometry.vertices array. The + * geometry's first vertex is the p5.Vector + * object at `myGeometry.vertices[0]`, its second vertex is + * `myGeometry.vertices[1]`, its third vertex is `myGeometry.vertices[2]`, + * and so on. + * + * For example, a geometry made from a rectangle has two faces because a + * rectangle is made by joining two triangles. `myGeometry.faces` for a + * rectangle would be the two-dimensional array `[[0, 1, 2], [2, 1, 3]]`. + * The first face, `myGeometry.faces[0]`, is the array `[0, 1, 2]` because + * it's formed by connecting `myGeometry.vertices[0]`, + * `myGeometry.vertices[1]`,and `myGeometry.vertices[2]`. The second face, + * `myGeometry.faces[1]`, is the array `[2, 1, 3]` because it's formed by + * connecting `myGeometry.vertices[2]`, `myGeometry.vertices[1]`,and + * `myGeometry.vertices[3]`. + * + * @property faces + * @name faces + * @for p5.Geometry * + * @example *
* * // Click and drag the mouse to view the scene from different angles. @@ -1286,16 +2376,15 @@ p5.Geometry = class Geometry { * createCanvas(100, 100, WEBGL); * * // Create a p5.Geometry object. - * myGeometry = buildGeometry(createShape); - * - * // Compute normals using default (FLAT) shading. - * myGeometry.computeNormals(FLAT); + * beginGeometry(); + * sphere(); + * myGeometry = endGeometry(); * - * describe('A white, helical structure drawn on a dark gray background. Its faces appear faceted.'); + * describe("A sphere drawn on a gray background. The sphere's surface is a grayscale patchwork of triangles."); * } * * function draw() { - * background(50); + * background(200); * * // Enable orbiting with the mouse. * orbitControl(); @@ -1303,1191 +2392,107 @@ p5.Geometry = class Geometry { * // Turn on the lights. * lights(); * - * // Rotate the coordinate system. - * rotateX(1); - * - * // Style the helix. + * // Style the p5.Geometry object. * 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(); + * // Set a random seed. + * randomSeed(1234); * - * // Rotate the coordinate system. - * rotateX(1); + * // Iterate over the faces array. + * for (let face of myGeometry.faces) { * - * // Style the helix. - * noStroke(); + * // Style the face. + * let g = random(0, 255); + * fill(g); * - * // Display the helix. - * model(myGeometry); - * } + * // Draw the face. + * beginShape(); + * // Iterate over the vertices that form the face. + * for (let f of face) { + * // Get the vertex's p5.Vector object. + * let v = myGeometry.vertices[f]; + * vertex(v.x, v.y, v.z); + * } + * endShape(); * - * 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(); * } * *
+ */ + + /** + * An array that lists the texture coordinates for each of the geometry's + * vertices. + * + * In order for texture() to work, the geometry + * needs a way to map the points on its surface to the pixels in a + * rectangular image that's used as a texture. The geometry's vertex at + * coordinates `(x, y, z)` maps to the texture image's pixel at coordinates + * `(u, v)`. + * + * The `myGeometry.uvs` array stores the `(u, v)` coordinates for each + * vertex in the order it was added to the geometry. For example, the + * first vertex, `myGeometry.vertices[0]`, has its `(u, v)` coordinates + * stored at `myGeometry.uvs[0]` and `myGeometry.uvs[1]`. + * + * @property uvs + * @name uvs + * @for p5.Geometry * + * @example *
* - * // Click and drag the mouse to view the scene from different angles. + * let img; * - * let myGeometry; + * // Load the image and create a p5.Image object. + * function preload() { + * img = loadImage('assets/laDefense.jpg'); + * } * * 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); + * background(200); * - * // Enable orbiting with the mouse. - * orbitControl(); + * // Create p5.Geometry objects. + * let geom1 = buildGeometry(createShape); + * let geom2 = buildGeometry(createShape); * - * // Turn on the lights. - * lights(); + * // Left (original). + * push(); + * translate(-25, 0, 0); + * texture(img); + * noStroke(); + * model(geom1); + * pop(); * - * // Rotate the coordinate system. - * rotateX(1); + * // Set geom2's texture coordinates. + * geom2.uvs = [0.25, 0.25, 0.75, 0.25, 0.25, 0.75, 0.75, 0.75]; * - * // Style the helix. + * // Right (zoomed in). + * push(); + * translate(25, 0, 0); + * texture(img); * noStroke(); + * model(geom2); + * pop(); * - * // Display the helix. - * model(myGeometry); + * describe( + * 'Two photos of a ceiling on a gray background. The photo on the right zooms in to the center of the photo.' + * ); * } * * function createShape() { - * // 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(); + * plane(40); * } * *
*/ - 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 - *
- * - * 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]; - } -}; - -/** - * An array with the geometry's vertices. - * - * The geometry's vertices are stored as - * p5.Vector objects in the `myGeometry.vertices` - * array. The geometry's first vertex is the - * p5.Vector object at `myGeometry.vertices[0]`, - * its second vertex is `myGeometry.vertices[1]`, its third vertex is - * `myGeometry.vertices[2]`, and so on. - * - * @property vertices - * @for p5.Geometry - * @name vertices - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Geometry object. - * myGeometry = new p5.Geometry(); - * - * // Create p5.Vector objects to position the vertices. - * let v0 = createVector(-40, 0, 0); - * let v1 = createVector(0, -40, 0); - * let v2 = createVector(40, 0, 0); - * - * // Add the vertices to the p5.Geometry object's vertices array. - * myGeometry.vertices.push(v0, v1, v2); - * - * describe('A white triangle drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Draw the p5.Geometry object. - * model(myGeometry); - * } - * - *
- * - *
- * - * // 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(30, 15, 10, 8); - * myGeometry = endGeometry(); - * - * describe('A white torus rotates slowly against a dark gray background. Red spheres mark its vertices.'); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Rotate the coordinate system. - * rotateY(frameCount * 0.01); - * - * // Style the p5.Geometry object. - * fill(255); - * stroke(0); - * - * // Display the p5.Geometry object. - * model(myGeometry); - * - * // Style the vertices. - * fill(255, 0, 0); - * noStroke(); - * - * // Iterate over the vertices array. - * for (let v of myGeometry.vertices) { - * // Draw a sphere to mark the vertex. - * push(); - * translate(v); - * sphere(2.5); - * pop(); - * } - * } - * - *
- */ - -/** - * An array with the vectors that are normal to the geometry's vertices. - * - * A face's orientation is defined by its *normal vector* which points out - * of the face and is normal (perpendicular) to the surface. Calling - * `myGeometry.computeNormals()` first calculates each face's normal - * vector. Then it calculates the normal vector for each vertex by - * averaging the normal vectors of the faces surrounding the vertex. The - * vertex normals are stored as p5.Vector - * objects in the `myGeometry.vertexNormals` array. - * - * @property vertexNormals - * @name vertexNormals - * @for p5.Geometry - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Geometry object. - * beginGeometry(); - * torus(30, 15, 10, 8); - * myGeometry = endGeometry(); - * - * // Compute the vertex normals. - * myGeometry.computeNormals(); - * - * describe( - * 'A white torus rotates against a dark gray background. Red lines extend outward from its vertices.' - * ); - * } - * - * function draw() { - * background(50); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Rotate the coordinate system. - * rotateY(frameCount * 0.01); - * - * // Style the p5.Geometry object. - * stroke(0); - * - * // Display the p5.Geometry object. - * model(myGeometry); - * - * // Style the normal vectors. - * stroke(255, 0, 0); - * - * // Iterate over the vertices and vertexNormals arrays. - * for (let i = 0; i < myGeometry.vertices.length; i += 1) { - * - * // Get the vertex p5.Vector object. - * let v = myGeometry.vertices[i]; - * - * // Get the vertex normal p5.Vector object. - * let n = myGeometry.vertexNormals[i]; - * - * // Calculate a point along the vertex normal. - * let p = p5.Vector.mult(n, 8); - * - * // Draw the vertex normal as a red line. - * push(); - * translate(v); - * line(0, 0, 0, p.x, p.y, p.z); - * pop(); - * } - * } - * - *
- * - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Geometry object. - * myGeometry = new p5.Geometry(); - * - * // Create p5.Vector objects to position the vertices. - * let v0 = createVector(-40, 0, 0); - * let v1 = createVector(0, -40, 0); - * let v2 = createVector(0, 40, 0); - * let v3 = createVector(40, 0, 0); - * - * // Add the vertices to the p5.Geometry object's vertices array. - * myGeometry.vertices.push(v0, v1, v2, v3); - * - * // Compute the faces array. - * myGeometry.computeFaces(); - * - * // Compute the surface normals. - * myGeometry.computeNormals(); - * - * describe('A red square drawn on a gray background.'); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Add a white point light. - * pointLight(255, 255, 255, 0, 0, 10); - * - * // Style the p5.Geometry object. - * noStroke(); - * fill(255, 0, 0); - * - * // Display the p5.Geometry object. - * model(myGeometry); - * } - * - *
- */ - -/** - * An array that lists which of the geometry's vertices form each of its - * faces. - * - * All 3D shapes are made by connecting sets of points called *vertices*. A - * geometry's surface is formed by connecting vertices to form triangles - * that are stitched together. Each triangular patch on the geometry's - * surface is called a *face*. - * - * The geometry's vertices are stored as - * p5.Vector objects in the - * myGeometry.vertices array. The - * geometry's first vertex is the p5.Vector - * object at `myGeometry.vertices[0]`, its second vertex is - * `myGeometry.vertices[1]`, its third vertex is `myGeometry.vertices[2]`, - * and so on. - * - * For example, a geometry made from a rectangle has two faces because a - * rectangle is made by joining two triangles. `myGeometry.faces` for a - * rectangle would be the two-dimensional array `[[0, 1, 2], [2, 1, 3]]`. - * The first face, `myGeometry.faces[0]`, is the array `[0, 1, 2]` because - * it's formed by connecting `myGeometry.vertices[0]`, - * `myGeometry.vertices[1]`,and `myGeometry.vertices[2]`. The second face, - * `myGeometry.faces[1]`, is the array `[2, 1, 3]` because it's formed by - * connecting `myGeometry.vertices[2]`, `myGeometry.vertices[1]`,and - * `myGeometry.vertices[3]`. - * - * @property faces - * @name faces - * @for p5.Geometry - * - * @example - *
- * - * // Click and drag the mouse to view the scene from different angles. - * - * let myGeometry; - * - * function setup() { - * createCanvas(100, 100, WEBGL); - * - * // Create a p5.Geometry object. - * beginGeometry(); - * sphere(); - * myGeometry = endGeometry(); - * - * describe("A sphere drawn on a gray background. The sphere's surface is a grayscale patchwork of triangles."); - * } - * - * function draw() { - * background(200); - * - * // Enable orbiting with the mouse. - * orbitControl(); - * - * // Turn on the lights. - * lights(); - * - * // Style the p5.Geometry object. - * noStroke(); - * - * // Set a random seed. - * randomSeed(1234); - * - * // Iterate over the faces array. - * for (let face of myGeometry.faces) { - * - * // Style the face. - * let g = random(0, 255); - * fill(g); - * - * // Draw the face. - * beginShape(); - * // Iterate over the vertices that form the face. - * for (let f of face) { - * // Get the vertex's p5.Vector object. - * let v = myGeometry.vertices[f]; - * vertex(v.x, v.y, v.z); - * } - * endShape(); - * - * } - * } - * - *
- */ - -/** - * An array that lists the texture coordinates for each of the geometry's - * vertices. - * - * In order for texture() to work, the geometry - * needs a way to map the points on its surface to the pixels in a - * rectangular image that's used as a texture. The geometry's vertex at - * coordinates `(x, y, z)` maps to the texture image's pixel at coordinates - * `(u, v)`. - * - * The `myGeometry.uvs` array stores the `(u, v)` coordinates for each - * vertex in the order it was added to the geometry. For example, the - * first vertex, `myGeometry.vertices[0]`, has its `(u, v)` coordinates - * stored at `myGeometry.uvs[0]` and `myGeometry.uvs[1]`. - * - * @property uvs - * @name uvs - * @for p5.Geometry - * - * @example - *
- * - * let img; - * - * // Load the image and create a p5.Image object. - * 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); - * - * // Left (original). - * push(); - * translate(-25, 0, 0); - * texture(img); - * noStroke(); - * model(geom1); - * pop(); - * - * // Set geom2's texture coordinates. - * geom2.uvs = [0.25, 0.25, 0.75, 0.25, 0.25, 0.75, 0.75, 0.75]; - * - * // Right (zoomed in). - * push(); - * translate(25, 0, 0); - * texture(img); - * noStroke(); - * model(geom2); - * pop(); - * - * describe( - * 'Two photos of a ceiling on a gray background. The photo on the right zooms in to the center of the photo.' - * ); - * } - * - * function createShape() { - * plane(40); - * } - * - *
- */ +} -export default p5.Geometry; +export default geometry; +if(typeof p5 !== 'undefined'){ + geometry(p5, p5.prototype); +} diff --git a/src/webgl/p5.Matrix.js b/src/webgl/p5.Matrix.js index 76deeef6a6..8c41715238 100644 --- a/src/webgl/p5.Matrix.js +++ b/src/webgl/p5.Matrix.js @@ -7,978 +7,983 @@ * Reference/Global_Objects/SIMD */ -import p5 from '../core/main'; +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; + } -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]; + } -/** - * 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; - } + 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]); + 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; } - 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' - ); + /** + * 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; } - for (let i = 0; i < 16; i++) { - this.mat4[i] = refArray[i]; + + /** + * 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 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 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; + } - /** - * 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(); + } - clone() { - return this.copy(); - } + /** + * return an identity matrix + * @return {p5.Matrix} the result matrix + */ + static identity(pInst){ + return new p5.Matrix(pInst); + } - /** - * return an identity matrix - * @return {p5.Matrix} the result matrix - */ - static identity(pInst){ - return new p5.Matrix(pInst); - } + /** + * 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; + } - /** - * 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; - } + /** + * 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; - /** - * 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; - } + 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; - 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; + 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; - } + /** + * 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]; - /** - * 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; + } + + /** + * 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; } - return this; - } - /** - * 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; - } + /** + * 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; + } - /** - * 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. - } - - // 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; - } + /** + * 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. + } - 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. - } - - 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; - } + // 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]; - /** - * 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 - } - - 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; - - 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. + } - /** - * @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; - } + 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; - 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; - } + /** + * 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 + } - /** - * 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; - } + 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; - /** - * 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 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; + /** + * 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 + } - return result; - } + 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; - /** - * 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 this; + } - /** - * 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]); - } + /** + * @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; + } - /** - * 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]); - } + rotateX(a) { + this.rotate(a, 1, 0, 0); + } + rotateY(a) { + this.rotate(a, 0, 1, 0); + } + rotateZ(a) { + this.rotate(a, 0, 0, 1); + } - /** - * 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. - } - - // 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; - } + /** + * 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; - /** - * 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] - ); - } + return this; + } - /** - * 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] - ); - } + /** + * 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; - /** - * 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; } - 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; - } + /** + * 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; + + result[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; + result[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; + result[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; + result[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; + + return result; + } - /** - * 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; - } + /** + * 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]); + } - /** - * 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 + /** + * 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 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]); + } + + /** + * 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. + } + + // 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; + } + + /** + * 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 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]]; + } + + /** + * 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 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 //]; - //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 -//]; -}; -export default p5.Matrix; + }; +} + +export default matrix; + +if(typeof p5 !== 'undefined'){ + matrix(p5, p5.prototype); +} diff --git a/src/webgl/p5.Quat.js b/src/webgl/p5.Quat.js index 64b2cf63a9..1eaf41b7f3 100644 --- a/src/webgl/p5.Quat.js +++ b/src/webgl/p5.Quat.js @@ -3,93 +3,97 @@ * @submodule Quaternion */ -import p5 from '../core/main'; +function quat(p5, fn){ + /** + * A class to describe a Quaternion + * for vector rotations in the p5js webgl renderer. + * Please refer the following link for details on the implementation + * https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html + * @class p5.Quat + * @constructor + * @param {Number} [w] Scalar part of the quaternion + * @param {Number} [x] x component of imaginary part of quaternion + * @param {Number} [y] y component of imaginary part of quaternion + * @param {Number} [z] z component of imaginary part of quaternion + * @private + */ + p5.Quat = class { + constructor(w, x, y, z) { + this.w = w; + this.vec = new p5.Vector(x, y, z); + } -/** - * A class to describe a Quaternion - * for vector rotations in the p5js webgl renderer. - * Please refer the following link for details on the implementation - * https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html - * @class p5.Quat - * @constructor - * @param {Number} [w] Scalar part of the quaternion - * @param {Number} [x] x component of imaginary part of quaternion - * @param {Number} [y] y component of imaginary part of quaternion - * @param {Number} [z] z component of imaginary part of quaternion - * @private - */ -p5.Quat = 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); + } - /** - * 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); + } - 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 */ + } - /** - * Multiplies a quaternion with other quaternion. - * @method mult - * @param {p5.Quat} [quat] quaternion to multiply with the quaternion calling the method. - * @chainable + /** + * 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 */ - 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 */ - } + 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(); + } - /** - * 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(); + } + }; +} - /** - * 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(); - } -}; +export default quat; -export default p5.Quat; +if(typeof p5 !== 'undefined'){ + quat(p5, p5.prototype); +} diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index 96fd2fc5e9..99850f4713 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -1,74 +1,78 @@ -import p5 from '../core/main'; - -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 - } - - /** - * 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; +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 } - // 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(); + /** + * 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; } - // 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; + // 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(); + } + // 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); } - // 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); } - } -}; + }; +} + +export default renderBuffer; -export default p5.RenderBuffer; +if(typeof p5 !== 'undefined'){ + renderBuffer(p5, p5.prototype); +} diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index acd67c390f..c031e48851 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -13,7 +13,6 @@ */ import p5 from '../core/main'; import * as constants from '../core/constants'; -import './p5.RenderBuffer'; /** * Begin shape drawing. This is a helpful way of generating diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 12055cfa03..57b661b6ce 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -1,8 +1,6 @@ //Retained Mode. The default mode for rendering 3D primitives //in WEBGL. import p5 from '../core/main'; -import './p5.RendererGL'; -import './p5.RenderBuffer'; import * as constants from '../core/constants'; /** diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index dbcd972168..1b80c536ec 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -2,11 +2,7 @@ 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 './p5.Shader'; -import './p5.Camera'; import Renderer from '../core/p5.Renderer'; -import './p5.Matrix'; -import './p5.Framebuffer'; import { MipmapTexture } from './p5.Texture'; const STROKE_CAP_ENUM = {}; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 87a2757195..68cb21131d 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -6,599 +6,62 @@ * @requires core */ -import p5 from '../core/main'; - -/** - * 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 = 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'); - } - +function shader(p5, fn){ /** - * 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: + * 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 - * 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); + * { + * vertex: { + * 'void beforeVertex': '() {}' + * } * } - * - *
- * - * @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; - * }` - * }); - * } + * Then, in your vertex shader source, you can run a hook by calling a function + * with the same name prefixed by `HOOK_`: * - * function draw() { - * background(255); - * shader(myShader); - * myShader.setUniform('time', millis()); - * lights(); - * noStroke(); - * fill('red'); - * sphere(50); + * ```glsl + * void main() { + * HOOK_beforeVertex(); + * // Add the rest ofy our shader code here! * } - * - *
- */ - 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. + * Note: createShader(), + * createFilterShader(), and + * loadShader() are the recommended ways to + * create an instance of this class. * - * @param {p5|p5.Graphics} context WebGL context for the copied shader. - * @returns {p5.Shader} new shader compiled for the target context. + * @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 *
@@ -626,813 +89,1354 @@ p5.Shader = class Shader { * // Create a string with the fragment shader program. * // The fragment shader is called for each pixel. * let fragSrc = ` - * precision mediump float; - * varying vec2 vTexCoord; + * precision highp float; * * 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);\ + * // Set each pixel's RGBA value to yellow. + * gl_FragColor = vec4(1.0, 1.0, 0.0, 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(); + * let myShader = createShader(vertSrc, fragSrc); * - * // Add a display surface for the shader. - * pg.plane(50, 50); + * // Apply the p5.Shader object. + * shader(myShader); * - * describe('A square with purple-blue gradient on its surface drawn against a gray background.'); - * } + * // Style the drawing surface. + * noStroke(); * - * function draw() { - * background(200); + * // Add a plane as a drawing surface. + * plane(100, 100); * - * // Draw the p5.Graphics object to the main canvas. - * image(pg, -25, -25); + * describe('A yellow 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; - * - * varying vec2 vTexCoord; + * let mandelbrot; * - * 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); + * // Load the shader and create a p5.Shader object. + * function preload() { + * mandelbrot = loadShader('assets/shader.vert', 'assets/shader.frag'); * } - * `; - * - * 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); + * // Use the p5.Shader object. + * shader(mandelbrot); * - * // Apply the copied shader to the main canvas. - * shader(copied); + * // Set the shader uniform p to an array. + * mandelbrot.setUniform('p', [-0.74364388703, 0.13182590421]); * - * describe('A rotating cube with a purple-blue gradient on its surface drawn against a gray background.'); + * describe('A fractal image zooms in and out of focus.'); * } * * function draw() { - * background(200); - * - * // Rotate around the x-, y-, and z-axes. - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * rotateZ(frameCount * 0.01); + * // Set the shader uniform r to a value that oscillates between 0 and 2. + * mandelbrot.setUniform('r', sin(frameCount * 0.01) + 1); * - * // Draw the box. - * box(50); + * // Add a quad as a display surface for the shader. + * quad(-1, -1, 1, -1, 1, 1, -1, 1); * } * *
*/ - copyToContext(context) { - const shader = new p5.Shader( - context._renderer, - this._vertSrc, - this._fragSrc - ); - shader.ensureCompiledOnContext(context); - return shader; - } + 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) || {} + } + }; + } - /** - * @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(); + 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; } - } - /** - * Queries the active attributes for this shader and loads - * their names and locations into the attributes array. - * @private - */ - _loadAttributes() { - if (this._loadedAttributes) { - return; + /** + * 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'; + } } - 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; + vertSrc() { + return this.shaderSrc(this._vertSrc, 'vertex'); } - this._loadedAttributes = true; - } + fragSrc() { + return this.shaderSrc(this._fragSrc, 'fragment'); + } - /** - * Queries the active uniforms for this shader and loads - * their names and locations into the uniforms array. - * @private - */ - _loadUniforms() { - if (this._loadedUniforms) { - return; + /** + * 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] + ); + } } - const gl = this._renderer.GL; + /** + * 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; + } - // Inspect shader and cache uniform info - const numUniforms = gl.getProgramParameter( - this._glProgram, - gl.ACTIVE_UNIFORMS - ); + 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 + } + }); + } - 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 + /** + * 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 ); - 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]')); + 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; } - uniform.name = uniformName; - uniform.type = uniformInfo.type; - uniform._cachedData = undefined; - if (uniform.type === gl.SAMPLER_2D) { - uniform.samplerIndex = samplerIndex; - samplerIndex++; - this.samplers.push(uniform); + + 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; } - 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._loadedAttributes = true; } - this._loadedUniforms = true; - } - compile() { - // TODO - } + /** + * Queries the active uniforms for this shader and loads + * their names and locations into the uniforms array. + * @private + */ + _loadUniforms() { + if (this._loadedUniforms) { + return; + } - /** - * initializes (if needed) and binds the shader program. - * @private - */ - bindShader() { - this.init(); - if (!this._bound) { - this.useProgram(); - this._bound = true; + const gl = this._renderer.GL; + + // Inspect shader and cache uniform info + const numUniforms = gl.getProgramParameter( + this._glProgram, + gl.ACTIVE_UNIFORMS + ); - this._setMatrixUniforms(); + 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); + } - this.setUniform('uViewport', this._renderer._viewport); + 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; } - } - /** - * @chainable - * @private - */ - unbindShader() { - if (this._bound) { - this.unbindTextures(); - //this._renderer.GL.useProgram(0); ?? - this._bound = false; + 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); + } } - 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(); + + /** + * @chainable + * @private + */ + unbindShader() { + if (this._bound) { + this.unbindTextures(); + //this._renderer.GL.useProgram(0); ?? + this._bound = false; } - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - tex.bindTexture(); - tex.update(); - gl.uniform1i(uniform.location, uniform.samplerIndex); + return this; } - } - updateTextures() { - for (const uniform of this.samplers) { - const tex = uniform.texture; - if (tex) { + 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()); + 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; + /** + * @chainable + * @private + */ + useProgram() { + const gl = this._renderer.GL; + if (this._renderer._curShader !== this) { + gl.useProgram(this._glProgram); + this._renderer._curShader = this; + } + return 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) - ) { + 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 { - uniform._cachedData = data.slice(0); + if (Array.isArray(data)) { + uniform._cachedData = data.slice(0); + } else { + uniform._cachedData = data; + } } - } 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; } - const location = uniform.location; + /* 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); + } - this.useProgram(); + isNormalShader() { + return this.attributes.aNormal !== undefined; + } - 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 + isTextureShader() { + return this.samplers.length > 0; } - 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; - } + isColorShader() { + return ( + this.attributes.aVertexColor !== undefined || + this.uniforms.uMaterialColor !== 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); + 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 + ); } - 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); + } } } - } -}; + }; +} + +export default shader; -export default p5.Shader; +if(typeof p5 !== 'undefined'){ + shader(p5, p5.prototype); +} diff --git a/src/webgl/text.js b/src/webgl/text.js index 63eede213c..f86d14f620 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -1,761 +1,762 @@ -import p5 from '../core/main'; import * as constants from '../core/constants'; -import './p5.Shader'; -import './p5.RendererGL.Retained'; - -// Text/Typography -// @TODO: -p5.RendererGL.prototype._applyTextProperties = function() { - //@TODO finish implementation - //console.error('text commands not yet implemented in webgl'); -}; - -p5.RendererGL.prototype.textWidth = function(s) { - if (this._isOpenType()) { - return this._textFont._textWidth(s, this._textSize); - } - return 0; // TODO: error -}; - -// rendering constants - -// the number of rows/columns dividing each glyph -const charGridWidth = 9; -const charGridHeight = charGridWidth; - -// size of the image holding the bezier stroke info -const strokeImageWidth = 64; -const strokeImageHeight = 64; - -// size of the image holding the stroke indices for each row/col -const gridImageWidth = 64; -const gridImageHeight = 64; - -// size of the image holding the offset/length of each row/col stripe -const cellImageWidth = 64; -const cellImageHeight = 64; - -/** - * @private - * @class ImageInfos - * @param {Integer} width - * @param {Integer} height - * - * the ImageInfos class holds a list of ImageDatas of a given size. - */ -class ImageInfos { - constructor(width, height) { - this.width = width; - this.height = height; - this.infos = []; // the list of images - } - /** - * - * @param {Integer} space - * @return {Object} contains the ImageData, and pixel index into that - * ImageData where the free space was allocated. - * - * finds free space of a given size in the ImageData list - */ - findImage (space) { - const imageSize = this.width * this.height; - if (space > imageSize) - throw new Error('font is too complex to render in 3D'); - - // search through the list of images, looking for one with - // anough unused space. - let imageInfo, imageData; - for (let ii = this.infos.length - 1; ii >= 0; --ii) { - const imageInfoTest = this.infos[ii]; - if (imageInfoTest.index + space < imageSize) { - // found one - imageInfo = imageInfoTest; - imageData = imageInfoTest.imageData; - break; - } +function text(p5, fn){ + // Text/Typography + // @TODO: + p5.RendererGL.prototype._applyTextProperties = function() { + //@TODO finish implementation + //console.error('text commands not yet implemented in webgl'); + }; + + p5.RendererGL.prototype.textWidth = function(s) { + if (this._isOpenType()) { + return this._textFont._textWidth(s, this._textSize); } - if (!imageInfo) { - try { - // create a new image - imageData = new ImageData(this.width, this.height); - } catch (err) { - // for browsers that don't support ImageData constructors (ie IE11) - // create an ImageData using the old method - let canvas = document.getElementsByTagName('canvas')[0]; - const created = !canvas; - if (!canvas) { - // create a temporary canvas - canvas = document.createElement('canvas'); - canvas.style.display = 'none'; - document.body.appendChild(canvas); - } - const ctx = canvas.getContext('2d'); - if (ctx) { - imageData = ctx.createImageData(this.width, this.height); - } - if (created) { - // distroy the temporary canvas, if necessary - document.body.removeChild(canvas); - } - } - // construct & dd the new image info - imageInfo = { index: 0, imageData }; - this.infos.push(imageInfo); - } + return 0; // TODO: error + }; - const index = imageInfo.index; - imageInfo.index += space; // move to the start of the next image - imageData._dirty = true; - return { imageData, index }; - } -} + // rendering constants -/** - * @function setPixel - * @param {Object} imageInfo - * @param {Number} r - * @param {Number} g - * @param {Number} b - * @param {Number} a - * - * writes the next pixel into an indexed ImageData - */ -function setPixel(imageInfo, r, g, b, a) { - const imageData = imageInfo.imageData; - const pixels = imageData.data; - let index = imageInfo.index++ * 4; - pixels[index++] = r; - pixels[index++] = g; - pixels[index++] = b; - pixels[index++] = a; -} + // the number of rows/columns dividing each glyph + const charGridWidth = 9; + const charGridHeight = charGridWidth; -const SQRT3 = Math.sqrt(3); - -/** - * @private - * @class FontInfo - * @param {Object} font an opentype.js font object - * - * contains cached images and glyph information for an opentype font - */ -class FontInfo { - constructor(font) { - this.font = font; - // the bezier curve coordinates - this.strokeImageInfos = new ImageInfos(strokeImageWidth, strokeImageHeight); - // lists of curve indices for each row/column slice - this.colDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight); - this.rowDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight); - // the offset & length of each row/col slice in the glyph - this.colCellImageInfos = new ImageInfos(cellImageWidth, cellImageHeight); - this.rowCellImageInfos = new ImageInfos(cellImageWidth, cellImageHeight); - - // the cached information for each glyph - this.glyphInfos = {}; - } - /** - * @param {Glyph} glyph the x positions of points in the curve - * @returns {Object} the glyphInfo for that glyph - * - * calculates rendering info for a glyph, including the curve information, - * row & column stripes compiled into textures. - */ - getGlyphInfo (glyph) { - // check the cache - let gi = this.glyphInfos[glyph.index]; - if (gi) return gi; - - // get the bounding box of the glyph from opentype.js - const bb = glyph.getBoundingBox(); - const xMin = bb.x1; - const yMin = bb.y1; - const gWidth = bb.x2 - xMin; - const gHeight = bb.y2 - yMin; - const cmds = glyph.path.commands; - // don't bother rendering invisible glyphs - if (gWidth === 0 || gHeight === 0 || !cmds.length) { - return (this.glyphInfos[glyph.index] = {}); - } + // size of the image holding the bezier stroke info + const strokeImageWidth = 64; + const strokeImageHeight = 64; + + // size of the image holding the stroke indices for each row/col + const gridImageWidth = 64; + const gridImageHeight = 64; - let i; - const strokes = []; // the strokes in this glyph - const rows = []; // the indices of strokes in each row - const cols = []; // the indices of strokes in each column - for (i = charGridWidth - 1; i >= 0; --i) cols.push([]); - for (i = charGridHeight - 1; i >= 0; --i) rows.push([]); + // size of the image holding the offset/length of each row/col stripe + const cellImageWidth = 64; + const cellImageHeight = 64; + /** + * @private + * @class ImageInfos + * @param {Integer} width + * @param {Integer} height + * + * the ImageInfos class holds a list of ImageDatas of a given size. + */ + class ImageInfos { + constructor(width, height) { + this.width = width; + this.height = height; + this.infos = []; // the list of images + } /** - * @function push - * @param {Number[]} xs the x positions of points in the curve - * @param {Number[]} ys the y positions of points in the curve - * @param {Object} v the curve information * - * adds a curve to the rows & columns that it intersects with + * @param {Integer} space + * @return {Object} contains the ImageData, and pixel index into that + * ImageData where the free space was allocated. + * + * finds free space of a given size in the ImageData list */ - function push(xs, ys, v) { - const index = strokes.length; // the index of this stroke - strokes.push(v); // add this stroke to the list - - /** - * @function minMax - * @param {Number[]} rg the list of values to compare - * @param {Number} min the initial minimum value - * @param {Number} max the initial maximum value - * - * find the minimum & maximum value in a list of values - */ - function minMax(rg, min, max) { - for (let i = rg.length; i-- > 0; ) { - const v = rg[i]; - if (min > v) min = v; - if (max < v) max = v; + findImage (space) { + const imageSize = this.width * this.height; + if (space > imageSize) + throw new Error('font is too complex to render in 3D'); + + // search through the list of images, looking for one with + // anough unused space. + let imageInfo, imageData; + for (let ii = this.infos.length - 1; ii >= 0; --ii) { + const imageInfoTest = this.infos[ii]; + if (imageInfoTest.index + space < imageSize) { + // found one + imageInfo = imageInfoTest; + imageData = imageInfoTest.imageData; + break; } - return { min, max }; } - // Expand the bounding box of the glyph by the number of cells below - // before rounding. Curves only partially through a cell won't be added - // to adjacent cells, but ones that are close will be. This helps fix - // small visual glitches that occur when curves are close to grid cell - // boundaries. - const cellOffset = 0.5; - - // loop through the rows & columns that the curve intersects - // adding the curve to those slices - const mmX = minMax(xs, 1, 0); - const ixMin = Math.max( - Math.floor(mmX.min * charGridWidth - cellOffset), - 0 - ); - const ixMax = Math.min( - Math.ceil(mmX.max * charGridWidth + cellOffset), - charGridWidth - ); - for (let iCol = ixMin; iCol < ixMax; ++iCol) cols[iCol].push(index); + if (!imageInfo) { + try { + // create a new image + imageData = new ImageData(this.width, this.height); + } catch (err) { + // for browsers that don't support ImageData constructors (ie IE11) + // create an ImageData using the old method + let canvas = document.getElementsByTagName('canvas')[0]; + const created = !canvas; + if (!canvas) { + // create a temporary canvas + canvas = document.createElement('canvas'); + canvas.style.display = 'none'; + document.body.appendChild(canvas); + } + const ctx = canvas.getContext('2d'); + if (ctx) { + imageData = ctx.createImageData(this.width, this.height); + } + if (created) { + // distroy the temporary canvas, if necessary + document.body.removeChild(canvas); + } + } + // construct & dd the new image info + imageInfo = { index: 0, imageData }; + this.infos.push(imageInfo); + } - const mmY = minMax(ys, 1, 0); - const iyMin = Math.max( - Math.floor(mmY.min * charGridHeight - cellOffset), - 0 - ); - const iyMax = Math.min( - Math.ceil(mmY.max * charGridHeight + cellOffset), - charGridHeight - ); - for (let iRow = iyMin; iRow < iyMax; ++iRow) rows[iRow].push(index); + const index = imageInfo.index; + imageInfo.index += space; // move to the start of the next image + imageData._dirty = true; + return { imageData, index }; } + } - /** - * @function clamp - * @param {Number} v the value to clamp - * @param {Number} min the minimum value - * @param {Number} max the maxmimum value - * - * clamps a value between a minimum & maximum value - */ - function clamp(v, min, max) { - if (v < min) return min; - if (v > max) return max; - return v; - } + /** + * @function setPixel + * @param {Object} imageInfo + * @param {Number} r + * @param {Number} g + * @param {Number} b + * @param {Number} a + * + * writes the next pixel into an indexed ImageData + */ + function setPixel(imageInfo, r, g, b, a) { + const imageData = imageInfo.imageData; + const pixels = imageData.data; + let index = imageInfo.index++ * 4; + pixels[index++] = r; + pixels[index++] = g; + pixels[index++] = b; + pixels[index++] = a; + } - /** - * @function byte - * @param {Number} v the value to scale - * - * converts a floating-point number in the range 0-1 to a byte 0-255 - */ - function byte(v) { - return clamp(255 * v, 0, 255); - } + const SQRT3 = Math.sqrt(3); + /** + * @private + * @class FontInfo + * @param {Object} font an opentype.js font object + * + * contains cached images and glyph information for an opentype font + */ + class FontInfo { + constructor(font) { + this.font = font; + // the bezier curve coordinates + this.strokeImageInfos = new ImageInfos(strokeImageWidth, strokeImageHeight); + // lists of curve indices for each row/column slice + this.colDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight); + this.rowDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight); + // the offset & length of each row/col slice in the glyph + this.colCellImageInfos = new ImageInfos(cellImageWidth, cellImageHeight); + this.rowCellImageInfos = new ImageInfos(cellImageWidth, cellImageHeight); + + // the cached information for each glyph + this.glyphInfos = {}; + } /** - * @private - * @class Cubic - * @param {Number} p0 the start point of the curve - * @param {Number} c0 the first control point - * @param {Number} c1 the second control point - * @param {Number} p1 the end point + * @param {Glyph} glyph the x positions of points in the curve + * @returns {Object} the glyphInfo for that glyph * - * a cubic curve + * calculates rendering info for a glyph, including the curve information, + * row & column stripes compiled into textures. */ - class Cubic { - constructor(p0, c0, c1, p1) { - this.p0 = p0; - this.c0 = c0; - this.c1 = c1; - this.p1 = p1; - } - /** - * @return {Object} the quadratic approximation - * - * converts the cubic to a quadtratic approximation by - * picking an appropriate quadratic control point - */ - toQuadratic () { - return { - x: this.p0.x, - y: this.p0.y, - x1: this.p1.x, - y1: this.p1.y, - cx: ((this.c0.x + this.c1.x) * 3 - (this.p0.x + this.p1.x)) / 4, - cy: ((this.c0.y + this.c1.y) * 3 - (this.p0.y + this.p1.y)) / 4 - }; + getGlyphInfo (glyph) { + // check the cache + let gi = this.glyphInfos[glyph.index]; + if (gi) return gi; + + // get the bounding box of the glyph from opentype.js + const bb = glyph.getBoundingBox(); + const xMin = bb.x1; + const yMin = bb.y1; + const gWidth = bb.x2 - xMin; + const gHeight = bb.y2 - yMin; + const cmds = glyph.path.commands; + // don't bother rendering invisible glyphs + if (gWidth === 0 || gHeight === 0 || !cmds.length) { + return (this.glyphInfos[glyph.index] = {}); } + let i; + const strokes = []; // the strokes in this glyph + const rows = []; // the indices of strokes in each row + const cols = []; // the indices of strokes in each column + for (i = charGridWidth - 1; i >= 0; --i) cols.push([]); + for (i = charGridHeight - 1; i >= 0; --i) rows.push([]); + /** - * @return {Number} the error + * @function push + * @param {Number[]} xs the x positions of points in the curve + * @param {Number[]} ys the y positions of points in the curve + * @param {Object} v the curve information + * + * adds a curve to the rows & columns that it intersects with + */ + function push(xs, ys, v) { + const index = strokes.length; // the index of this stroke + strokes.push(v); // add this stroke to the list + + /** + * @function minMax + * @param {Number[]} rg the list of values to compare + * @param {Number} min the initial minimum value + * @param {Number} max the initial maximum value * - * calculates the magnitude of error of this curve's - * quadratic approximation. + * find the minimum & maximum value in a list of values */ - quadError () { - return ( - p5.Vector.sub( - p5.Vector.sub(this.p1, this.p0), - p5.Vector.mult(p5.Vector.sub(this.c1, this.c0), 3) - ).mag() / 2 + function minMax(rg, min, max) { + for (let i = rg.length; i-- > 0; ) { + const v = rg[i]; + if (min > v) min = v; + if (max < v) max = v; + } + return { min, max }; + } + + // Expand the bounding box of the glyph by the number of cells below + // before rounding. Curves only partially through a cell won't be added + // to adjacent cells, but ones that are close will be. This helps fix + // small visual glitches that occur when curves are close to grid cell + // boundaries. + const cellOffset = 0.5; + + // loop through the rows & columns that the curve intersects + // adding the curve to those slices + const mmX = minMax(xs, 1, 0); + const ixMin = Math.max( + Math.floor(mmX.min * charGridWidth - cellOffset), + 0 + ); + const ixMax = Math.min( + Math.ceil(mmX.max * charGridWidth + cellOffset), + charGridWidth ); + for (let iCol = ixMin; iCol < ixMax; ++iCol) cols[iCol].push(index); + + const mmY = minMax(ys, 1, 0); + const iyMin = Math.max( + Math.floor(mmY.min * charGridHeight - cellOffset), + 0 + ); + const iyMax = Math.min( + Math.ceil(mmY.max * charGridHeight + cellOffset), + charGridHeight + ); + for (let iRow = iyMin; iRow < iyMax; ++iRow) rows[iRow].push(index); } /** - * @param {Number} t the value (0-1) at which to split - * @return {Cubic} the second part of the curve - * - * splits the cubic into two parts at a point 't' along the curve. - * this cubic keeps its start point and its end point becomes the - * 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); - - 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); - const part1 = new Cubic(this.p0, m1, mm1, pt); - this.p0 = pt; - return part1; + * @function clamp + * @param {Number} v the value to clamp + * @param {Number} min the minimum value + * @param {Number} max the maxmimum value + * + * clamps a value between a minimum & maximum value + */ + function clamp(v, min, max) { + if (v < min) return min; + if (v > max) return max; + return v; } /** - * @return {Cubic[]} the non-inflecting pieces of this cubic - * - * returns an array containing 0, 1 or 2 cubics split resulting - * from splitting this cubic at its inflection points. - * 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) - ); + * @function byte + * @param {Number} v the value to scale + * + * converts a floating-point number in the range 0-1 to a byte 0-255 + */ + function byte(v) { + return clamp(255 * v, 0, 255); + } - const cubics = []; - - // find the derivative coefficients - let A = b.x * c.y - b.y * c.x; - if (A !== 0) { - let B = a.x * c.y - a.y * c.x; - let C = a.x * b.y - a.y * b.x; - const disc = B * B - 4 * A * C; - if (disc >= 0) { - if (A < 0) { - A = -A; - B = -B; - C = -C; - } + /** + * @private + * @class Cubic + * @param {Number} p0 the start point of the curve + * @param {Number} c0 the first control point + * @param {Number} c1 the second control point + * @param {Number} p1 the end point + * + * a cubic curve + */ + class Cubic { + constructor(p0, c0, c1, p1) { + this.p0 = p0; + this.c0 = c0; + this.c1 = c1; + this.p1 = p1; + } + /** + * @return {Object} the quadratic approximation + * + * converts the cubic to a quadtratic approximation by + * picking an appropriate quadratic control point + */ + toQuadratic () { + return { + x: this.p0.x, + y: this.p0.y, + x1: this.p1.x, + y1: this.p1.y, + cx: ((this.c0.x + this.c1.x) * 3 - (this.p0.x + this.p1.x)) / 4, + cy: ((this.c0.y + this.c1.y) * 3 - (this.p0.y + this.p1.y)) / 4 + }; + } - const Q = Math.sqrt(disc); - const t0 = (-B - Q) / (2 * A); // the first inflection point - let t1 = (-B + Q) / (2 * A); // the second inflection point + /** + * @return {Number} the error + * + * calculates the magnitude of error of this curve's + * quadratic approximation. + */ + quadError () { + return ( + p5.Vector.sub( + p5.Vector.sub(this.p1, this.p0), + p5.Vector.mult(p5.Vector.sub(this.c1, this.c0), 3) + ).mag() / 2 + ); + } - // test if the first inflection point lies on the curve - if (t0 > 0 && t0 < 1) { - // split at the first inflection point - cubics.push(this.split(t0)); - // scale t2 into the second part - t1 = 1 - (1 - t1) / (1 - t0); - } + /** + * @param {Number} t the value (0-1) at which to split + * @return {Cubic} the second part of the curve + * + * splits the cubic into two parts at a point 't' along the curve. + * this cubic keeps its start point and its end point becomes the + * 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); + + 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); + const part1 = new Cubic(this.p0, m1, mm1, pt); + this.p0 = pt; + return part1; + } - // test if the second inflection point lies on the curve - if (t1 > 0 && t1 < 1) { - // split at the second inflection point - cubics.push(this.split(t1)); + /** + * @return {Cubic[]} the non-inflecting pieces of this cubic + * + * returns an array containing 0, 1 or 2 cubics split resulting + * from splitting this cubic at its inflection points. + * 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 cubics = []; + + // find the derivative coefficients + let A = b.x * c.y - b.y * c.x; + if (A !== 0) { + let B = a.x * c.y - a.y * c.x; + let C = a.x * b.y - a.y * b.x; + const disc = B * B - 4 * A * C; + if (disc >= 0) { + if (A < 0) { + A = -A; + B = -B; + C = -C; + } + + const Q = Math.sqrt(disc); + const t0 = (-B - Q) / (2 * A); // the first inflection point + let t1 = (-B + Q) / (2 * A); // the second inflection point + + // test if the first inflection point lies on the curve + if (t0 > 0 && t0 < 1) { + // split at the first inflection point + cubics.push(this.split(t0)); + // scale t2 into the second part + t1 = 1 - (1 - t1) / (1 - t0); + } + + // test if the second inflection point lies on the curve + if (t1 > 0 && t1 < 1) { + // split at the second inflection point + cubics.push(this.split(t1)); + } } } - } - cubics.push(this); - return cubics; + cubics.push(this); + return cubics; + } } - } - /** - * @function cubicToQuadratics - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} cx0 - * @param {Number} cy0 - * @param {Number} cx1 - * @param {Number} cy1 - * @param {Number} x1 - * @param {Number} y1 - * @returns {Cubic[]} an array of cubics whose quadratic approximations - * closely match the civen cubic. - * - * converts a cubic curve to a list of quadratics. - */ - 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) - ).splitInflections(); - - const qs = []; // the final list of quadratics - const precision = 30 / SQRT3; - - // for each of the non-inflected pieces of the original cubic - for (let cubic of cubics) { - // the cubic is iteratively split in 3 pieces: - // the first piece is accumulated in 'qs', the result. - // the last piece is accumulated in 'tail', temporarily. - // the middle piece is repeatedly split again, while necessary. - const tail = []; - - let t3; - for (;;) { - // calculate this cubic's precision - t3 = precision / cubic.quadError(); - if (t3 >= 0.5 * 0.5 * 0.5) { - break; // not too bad, we're done - } + /** + * @function cubicToQuadratics + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} cx0 + * @param {Number} cy0 + * @param {Number} cx1 + * @param {Number} cy1 + * @param {Number} x1 + * @param {Number} y1 + * @returns {Cubic[]} an array of cubics whose quadratic approximations + * closely match the civen cubic. + * + * converts a cubic curve to a list of quadratics. + */ + 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) + ).splitInflections(); + + const qs = []; // the final list of quadratics + const precision = 30 / SQRT3; + + // for each of the non-inflected pieces of the original cubic + for (let cubic of cubics) { + // the cubic is iteratively split in 3 pieces: + // the first piece is accumulated in 'qs', the result. + // the last piece is accumulated in 'tail', temporarily. + // the middle piece is repeatedly split again, while necessary. + const tail = []; + + let t3; + for (;;) { + // calculate this cubic's precision + t3 = precision / cubic.quadError(); + if (t3 >= 0.5 * 0.5 * 0.5) { + break; // not too bad, we're done + } - // find a split point based on the error - const t = Math.pow(t3, 1.0 / 3.0); - // split the cubic in 3 - const start = cubic.split(t); - const middle = cubic.split(1 - t / (1 - t)); + // find a split point based on the error + const t = Math.pow(t3, 1.0 / 3.0); + // split the cubic in 3 + const start = cubic.split(t); + const middle = cubic.split(1 - t / (1 - t)); - qs.push(start); // the first part - tail.push(cubic); // the last part - cubic = middle; // iterate on the middle piece - } + qs.push(start); // the first part + tail.push(cubic); // the last part + cubic = middle; // iterate on the middle piece + } - if (t3 < 1) { - // a little excess error, split the middle in two - qs.push(cubic.split(0.5)); + if (t3 < 1) { + // a little excess error, split the middle in two + qs.push(cubic.split(0.5)); + } + // add the middle piece to the result + qs.push(cubic); + + // finally add the tail, reversed, onto the result + Array.prototype.push.apply(qs, tail.reverse()); } - // add the middle piece to the result - qs.push(cubic); - // finally add the tail, reversed, onto the result - Array.prototype.push.apply(qs, tail.reverse()); + return qs; } - return qs; - } - - /** - * @function pushLine - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} x1 - * @param {Number} y1 - * - * add a straight line to the row/col grid of a glyph - */ - function pushLine(x0, y0, x1, y1) { - const mx = (x0 + x1) / 2; - const my = (y0 + y1) / 2; - push([x0, x1], [y0, y1], { x: x0, y: y0, cx: mx, cy: my }); - } + /** + * @function pushLine + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} x1 + * @param {Number} y1 + * + * add a straight line to the row/col grid of a glyph + */ + function pushLine(x0, y0, x1, y1) { + const mx = (x0 + x1) / 2; + const my = (y0 + y1) / 2; + push([x0, x1], [y0, y1], { x: x0, y: y0, cx: mx, cy: my }); + } - /** - * @function samePoint - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} x1 - * @param {Number} y1 - * @return {Boolean} true if the two points are sufficiently close - * - * tests if two points are close enough to be considered the same - */ - function samePoint(x0, y0, x1, y1) { - return Math.abs(x1 - x0) < 0.00001 && Math.abs(y1 - y0) < 0.00001; - } + /** + * @function samePoint + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} x1 + * @param {Number} y1 + * @return {Boolean} true if the two points are sufficiently close + * + * tests if two points are close enough to be considered the same + */ + function samePoint(x0, y0, x1, y1) { + return Math.abs(x1 - x0) < 0.00001 && Math.abs(y1 - y0) < 0.00001; + } - let x0, y0, xs, ys; + let x0, y0, xs, ys; - for (const cmd of cmds) { - // scale the coordinates to the range 0-1 - const x1 = (cmd.x - xMin) / gWidth; - const y1 = (cmd.y - yMin) / gHeight; + for (const cmd of cmds) { + // scale the coordinates to the range 0-1 + const x1 = (cmd.x - xMin) / gWidth; + const y1 = (cmd.y - yMin) / gHeight; - // don't bother if this point is the same as the last - if (samePoint(x0, y0, x1, y1)) continue; + // don't bother if this point is the same as the last + if (samePoint(x0, y0, x1, y1)) continue; - switch (cmd.type) { - case 'M': { - // move - xs = x1; - ys = y1; - break; - } - case 'L': { - // line - pushLine(x0, y0, x1, y1); - break; - } - case 'Q': { - // quadratic - const cx = (cmd.x1 - xMin) / gWidth; - const cy = (cmd.y1 - yMin) / gHeight; - push([x0, x1, cx], [y0, y1, cy], { x: x0, y: y0, cx, cy }); - break; - } - case 'Z': { - // end - if (!samePoint(x0, y0, xs, ys)) { - // add an extra line closing the loop, if necessary - pushLine(x0, y0, xs, ys); - strokes.push({ x: xs, y: ys }); - } else { - strokes.push({ x: x0, y: y0 }); + switch (cmd.type) { + case 'M': { + // move + xs = x1; + ys = y1; + break; } - break; - } - case 'C': { - // cubic - const cx1 = (cmd.x1 - xMin) / gWidth; - const cy1 = (cmd.y1 - yMin) / gHeight; - const cx2 = (cmd.x2 - xMin) / gWidth; - const cy2 = (cmd.y2 - yMin) / gHeight; - const qs = cubicToQuadratics(x0, y0, cx1, cy1, cx2, cy2, x1, y1); - for (let iq = 0; iq < qs.length; iq++) { - const q = qs[iq].toQuadratic(); - push([q.x, q.x1, q.cx], [q.y, q.y1, q.cy], q); + case 'L': { + // line + pushLine(x0, y0, x1, y1); + break; } - break; + case 'Q': { + // quadratic + const cx = (cmd.x1 - xMin) / gWidth; + const cy = (cmd.y1 - yMin) / gHeight; + push([x0, x1, cx], [y0, y1, cy], { x: x0, y: y0, cx, cy }); + break; + } + case 'Z': { + // end + if (!samePoint(x0, y0, xs, ys)) { + // add an extra line closing the loop, if necessary + pushLine(x0, y0, xs, ys); + strokes.push({ x: xs, y: ys }); + } else { + strokes.push({ x: x0, y: y0 }); + } + break; + } + case 'C': { + // cubic + const cx1 = (cmd.x1 - xMin) / gWidth; + const cy1 = (cmd.y1 - yMin) / gHeight; + const cx2 = (cmd.x2 - xMin) / gWidth; + const cy2 = (cmd.y2 - yMin) / gHeight; + const qs = cubicToQuadratics(x0, y0, cx1, cy1, cx2, cy2, x1, y1); + for (let iq = 0; iq < qs.length; iq++) { + const q = qs[iq].toQuadratic(); + push([q.x, q.x1, q.cx], [q.y, q.y1, q.cy], q); + } + break; + } + default: + throw new Error(`unknown command type: ${cmd.type}`); } - default: - throw new Error(`unknown command type: ${cmd.type}`); + x0 = x1; + y0 = y1; } - x0 = x1; - y0 = y1; - } - // allocate space for the strokes - const strokeCount = strokes.length; - const strokeImageInfo = this.strokeImageInfos.findImage(strokeCount); - const strokeOffset = strokeImageInfo.index; + // allocate space for the strokes + const strokeCount = strokes.length; + const strokeImageInfo = this.strokeImageInfos.findImage(strokeCount); + const strokeOffset = strokeImageInfo.index; - // fill the stroke image - for (let il = 0; il < strokeCount; ++il) { - const s = strokes[il]; - setPixel(strokeImageInfo, byte(s.x), byte(s.y), byte(s.cx), byte(s.cy)); - } - - /** - * @function layout - * @param {Number[][]} dim - * @param {ImageInfo[]} dimImageInfos - * @param {ImageInfo[]} cellImageInfos - * @return {Object} - * - * lays out the curves in a dimension (row or col) into two - * images, one for the indices of the curves themselves, and - * one containing the offset and length of those index spans. - */ - function layout(dim, dimImageInfos, cellImageInfos) { - const dimLength = dim.length; // the number of slices in this dimension - const dimImageInfo = dimImageInfos.findImage(dimLength); - const dimOffset = dimImageInfo.index; - // calculate the total number of stroke indices in this dimension - let totalStrokes = 0; - for (let id = 0; id < dimLength; ++id) { - totalStrokes += dim[id].length; + // fill the stroke image + for (let il = 0; il < strokeCount; ++il) { + const s = strokes[il]; + setPixel(strokeImageInfo, byte(s.x), byte(s.y), byte(s.cx), byte(s.cy)); } - // allocate space for the stroke indices - const cellImageInfo = cellImageInfos.findImage(totalStrokes); - - // for each slice in the glyph - for (let i = 0; i < dimLength; ++i) { - const strokeIndices = dim[i]; - const strokeCount = strokeIndices.length; - const cellLineIndex = cellImageInfo.index; - - // write the offset and count into the glyph slice image - setPixel( - dimImageInfo, - cellLineIndex >> 7, - cellLineIndex & 0x7f, - strokeCount >> 7, - strokeCount & 0x7f - ); + /** + * @function layout + * @param {Number[][]} dim + * @param {ImageInfo[]} dimImageInfos + * @param {ImageInfo[]} cellImageInfos + * @return {Object} + * + * lays out the curves in a dimension (row or col) into two + * images, one for the indices of the curves themselves, and + * one containing the offset and length of those index spans. + */ + function layout(dim, dimImageInfos, cellImageInfos) { + const dimLength = dim.length; // the number of slices in this dimension + const dimImageInfo = dimImageInfos.findImage(dimLength); + const dimOffset = dimImageInfo.index; + // calculate the total number of stroke indices in this dimension + let totalStrokes = 0; + for (let id = 0; id < dimLength; ++id) { + totalStrokes += dim[id].length; + } - // for each stroke index in that slice - for (let iil = 0; iil < strokeCount; ++iil) { - // write the stroke index into the slice's image - const strokeIndex = strokeIndices[iil] + strokeOffset; - setPixel(cellImageInfo, strokeIndex >> 7, strokeIndex & 0x7f, 0, 0); + // allocate space for the stroke indices + const cellImageInfo = cellImageInfos.findImage(totalStrokes); + + // for each slice in the glyph + for (let i = 0; i < dimLength; ++i) { + const strokeIndices = dim[i]; + const strokeCount = strokeIndices.length; + const cellLineIndex = cellImageInfo.index; + + // write the offset and count into the glyph slice image + setPixel( + dimImageInfo, + cellLineIndex >> 7, + cellLineIndex & 0x7f, + strokeCount >> 7, + strokeCount & 0x7f + ); + + // for each stroke index in that slice + for (let iil = 0; iil < strokeCount; ++iil) { + // write the stroke index into the slice's image + const strokeIndex = strokeIndices[iil] + strokeOffset; + setPixel(cellImageInfo, strokeIndex >> 7, strokeIndex & 0x7f, 0, 0); + } } + + return { + cellImageInfo, + dimOffset, + dimImageInfo + }; } - return { - cellImageInfo, - dimOffset, - dimImageInfo + // initialize the info for this glyph + gi = this.glyphInfos[glyph.index] = { + glyph, + uGlyphRect: [bb.x1, -bb.y1, bb.x2, -bb.y2], + strokeImageInfo, + strokes, + colInfo: layout(cols, this.colDimImageInfos, this.colCellImageInfos), + rowInfo: layout(rows, this.rowDimImageInfos, this.rowCellImageInfos) }; + gi.uGridOffset = [gi.colInfo.dimOffset, gi.rowInfo.dimOffset]; + return gi; } - - // initialize the info for this glyph - gi = this.glyphInfos[glyph.index] = { - glyph, - uGlyphRect: [bb.x1, -bb.y1, bb.x2, -bb.y2], - strokeImageInfo, - strokes, - colInfo: layout(cols, this.colDimImageInfos, this.colCellImageInfos), - rowInfo: layout(rows, this.rowDimImageInfos, this.rowCellImageInfos) - }; - gi.uGridOffset = [gi.colInfo.dimOffset, gi.rowInfo.dimOffset]; - return gi; } -} -p5.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.' - ); - return; - } - if (y >= maxY || !this.states.doFill) { - return; // don't render lines beyond our maxY position - } + p5.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.' + ); + return; + } + if (y >= maxY || !this.states.doFill) { + return; // don't render lines beyond our maxY position + } - if (!this._isOpenType()) { - console.log( - 'WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts are supported' - ); - return p; - } + if (!this._isOpenType()) { + console.log( + 'WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts are supported' + ); + return p; + } - p.push(); // fix to #803 + p.push(); // fix to #803 - // remember this state, so it can be restored later - const doStroke = this.states.doStroke; - const drawMode = this.states.drawMode; + // remember this state, so it can be restored later + const doStroke = this.states.doStroke; + const drawMode = this.states.drawMode; - this.states.doStroke = false; - this.states.drawMode = constants.TEXTURE; + this.states.doStroke = false; + this.states.drawMode = constants.TEXTURE; - // get the cached FontInfo object - const font = this._textFont.font; - let fontInfo = this._textFont._fontInfo; - if (!fontInfo) { - fontInfo = this._textFont._fontInfo = new FontInfo(font); - } + // get the cached FontInfo object + const font = this._textFont.font; + let fontInfo = this._textFont._fontInfo; + if (!fontInfo) { + fontInfo = this._textFont._fontInfo = new FontInfo(font); + } - // calculate the alignment and move/scale the view accordingly - const pos = this._textFont._handleAlignment(this, line, x, y); - const fontSize = this._textSize; - const scale = fontSize / font.unitsPerEm; - this.translate(pos.x, pos.y, 0); - this.scale(scale, scale, 1); - - // initialize the font shader - const gl = this.GL; - const initializeShader = !this._defaultFontShader; - const sh = this._getFontShader(); - sh.init(); - sh.bindShader(); // first time around, bind the shader fully - - if (initializeShader) { - // these are constants, really. just initialize them one-time. - sh.setUniform('uGridImageSize', [gridImageWidth, gridImageHeight]); - sh.setUniform('uCellsImageSize', [cellImageWidth, cellImageHeight]); - sh.setUniform('uStrokeImageSize', [strokeImageWidth, strokeImageHeight]); - sh.setUniform('uGridSize', [charGridWidth, charGridHeight]); - } - this._applyColorBlend(this.states.curFillColor); - - 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() { - for (let i = 0; i <= 1; i++) { - for (let j = 0; j <= 1; j++) { - this.vertices.push(new p5.Vector(j, i, 0)); - this.uvs.push(j, i); + // calculate the alignment and move/scale the view accordingly + const pos = this._textFont._handleAlignment(this, line, x, y); + const fontSize = this._textSize; + const scale = fontSize / font.unitsPerEm; + this.translate(pos.x, pos.y, 0); + this.scale(scale, scale, 1); + + // initialize the font shader + const gl = this.GL; + const initializeShader = !this._defaultFontShader; + const sh = this._getFontShader(); + sh.init(); + sh.bindShader(); // first time around, bind the shader fully + + if (initializeShader) { + // these are constants, really. just initialize them one-time. + sh.setUniform('uGridImageSize', [gridImageWidth, gridImageHeight]); + sh.setUniform('uCellsImageSize', [cellImageWidth, cellImageHeight]); + sh.setUniform('uStrokeImageSize', [strokeImageWidth, strokeImageHeight]); + sh.setUniform('uGridSize', [charGridWidth, charGridHeight]); + } + this._applyColorBlend(this.states.curFillColor); + + 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() { + for (let i = 0; i <= 1; i++) { + for (let j = 0; j <= 1; j++) { + this.vertices.push(new p5.Vector(j, i, 0)); + this.uvs.push(j, i); + } } - } - })); - geom.computeFaces().computeNormals(); - g = this.createBuffers('glyph', geom); - } + })); + geom.computeFaces().computeNormals(); + g = this.createBuffers('glyph', geom); + } - // bind the shader buffers - for (const buff of this.retainedMode.buffers.text) { - buff._prepareBuffer(g, sh); - } - this._bindBuffer(g.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); - - // this will have to do for now... - sh.setUniform('uMaterialColor', this.states.curFillColor); - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); - - try { - let dx = 0; // the x position in the line - let glyphPrev = null; // the previous glyph, used for kerning - // fetch the glyphs in the line of text - const glyphs = font.stringToGlyphs(line); - - for (const glyph of glyphs) { - // kern - if (glyphPrev) dx += font.getKerningValue(glyphPrev, glyph); - - const gi = fontInfo.getGlyphInfo(glyph); - if (gi.uGlyphRect) { - const rowInfo = gi.rowInfo; - const colInfo = gi.colInfo; - sh.setUniform('uSamplerStrokes', gi.strokeImageInfo.imageData); - sh.setUniform('uSamplerRowStrokes', rowInfo.cellImageInfo.imageData); - sh.setUniform('uSamplerRows', rowInfo.dimImageInfo.imageData); - sh.setUniform('uSamplerColStrokes', colInfo.cellImageInfo.imageData); - sh.setUniform('uSamplerCols', colInfo.dimImageInfo.imageData); - sh.setUniform('uGridOffset', gi.uGridOffset); - sh.setUniform('uGlyphRect', gi.uGlyphRect); - sh.setUniform('uGlyphOffset', dx); - - sh.bindTextures(); // afterwards, only textures need updating - - // draw it - gl.drawElements(gl.TRIANGLES, 6, this.GL.UNSIGNED_SHORT, 0); - } - dx += glyph.advanceWidth; - glyphPrev = glyph; + // bind the shader buffers + for (const buff of this.retainedMode.buffers.text) { + buff._prepareBuffer(g, sh); } - } finally { - // clean up - sh.unbindShader(); + this._bindBuffer(g.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + + // this will have to do for now... + sh.setUniform('uMaterialColor', this.states.curFillColor); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + + try { + let dx = 0; // the x position in the line + let glyphPrev = null; // the previous glyph, used for kerning + // fetch the glyphs in the line of text + const glyphs = font.stringToGlyphs(line); + + for (const glyph of glyphs) { + // kern + if (glyphPrev) dx += font.getKerningValue(glyphPrev, glyph); + + const gi = fontInfo.getGlyphInfo(glyph); + if (gi.uGlyphRect) { + const rowInfo = gi.rowInfo; + const colInfo = gi.colInfo; + sh.setUniform('uSamplerStrokes', gi.strokeImageInfo.imageData); + sh.setUniform('uSamplerRowStrokes', rowInfo.cellImageInfo.imageData); + sh.setUniform('uSamplerRows', rowInfo.dimImageInfo.imageData); + sh.setUniform('uSamplerColStrokes', colInfo.cellImageInfo.imageData); + sh.setUniform('uSamplerCols', colInfo.dimImageInfo.imageData); + sh.setUniform('uGridOffset', gi.uGridOffset); + sh.setUniform('uGlyphRect', gi.uGlyphRect); + sh.setUniform('uGlyphOffset', dx); + + sh.bindTextures(); // afterwards, only textures need updating + + // draw it + gl.drawElements(gl.TRIANGLES, 6, this.GL.UNSIGNED_SHORT, 0); + } + dx += glyph.advanceWidth; + glyphPrev = glyph; + } + } finally { + // clean up + sh.unbindShader(); - this.states.doStroke = doStroke; - this.states.drawMode = drawMode; - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + this.states.doStroke = doStroke; + this.states.drawMode = drawMode; + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - p.pop(); - } + p.pop(); + } + + return p; + }; +} - return p; -}; +export default text; From 3b75c5d2b2b5ab56746ad170023b62822b7bd604 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Wed, 2 Oct 2024 17:00:40 +0100 Subject: [PATCH 118/120] Clean up --- src/app.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/app.js b/src/app.js index e5189e7eb1..a0f677bfb8 100644 --- a/src/app.js +++ b/src/app.js @@ -70,24 +70,10 @@ utilities(p5); // webgl import webgl from './webgl'; webgl(p5); -// import './webgl/3d_primitives'; -// import './webgl/interaction'; -// import './webgl/light'; -// import './webgl/loading'; -// import './webgl/material'; -// import './webgl/p5.Camera'; -// import './webgl/p5.DataArray'; -// import './webgl/p5.Geometry'; -// import './webgl/p5.Matrix'; -// import './webgl/p5.Quat'; import './webgl/p5.RendererGL.Immediate'; import './webgl/p5.RendererGL'; import './webgl/p5.RendererGL.Retained'; -// import './webgl/p5.Framebuffer'; -// import './webgl/p5.Shader'; -// import './webgl/p5.RenderBuffer'; import './webgl/p5.Texture'; -// import './webgl/text'; import './core/init'; From 07f4b1b21d4a50d88795e416c3a3996c50f27f58 Mon Sep 17 00:00:00 2001 From: miaoye que Date: Sat, 5 Oct 2024 15:08:53 -0400 Subject: [PATCH 119/120] update sketch verifier to use acorn/acorn walk and include line numbers in results --- package-lock.json | 16 +++- package.json | 5 +- src/core/friendly_errors/sketch_verifier.js | 90 ++++++++++-------- test/unit/core/sketch_overrides.js | 100 +++++++++++++++++--- 4 files changed, 155 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index e753dbafb6..8faf4d39d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,9 @@ "version": "1.9.4", "license": "LGPL-2.1", "dependencies": { + "acorn": "^8.12.1", + "acorn-walk": "^8.3.4", "colorjs.io": "^0.5.2", - "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", @@ -2579,6 +2580,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -11664,4 +11676,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 750620c1ea..37c0804100 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ }, "version": "1.9.4", "dependencies": { + "acorn": "^8.12.1", + "acorn-walk": "^8.3.4", "colorjs.io": "^0.5.2", - "espree": "^10.2.0", "file-saver": "^1.3.8", "gifenc": "^1.0.3", "libtess": "^1.2.2", @@ -83,4 +84,4 @@ "pre-commit": "lint-staged" } } -} +} \ No newline at end of file diff --git a/src/core/friendly_errors/sketch_verifier.js b/src/core/friendly_errors/sketch_verifier.js index c646360f81..f90eb4f204 100644 --- a/src/core/friendly_errors/sketch_verifier.js +++ b/src/core/friendly_errors/sketch_verifier.js @@ -1,4 +1,5 @@ -import * as espree from 'espree'; +import * as acorn from 'acorn'; +import * as walk from 'acorn-walk'; /** * @for p5 @@ -14,8 +15,14 @@ function sketchVerifier(p5, fn) { */ fn.fetchScript = async function (script) { if (script.src) { - const contents = await fetch(script.src).then((res) => res.text()); - return contents; + try { + const contents = await fetch(script.src).then((res) => res.text()); + return contents; + } catch (error) { + // TODO: Handle CORS error here. + console.error('Error fetching script:', error); + return ''; + } } else { return script.textContent; } @@ -30,6 +37,8 @@ function sketchVerifier(p5, fn) { * @returns {Promise} The user's code as a string. */ fn.getUserCode = async function () { + // TODO: think of a more robust way to get the user's code. Refer to + // https://github.com/processing/p5.js/pull/7293. const scripts = document.querySelectorAll('script'); const userCodeScript = scripts[scripts.length - 1]; const userCode = await fn.fetchScript(userCodeScript); @@ -42,59 +51,58 @@ function sketchVerifier(p5, fn) { * the help of Espree parser. * * @method extractUserDefinedVariablesAndFuncs - * @param {string} codeStr - The code to extract variables and functions from. + * @param {string} code - The code to extract variables and functions from. * @returns {Object} An object containing the user's defined variables and functions. - * @returns {string[]} [userDefinitions.variables] Array of user-defined variable names. - * @returns {strings[]} [userDefinitions.functions] Array of user-defined function names. + * @returns {Array<{name: string, line: number}>} [userDefinitions.variables] Array of user-defined variable names and their line numbers. + * @returns {Array<{name: string, line: number}>} [userDefinitions.functions] Array of user-defined function names and their line numbers. */ - fn.extractUserDefinedVariablesAndFuncs = function (codeStr) { + fn.extractUserDefinedVariablesAndFuncs = function (code) { const userDefinitions = { variables: [], functions: [] }; + // The line numbers from the parser are consistently off by one, add + // `lineOffset` here to correct them. + const lineOffset = -1; try { - const ast = espree.parse(codeStr, { + const ast = acorn.parse(code, { ecmaVersion: 2021, sourceType: 'module', - ecmaFeatures: { - jsx: true - } + locations: true // This helps us get the line number. }); - function traverse(node) { - const { type, declarations, id, init } = node; - - switch (type) { - case 'VariableDeclaration': - declarations.forEach(({ id, init }) => { - if (id.type === 'Identifier') { - const category = init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(init.type) - ? 'functions' - : 'variables'; - userDefinitions[category].push(id.name); - } + walk.simple(ast, { + VariableDeclarator(node) { + if (node.id.type === 'Identifier') { + const category = node.init && ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.init.type) + ? 'functions' + : 'variables'; + userDefinitions[category].push({ + name: node.id.name, + line: node.loc.start.line + lineOffset + }); + } + }, + FunctionDeclaration(node) { + if (node.id && node.id.type === 'Identifier') { + userDefinitions.functions.push({ + name: node.id.name, + line: node.loc.start.line + lineOffset + }); + } + }, + // We consider class declarations to be a special form of variable + // declaration. + ClassDeclaration(node) { + if (node.id && node.id.type === 'Identifier') { + userDefinitions.variables.push({ + name: node.id.name, + line: node.loc.start.line + lineOffset }); - break; - case 'FunctionDeclaration': - if (id?.type === 'Identifier') { - userDefinitions.functions.push(id.name); - } - break; - } - - for (const key in node) { - if (node[key] && typeof node[key] === 'object') { - if (Array.isArray(node[key])) { - node[key].forEach(child => traverse(child)); - } else { - traverse(node[key]); - } } } - } - - traverse(ast); + }); } catch (error) { // TODO: Replace this with a friendly error message. console.error('Error parsing code:', error); diff --git a/test/unit/core/sketch_overrides.js b/test/unit/core/sketch_overrides.js index 7c183ffd73..cf16edbff7 100644 --- a/test/unit/core/sketch_overrides.js +++ b/test/unit/core/sketch_overrides.js @@ -74,9 +74,49 @@ suite('Validate Params', function () { `; const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); - - expect(result.variables).toEqual(['x', 'y', 'z', 'v1', 'v2', 'v3']); - expect(result.functions).toEqual(['foo', 'bar', 'baz']); + const expectedResult = { + "functions": [ + { + "line": 5, + "name": "foo", + }, + { + "line": 6, + "name": "bar", + }, + { + "line": 7, + "name": "baz", + }, + ], + "variables": [ + { + "line": 1, + "name": "x", + }, + { + "line": 2, + "name": "y", + }, + { + "line": 3, + "name": "z", + }, + { + "line": 4, + "name": "v1", + }, + { + "line": 4, + "name": "v2", + }, + { + "line": 4, + "name": "v3", + }, + ], + }; + expect(result).toEqual(expectedResult); }); // Sketch verifier should ignore the following types of lines: @@ -101,9 +141,29 @@ suite('Validate Params', function () { `; const result = mockP5Prototype.extractUserDefinedVariablesAndFuncs(code); - - expect(result.variables).toEqual(['x', 'y', 'z', 'i']); - expect(result.functions).toEqual([]); + const expectedResult = { + "functions": [], + "variables": [ + { + "line": 2, + "name": "x", + }, + { + "line": 6, + "name": "y", + }, + { + "line": 11, + "name": "z", + }, + { + "line": 13, + "name": "i", + }, + ], + }; + + expect(result).toEqual(expectedResult); }); test('Handles parsing errors', function () { @@ -130,13 +190,31 @@ suite('Validate Params', function () { mockP5Prototype.getUserCode = vi.fn(() => Promise.resolve(mockScript)); const result = await mockP5Prototype.run(); + const expectedResult = { + "functions": [ + { + "line": 3, + "name": "foo", + }, + { + "line": 4, + "name": "bar", + }, + ], + "variables": [ + { + "line": 1, + "name": "x", + }, + { + "line": 2, + "name": "y", + }, + ], + }; expect(mockP5Prototype.getUserCode).toHaveBeenCalledTimes(1); - - expect(result).toEqual({ - variables: ['x', 'y'], - functions: ['foo', 'bar'] - }); + expect(result).toEqual(expectedResult); }); }); }); \ No newline at end of file From b49543425f4cb0bc0204611c74ec8ac11cffb6a4 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Tue, 8 Oct 2024 17:58:47 +0100 Subject: [PATCH 120/120] Convert p5.Texture to use new module syntax --- package-lock.json | 15 +- src/webgl/index.js | 2 + src/webgl/p5.RendererGL.js | 3 +- src/webgl/p5.Texture.js | 891 +++++++++++++++++++------------------ 4 files changed, 452 insertions(+), 459 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96a28156d2..db112294ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,7 @@ "gifenc": "^1.0.3", "libtess": "^1.2.2", "omggif": "^1.0.10", - "opentype.js": "^1.3.1", - "zod-validation-error": "^3.3.1" + "opentype.js": "^1.3.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", @@ -11606,21 +11605,11 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-validation-error": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.1.tgz", - "integrity": "sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA==", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.18.0" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/src/webgl/index.js b/src/webgl/index.js index 3fcb0efe6e..cd56d77f9e 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -12,6 +12,7 @@ import framebuffer from './p5.Framebuffer'; import dataArray from './p5.DataArray'; import shader from './p5.Shader'; import camera from './p5.Camera'; +import texture from './p5.Texture'; export default function(p5){ primitives3D(p5, p5.prototype); @@ -28,4 +29,5 @@ export default function(p5){ framebuffer(p5, p5.prototype); dataArray(p5, p5.prototype); shader(p5, p5.prototype); + texture(p5, p5.prototype); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 1b80c536ec..4ab0f7f706 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -3,7 +3,6 @@ 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 { MipmapTexture } from './p5.Texture'; const STROKE_CAP_ENUM = {}; const STROKE_JOIN_ENUM = {}; @@ -2104,7 +2103,7 @@ p5.RendererGL = class RendererGL extends Renderer { } // Free the Framebuffer framebuffer.remove(); - tex = new MipmapTexture(this, levels, {}); + tex = new p5.MipmapTexture(this, levels, {}); this.specularTextures.set(input, tex); return tex; } diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 1788332670..f2a14725d3 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -6,497 +6,499 @@ * @requires core */ -import p5 from '../core/main'; +// import p5 from '../core/main'; import * as constants from '../core/constants'; import Renderer from '../core/p5.Renderer'; -/** - * 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 = 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 Renderer); - this.isSrcP5Image = obj instanceof p5.Image; - this.isSrcP5Graphics = obj instanceof p5.Graphics; - this.isSrcP5Renderer = obj instanceof 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; - } - - _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){ /** - * Initializes common texture parameters, creates a gl texture, - * tries to upload the texture for the first time if data is - * already available. + * Texture class for WEBGL Mode * @private - * @method init + * @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. */ - init (data) { - const gl = this._renderer.GL; - if (!this.isFramebufferTexture) { - this.glTex = gl.createTexture(); - } + 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; + } - 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 - ); + // 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 Renderer); + this.isSrcP5Image = obj instanceof p5.Image; + this.isSrcP5Graphics = obj instanceof p5.Graphics; + this.isSrcP5Renderer = obj instanceof 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; } - } - /** - * 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! + _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; } - // 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; + /** + * 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); + + 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 + ); + } } - 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); + /** + * 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! } - } 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); + + // 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.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! + + 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; - 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(); + + // 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.isImageData) { - if (data._dirty) { - data._dirty = 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! + 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; } - } 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 - ); - } - - return updated; - } + if (updated) { + this.bindTexture(); + gl.texImage2D( + this.glTarget, + 0, + this.glFormat, + this.glFormat, + this.glDataType, + textureData + ); + } - /** - * 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 updated; + } - return this; - } + /** + * 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; + } - /** - * 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; + /** + * 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); } - } - /** - * 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; + getTexture() { + if (this.isFramebufferTexture) { + return this.src.rawTexture(); + } else { + return this.glTex; + } } - } - /** - * 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; + /** + * 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(); } - const widthPowerOfTwo = isPowerOfTwo(wrapWidth); - const heightPowerOfTwo = isPowerOfTwo(wrapHeight); + glFilter(filter) { + const gl = this._renderer.GL; + if (filter === constants.NEAREST) { + return gl.NEAREST; + } else { + return gl.LINEAR; + } + } - if (wrapX === constants.REPEAT) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - this.glWrapS = gl.REPEAT; + /** + * 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 { - 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; + wrapWidth = this.width; + wrapHeight = this.height; } - } else if (wrapX === constants.MIRROR) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - this.glWrapS = gl.MIRRORED_REPEAT; + + 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 { - 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' - ); + // falling back to default if didn't get a proper mode 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; + 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; + } } 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' - ); + // falling back to default if didn't get a proper mode 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; + + 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(); + } + }; + + 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; } - } 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(); - } -}; - -export 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; } - } - - 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] - ); + + _getTextureDataFromSource() { + return this.src; } - 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); + init(levels) { + const gl = this._renderer.GL; + this.glTex = gl.createTexture(); - this.unbindTexture(); - } + this.bindTexture(); + for (let level = 0; level < levels.length; level++) { + gl.texImage2D( + this.glTarget, + level, + this.glFormat, + this.glFormat, + this.glDataType, + levels[level] + ); + } - update() {} + 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() {} + }; } export function checkWebGLCapabilities({ GL, webglVersion }) { @@ -520,4 +522,5 @@ export function checkWebGLCapabilities({ GL, webglVersion }) { }; } -export default p5.Texture; +export default texture; +