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');
 });
});
asked Mar 21, 2014 at 22:38
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Can you provide a fiddle? So that we can see it in action? \$\endgroup\$ Commented Mar 22, 2014 at 2:22

1 Answer 1

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

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.