I'm trying to create a canvas project inspired by this gif where colors of an image are averaged and animated.
Thus far I have all of the functionality working but it only performs well with a large cell size.
My current thinking is that it's all of the .fillRect
s that are causing the issue because rendering is expensive.
Am I correct in thinking this? Is there any way to chain these fillRect
s together and use one render each iteration like one would with paths as opposed to a new render for each fillRect
?
Other performance improvements are welcome as well.
var imgCanvas = document.createElement('canvas'),
imgContext = imgCanvas.getContext('2d'),
pixelInterval = 5, // Rather than inspect every single pixel in the image inspect every 5th pixel
img = document.getElementById("imageToBeUsed"), // The image to be averaged
cellSize = 100, // The size of square to average the color
cw, // Canvas width
ch; // Canvas height
var cellCanvas = document.getElementById("cellCanvas"),
cellContext = cellCanvas.getContext('2d');
// Use a CORS proxy to allow the image to be used
img.crossOrigin = "Anonymous";
img.src = "https://crossorigin.me/" + img.src;
img.onload = function() {
// Once the CORS enabled image loads, do our work
newImg(img);
animateColorAvg();
}
// Updates the image to be used
function newImg(imgElem) {
img = imgElem;
// Set the height and width of the canvas elements to that of the image
cw = imgCanvas.width = cellCanvas.width = img.naturalWidth || img.offsetWidth || img.width;
ch = imgCanvas.height = cellCanvas.height = img.naturalHeight || img.offsetHeight || img.height;
// Draw the image to our hidden canvas for calculations
imgContext.drawImage(img, 0, 0);
}
// Animate the movement of the cells
var offset = 0,
playSpeed = 0, // Determines how often the canvas is updated
lastTime = Date.now();
function animateColorAvg() {
if(Date.now() - lastTime > playSpeed) {
lastTime = Date.now();
offset += 5;
if(offset >= cellSize)
offset = 0;
gridifyAvgColors(offset);
}
window.requestAnimationFrame(animateColorAvg);
}
// Creates a grid of average colors of the current image
function gridifyAvgColors(cellOffset) {
var numXCells = Math.ceil(cw / cellSize),
numYCells = Math.ceil(ch / cellSize);
// Start at the bottom right, go to top left
for(var i = numXCells; i >= 0; i--) {
for(var j = numYCells; j >= 0; j--) {
var x = i * cellSize - cellSize + cellOffset,
y = j * cellSize - cellSize + cellOffset,
color = getAvgColorAsRGB(x, y);
cellContext.fillStyle = "rgb(" + color.r + ", " + color.g + ", " + color.b + ")";
cellContext.fillRect(x, y, cellSize, cellSize);
}
}
}
// Gets the average color of the requested area
function getAvgColorAsRGB(startX, startY) {
// Check to see if a cell placed at this location covers any part of the image
if(!( (startX < cw
&& startY < ch)
|| (startX + cellSize >= 0
&& startY + cellSize >= 0)
))
return; // Don't calculate anything because it's not covering a part of the image
var cellSizeX = cellSize,
cellSizeY = cellSize;
// Account for negative starting values that still cover a part of the image
if(startX < 0) {
cellSizeX = 0 - startX;
startX = 0;
}
if(startY < 0) {
cellSizeY = 0 - startY;
startY = 0;
}
// Account for values that go outside of the image
if(startX + cellSizeX > cw && startX != cw)
cellSizeX = cw - startX;
if(startY + cellSizeY > ch && startY != ch)
cellSizeY = ch - startY;
var rgb = { r:0, g:0, b:0 },
count = 0,
i = -4,
data, length;
try {
data = imgContext.getImageData(startX, startY, cellSizeX, cellSizeY);
} catch(e) {
// Catch errors - usually due to cross domain security issues
console.log(e);
return;
}
data = data.data;
length = data.length;
while ((i += pixelInterval * 4) < length) {
count++;
rgb.r += data[i];
rgb.g += data[i + 1];
rgb.b += data[i + 2];
}
// Round the number values
rgb.r = Math.floor(rgb.r / count);
rgb.g = Math.floor(rgb.g / count);
rgb.b = Math.floor(rgb.b / count);
return rgb;
}
<canvas id="cellCanvas"></canvas>
<img id="imageToBeUsed" src="http://img2.timeinc.net/people/i/2013/pets/news/130304/kitten-3-600.jpg"/>