5
\$\begingroup\$

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'));
    }), this).promise().done(function () {
        $('.module-li').removeClass('hidden');
    });
});
\$\endgroup\$
  • 1
    \$\begingroup\$ Can you provide a fiddle? So that we can see it in action? \$\endgroup\$ – konijn Mar 22 '14 at 2:22
3
\$\begingroup\$

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 < 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);
  };
}());

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');
\$\endgroup\$
  • \$\begingroup\$ Added a working example over here with your changes. Thanks again! \$\endgroup\$ – d3c0y Mar 23 '14 at 20:40
  • \$\begingroup\$ @d3c0y Neat, glad it worked \$\endgroup\$ – Flambino Mar 24 '14 at 0:19

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.