diff --git a/lib/empty-example/sketch.js b/lib/empty-example/sketch.js index 69b202ee71..c614f47b93 100644 --- a/lib/empty-example/sketch.js +++ b/lib/empty-example/sketch.js @@ -1,5 +1,5 @@ function setup() { - // put setup code here + // put setup code here } function draw() { diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 7935255b49..02286c7c6c 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -11,13 +11,46 @@ import './p5.Renderer'; const styleEmpty = 'rgba(0,0,0,0)'; // const alphaThreshold = 0.00125; // minimum visible -class Renderer2D extends p5.Renderer{ - constructor (elt, pInst, isMainCanvas) { +class Renderer2D extends p5.Renderer { + constructor(elt, pInst, isMainCanvas) { super(elt, pInst, isMainCanvas); this.drawingContext = this.canvas.getContext('2d'); this._pInst._setProperty('drawingContext', this.drawingContext); } + getFilterGraphicsLayer() { + // create hidden webgl renderer if it doesn't exist + if (!this.filterGraphicsLayer) { + // the real _pInst is buried when this is a secondary p5.Graphics + const pInst = + this._pInst instanceof p5.Graphics ? + this._pInst._pInst : + this._pInst; + + // create secondary layer + this.filterGraphicsLayer = + new p5.Graphics( + this.width, + this.height, + constants.WEBGL, + pInst + ); + } + if ( + this.filterGraphicsLayer.width !== this.width || + this.filterGraphicsLayer.height !== this.height + ) { + // Resize the graphics layer + this.filterGraphicsLayer.resizeCanvas(this.width, this.height); + } + if ( + this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() + ) { + this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); + } + return this.filterGraphicsLayer; + } + _applyDefaults() { this._cachedFillStyle = this._cachedStrokeStyle = undefined; this._cachedBlendMode = constants.BLEND; @@ -45,7 +78,7 @@ class Renderer2D extends p5.Renderer{ if (args[0] instanceof p5.Image) { if (args[1] >= 0) { - // set transparency of background + // set transparency of background const img = args[0]; this.drawingContext.globalAlpha = args[1] / 255; this._pInst.image(img, 0, 0, this.width, this.height); @@ -253,15 +286,15 @@ class Renderer2D extends p5.Renderer{ } } - _getTintedImageCanvas (img) { + _getTintedImageCanvas(img) { if (!img.canvas) { return img; } if (!img.tintCanvas) { - // Once an image has been tinted, keep its tint canvas - // around so we don't need to re-incur the cost of - // creating a new one for each tint + // Once an image has been tinted, keep its tint canvas + // around so we don't need to re-incur the cost of + // creating a new one for each tint img.tintCanvas = document.createElement('canvas'); } @@ -281,11 +314,11 @@ class Renderer2D extends p5.Renderer{ ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); if (this._tint[0] < 255 || this._tint[1] < 255 || this._tint[2] < 255) { - // Color tint: we need to use the multiply blend mode to change the colors. - // However, the canvas implementation of this destroys the alpha channel of - // the image. To accommodate, we first get a version of the image with full - // opacity everywhere, tint using multiply, and then use the destination-in - // blend mode to restore the alpha channel again. + // Color tint: we need to use the multiply blend mode to change the colors. + // However, the canvas implementation of this destroys the alpha channel of + // the image. To accommodate, we first get a version of the image with full + // opacity everywhere, tint using multiply, and then use the destination-in + // blend mode to restore the alpha channel again. // Start with the original image ctx.drawImage(img.canvas, 0, 0); @@ -310,7 +343,7 @@ class Renderer2D extends p5.Renderer{ ctx.globalAlpha = this._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! + // If we only need to change the alpha, we can skip all the extra work! ctx.globalAlpha = this._tint[3] / 255; ctx.drawImage(img.canvas, 0, 0); } @@ -323,25 +356,25 @@ class Renderer2D extends p5.Renderer{ // IMAGE | Pixels ////////////////////////////////////////////// - blendMode (mode) { + blendMode(mode) { if (mode === constants.SUBTRACT) { console.warn('blendMode(SUBTRACT) only works in WEBGL mode.'); } else if ( mode === constants.BLEND || - mode === constants.REMOVE || - mode === constants.DARKEST || - mode === constants.LIGHTEST || - mode === constants.DIFFERENCE || - mode === constants.MULTIPLY || - mode === constants.EXCLUSION || - mode === constants.SCREEN || - mode === constants.REPLACE || - mode === constants.OVERLAY || - mode === constants.HARD_LIGHT || - mode === constants.SOFT_LIGHT || - mode === constants.DODGE || - mode === constants.BURN || - mode === constants.ADD + mode === constants.REMOVE || + mode === constants.DARKEST || + mode === constants.LIGHTEST || + mode === constants.DIFFERENCE || + mode === constants.MULTIPLY || + mode === constants.EXCLUSION || + mode === constants.SCREEN || + mode === constants.REPLACE || + mode === constants.OVERLAY || + mode === constants.HARD_LIGHT || + mode === constants.SOFT_LIGHT || + mode === constants.DODGE || + mode === constants.BURN || + mode === constants.ADD ) { this._cachedBlendMode = mode; this.drawingContext.globalCompositeOperation = mode; @@ -350,7 +383,7 @@ class Renderer2D extends p5.Renderer{ } } - blend (...args) { + blend(...args) { const currBlend = this.drawingContext.globalCompositeOperation; const blendMode = args[args.length - 1]; @@ -367,7 +400,7 @@ class Renderer2D extends p5.Renderer{ // .get() is not overridden // x,y are canvas-relative (pre-scaled by _pixelDensity) - _getPixel (x, y) { + _getPixel(x, y) { let imageData, index; imageData = this.drawingContext.getImageData(x, y, 1, 1).data; index = 0; @@ -379,7 +412,7 @@ class Renderer2D extends p5.Renderer{ ]; } - loadPixels () { + loadPixels() { const pixelsState = this._pixelsState; // if called by p5.Image const pd = pixelsState._pixelDensity; @@ -392,8 +425,8 @@ class Renderer2D extends p5.Renderer{ pixelsState._setProperty('pixels', imageData.data); } - set (x, y, imgOrCol) { - // round down to get integer numbers + set(x, y, imgOrCol) { + // round down to get integer numbers x = Math.floor(x); y = Math.floor(y); const pixelsState = this._pixelsState; @@ -413,11 +446,11 @@ class Renderer2D extends p5.Renderer{ b = 0, a = 0; let idx = - 4 * - (y * - pixelsState._pixelDensity * - (this.width * pixelsState._pixelDensity) + - x * pixelsState._pixelDensity); + 4 * + (y * + pixelsState._pixelDensity * + (this.width * pixelsState._pixelDensity) + + x * pixelsState._pixelDensity); if (!pixelsState.imageData) { pixelsState.loadPixels.call(pixelsState); } @@ -427,7 +460,7 @@ class Renderer2D extends p5.Renderer{ g = imgOrCol; b = imgOrCol; a = 255; - //this.updatePixels.call(this); + //this.updatePixels.call(this); } } else if (imgOrCol instanceof Array) { if (imgOrCol.length < 4) { @@ -438,7 +471,7 @@ class Renderer2D extends p5.Renderer{ g = imgOrCol[1]; b = imgOrCol[2]; a = imgOrCol[3]; - //this.updatePixels.call(this); + //this.updatePixels.call(this); } } else if (imgOrCol instanceof p5.Color) { if (idx < pixelsState.pixels.length) { @@ -446,19 +479,19 @@ class Renderer2D extends p5.Renderer{ g = imgOrCol.levels[1]; b = imgOrCol.levels[2]; a = imgOrCol.levels[3]; - //this.updatePixels.call(this); + //this.updatePixels.call(this); } } // loop over pixelDensity * pixelDensity for (let i = 0; i < pixelsState._pixelDensity; i++) { for (let j = 0; j < pixelsState._pixelDensity; j++) { - // loop over + // loop over idx = - 4 * - ((y * pixelsState._pixelDensity + j) * - this.width * - pixelsState._pixelDensity + - (x * pixelsState._pixelDensity + i)); + 4 * + ((y * pixelsState._pixelDensity + j) * + this.width * + pixelsState._pixelDensity + + (x * pixelsState._pixelDensity + i)); pixelsState.pixels[idx] = r; pixelsState.pixels[idx + 1] = g; pixelsState.pixels[idx + 2] = b; @@ -468,14 +501,14 @@ class Renderer2D extends p5.Renderer{ } } - updatePixels (x, y, w, h) { + updatePixels(x, y, w, h) { const pixelsState = this._pixelsState; const pd = pixelsState._pixelDensity; if ( x === undefined && - y === undefined && - w === undefined && - h === undefined + y === undefined && + w === undefined && + h === undefined ) { x = 0; y = 0; @@ -489,7 +522,7 @@ class Renderer2D extends p5.Renderer{ if (this.gifProperties) { this.gifProperties.frames[this.gifProperties.displayIndex].image = - pixelsState.imageData; + pixelsState.imageData; } this.drawingContext.putImageData(pixelsState.imageData, x, y, 0, 0, w, h); @@ -510,7 +543,7 @@ class Renderer2D extends p5.Renderer{ start, size ) { - // Evaluate constants. + // Evaluate constants. const alpha = size / 2.0, cos_alpha = Math.cos(alpha), sin_alpha = Math.sin(alpha), @@ -542,7 +575,7 @@ class Renderer2D extends p5.Renderer{ * * start <= stop < start + TWO_PI */ - arc (x, y, w, h, start, stop, mode) { + arc(x, y, w, h, start, stop, mode) { const ctx = this.drawingContext; const rx = w / 2.0; const ry = h / 2.0; @@ -568,10 +601,10 @@ class Renderer2D extends p5.Renderer{ ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); } /* eslint-disable indent */ - ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, - x + curve.cx * rx, y + curve.cy * ry, - x + curve.dx * rx, y + curve.dy * ry); - /* eslint-enable indent */ + ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, + x + curve.cx * rx, y + curve.cy * ry, + x + curve.dx * rx, y + curve.dy * ry); + /* eslint-enable indent */ }); if (mode === constants.PIE || mode == null) { ctx.lineTo(x, y); @@ -588,10 +621,10 @@ class Renderer2D extends p5.Renderer{ ctx.moveTo(x + curve.ax * rx, y + curve.ay * ry); } /* eslint-disable indent */ - ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, - x + curve.cx * rx, y + curve.cy * ry, - x + curve.dx * rx, y + curve.dy * ry); - /* eslint-enable indent */ + ctx.bezierCurveTo(x + curve.bx * rx, y + curve.by * ry, + x + curve.cx * rx, y + curve.cy * ry, + x + curve.dx * rx, y + curve.dy * ry); + /* eslint-enable indent */ }); if (mode === constants.PIE) { ctx.lineTo(x, y); @@ -604,7 +637,7 @@ class Renderer2D extends p5.Renderer{ return this; } - ellipse (args) { + ellipse(args) { const ctx = this.drawingContext; const doFill = this._doFill, doStroke = this._doStroke; @@ -647,7 +680,7 @@ class Renderer2D extends p5.Renderer{ } } - line (x1, y1, x2, y2) { + line(x1, y1, x2, y2) { const ctx = this.drawingContext; if (!this._doStroke) { return this; @@ -661,7 +694,7 @@ class Renderer2D extends p5.Renderer{ return this; } - point (x, y) { + point(x, y) { const ctx = this.drawingContext; if (!this._doStroke) { return this; @@ -682,7 +715,7 @@ class Renderer2D extends p5.Renderer{ } } - quad (x1, y1, x2, y2, x3, y3, x4, y4) { + quad(x1, y1, x2, y2, x3, y3, x4, y4) { const ctx = this.drawingContext; const doFill = this._doFill, doStroke = this._doStroke; @@ -710,7 +743,7 @@ class Renderer2D extends p5.Renderer{ return this; } - rect (args) { + rect(args) { const x = args[0]; const y = args[1]; const w = args[2]; @@ -734,11 +767,11 @@ class Renderer2D extends p5.Renderer{ if (!this._clipping) ctx.beginPath(); if (typeof tl === 'undefined') { - // No rounded corners + // No rounded corners ctx.rect(x, y, w, h); } else { - // At least one rounded corner - // Set defaults when not specified + // At least one rounded corner + // Set defaults when not specified if (typeof tr === 'undefined') { tr = tl; } @@ -800,7 +833,7 @@ class Renderer2D extends p5.Renderer{ } - triangle (args) { + triangle(args) { const ctx = this.drawingContext; const doFill = this._doFill, doStroke = this._doStroke; @@ -832,7 +865,7 @@ class Renderer2D extends p5.Renderer{ } } - endShape ( + endShape( mode, vertices, isCurve, @@ -869,9 +902,9 @@ class Renderer2D extends p5.Renderer{ ]; b[2] = [ vertices[i + 1][0] + - (s * vertices[i][0] - s * vertices[i + 2][0]) / 6, + (s * vertices[i][0] - s * vertices[i + 2][0]) / 6, vertices[i + 1][1] + - (s * vertices[i][1] - s * vertices[i + 2][1]) / 6 + (s * vertices[i][1] - s * vertices[i + 2][1]) / 6 ]; b[3] = [vertices[i + 1][0], vertices[i + 1][1]]; this.drawingContext.bezierCurveTo( @@ -890,7 +923,7 @@ class Renderer2D extends p5.Renderer{ } } else if ( isBezier && - (shapeKind === constants.POLYGON || shapeKind === null) + (shapeKind === constants.POLYGON || shapeKind === null) ) { if (!this._clipping) this.drawingContext.beginPath(); for (i = 0; i < numVerts; i++) { @@ -914,7 +947,7 @@ class Renderer2D extends p5.Renderer{ this._doFillStrokeClose(closeShape); } else if ( isQuadratic && - (shapeKind === constants.POLYGON || shapeKind === null) + (shapeKind === constants.POLYGON || shapeKind === null) ) { if (!this._clipping) this.drawingContext.beginPath(); for (i = 0; i < numVerts; i++) { @@ -993,8 +1026,8 @@ class Renderer2D extends p5.Renderer{ } } else if (shapeKind === constants.TRIANGLE_FAN) { if (numVerts > 2) { - // For performance reasons, try to batch as many of the - // fill and stroke calls as possible. + // For performance reasons, try to batch as many of the + // fill and stroke calls as possible. if (!this._clipping) this.drawingContext.beginPath(); for (i = 2; i < numVerts; i++) { v = vertices[i]; @@ -1006,7 +1039,7 @@ class Renderer2D extends p5.Renderer{ if (i < numVerts - 1) { if ( (this._doFill && v[5] !== vertices[i + 1][5]) || - (this._doStroke && v[6] !== vertices[i + 1][6]) + (this._doStroke && v[6] !== vertices[i + 1][6]) ) { if (!this._clipping && this._doFill) { this._pInst.fill(v[5]); @@ -1100,31 +1133,31 @@ class Renderer2D extends p5.Renderer{ // SHAPE | Attributes ////////////////////////////////////////////// - strokeCap (cap) { + strokeCap(cap) { if ( cap === constants.ROUND || - cap === constants.SQUARE || - cap === constants.PROJECT + cap === constants.SQUARE || + cap === constants.PROJECT ) { this.drawingContext.lineCap = cap; } return this; } - strokeJoin (join) { + strokeJoin(join) { if ( join === constants.ROUND || - join === constants.BEVEL || - join === constants.MITER + join === constants.BEVEL || + join === constants.MITER ) { this.drawingContext.lineJoin = join; } return this; } - strokeWeight (w) { + strokeWeight(w) { if (typeof w === 'undefined' || w === 0) { - // hack because lineWidth 0 doesn't work + // hack because lineWidth 0 doesn't work this.drawingContext.lineWidth = 0.0001; } else { this.drawingContext.lineWidth = w; @@ -1132,28 +1165,28 @@ class Renderer2D extends p5.Renderer{ return this; } - _getFill () { + _getFill() { if (!this._cachedFillStyle) { this._cachedFillStyle = this.drawingContext.fillStyle; } return this._cachedFillStyle; } - _setFill (fillStyle) { + _setFill(fillStyle) { if (fillStyle !== this._cachedFillStyle) { this.drawingContext.fillStyle = fillStyle; this._cachedFillStyle = fillStyle; } } - _getStroke () { + _getStroke() { if (!this._cachedStrokeStyle) { this._cachedStrokeStyle = this.drawingContext.strokeStyle; } return this._cachedStrokeStyle; } - _setStroke (strokeStyle) { + _setStroke(strokeStyle) { if (strokeStyle !== this._cachedStrokeStyle) { this.drawingContext.strokeStyle = strokeStyle; this._cachedStrokeStyle = strokeStyle; @@ -1163,7 +1196,7 @@ class Renderer2D extends p5.Renderer{ ////////////////////////////////////////////// // SHAPE | Curves ////////////////////////////////////////////// - bezier (x1, y1, x2, y2, x3, y3, x4, y4) { + bezier(x1, y1, x2, y2, x3, y3, x4, y4) { this._pInst.beginShape(); this._pInst.vertex(x1, y1); this._pInst.bezierVertex(x2, y2, x3, y3, x4, y4); @@ -1171,7 +1204,7 @@ class Renderer2D extends p5.Renderer{ return this; } - curve (x1, y1, x2, y2, x3, y3, x4, y4) { + curve(x1, y1, x2, y2, x3, y3, x4, y4) { this._pInst.beginShape(); this._pInst.curveVertex(x1, y1); this._pInst.curveVertex(x2, y2); @@ -1185,7 +1218,7 @@ class Renderer2D extends p5.Renderer{ // SHAPE | Vertex ////////////////////////////////////////////// - _doFillStrokeClose (closeShape) { + _doFillStrokeClose(closeShape) { if (closeShape) { this.drawingContext.closePath(); } @@ -1201,11 +1234,11 @@ class Renderer2D extends p5.Renderer{ // TRANSFORM ////////////////////////////////////////////// - applyMatrix (a, b, c, d, e, f) { + applyMatrix(a, b, c, d, e, f) { this.drawingContext.transform(a, b, c, d, e, f); } - resetMatrix () { + resetMatrix() { this.drawingContext.setTransform(1, 0, 0, 1, 0, 0); this.drawingContext.scale( this._pInst._pixelDensity, @@ -1214,17 +1247,17 @@ class Renderer2D extends p5.Renderer{ return this; } - rotate (rad) { + rotate(rad) { this.drawingContext.rotate(rad); } - scale (x, y) { + scale(x, y) { this.drawingContext.scale(x, y); return this; } - translate (x, y) { - // support passing a vector as the 1st parameter + translate(x, y) { + // support passing a vector as the 1st parameter if (x instanceof p5.Vector) { y = x.y; x = x.x; @@ -1240,7 +1273,7 @@ class Renderer2D extends p5.Renderer{ - _renderText (p, line, x, y, maxY, minY) { + _renderText(p, line, x, y, maxY, minY) { if (y < minY || y >= maxY) { return; // don't render lines beyond our minY/maxY bounds (see #5785) } @@ -1248,7 +1281,7 @@ class Renderer2D extends p5.Renderer{ p.push(); // fix to #803 if (!this._isOpenType()) { - // a system/browser font + // a system/browser font // no stroke unless specified by user if (this._doStroke && this._strokeSet) { @@ -1256,7 +1289,7 @@ class Renderer2D extends p5.Renderer{ } if (!this._clipping && this._doFill) { - // if fill hasn't been set by user, use default text fill + // if fill hasn't been set by user, use default text fill if (!this._fillSet) { this._setFill(constants._DEFAULT_TEXT_FILL); } @@ -1264,7 +1297,7 @@ class Renderer2D extends p5.Renderer{ this.drawingContext.fillText(line, x, y); } } else { - // an opentype font, let it handle the rendering + // an opentype font, let it handle the rendering this._textFont._renderPath(line, x, y, { renderer: this }); } @@ -1273,7 +1306,7 @@ class Renderer2D extends p5.Renderer{ return p; } - textWidth (s) { + textWidth(s) { if (this._isOpenType()) { return this._textFont._textWidth(s, this._textSize); } @@ -1281,7 +1314,7 @@ class Renderer2D extends p5.Renderer{ return this.drawingContext.measureText(s).width; } - _applyTextProperties () { + _applyTextProperties() { let font; const p = this._pInst; @@ -1296,7 +1329,7 @@ class Renderer2D extends p5.Renderer{ } this.drawingContext.font = `${this._textStyle || 'normal'} ${this._textSize || - 12}px ${font || 'sans-serif'}`; + 12}px ${font || 'sans-serif'}`; this.drawingContext.textAlign = this._textAlign; if (this._textBaseline === constants.CENTER) { @@ -1317,7 +1350,7 @@ class Renderer2D extends p5.Renderer{ // store on the push stack. // derived renderers should call the base class' push() method // to fetch the base style object. - push () { + push() { this.drawingContext.save(); // get the base renderer style @@ -1329,7 +1362,7 @@ class Renderer2D extends p5.Renderer{ // from its push() method. // derived renderers should pass this object to their base // class' pop method - pop (style) { + pop(style) { this.drawingContext.restore(); // Re-cache the fill / stroke state this._cachedFillStyle = this.drawingContext.fillStyle; @@ -1340,7 +1373,7 @@ class Renderer2D extends p5.Renderer{ } // Fix test -Renderer2D.prototype.text = function(str, x, y, maxWidth, maxHeight) { +Renderer2D.prototype.text = function (str, x, y, maxWidth, maxHeight) { let baselineHacked; // baselineHacked: (HACK) diff --git a/src/image/pixels.js b/src/image/pixels.js index eef7fb5171..c609ec334f 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -8,7 +8,6 @@ import p5 from '../core/main'; import Filters from './filters'; import '../color/p5.Color'; -import * as constants from '../core/constants'; /** * An array containing the color of each pixel on the canvas. Colors are @@ -544,6 +543,15 @@ p5.prototype._copyHelper = ( * */ +/** + * @method getFilterGraphicsLayer + * @private + * @returns {p5.Graphics} + */ +p5.prototype.getFilterGraphicsLayer = function() { + return this._renderer.getFilterGraphicsLayer(); +}; + /** * @method filter * @param {Constant} filterType @@ -560,7 +568,7 @@ p5.prototype.filter = function(...args) { let { shader, operation, value, useWebGL } = parseFilterArgs(...args); // when passed a shader, use it directly - if (shader) { + if (this._renderer.isP3D && shader) { p5.RendererGL.prototype.filter.call(this._renderer, shader); return; } @@ -586,27 +594,11 @@ p5.prototype.filter = function(...args) { // when this is P2D renderer, create/use hidden webgl renderer else { - // create hidden webgl renderer if it doesn't exist - if (!this.filterGraphicsLayer) { - // the real _pInst is buried when this is a secondary p5.Graphics - const pInst = - this._renderer._pInst instanceof p5.Graphics ? - this._renderer._pInst._pInst : - this._renderer._pInst; - - // create secondary layer - this.filterGraphicsLayer = - new p5.Graphics( - this.width, - this.height, - constants.WEBGL, - pInst - ); - } + const filterGraphicsLayer = this.getFilterGraphicsLayer(); // copy p2d canvas contents to secondary webgl renderer // dest - this.filterGraphicsLayer.copy( + filterGraphicsLayer.copy( // src this._renderer, // src coods @@ -616,12 +608,14 @@ p5.prototype.filter = function(...args) { ); //clearing the main canvas this._renderer.clear(); + + this._renderer.resetMatrix(); // filter it with shaders - this.filterGraphicsLayer.filter(operation, value); + filterGraphicsLayer.filter(...args); // copy secondary webgl renderer back to original p2d canvas - this._renderer._pInst.image(this.filterGraphicsLayer, 0, 0); - this.filterGraphicsLayer.clear(); // prevent feedback effects on p2d canvas + this._renderer._pInst.image(filterGraphicsLayer, 0, 0); + filterGraphicsLayer.clear(); // prevent feedback effects on p2d canvas } }; @@ -925,4 +919,4 @@ p5.prototype.updatePixels = function(x, y, w, h) { this._renderer.updatePixels(x, y, w, h); }; -export default p5; +export default p5; \ No newline at end of file diff --git a/src/webgl/material.js b/src/webgl/material.js index 849e901a63..26ba269186 100644 --- a/src/webgl/material.js +++ b/src/webgl/material.js @@ -61,7 +61,7 @@ import './p5.Texture'; * @alt * zooming Mandelbrot set. a colorful, infinitely detailed fractal. */ -p5.prototype.loadShader = function( +p5.prototype.loadShader = function ( vertFilename, fragFilename, callback, @@ -194,8 +194,7 @@ p5.prototype.loadShader = function( * @alt * zooming Mandelbrot set. a colorful, infinitely detailed fractal. */ -p5.prototype.createShader = function(vertSrc, fragSrc) { - this._assert3d('createShader'); +p5.prototype.createShader = function (vertSrc, fragSrc) { p5._validateParameters('createShader', arguments); return new p5.Shader(this._renderer, vertSrc, fragSrc); }; @@ -204,13 +203,13 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * Creates a new p5.Shader using only a fragment shader, as a convenience method for creating image effects. * It's like createShader() but with a default vertex shader included. * - * createFilterShader() is intended to be used along with filter() for filtering the contents of a canvas in WebGL mode. + * createFilterShader() is intended to be used along with filter() for filtering the contents of a canvas. * A filter shader will not be applied to any geometries. * * The fragment shader receives some uniforms: * - `sampler2D tex0`, which contains the canvas contents as a texture - * - `vec2 canvasSize`, which is the width and height of the canvas - * - `vec2 texelSize`, which is the size of a pixel (`1.0/width`, `1.0/height`) +* - `vec2 canvasSize`, which is the p5 width and height of the canvas (not including pixel density) + * - `vec2 texelSize`, which is the size of a physical pixel including pixel density (`1.0/(width*density)`, `1.0/(height*density)`) * * For more info about filters and shaders, see Adam Ferriss' repo of shader examples * or the introduction to shaders page. @@ -277,8 +276,7 @@ p5.prototype.createShader = function(vertSrc, fragSrc) { * * */ -p5.prototype.createFilterShader = function(fragSrc) { - this._assert3d('createFilterShader'); +p5.prototype.createFilterShader = function (fragSrc) { p5._validateParameters('createFilterShader', arguments); let defaultVertV1 = ` uniform mat4 uModelViewMatrix; @@ -322,7 +320,11 @@ p5.prototype.createFilterShader = function(fragSrc) { `; let vertSrc = fragSrc.includes('#version 300 es') ? defaultVertV2 : defaultVertV1; const shader = new p5.Shader(this._renderer, vertSrc, fragSrc); - shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); + if (this._renderer.GL) { + shader.ensureCompiledOnContext(this); + } else { + shader.ensureCompiledOnContext(this._renderer.getFilterGraphicsLayer()); + } return shader; }; @@ -411,7 +413,7 @@ p5.prototype.createFilterShader = function(fragSrc) { * @alt * canvas toggles between a circular gradient of orange and blue vertically. and a circular gradient of red and green moving horizontally when mouse is clicked/pressed. */ -p5.prototype.shader = function(s) { +p5.prototype.shader = function (s) { this._assert3d('shader'); p5._validateParameters('shader', arguments); @@ -511,7 +513,7 @@ p5.prototype.shader = function(s) { * Two rotating cubes. The left one is painted using a custom (user-defined) shader, * while the right one is painted using the default fill shader. */ -p5.prototype.resetShader = function() { +p5.prototype.resetShader = function () { this._renderer.userFillShader = this._renderer.userStrokeShader = null; return this; }; @@ -646,7 +648,7 @@ p5.prototype.resetShader = function() { * @alt * quad with a texture, mapped using normalized coordinates */ -p5.prototype.texture = function(tex) { +p5.prototype.texture = function (tex) { this._assert3d('texture'); p5._validateParameters('texture', arguments); if (tex.gifProperties) { @@ -729,7 +731,7 @@ p5.prototype.texture = function(tex) { * @alt * quad with a texture, mapped using image coordinates */ -p5.prototype.textureMode = function(mode) { +p5.prototype.textureMode = function (mode) { if (mode !== constants.IMAGE && mode !== constants.NORMAL) { console.warn( `You tried to set ${mode} textureMode only supports IMAGE & NORMAL ` @@ -802,7 +804,7 @@ p5.prototype.textureMode = function(mode) { * @alt * an image of the rocky mountains repeated in mirrored tiles */ -p5.prototype.textureWrap = function(wrapX, wrapY = wrapX) { +p5.prototype.textureWrap = function (wrapX, wrapY = wrapX) { this._renderer.textureWrapX = wrapX; this._renderer.textureWrapY = wrapY; @@ -843,7 +845,7 @@ p5.prototype.textureWrap = function(wrapX, wrapY = wrapX) { * @alt * Sphere with normal material */ -p5.prototype.normalMaterial = function(...args) { +p5.prototype.normalMaterial = function (...args) { this._assert3d('normalMaterial'); p5._validateParameters('normalMaterial', args); this._renderer.drawMode = constants.FILL; @@ -956,7 +958,7 @@ p5.prototype.normalMaterial = function(...args) { * as an array, or as a CSS string * @chainable */ -p5.prototype.ambientMaterial = function(v1, v2, v3) { +p5.prototype.ambientMaterial = function (v1, v2, v3) { this._assert3d('ambientMaterial'); p5._validateParameters('ambientMaterial', arguments); @@ -1027,7 +1029,7 @@ p5.prototype.ambientMaterial = function(v1, v2, v3) { * as an array, or as a CSS string * @chainable */ -p5.prototype.emissiveMaterial = function(v1, v2, v3, a) { +p5.prototype.emissiveMaterial = function (v1, v2, v3, a) { this._assert3d('emissiveMaterial'); p5._validateParameters('emissiveMaterial', arguments); @@ -1113,7 +1115,7 @@ p5.prototype.emissiveMaterial = function(v1, v2, v3, a) { * as an array, or as a CSS string * @chainable */ -p5.prototype.specularMaterial = function(v1, v2, v3, alpha) { +p5.prototype.specularMaterial = function (v1, v2, v3, alpha) { this._assert3d('specularMaterial'); p5._validateParameters('specularMaterial', arguments); @@ -1162,7 +1164,7 @@ p5.prototype.specularMaterial = function(v1, v2, v3, alpha) { * @alt * two spheres, one more shiny than the other */ -p5.prototype.shininess = function(shine) { +p5.prototype.shininess = function (shine) { this._assert3d('shininess'); p5._validateParameters('shininess', arguments); @@ -1180,7 +1182,7 @@ p5.prototype.shininess = function(shine) { * @param {Number[]} color [description] * @return {Number[]]} Normalized numbers array */ -p5.RendererGL.prototype._applyColorBlend = function(colors) { +p5.RendererGL.prototype._applyColorBlend = function (colors) { const gl = this.GL; const isTexture = this.drawMode === constants.TEXTURE; @@ -1215,7 +1217,7 @@ p5.RendererGL.prototype._applyColorBlend = function(colors) { * @param {Number[]} color [description] * @return {Number[]]} Normalized numbers array */ -p5.RendererGL.prototype._applyBlendMode = function() { +p5.RendererGL.prototype._applyBlendMode = function () { if (this._cachedBlendMode === this.curBlendMode) { return; } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 171b4aa622..3c4aa212fc 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -633,8 +633,8 @@ p5.RendererGL = class RendererGL extends p5.Renderer { // for post processing step this.filterShader = undefined; - this.filterGraphicsLayer = undefined; - this.filterGraphicsLayerTemp = undefined; + this.filterLayer = undefined; + this.filterLayerTemp = undefined; this.defaultFilterShaders = {}; this.textureMode = constants.IMAGE; @@ -996,75 +996,33 @@ p5.RendererGL = class RendererGL extends p5.Renderer { strokeJoin(join) { this.curStrokeJoin = join; } - - getFilterGraphicsLayer() { - // Lazily initialize the filter graphics layer. We only do this on demand - // because the graphics layer itself has a p5.RendererGL, which would then - // try to make its own filter layer, infinitely looping. - if (!this.filterGraphicsLayer) { - // the real _pInst is buried when this is a secondary p5.Graphics - - const pInst = - this._pInst instanceof p5.Graphics ? this._pInst._pInst : this._pInst; - - // create secondary layer - this.filterGraphicsLayer = - new p5.Graphics( - this.width, - this.height, - constants.WEBGL, - pInst - ); - // geometries/borders on this layer should always be invisible - this.filterGraphicsLayer.noStroke(); + getFilterLayer() { + if (!this.filterLayer) { + this.filterLayer = this._pInst.createFramebuffer(); } - if ( - this.filterGraphicsLayer.width !== this.width || - this.filterGraphicsLayer.height !== this.height - ) { - // Resize the graphics layer - this.filterGraphicsLayer.resizeCanvas(this.width, this.height); - } - if ( - this.filterGraphicsLayer.pixelDensity() !== this._pInst.pixelDensity() - ) { - this.filterGraphicsLayer.pixelDensity(this._pInst.pixelDensity()); - } - return this.filterGraphicsLayer; - } - - getFilterGraphicsLayerTemp() { - // two-pass blur filter needs another graphics layer - if (!this.filterGraphicsLayerTemp) { - const pInst = this._pInst instanceof p5.Graphics ? - this._pInst._pInst : this._pInst; - // create secondary layer - this.filterGraphicsLayerTemp = - new p5.Graphics( - this.width, - this.height, - constants.WEBGL, - pInst - ); - this.filterGraphicsLayerTemp.noStroke(); + return this.filterLayer; + } + getFilterLayerTemp() { + if (!this.filterLayerTemp) { + this.filterLayerTemp = this._pInst.createFramebuffer(); } + return this.filterLayerTemp; + } + matchSize(fboToMatch, target) { if ( - this.filterGraphicsLayerTemp.width !== this.width || - this.filterGraphicsLayerTemp.height !== this.height + fboToMatch.width !== target.width || + fboToMatch.height !== target.height ) { - // Resize the graphics layer - this.filterGraphicsLayerTemp.resizeCanvas(this.width, this.height); + fboToMatch.resize(target.width, target.height); } - if ( - this.filterGraphicsLayerTemp.pixelDensity() !== this._pInst.pixelDensity() - ) { - this.filterGraphicsLayerTemp.pixelDensity(this._pInst.pixelDensity()); + + if (fboToMatch.pixelDensity() !== target.pixelDensity()) { + fboToMatch.pixelDensity(target.pixelDensity()); } - return this.filterGraphicsLayerTemp; } - filter(...args) { - let pg = this.getFilterGraphicsLayer(); + + let fbo = this.getFilterLayer(); // use internal shader for filter constants BLUR, INVERT, etc let filterParameter = undefined; @@ -1084,7 +1042,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { // eg. filter(BLUR) then filter(GRAY) if (!(operation in this.defaultFilterShaders)) { this.defaultFilterShaders[operation] = new p5.Shader( - pg._renderer, + fbo._renderer, filterShaderVert, filterShaderFrags[operation] ); @@ -1097,76 +1055,104 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.filterShader = args[0]; } - pg.clear(); // prevent undesirable feedback effects accumulating secretly + // Setting the target to the framebuffer when applying a filter to a framebuffer. - let pd = this._pInst.pixelDensity(); - let texelSize = [1 / (this.width * pd), 1 / (this.height * pd)]; + const target = this.activeFramebuffer() || this; - // apply blur shader with multiple passes - if (operation === constants.BLUR) { + // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. + this.matchSize(fbo, target); + + // Set filterCamera for framebuffers. + if (target !== this) { + this.filterCamera = this.getFilterLayer().createCamera(); + } + fbo.draw(() => this._pInst.clear()); // prevent undesirable feedback effects accumulating secretly. - const tmp = this.getFilterGraphicsLayerTemp(); - tmp.clear(); // prevent feedback effects here too + let texelSize = [ + 1 / (target.width * target.pixelDensity()), + 1 / (target.height * target.pixelDensity()) + ]; + // apply blur shader with multiple passes. + if (operation === constants.BLUR) { + // Treating 'tmp' as a framebuffer. + const tmp = this.getFilterLayerTemp(); + // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. + this.matchSize(tmp, target); // setup this._pInst.push(); this._pInst.noStroke(); // draw main to temp buffer - tmp.image(this, -this.width / 2, -this.height / 2); - - pg.shader(this.filterShader); + this._pInst.shader(this.filterShader); this.filterShader.setUniform('texelSize', texelSize); - this.filterShader.setUniform('canvasSize', [this.width, this.height]); + this.filterShader.setUniform('canvasSize', [target.width, target.height]); this.filterShader.setUniform('radius', Math.max(1, filterParameter)); - // horiz pass - this.filterShader.setUniform('direction', [1, 0]); - this.filterShader.setUniform('tex0', tmp); - pg.clear(); - pg.rect(-this.width / 2, -this.height / 2, this.width, this.height); - - // read back to temp buffer - tmp.clear(); - tmp.image(pg, -this.width / 2, -this.height / 2); + // Horiz pass: draw `target` to `tmp` + tmp.draw(() => { + this.filterShader.setUniform('direction', [1, 0]); + this.filterShader.setUniform('tex0', target); + this._pInst.clear(); + this._pInst.shader(this.filterShader); + this._pInst.rect(-target.width / 2, + -target.height / 2, target.width, target.height); + }); - // vert pass - this.filterShader.setUniform('direction', [0, 1]); - this.filterShader.setUniform('tex0', tmp); - pg.clear(); - pg.rect(-this.width / 2, -this.height / 2, this.width, this.height); + // Vert pass: draw `tmp` to `fbo` + fbo.draw(() => { + this.filterShader.setUniform('direction', [0, 1]); + this.filterShader.setUniform('tex0', tmp); + this._pInst.clear(); + this._pInst.shader(this.filterShader); + this._pInst.rect(-target.width / 2, + -target.height / 2, target.width, target.height); + }); this._pInst.pop(); } // every other non-blur shader uses single pass else { - pg.shader(this.filterShader); - this.filterShader.setUniform('tex0', this); - this.filterShader.setUniform('texelSize', texelSize); - this.filterShader.setUniform('canvasSize', [this.width, this.height]); - // filterParameter uniform only used for POSTERIZE, and THRESHOLD - // but shouldn't hurt to always set - this.filterShader.setUniform('filterParameter', filterParameter); - pg.rect(-this.width / 2, -this.height / 2, this.width, this.height); + fbo.draw(() => { + this._pInst.noStroke(); + this._pInst.shader(this.filterShader); + this.filterShader.setUniform('tex0', target); + this.filterShader.setUniform('texelSize', texelSize); + this.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._pInst.rect(-target.width / 2, -target.height / 2, + target.width, target.height); + }); } - // draw pg contents onto main renderer + // draw fbo contents onto main renderer. this._pInst.push(); - pg._pInst.resetMatrix(); this._pInst.noStroke(); - pg._pInst.imageMode(constants.CORNER); - pg._pInst.blendMode(constants.BLEND); this.clear(); this._pInst.push(); + this._pInst.imageMode(constants.CORNER); + this._pInst.blendMode(constants.BLEND); this.filterCamera._resize(); this._pInst.setCamera(this.filterCamera); this._pInst.resetMatrix(); - this._pInst.image(pg, -this.width / 2, -this.height / 2, + this._pInst.image(fbo, -this.width / 2, -this.height / 2, this.width, this.height); this._pInst.pop(); this._pInst.pop(); } + // Pass this off to the host instance so that we can treat a renderer and a + // framebuffer the same in filter() + + pixelDensity(newDensity) { + if (newDensity) { + return this._pInst.pixelDensity(newDensity); + } + return this._pInst.pixelDensity(); + } + blendMode(mode) { if ( mode === constants.DARKEST || @@ -1465,11 +1451,6 @@ p5.RendererGL = class RendererGL extends p5.Renderer { // can also update their size framebuffer._canvasSizeChanged(); } - - // resize filter graphics layer - if (this.filterGraphicsLayer) { - p5.Renderer.prototype.resize.call(this.filterGraphicsLayer, w, h); - } } /** diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 587247cd1f..0366df75e4 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -114,6 +114,18 @@ p5.Shader = class { * @param {p5|p5.Graphics} context The graphic or instance to copy this shader to. * Pass `window` if you need to copy to the main canvas. * @returns {p5.Shader} A new shader on the target context. + * + * @example + *
+ * let graphic = createGraphics(200, 200, WEBGL);
+ * let graphicShader = graphic.createShader(vert, frag);
+ * graphic.shader(graphicShader); // Use graphicShader on the graphic
+ *
+ * let mainShader = graphicShader.copyToContext(window);
+ * shader(mainShader); // Use `mainShader` on the main canvas
+ *
+ *