The Road to HTML5: SVG, Canvas, and faster performance

For the upcoming 4.5.5 release of the Gliffy Confluence Plugin, we improved both performance and consistency of SVG rendering across various web browsers. It was a big step in our HTML5 development. Keep reading to find out how we did it:

Our HTML5 Viewer release was a great milestone for us. It allowed us to see whether we could accurately display our symbols and diagrams using only the canvas api. It was a great proof of concept, but we found that our initial implementation was much too slow for our liking.

Our 4.5.5 release features a much quicker, nimbler, and less resource hungry HTML5 viewer implementation. In this post, we’ll walk you through some of the problems we overcame.

What is Canvas?
If you’re unfamiliar with canvas, the basic idea is that you have a canvas element in the page with a given width and height. This is essentially a blank box that you draw to with javascript. The commands are quite simple: just imagine that you are verbally giving very precise and detailed commands to someone holding a pen. Pick up the pen and move to point. Draw a line to this point. Fill in the drawn path.

How hard is it to convert a flash app to an HTML5 app? Well, from our experience, it is more challenging than it sounds. The canvas api is, at its heart, a very low level HTML5 drawing api. It doesn’t handle things like multi-line text, or dashed lines, or pixel snapping. Heck, it doesn’t even provide simple font metrics for things like text height, or let you query for the current state of the canvas (the matrix transform). While canvas libraries are in development, you may still find yourself solving low level graphics problems, debugging performance issues, and trying to hack your way around annoying browser bugs.

So, why Canvas?
HTML5 actually includes a slew of technology. You may wonder why Gliffy went ahead with something like canvas versus scalable vector graphics (SVG).

To give you a quick overview, SVG is basically XML that describes a scene, and while canvas and SVG are often compared, they are quite different from one another. The most obvious difference is that canvas is a bitmap. This means that it keeps track of all the pixels that it contains and basically what color it should be. Canvas acts a bit like Paint: you can draw a circle to it, but you can’t select the circle again. Canvas simply doesn’t know that a circle was drawn to it — it’s all just bits. With that in mind, when you resize a canvas it will get blurry, like an image does, because it is just a bitmap.

SVG is also declarative whereas canvas is procedural, which means that instead of telling someone exactly how to draw a circle, we just say it’s a circle and let the browser sort out the details. SVG is far more human-readable and doesn’t require a long list of canvas commands. In fact, our symbols are stored in our codebase as SVGs before they’re processed as flash assets.

So why not SVG?
The main reason was inconsistent implementation. Browser support for SVG is still not as good as canvas, even though the 1.1 recommendation is nearly a decade old. Most modern browsers do (thankfully) support basic SVG elements but have not yet implemented some of the more advanced functions.

Another gotcha is data export. While the canvas api has a toDataUrl() method, which allows you to get the drawn canvas bitmap as a base 64 encoded string, there is no such equivalent call for SVG. Converting SVG to to an image would require a server side call, and this means that images may be rendered differently, depending on how good our server implementation of SVG (batik) is compared with a given browser.

The first approach
So, to recap the problem, our symbols were stored in SVG format, but we wanted to use canvas. How did we solve this?

Here’s what we came up with on our first try:

Create an SVG template

Below is an example of our circle symbol template, using the fresh skin:

<svg xmlns=”http://www.w3.org/2000/svg” version=”1.1″ preserveAspectRatio=”none”>
    <defs>
        <linearGradient id=”grad2″ x1=”100%” y1=”0%” x2=”0%” y2=”0%”>
            <stop stop-color=”{{stop_color_1}}” offset=”0%”/>
            <stop stop-color=”{{stop_multiply_color_2}}” offset=”100%”/>
        </linearGradient>
        </defs>
<svg xmlns=”http://www.w3.org/2000/svg” version=”1.1″ width=”{{width}}” height=”{{height}}” viewBox=”0 0 50 50″ overflow=”visible” preserveAspectRatio=”none”>
    <path fill=”{{stroke}}” d=”M25,50C11.214,50,0,38.785,0,25S11.214,0,25,0c13.785,0,25,11.215,25,25S38.784,50,25,50L25,50z”/>
    <circle fill=”#000000″ fill-opacity=”.3″ cx=”25.806″ cy=”25.918″ r=”23″/>
    <circle cx=”25″ cy=”25″ r=”23.148″ style=”{{#gradient}}fill:url(#grad2);{{/gradient}}” fill=”{{^gradient}}{{fill}}{{/gradient}}”/>
</svg>
</svg>

Fill the details in using Mustache
If you didn’t catch it before, the above snippet wasn’t valid SVG, it was a Mustache template. Each variable is enclosed by brackets, like {{my_varianble}}, which can be replaced by a string value.

Mustache is a popular logic-less templating system. It’s most well known for creating client-side html templates, but there’s nothing to stop you from creating SVG templates with it.

Render the SVG template with Canvg
Finally render the SVG template with Canvg.

Wait, what? Let’s back up a bit.

Here’s where the merits of HTML5 really shine. Someone else had our same issue and solved it already. Canvg is an SVG parser and renderer written by Gabe Lerner. It does exactly what you expect it to, taking an SVG file, parsing and translating the SVG into canvas commands and drawing it to a canvas element using javascript.

You can play around with it here. It’s actually quite good.

In our first viewer implementation, we added a few improvements on our branch of Canvg, like non-scaling stroke support, dashed line support (lots of math), and a bunch of other smaller bug fixes and goodies.

It worked! It was great! It allowed us to standardize all the shapes we’d been building since 2005. But…

Okay, so it was a bit slow…
As you would expect, doing intensive string parsing in your browser with javascript was maybe not the best idea. Canvg, while fast for javascript, was just too slow for our use.

We profiled our viewer, and time and again, Canvg rose to the top as being the slowest bottleneck.

The Answer: Transforming SVG to Canvas Drawing Commands with Java
So what exactly is in our 4.5.5 release? We removed the Canvg and Mustache libraries, and replaced them with a stencil service that returns canvas drawing commands.

To make this possible, I was tasked with creating a transformer in Java that would take SVG and return canvas drawing commands.

But it does more than just that.

This transformer is able to not only handle valid SVG, it can also process simple SVG Mustache templates. You can put simple Mustache variables like width, height, shape color, stroke width, and so on, in the SVG template, and this transformer will return canvas commands, keeping those mustache variables as javascript variables in the output.

To see this in action, our circle template from above, turns into:

ctx.save();
ctx.strokeStyle='rgba(0,0,0,0)';
ctx.lineCap='butt';
ctx.lineJoin='miter';
ctx.miterLimit=10.0;
ctx.font='10px sans-serif';
ctx.save();
ctx.strokeStyle='rgba(0,0,0,0)';
ctx.lineCap='butt';
ctx.lineJoin='miter';
ctx.miterLimit=10.0;
ctx.font='10px sans-serif';
ctx.translate((((Math.min((data.width/5E1),(data.height/5E1)))*(-1E0))*0E0),(((Math.min((data.width/5E1),(data.height/5E1)))*(-1E0))*0E0));
ctx.scale((data.width/5E1),(data.height/5E1));
ctx.save();
ctx.fillStyle=(data.stroke==='none')?'rgba(0,0,0,0)':data.stroke;
ctx.font='10px sans-serif';
ctx.beginPath();
ctx.moveTo(2.5E1,5E1);
ctx.bezierCurveTo(1.1214E1,5E1,0E0,3.8785E1,0E0,2.5E1);
ctx.bezierCurveTo(0E0,1.1215E1,1.1214E1,0E0,2.5E1,0E0);
ctx.bezierCurveTo(3.8785E1,0E0,5E1,1.1215E1,5E1,2.5E1);
ctx.bezierCurveTo(5E1,3.8785E1,3.8784E1,5E1,2.5E1,5E1);
ctx.lineTo(2.5E1,5E1);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
ctx.save();
ctx.fillStyle='#000000';
ctx.fillStyle='rgba(0,0,0,.3)';
ctx.font='10px sans-serif';
ctx.beginPath();
ctx.arc(2.5806E1,2.5918E1,2.3E1,0.0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
ctx.save();
if(!data.gradient) {
ctx.fillStyle=(data.fill==='none')?'rgba(0,0,0,0)':data.fill;
}
if(data.gradient) {
var linearGradient = ctx.createLinearGradient(4.8148E1,1.852E0,1.852E0,1.852E0);
linearGradient.addColorStop(0E0,data.stop_color_1==='none'?'rgba(0,0,0,0)':data.stop_color_1);
linearGradient.addColorStop(1E0,data.stop_multiply_color_2==='none'?'rgba(0,0,0,0)':data.stop_multiply_color_2);
ctx.fillStyle=linearGradient;
}
ctx.font='10px sans-serif';
ctx.beginPath();
ctx.arc(2.5E1,2.5E1,2.3148E1,0.0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
ctx.restore();
ctx.restore();

Verbose, but fast!

These SVG templates, instead of being laboriously converted in your browser at each page load, are now only being converted once during the gliffy project build step. Your browser no longer has to do the grunt work.

It’s really the best of both worlds. Our shapes get to stay as SVG templates in our codebase for maintainability, and we’re able to provide canvas drawing commands directly for our users.

It’s a win-win.

An added bonus to all this is that since we have the ability to convert SVG and simple SVG templates to canvas drawing commands, the fantasy of supporting user-defined symbols becomes much closer to reality.

Also, it’s quite a bit faster
To prove it, I’ve created a few jsPerf tests. Depending on your browser and how it handles string manipulation, directly drawing a shape with canvas commands is around 3000% times faster!

Don’t believe it? Check out the jsperf tests yourself here: http://jsperf.com/canvg-vs-canvas/19