4
\$\begingroup\$

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 .fillRects that are causing the issue because rendering is expensive.

Am I correct in thinking this? Is there any way to chain these fillRects 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"/>

\$\endgroup\$
  • \$\begingroup\$ Can you please update your examples? I think the image is broken \$\endgroup\$ – jaggedsoft Oct 20 '16 at 19:05
  • \$\begingroup\$ @NextLocal It works for me in Chrome \$\endgroup\$ – Zach Saucier Oct 20 '16 at 23:53
1
\$\begingroup\$

After a suggestion by a friend, I realized it wasn't the .fillRect that was causing the performance issue, but rather the looped imgContext.getImageData(). I originally thought it wasn't this because it seemed to work fine with large cells (it seems dumb to think that now).

After changing to a single getImageData() and converting the rest of the calculations to match, it now performs flawlessly!

var imgCanvas = document.createElement('canvas'),
    imgContext = imgCanvas.getContext('2d'),
    pixelInterval = 5, // Rather than inspect every single pixel in the image inspect every 5^2 pixel
    img = document.getElementById("imageToBeUsed"), // The image to be averaged
    cellSize = 20, // 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();
}

var data; // A 1D array

var numXCells,
    numYCells;

// 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);
  
  // Set the image data
  try {
    data = imgContext.getImageData(0, 0, cw, ch);
  } catch(e) {
    // Catch errors - usually due to cross domain security issues
    console.log(e);
    return;
  }

  data = data.data;
}

// Animate the movement of the cells
var offset = -5,
    playSpeed = 1000, // 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) {
  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) {
	var rgb = { r:0, g:0, b:0 };
  
  // 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 rgb; // 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 count = 0;
      
  for(var j = startY; j < startY + cellSizeY; j += pixelInterval) { 
  	for(var i = startX; i < startX + cellSizeX; i += pixelInterval) { // Do this second for lookup perf reasons
      count++;
      var index = (i + j * numXCells * cellSize) * 4;
      rgb.r += data[index];
      rgb.g += data[index + 1];
      rgb.b += data[index + 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"/>

P.S. There's no way to chain .fillRects, but that likely isn't the cause of the performance issue.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.