Da Gampa's Code

Personal weblog of Jakub Hampl who is an AI & Psychology student at Edinburgh University.

Ask me whatever you want. I'll reply to whatever I want.

Writing your own Canvas Scene Graph

Writing a scene-what? The HTML5 Canvas api provides primitive methods to express shapes, strokes and fills, which is all fine and dandy, but in a lot of situations we tend to think of drawing as composed of objects. E.g. to draw what I see from my window I need to draw a tree, a house and some grass, not a sequence of paths.

This is true even more when we start animating, typically objects that are part of another object move when the parent object moves.

One way to abstract this is to have a graph of objects where some objects are contained in other objects and each object takes care of drawing itself. This is called scene graph rendering.

In this article I will show how to build your very own scene graph renderer for the HTML5 canvas. If you are simply looking for a full-featured, pre-built solution, I recommend checking out CAAT. But sometimes you need something easily customizable/lightweight. We will build a single CoffeeScript class that implements everything necessary.

The basics

The first thing that any computer graphics program has to deal with are coordinate systems. We want each object to have it’s own independent coordinate system and itself be defined in terms of it’s parents. In general this indepence is one of the best reasons for using a scene-graph solution, since canvas tends to have a lot of global state, and you might often face the situation that changing one part of your composition affects a very different part. We wish to avoid that.

The simplest way to solve this is to have each object be it’s own canvas. So let’s start coding:

class Entity
  width: 0
  height: 0

  constructor: (@width, @height) ->
    # Create the canvas we will be rendering the object to
    @canvas = document.createElement('canvas')
    @canvas.width = @width
    @canvas.height = @height

  # Call this function to render out the object, returns a Canvas instance
  render: ->
    ctx = @canvas.getContext('2d')
    # Clear the canvas, important for animation
    ctx.clearRect(0, 0, @width, @height)
    @draw(ctx)
    @canvas # return the canvas

  # This function should be overriden in subclasses  
  draw: (ctx) ->

Now each subclass has to only implement the draw method with it’s own set of drawing primitives.

Composition

We hover are still not a graph, there is no way in which objects can have no children. First we need to add a few more properties to our class:

x: 0
y: 0
children: []
parent: null

x and y are numbers that determine where will the object be drawn in the parent’s coordinate system. Let’s implement a default draw method, that will render the children:

draw: (ctx) ->
  for child in @children
    ctx.drawImage(child.render(), child.x, child.y)
  false # return some value, otherwise CoffeeScript will return an array

The drawImage method takes an Image, Canvas or Video object and draws it at specified coordinates. Since render returns a canvas instance with the object drawn in it, we can now render it in the parent canvas. A simple demo of this class:

Rotations

To make this even more worth it, we would like to support rotating any object and having all it’s children be rotated as well. Again we need to add a rotation attribute:

rotation: 0

and tweak our draw function:

draw: (ctx) ->
  for child in @children
    if child.rotation isnt 0
      ctx.save()
      ctx.translate(child.x, child.y)
      ctx.rotate(child.rotation)
      ctx.drawImage(child.render(), 0, 0)
      ctx.restore()
    else
      ctx.drawImage(child.render(), child.x, child.y)
  false

Canvas provides us with all the magic we need. We save the current context, then shift our origin point to the child’s coordinates (NB: we are going to be rotating children around their origin point, perhaps you would like to implement rotation around center point). Then we do the rotation and drawing and then we simply restore our context to it’s original point of origin.

Shaping up the API

First of all since most entity objects will want to override draw in one way or another we might simply use a Kestrel in the constructor and allow the user to optionally provide it when initiating the class:

constructor: (@width, @height, @draw = @draw) ->
    @canvas = document.createElement('canvas')
    @canvas.width = @width
    @canvas.height = @height

The @draw = @draw might look a bit awkward at first glance, but it assigns as a default our own implementation of draw if the user hasn’t provided one. The js looks like this:

function(draw) {
  this.draw = (draw != null ? draw : this.draw);
}

Next let’s have a single function for instantiating new Entities and simultaneously adding them as children:

add_child: (width, height, draw) ->
  child = new Entity width, height, draw
  @children.push child
  child.parent = @
  child # return `child` so that we can do stuff like `collection = scene.add_child 30, 30`

You might want to create another class that wraps up some common functionality as creating the top level Entity (typically called Scene) and setting its canvas as the one actually displayed in the document and setting up animation loops and so on. I’ll leave that as an exercise for the reader (hint: look at the fiddles, there you have the basics).

Caching

If you draw expensive things in your objects (and some things in canvas are pretty expensive like shapes with multiple gradients/shadows), you might not want to redraw them 60 times per second. Our architecture allows us to prevent that rather easily.

Again we add two more attributes:

# This is what the user sets
perform_caching: no
# This tracks whether or not we should render
is_cached: no

Now let’s modify our render function:

render: ->
  return @canvas if @perform_caching and @is_cached
  ctx = @canvas.getContext('2d')
  ctx.clearRect(0,0,@width, @height)
  @draw(ctx)
  @is_cached = true
  @canvas

We skip all the work when caching is enabled, since the canvas still contains all that was drawn into it.

Wrapping up

And that’s basically all there is to it. There are a myriad of features you can add like more transformations apart from rotations, primitive entity subclasses (think something like Sprite entity), etc. The complete class is here:

#canvas #coffee-script #html5 

All the best in 2011!

A few technical details: I wrote a nice piece of js/canvas code to make the rendering (available here). I originally planed to do an ant colony intelligence inspired animation (so the source code still contains a function to draw an ant).

#art #tech #canvas #javascript