Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 61 additions & 26 deletions src/core/p5.Renderer2D.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import p5 from './main';
import * as constants from './constants';
import filters from '../image/filters';

import './p5.Renderer';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};

//////////////////////////////////////////////
Expand Down
30 changes: 2 additions & 28 deletions src/image/loading_displaying.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions test/manual-test-examples/tint-performance/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<head>
<script language="javascript" type="text/javascript" src="../../../lib/p5.js"></script>
<script language="javascript" type="text/javascript" src="sketch.js"></script>
</head>

<body>
</body>
54 changes: 54 additions & 0 deletions test/manual-test-examples/tint-performance/sketch.js
Original file line number Diff line number Diff line change
@@ -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();
}
Binary file added test/unit/assets/cat-with-hole.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 103 additions & 0 deletions test/unit/image/loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});