About a week ago, I posted about drawing simple shapes in HTML5. HTML5’s Canvas isn’t only useful on the client side.
About a year ago, I rewrote my Quaketracker mashup which pulls rss content from USGS and displays markers on a Google Map. For part of this rewrite, I wanted to use custom markers to indicate the magnitude of the earthquake, both numerically and colorfully. That task was more involved than I realized it would be. There doesn’t seem to be any easy way to generate such a sprite in The Gimp or other graphics software, so I set out to generate a sprite of magnitudes 0.0 through 9.9 with a color range from yellow to orange using node-canvas. This actually made the task very simple.
Before writing the script, I had to decide on an image. I created a pretty simple quote-bubble-like marker with a white background and a black outline. The image is 35×35 square with some transparency padding the actual marker image. This allows me to calculate the layout pretty easily. The white background on black outline gives me a very specific color to replace with the desired color of intensity.
node-canvas is a custom implementation of Canvas for node.js. When I originally wrote this script, I had a few issues compiling node-canvas. When I tweaked the script today, I had no issues. However, it seems like some things are not well-documented or incomplete (PixelArray).
Scripting it in node.js
A lot of times, if your script writes out to files, it’s best to check for dependencies and cause your script to exit if they’re not met. For example, I know this script is completely useless without node-canvas, so I can probe for the availability of that module and provide directions for installation. I’d do this before attempting to load any other functions from the canvas module. You could also check for versions at this point. If someone runs this script with node-canvas 0.6.0 it will still work, but this may not be the case with other modules. Here’s how I start off the script.
var p = require('util').puts;
try {
var probe = require('canvas');
} catch(e) {
p("Cannot find node-canvas module.");
p("Did you install dependencies:\n");
p("\tnpm install -d");
process.exit();
}
var fs = require('fs');
var Canvas = require('canvas');
var Image = Canvas.Image;
var canvas, ctx, baseImage, outImage, img;
I don’t usually use ‘global’ variables like those in the last line of the above snippet for applications or client-side scripting. For a script like this, you could have everything as a global variable as long as the names don’t clash with your imported modules.
To initialize shared variables, I like to define an init() function and call that function at the end of the script. This is pretty common in other languages like Ruby and Python, so why node server-side JavaScript?
var init = function() {
// our working image is 35x35, we want 10x10 sheet of sprites
canvas = new Canvas(35*10,35*10);
ctx = canvas.getContext('2d');
baseImage = __dirname + '/base.png';
outImage = __dirname + '/markers.png';
// pre-load the image
img = new Image;
img.onload = function() {
processImage(fileProcessingComplete);
};
img.src = baseImage;
};
The cool thing about node-canvas is that the usage is very similar to the Canvas API implemented in browsers supporting the HTML5 Canvas. In fact, aside from the __dirname special property of node.js, you should be able to drop this init function into a browser with no problems. For this script, I want 100 different magnitudes to be generated from 0.0 through 9.9 (00-99 is 100) so the canvas is initialized to 10 rows and 10 columns of 35×35 squares. Just like a client-side Image object, we want to bind any post-processing of the image via the onload() function.
I’m going to Memento you a bit, and for that I’m sorry but the end is the easier concept. When all the processing is complete, we want to pull image data from the Canvas element and write it out to a file.
var fileProcessingComplete = function() {
var out = fs.createWriteStream(outImage),
stream = canvas.createPNGStream();
stream.on('data', function(chunk){
out.write(chunk);
});
// when PNG stream is done, drain WriteStream
stream.on('end', function(){
p('saved ' + outImage);
out.destroySoon();
});
};
Accessing streams in node.js is usually done asynchronously. We can take chunks of data from the ReadStream of createPNGStream() and pump that to the WriteStream of the output file. When the data from the PNG is done streaming, we tell the output stream to finish writing its buffered data and destroy itself. It sounds intense, but it’s pretty straightforward.
Processing the colors and marker images is the fun part. To match the function call in the init function, we just create a function with a callback in the standard way.
var processImage = function(cb) {
// other code removed
if(typeof cb === "function") {
cb.call(this);
}
}
If there’s a callback function passed as a parameter, when processImage is finished, it will call that function passing the current scope as the execution scope and no parameters.
To draw out the custom marker, we’ll want to define how we want to display the text and at what color we want to have the 0.0 marker start.
ctx.font = 'normal 12px Impact';
ctx.textAlign = 'center';
var color = [255, 255, 0, 235];
The color array represents red, green, blue, and alpha channel (opacity). This may look odd if you’re used to specifying alpha channels in css as “rgba(255,255,0,0.92)”, but that value of 235 will give roughly 92% opacity.
We can make the script run pretty quickly by using a second canvas 2d context for the recolor phases (which modifies every white pixel).
// use a temp Canvas and Context of 35x35 size.
var tempCanvas = new Canvas(35,35);
var tempCtx = tempCanvas.getContext('2d');
This is an optimization I made today when tweaking the original script. In the original version of this script, I would write the base image to the output canvas then iterate over ‘NxN’ pixels on the output canvas to change the color of one pixel. Using a temporary canvas context, we only have to iterate over 35 pixels wide by 35 pixels high for every new marker. This not only speeds up the process, but it could be a difference of having a script that won’t run on slower machines.
For each of the desired 100 magnitudes, we’ll want to find the x and y values (top-left pixel) to which we’ll write the updated marker. For every 1/2 magnitude, the Green color value decrements by 13 points.
for(var magnitude = 0; magnitude <= 100; magnitude++) {
var y = 35 * Math.floor(magnitude/10),
x = ( 35*(magnitude % 10) );
// This increments the color slightly
if(magnitude % 5 == 0){
color[1] = color[1] - 13;
}
// some code removed
ctx.fillText("" + parseFloat(magnitude / 10, 1), x + (35/2), y + (35/2), 35);
}
The last line of the above snippet fills the magnitude text on the output canvas. This could be moved to the next part of the script for a further optimization: instead of just modifying each white pixel’s color, we could recolor and apply the text.
The part of the script which changes the color according to the magnitude looks like this:
tempCtx.drawImage(img, 0, 0, 35,35);
var imgData = tempCtx.getImageData(0, 0, 35, 35);
var data = imgData && imgData.data;
if(data) {
try {
for(var pixel=0;pixel<data.length;pixel=pixel+4) {
var red = data[pixel]
var green = data[pixel+1];
var blue = data[pixel+2];
var alpha = data[pixel+3];
if(red == 255 && green == 255 && blue == 255) {
data[pixel] = color[0];
data[pixel+1] = color[1];
data[pixel+2] = color[2];
data[pixel+3] = color[3];
}
}
} catch (err) { console.error(err.message); }
// Write our temp image data to the final canvas context
/* imageData, dx, dy, sx, sy, sw, sh */
ctx.putImageData(imgData,x,y,0,0, 35, 35);
}
Writing to the temporary canvas’s 2d context gives us access to a data buffer which represents a 35×35 image where each pixel is represented by 4 bytes (rgba). Iterating this buffer by 4’s, we can check that the given pixel (excluding alpha because that doesn’t matter in this case) is white or rgba(255,255,255). If the pixel is white, we replace those 4 indexes with the values in our color array. When we’re done iterating the buffer representing the 35×35 image, we can call putImageData on the output canvas. With that, the sprite is complete and our callback handles writing the file.
A note about putImageData
putImageData can be a strange beast. Notice the comment imageData, dx, dy, sx, sy, sw, sh… these are the parameters accepted by the function. Even the HTML5 Specs might make you go, “Uhm… what?” The idea is pretty simple once you understand it. You want to write a buffer (imgData) where the top-left pixel is at dx,dy. sx and sy (or dirtyX and dirtyY) represent a dirty rectangle of size sw x sh (or dirtyWidth by dirtyHeight).
If the imageData passed to the function is the same height and width as the target canvas context, you wouldn’t want to overwrite every single pixel would you? You’d only want to overwrite pixels that have changed. Suppose in this script, I had an imageData object of 350×350 to complement the output canvas. When putting the modified imageData, I would have to calculate the current 35×35 box offset and write out the imageData to 0,0. As an example, if I wanted to overwrite the 35×35 box for the 9.9 magnitude marker with an imageData buffer of 350×350, I might write:
ctx.putImageData(imgData, 0, 0, 315, 315, 35, 35);
This would tell the context that although my buffer is 350×350, I’ve only changed a 35×35 rectangle starting at 315×15. Canvas is smart enough to only update those pixels rather than every pixel in the canvas.
On the other hand, in the script I’ve written, I only have an imageData object which represents a 35×35 buffer. So, I can tell putImageData to start at the point defined by (x,y) and write the buffer into a 35×35 rectangle. I like this method a little more, but it may not always be the case that you’re writing a smaller buffer to the context.
The code
As always, the code for this blog post is available on github at jimschubert/blogs.
The sprite
Here’s what the generated sprite looks like.