Progressive drawing of simple SVGs on the HTML5 Canvas element

The HTML5 (is there supposed to be a space? I’m not sure) Canvas element is pretty cool! A recent school project involved visualizing some different data sets from factual.com in an “interesting” manner. Most of what we did just a standard Google Maps API mash-up but I decided to spruce it up a bit with an intro animation using the Canvas element. My group was visualizing TARP data and from this decidedly American theme I added an animation to the background that drew an outline of the 50 states. This really was my first exposure to JavaScript and HTML5 (mainly being a C++ developer), so it was an interesting learning experience.
Start animation

If you had an HTML5 browser (IE 9+, Firefox 2.0+, Safari 3.1+, Chrome 4+) you’d see something cool here!


A lot of the simpler SVGs contain only simple line drawing commands, so writing a parser for them wasn’t hard (although extending it to curves is generally straightforward). The original SVG for the animation is here. The bulk of the data is kept inside path elements like this:


Apart from the style attributes (which I’ve chosen to ignore), the “d” attribute is really where the meat is. The general format is a list of one-character commands, followed possibly by an argument. The ones we’re interested are:

  • M: Move to
  • L: Line to
  • z: Finish path

As you can imagine, these translate wonderfully to the Canvas functions

I combined all of the states into one massive drawing command and parsed them into a 2-dimensional array:

function parseSVG(svg,scale,normalx,normaly) {
	var parts = svg.split(" ");
	var ret = new Array();
	
	var i = 0;
	var len = parts.length;
	for(i = 0 ; i < len ; ++i)
	{
		var type = -1;
		var first = 0;
		var second = 0;
		
		if(parts[i] == "M") //absolute move
			type = 0;
		else if(parts[i] == "L") //absolute line to
			type = 1;
		else if(parts[i] == "z") //end path
			type = 2;
			
		switch(type)
		{
			case 0: case 1:
				//parse point
				var coord = parts[++i].split(",");
				first = parseFloat(coord[0]) * (scale / normalx);
				second = parseFloat(coord[1]) * (scale / normaly);
				ret.push(new Array(type,first,second));
				break;
			case 2:
				ret.push(new Array(type));
				break;
		}
	}
	return ret;
}

The function takes the original width and height values of the SVG and scales each vertex by this value, essentially giving us "normalized" points that can be drawn to arbitrary scale. In my actual project the drawing is the background of login page, and needs to resize nicely as the window does.

Once we have the points parsed the obvious next step is drawing. Each animation frame, we calculate the percentage of the total animation time (determined by an arbitrary constant) that has elapsed, and then draw that many more points from the parsed data onto an offscreen buffer (saving where we left off or the next iteration), and then copy it over to the visual canvas. The offscreen buffer is especially necessary if we're going to draw anything over the animation, since that needs to be applied after the drawing of the animation. The final drawing code looks something like this:

function animate(lastDrawTime) {

	var now = new Date().getTime();
	var canvas = document.getElementById(canvasName);
	var canvasParent = document.getElementById(canvasParentName);
	var context = canvas.getContext("2d");
	
	//check if the window has been resized
	var resized = 0;
	if( lastWidth != canvasParent.clientWidth || lastHeight != canvasParent.clientHeight || firstMapTime == 0) {
		context.canvas.width = (canvasParent.clientWidth) - 20;
		if(flexible)
			context.canvas.height = (canvasParent.clientWidth * aspectRatio) - 20;
		else
			context.canvas.height = (canvasParent.clientHeight) - 20;
		resized = 1;
		
		lastWidth = canvasParent.clientWidth;
		lastHeight = canvasParent.clientHeight;
		
		offscreenMapBuffer = document.createElement("canvas");
		offscreenMapBuffer.width = lastWidth;
		offscreenMapBuffer.height = lastHeight;
		
	}

	if( (finished == 0) || (resized == 1))
	{
		//clear the canvas
		context.clearRect(0, 0, canvas.width, canvas.height);
	
		/* draw a new frame */
		if(startTime == 0)
			startTime = now;
			
		//procedurally draw the map onto an offscreen buffer, and then blit it onto the visible screen.
		//this saves us from having to redraw the entire map on every frame (pretty slow unless you have a fast computer)
		if(firstMapTime == 0) {
			if(now - startTime > startMapTime)
				firstMapTime = now;
		}
		if(firstMapTime != 0) {
			var mapContext = offscreenMapBuffer.getContext("2d");
			mapContext.lineWidth = 1;
			
			//how far into drawing are we?
			var diff = now - firstMapTime;
			var percentdone = diff >= totalMapTime ? 1 : (diff / totalMapTime);
			var to = path.length * percentdone;
			var i = resized ? 0 : lastMapVertex;
			
			var width = canvas.clientWidth;
			var height = canvas.clientHeight;
			
			if(resized == 1) //need to clear the offscreen buffer
				mapContext.clearRect(0, 0, width, height);
			
			mapContext.strokeStyle = "#D3D3D3";
			while( i < to)
			{
				switch(path[i][0]) {
					case 0: //move to
						mapContext.moveTo( (path[i][1] * width), (path[i][2] * height) );
						break;
					case 1: //line to
						mapContext.lineTo( (path[i][1] * width), (path[i][2] * height) );
						break;
					case 2:
						mapContext.stroke();
						break;
				}
				++i;
			} 
			lastMapVertex = i;
			mapContext.stroke();
				
			if(to >= path.length)
				finished = 1;
			
			context.drawImage( offscreenMapBuffer, 0, 0 );
		}
		
	}

	//request new frame
	if(!finished)
		requestAnimFrame( function() { animate(now) } );
}

The JavaScript is pretty ugly but it gets the job done. The good thing about using Canvas instead of something like Flash is it works on my iPad :D.

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.


Posted

in

by

Comments

Leave a Reply to Anonymous Cancel reply

Your email address will not be published. Required fields are marked *