diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 345e7d9cbe..56c1a137c3 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1,6 +1,5 @@ import p5 from './main'; import * as constants from './constants'; -import filters from '../image/filters'; import './p5.Renderer'; @@ -155,13 +154,8 @@ p5.Renderer2D.prototype.image = function( } try { - if (this._tint) { - if (p5.MediaElement && img instanceof p5.MediaElement) { - img.loadPixels(); - } - if (img.canvas) { - cnv = this._getTintedImageCanvas(img); - } + if (this._tint && img.canvas) { + cnv = this._getTintedImageCanvas(img); } if (!cnv) { cnv = img.canvas || img.elt; @@ -198,25 +192,66 @@ p5.Renderer2D.prototype._getTintedImageCanvas = function(img) { if (!img.canvas) { return img; } - const pixels = filters._toPixels(img.canvas); - const tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = img.canvas.width; - tmpCanvas.height = img.canvas.height; - const tmpCtx = tmpCanvas.getContext('2d'); - const id = tmpCtx.createImageData(img.canvas.width, img.canvas.height); - const newPixels = id.data; - for (let i = 0; i < pixels.length; i += 4) { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const a = pixels[i + 3]; - newPixels[i] = r * this._tint[0] / 255; - newPixels[i + 1] = g * this._tint[1] / 255; - newPixels[i + 2] = b * this._tint[2] / 255; - newPixels[i + 3] = a * this._tint[3] / 255; + + if (!img.tintCanvas) { + // Once an image has been tinted, keep its tint canvas + // around so we don't need to re-incur the cost of + // creating a new one for each tint + img.tintCanvas = document.createElement('canvas'); + } + + // Keep the size of the tint canvas up-to-date + if (img.tintCanvas.width !== img.canvas.width) { + img.tintCanvas.width = img.canvas.width; + } + if (img.tintCanvas.height !== img.canvas.height) { + img.tintCanvas.height = img.canvas.height; } - tmpCtx.putImageData(id, 0, 0); - return tmpCanvas; + + // Goal: multiply the r,g,b,a values of the source by + // the r,g,b,a values of the tint color + const ctx = img.tintCanvas.getContext('2d'); + + ctx.save(); + ctx.clearRect(0, 0, img.canvas.width, img.canvas.height); + + if (this._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. + + // Start with the original image + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode makes everything opaque but forces the luma to match + // the original image again + ctx.globalCompositeOperation = 'luminosity'; + ctx.drawImage(img.canvas, 0, 0); + + // This blend mode forces the hue and chroma to match the original image. + // After this we should have the original again, but with full opacity. + ctx.globalCompositeOperation = 'color'; + ctx.drawImage(img.canvas, 0, 0); + + // Apply color tint + ctx.globalCompositeOperation = 'multiply'; + ctx.fillStyle = `rgb(${this._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.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.drawImage(img.canvas, 0, 0); + } + + ctx.restore(); + return img.tintCanvas; }; ////////////////////////////////////////////// diff --git a/src/image/loading_displaying.js b/src/image/loading_displaying.js index f961740e89..5f3b3d7c13 100644 --- a/src/image/loading_displaying.js +++ b/src/image/loading_displaying.js @@ -6,7 +6,6 @@ */ import p5 from '../core/main'; -import Filters from './filters'; import canvas from '../core/helpers'; import * as constants from '../core/constants'; import omggif from 'omggif'; @@ -602,33 +601,8 @@ p5.prototype.noTint = function() { * @param {p5.Image} The image to be tinted * @return {canvas} The resulting tinted canvas */ -p5.prototype._getTintedImageCanvas = function(img) { - if (!img.canvas) { - return img; - } - const pixels = Filters._toPixels(img.canvas); - const tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = img.canvas.width; - tmpCanvas.height = img.canvas.height; - const tmpCtx = tmpCanvas.getContext('2d'); - const id = tmpCtx.createImageData(img.canvas.width, img.canvas.height); - const newPixels = id.data; - - for (let i = 0; i < pixels.length; i += 4) { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const a = pixels[i + 3]; - - newPixels[i] = r * this._renderer._tint[0] / 255; - newPixels[i + 1] = g * this._renderer._tint[1] / 255; - newPixels[i + 2] = b * this._renderer._tint[2] / 255; - newPixels[i + 3] = a * this._renderer._tint[3] / 255; - } - - tmpCtx.putImageData(id, 0, 0); - return tmpCanvas; -}; +p5.prototype._getTintedImageCanvas = + p5.Renderer2D.prototype._getTintedImageCanvas; /** * Set image mode. Modifies the location from which images are drawn by diff --git a/test/manual-test-examples/tint-performance/flowers-large.jpg b/test/manual-test-examples/tint-performance/flowers-large.jpg new file mode 100644 index 0000000000..1a54909ceb Binary files /dev/null and b/test/manual-test-examples/tint-performance/flowers-large.jpg differ diff --git a/test/manual-test-examples/tint-performance/index.html b/test/manual-test-examples/tint-performance/index.html new file mode 100644 index 0000000000..8624ec9ae5 --- /dev/null +++ b/test/manual-test-examples/tint-performance/index.html @@ -0,0 +1,7 @@ +
+ + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/tint-performance/sketch.js b/test/manual-test-examples/tint-performance/sketch.js new file mode 100644 index 0000000000..34666a31fb --- /dev/null +++ b/test/manual-test-examples/tint-performance/sketch.js @@ -0,0 +1,54 @@ +var img; +var times = []; + +function preload() { + img = loadImage('flowers-large.jpg'); +} + +function setup() { + createCanvas(800, 160); +} + +function drawScaledImage(img, x, y) { + push(); + translate(x, y); + scale(0.125); + image(img, 0, 0); + pop(); +} + +function draw() { + times.push(deltaTime); + if (times.length > 60) { + times.shift(); + } + const avgDelta = + times.reduce(function(acc, next) { + return acc + next; + }) / times.length; + const avgRate = 1000 / avgDelta; + + clear(); + push(); + translate(50 * sin(millis() / 1000), 50 * cos(millis() / 1000)); + fill(255, 255, 255); + rect(0, 0, 480, 160); + drawScaledImage(img, 0, 0); + tint(0, 0, 150, 150); // Tint alpha blue + drawScaledImage(img, 160, 0); + tint(255, 255, 255); + drawScaledImage(img, 320, 0); + tint(0, 153, 150); // Tint turquoise + drawScaledImage(img, 480, 0); + noTint(); + drawScaledImage(img, 640, 0); + pop(); + + push(); + textAlign(LEFT, TOP); + textSize(20); + noStroke(); + fill(0); + text(avgRate.toFixed(2) + ' FPS', 10, 10); + pop(); +} diff --git a/test/unit/assets/cat-with-hole.png b/test/unit/assets/cat-with-hole.png new file mode 100644 index 0000000000..249b38010c Binary files /dev/null and b/test/unit/assets/cat-with-hole.png differ diff --git a/test/unit/image/loading.js b/test/unit/image/loading.js index 557de60f29..cad2c2a3e8 100644 --- a/test/unit/image/loading.js +++ b/test/unit/image/loading.js @@ -340,3 +340,106 @@ suite('loading animated gif images', function() { new p5(mySketch, null, false); }); }); + +suite('displaying images', function() { + var myp5; + var pImg; + var imagePath = 'unit/assets/cat-with-hole.png'; + var chanNames = ['red', 'green', 'blue', 'alpha']; + + setup(function(done) { + new p5(function(p) { + p.setup = function() { + myp5 = p; + myp5.pixelDensity(1); + myp5.loadImage( + imagePath, + function(img) { + pImg = img; + myp5.resizeCanvas(pImg.width, pImg.height); + done(); + }, + function() { + throw new Error('Error loading image'); + } + ); + }; + }); + }); + + teardown(function() { + myp5.remove(); + }); + + function checkTint(tintColor) { + myp5.loadPixels(); + pImg.loadPixels(); + for (var i = 0; i < myp5.pixels.length; i += 4) { + var x = (i / 4) % myp5.width; + var y = Math.floor(i / 4 / myp5.width); + for (var chan = 0; chan < tintColor.length; chan++) { + var inAlpha = 1; + var outAlpha = 1; + if (chan < 3) { + // The background of the canvas is black, so after applying the + // image's own alpha + the tint alpha to its color channels, we + // should arrive at the same color that we see on the canvas. + inAlpha = tintColor[3] / 255; + outAlpha = pImg.pixels[i + 3] / 255; + + // Applying the tint involves un-multiplying the alpha of the source + // image, which causes a bit of loss of precision. I'm allowing a + // loss of 10 / 255 in this test. + assert.approximately( + myp5.pixels[i + chan], + pImg.pixels[i + chan] * + (tintColor[chan] / 255) * + outAlpha * + inAlpha, + 10, + 'Tint output for the ' + + chanNames[chan] + + ' channel of pixel (' + + x + + ', ' + + y + + ') should be equivalent to multiplying the image value by tint fraction' + ); + } + } + } + } + + test('tint() with color', function() { + assert.ok(pImg, 'image loaded'); + var tintColor = [150, 100, 50, 255]; + myp5.clear(); + myp5.background(0); + myp5.tint(tintColor[0], tintColor[1], tintColor[2], tintColor[3]); + myp5.image(pImg, 0, 0); + + checkTint(tintColor); + }); + + test('tint() with alpha', function() { + assert.ok(pImg, 'image loaded'); + var tintColor = [255, 255, 255, 100]; + myp5.clear(); + myp5.background(0); + myp5.tint(tintColor[0], tintColor[1], tintColor[2], tintColor[3]); + myp5.image(pImg, 0, 0); + + checkTint(tintColor); + }); + + test('tint() with color and alpha', function() { + assert.ok(pImg, 'image loaded'); + var tintColor = [255, 100, 50, 100]; + myp5.clear(); + myp5.background(0); + myp5.tint(tintColor[0], tintColor[1], tintColor[2], tintColor[3]); + myp5.image(pImg, 0, 0); + + checkTint(tintColor); + }); +});