[nolan@nprescott.com] $>  cat blog archive feed

Experimenting with HTML5: Canvas

2015-11-24

Recently interested in exploring new browser capabilities, I'm taking the opportunity to play around with the HTML5 canvas element and see if I can create something at least passingly interesting (or useful).

The easiest purely-JavaScript-way to get started without creating an HTML template is to create the <canvas> and then append it to the current document; which is handled by the setup function below:

    function setup(canvasTarget, w, h) {
        var canvas = document.createElement( 'canvas' );
        canvas.width = w;
        canvas.height = h;

        var context = canvas.getContext( '2d' );
        document.querySelector(canvasTarget).appendChild(canvas);
        return context;
    }

    (function(){
        var WIDTH = 500;
        var HEIGHT = 100;
        var context = setup('#canvas-container-1', WIDTH, HEIGHT);

        function draw() {
            var time = new Date().getTime() * 0.001;
            var x = Math.sin(time) * 200 + 210;
            var y = HEIGHT/1.5;
            var radius = 5;

            context.fillStyle = 'black';
            context.beginPath();
            context.arc(x, y, radius, 0, Math.PI * 2);
            context.closePath();
            context.fillText('current x: ' + x.toFixed(0), 10, 20);
            context.fillText('current y: ' + y.toFixed(0), 10, 40);
            context.font = '16px Sans Serif';
            context.fill();
        }

The draw function is where most of the interest lies with this first example. The context.arc method is being used to create the circle (here just a black dot) with an x position dependent on the current time. The multiplier and addition to the sine function is simply there to exaggerate the motion and make it more visually interesting.

I added the position text to illustrate the same.

      function animate() {
          requestAnimationFrame(animate);
          context.clearRect(0, 0, WIDTH, HEIGHT);
          draw();
      }

      animate();
  })();

The real point of interest is in the call to requestAnimationFrame, which hooks into the browser's repaint cycle in an effort to maximize the refresh rate (hopefully somewhere near 60FPS). The animate function is passed as a parameter, a callback to be executed on the next repaint.

The two big wins for rAF are the single repaint cycle, which can be optimized by the browser to align with CSS and DOM elements; and the fact that the animate callback will only fire when the browser tab has focus.

A 5px pendulum is only so interesting though. One thing that is surprisingly hard is framing ideas for a canvas element declaratively. I might be able to draw something, but it's entirely different to give explicit instruction for how to draw it. Moreover, I don't really care at this point about a contrived example of animating a simple canvas element. I decided what would be more interesting is a more "real world" use case for the canvas element.

An Image Placeholder

I settled on a client-side image placeholder. Several examples exist, some require a remote server (placekitten) and some are entirely client-side (holderjs). But what I really wanted was something simple. I can't quite imagine requiring 2000+ lines of code or an internet connection for an image placeholder - no matter how much it is capable of or how cute the placeholders are.

For my barebones placeholder I limited functionality to:

function placeholder(canvasTarget, width, height, bgColor, textColor) {
    var text = width + "x" + height;
    var canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;

    var context = canvas.getContext('2d');
    context.font = (width >= 250) ? "50px Arial" : "25px Arial";

The usefulness of the font-size logic could be argued, but I thought a sane default would be to resize when images were too small. In my minimal testing, I wouldn't ever want text larger than 50px and 25px is small enough to fit most thumbnail-sized images (75x75).

    context.fillStyle = bgColor;
    context.fillRect(0, 0, width, height);
    context.fill();

    var textWidth = context.measureText(text).width;
    context.fillStyle = textColor || "grey";
    context.textBaseline = 'middle';
    context.fillText(text, width/2 - textWidth/2, height/2);

    document.querySelector(canvasTarget).appendChild(canvas);
}

One slight stumbling block in my inital understanding of the canvas element was how fills were rendered. I had initally placed the call to context.fill() at the end of the style declarations, which resulted in the text being overwritten with the background color. It seems calls to "draw" on the canvas implicitly overwrite the existing elements, which is fixed by first filling the canvas with the background rectangle and then filling the text content.

Another thing to note was the text placement with textBaseline, to vertically align the text within the canvas. Horizontal placement is straight-forward once you remember to subtract half the width of the text itself from the centering of the textbox.

The only requirement for the placeholder function to work is a DOM selector to inject the canvas element into. In the following examples I've used <span> tags due to their default inline display (similar to <img> tags).

<span id="canvas-container-2"></span>
placeholder('#canvas-container-2', 250, 350, "#d8d8d8");
placeholder('#canvas-container-3', 130, 120, "#a7a7a7");
placeholder('#canvas-container-4', 450, 100, "#c8c8c8", "black");

Addendum for Mobile Use

Mobile users may note that the image widths are not indicative of actual image size, this is due to the following CSS rule:

canvas { max-width: 100%; }

Which makes for a more pleasant browsing experience, at the expense of confusing the image text (i.e. 450x100 may get resized to be smaller than 250x350). I feel like this is a more appropriate action than the alternative of forcing a huge screen width to maintain the real image dimensions. This was a fun bug to fix because it provided immediate use for the placeholders as they were intended when I wrote them (an hour ago). Time to add "responsive web design" to my resume.

All in all, I found the canvas element a pleasant surprise to work with. I can't yet speak to it's usefulness for things like games but the API feels well considered and very much a product of the "modern web". Any complaints I may have about the declarative nature of "drawing" on the canvas seem more a product of computers than of the canvas element itself.

[nolan@nprescott.com] $> █