Joseph Hallenbeck


February 3, 2014

Fiddling with HTML5’s Canvas

Filed under: Software Development — Tags: , , — Joseph @ 8:33 pm

Flattr this!

I had my first real exposure to the HTML5 Canvas element this week. It was a fairly fun transport back to Intro to Computer Graphics and my school days working in C.

Canvas provides a very simple bitmap surface for drawing, but it does so at the expense of loosing out on a lot of the built-in DOM. I suppose there is a good reason for not building an interface into canvas to treat drawings created with contexts as interactive objects, but sadly this leaves us with having to recreate a lot of that interactivity (has a user clicked on a polygon in the canvas? is the user hovering over a polygon on the canvas?) up to us to implement using javascript.

So let’s dive in and see what canvas is capable of doing!

This complete tutorial is available as a fiddle on jsfiddle.net. Check it out.

Getting Started

Let’s begin with the absolute basics. First, we need the element itself which is simply a “canvas” element with a specified id that we’ll later use to interact with it. By putting some textual content inside the canvas element we give some fallback for older browsers that might not offer canvas support.

<canvas id="myCanvas">Your browser does not support canvas.</canvas>

Now we need to interface with the element itself. This is done using javascript:

var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d")

We are doing two things here. First, we are getting the canvas element from the DOM, second we are getting a context from that element. In this case that context is the “2d” context which defines a simple drawing API that we can use to draw on our canvas.

Drawing a Polygon

The “2d” context API defines a number of methods for interacting with the canvas element. Let’s look at how we can use this to draw a blue triangle on our canvas:

context.moveTo(25, 25);
context.lineTo(75, 100);
context.lineTo(125, 25);
context.fillStyle = "#0000ff"; // Alternative: "rgba(0,0,255,1)"
context.fill();

Recall that pixels on a computer screen are mapped as though the screen was in the fourth quadrant of a plane — that is they spread out with x values growing larger as the pixels are placed further to the right and y values growing larger as they move towards the bottom of the screen. This puts the value 0,0 at the upper left corner of your screen and 25,100 located twenty five pixels to the right and one hundred pixels from the top.

The first three lines of code can be thought of as moving an invisible (or very light) pencil around the canvas. The first moves our pencil to the position 25,25 which should start the drawing near the upper-left corner of the canvas. The second line draws a line down 75 pixels and over and additional 25 pixels. The third returns to 25 pixels from the top, but 125 pixels from the left-hand side of the canvas.

The forth and fifth lines simply define the color to fill our polygon with and to actually do the filling. In this case we passed a hex value for blue, but we could alternatively used and rgba (red, green, blue, alpha) value if we wanted transparency.

Adding Interactivity

One thing you will note about our blue triangle: we can not tie off DOM events to it. The context merely draws on the canvas, but the drawings themselves do not exist in the DOM. The closest we can do is capture events on the canvas itself (onClick, hover, etc.). It is up to us to then decide if those events were just interacting with the canvas or whether they should are interacting with something drawn on the canvas.

First, we must recognize that each position that we move or draw the context to is a vertices.

PNPOLY is our solution, and to be honest, I did not come up with this one but found the answer on Stack Overflow

function pnpoly( nvert, vertx, verty, testx, testy ) {
  var i, j, c = false;
  for( i = 0, j = nvert-1; i < nvert; j = i++ ) {
    if( ( ( verty[i] > testy ) != ( verty[j] > testy ) ) &&
      ( testx < ( vertx[j] - vertx[i] ) * ( testy - verty[i] ) / 
      ( verty[j] - verty[i] ) + vertx[i] ) )           
    {
        c = !c;
    }
  }
  return c;
}
$('#myCanvas').click( function( e ) {
  var x = e.clientX;
  var y = e.clientY;
  alert( pnpoly( 3, [25,75,125], [25,100,25], x, y ) + ' x:' + x + ' y:'  + y );
});

PNPOLY takes five variables: the number of vertices (corners) on our polygon, an array of the X values, an array of the Y values, and the x/y cordinates where the user clicked on the canvas. Now if we add this to our code and run it we should see an alert saying either true or false as to whether we clicked inside or outside of our triangle.

Accounting for Global (Window) and Local Cordinate Systems

It is not easy to see on the jsFiddle website, but we can run into some issues with mapping between the local and global coordinate systems. e.clientX and e.clientY map to the document coordinate system not the canvas itself. We may, in some instances find ourselves needing to map between the local (canvas) coordinates which begins with 0,0 at the upper-left corner of the canvas element and the document coordinate system which begins with 0,0 at the upper-left most corner of the page.

This can occur when our canvas is absolutely positioned or positioned inside a fixed element. In these cases we must include the offset of the canvas from the document coordinate system to find where the click is actually occurring:

$('#myCanvas').click( function( e ) {
  var offset = $(this).offset();
  var x = e.clientX - offset.left;
  var y = e.clientY - offset.top;
  alert( pnpoly( 3, [25,75,125], [25,100,25], x, y ) + ' x:' + x + ' y:'  + y );
});

Note our additions to the first three lines in our function. The first line retrieves the offset for the position of our canvas from it’s global position. We then subtract that offset from e.clientX and e.clientY to get the coordinates of the click in the canvas’s coordinate system.

We might also need to add another variable to our offsets and that is to account for scrolling. If we have a canvas inside a fixed position element then we must also account for any potential scrolling that might have occurred. We do this via the scrollTop() and scrollLeft() jQuery functions:

$('#myCanvas').click( function( e ) {
  var offset = $(this).offset();
  var x = e.clientX - offset.left + $(window).scrollLeft();
  var y = e.clientY - offset.top + $(window).scrollTop();
  alert( pnpoly( 3, [25,75,125], [25,100,25], x, y ) + ' x:' + x + ' y:'  + y );
});

In fact, we can safely include the offset(), scrollLeft(), and scrollTop() calls even if we are neither using absolute nor fixed positioned elements since these values will simply be 0 in the case of a statically positioned canvas.




Copyright 2011 - 2017 Joseph Hallenbeck Powered by WordPress