HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Utilizing HTML5 Canvas to apply color theme to .png icons

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
iconspngutilizingapplycolorhtml5themecanvas

Problem

Because we have a mix of both icons for core items and .png icons for custom items, I've developed an easy way to theme them (alter the color) all of them easily that will work across all browsers.

Prior to this, I tried CSS and SVG options and they just weren't as simple as this option (but probably less overhead of course). I have to call this after load because of course can't start any canvas work until the items actually hit the DOM and they're parsing through via PHP. There's about 50 of these icons max, more like 25 of them to iterate thought on 90% of users.

Any suggestions on how to streamline this or possibly simplify it? Also I would have posted a test (fiddle or bin) but canvas has a strict cross domain policy.

Update: Found an image on the jsfiddle shell I can use for an example that adheres to the cross-domain policy.

JSFiddle: http://jsfiddle.net/d3c0y/6BSsy/

```
function changeColor(imgObject, imgColor) {
// create hidden canvas
var canvas = document.createElement("canvas");
canvas.width = 40;
canvas.height = 40;

var ctx = canvas.getContext("2d");
ctx.drawImage(imgObject, 0, 0, 40, 40);

var map = ctx.getImageData(0, 0, 40, 40);
var imdata = map.data;

// convert image to grayscale first
var r, g, b, avg;
for (var p = 0, len = imdata.length; p < len; p += 4) {
r = imdata[p]
g = imdata[p + 1];
b = imdata[p + 2];

avg = Math.floor((r + g + b) / 3);

imdata[p] = imdata[p + 1] = imdata[p + 2] = avg;
}

ctx.putImageData(map, 0, 0);

// overlay using source-atop to follow transparency
ctx.globalCompositeOperation = "source-atop"
ctx.globalAlpha = 0.3;
ctx.fillStyle = imgColor;
ctx.fillRect(0, 0, 40, 40);

// replace image source with canvas data
return canvas.toDataURL("image/png", 1);
}

jQuery(window).load(function () {
$('.custom-module').each($.proxy(function () {
$(this).attr('src', changeColor(this, '#33CC33'));

Solution

Pretty nice overall. You're missing a semi-colon or two, but otherwise it's a nice solution.

The only real criticism I have are the "magic numbers" in there, like the 40x40 size that's hardcoded everywhere. Technically, you could derive the canvas dimension from the image, or pass the width/height as arguments. (Incidentally, setting a canvas's size will also clear its contents, which is helpful if there's one shared canvas that needs to be cleared between uses.) In either case, though, you'd have to get rid of the hardcoded "40".

Optimization-wise, the only addition I've made is to simply skip pixels with a zero alpha component; if they're transparent anyway, there's no need to bother with them.

Other than that, I've just broken the code into a couple of functions; nothing special (Note, haven't tested this yet.)

var changeIconColor = (function () {
  var canvas  = document.createElement("canvas"), // shared instance
      context = canvas.getContext("2d");

  // only place the dimensions are hardcoded
  // everything else just references canvas.width/canvas.height
  canvas.width  = 40;
  canvas.height = 40;

  function desaturate() {
    var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
        pixels    = imageData.data,
        i, l, r, g, b, a, average;

    for(i = 0, l = pixels.length ; i >> 0; // quick floor
      pixels[i] = pixels[i + 1] = pixels[i + 2] = average;
    }

    context.putImageData(imageData, 0, 0);
  }

  function colorize(color) {
    context.globalCompositeOperation = "source-atop";
    context.globalAlpha = 0.3; // you may want to make this an argument
    context.fillStyle   = color;
    context.fillRect(0, 0, canvas.width, canvas.height);
    // reset
    context.globalCompositeOperation = "source-over";
    context.globalAlpha = 1.0;
  }

  return function (iconElement, color) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.drawImage(iconElement, 0, 0, canvas.width, canvas.height);
    desaturate();
    colorize(color);
    return canvas.toDataURL("image/png", 1);
  };
}());


As for invoking it, I don't think you need the $.proxy and promise stuff. I would think it'd be enough to just do

$('.custom-module').each(function () {
    this.src = changeIconColor(this, '#33CC33');
});
$('.module-li').removeClass('hidden');

Code Snippets

var changeIconColor = (function () {
  var canvas  = document.createElement("canvas"), // shared instance
      context = canvas.getContext("2d");

  // only place the dimensions are hardcoded
  // everything else just references canvas.width/canvas.height
  canvas.width  = 40;
  canvas.height = 40;

  function desaturate() {
    var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
        pixels    = imageData.data,
        i, l, r, g, b, a, average;

    for(i = 0, l = pixels.length ; i < l ; i += 4) {
      a = pixels[i + 3];
      if( a === 0 ) { continue; } // skip if pixel is transparent

      r = pixels[i];
      g = pixels[i + 1];
      b = pixels[i + 2];

      average = (r + g + b) / 3 >>> 0; // quick floor
      pixels[i] = pixels[i + 1] = pixels[i + 2] = average;
    }

    context.putImageData(imageData, 0, 0);
  }

  function colorize(color) {
    context.globalCompositeOperation = "source-atop";
    context.globalAlpha = 0.3; // you may want to make this an argument
    context.fillStyle   = color;
    context.fillRect(0, 0, canvas.width, canvas.height);
    // reset
    context.globalCompositeOperation = "source-over";
    context.globalAlpha = 1.0;
  }

  return function (iconElement, color) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.drawImage(iconElement, 0, 0, canvas.width, canvas.height);
    desaturate();
    colorize(color);
    return canvas.toDataURL("image/png", 1);
  };
}());
$('.custom-module').each(function () {
    this.src = changeIconColor(this, '#33CC33');
});
$('.module-li').removeClass('hidden');

Context

StackExchange Code Review Q#45041, answer score: 3

Revisions (0)

No revisions yet.