WebGL Animation

Your browser does not support the canvas tag. This is a static example of what would be seen.

This is the second in a series of posts about using WebGL. So far I’ve made a small green rectangle (exciting!), and this builds on that by adding animation to it. Hover on the above square to see animation in action.

I’m not quite ready to get into actual rendering code just yet… so no shaders, buffers, etc… but I want to do enough to prove that I can update a canvas at regular intervals and I’m happy for now just to clear to a random colour every 1/30th of a second, just so I have some evidence that the updates are actually happening.

The code here, which builds on my earlier example, does just that, and a bit more…


var loadIntoCanvas = function(canvas, frameRate, client_onLoad, client_onRender) {

  var gl;

  var initialWidth = 0
  var initialHeight = 0;

  var updateRequest;
  var frameDelta = 0;
  var then;

  function setCanvasSize() {

    /* match the canvas size to the containing div */
    var parentSize = canvas.parentNode.parentNode.getBoundingClientRect();
    if (parentSize.width < initialWidth) {
      canvas.width = initialWidth / 2;
      canvas.height = initialHeight / 2;
    } else {
      canvas.width = initialWidth;
      canvas.height = initialHeight;
    }
	    
  }

  /* called when the canvas needs to be redrawn */
  function onRender() {

    /* update the canvas size */
    setCanvasSize();

    /* redraw the canvas */
    client_onRender(gl, canvas);

  }

  function onAnimationUpdate() {

    /* request another animation call ASAP */
    updateRequest = requestAnimationFrame(onAnimationUpdate);

    /* trigger calls to onRender at regular intervals */
    var now = Date.now();
    var elapsed = now - then;
    if (elapsed > frameDelta) {
      then = now - (elapsed % frameDelta);
      onRender();
    }  

  }

  /* called when the canvas is first loaded */  
  function onLoad() {

    /* initialise WebGL */	
    gl = canvas.getContext("webgl"); 
    if (gl == null)
      gl = canvas.getContext("experimental-webgl"); 

    /* initial update of the canvas size */
    initialWidth = canvas.width;
    initialHeight = canvas.height;
    setCanvasSize();

    /* record the initial time */
    then = Date.now();

    /* give the client a chance to initialise against the gl context */
    client_onLoad(gl, canvas);

    /* start and stop the animation based on mouse events */
    if (frameRate > 0.0) {
      frameDelta = Math.floor(1000.0 / frameRate);
      canvas.addEventListener('mouseenter', 
        function(){ requestAnimationFrame(onAnimationUpdate) }, false)
      canvas.addEventListener('mouseleave', 
        function(){ cancelAnimationFrame(updateRequest) }, false)
    }

    /* trigger a redraw (and a canvas resize whenever a resize occurs */
    window.addEventListener('resize', onRender(), false);

    /* initial render of the canvas */
    onRender();

  }  

  onLoad();

};

loadIntoCanvas(
  document.getElementById("gl_animatedSquare"),
  30.0,
  function(gl, canvas) {
    /* setup would go here */
  },
  function(gl, canvas) {
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(Math.random(), Math.random(), Math.random(), 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);      
  });

The code that’s most relevant to animation support is highlighted.

On initializing the canvas we record the current time using Data.now(), and assuming animation is requested (frameRate not 0) we store a frame-delta (time between frames) to be used during the animation updates to work out if a new frame is due. We attach two listeners that activate and deactivate the animation as the mouse moves over and leaves the canvas area.

Our animation function will end up being called more often than we would like, so we track the time elapsed since the last redraw and issue calls to onRender as required. The an8imation function also requests another animation tick, so it will keep getting called over and over.

Clearing A WebGL Canvas To A Colour

Your browser does not support the canvas tag. This is a static example of what would be seen.

As a long term user of graphics APIs I'm really excited by the sort of content we might see in the future as people find more and more interesting ways to harness the power of the GPU to fill web pages with all manner of complex interactive content.

I don't have anything specific in mind but I wanted to learn about WebGL and keep a record of what I discover along the way... and this is hopefully the first is a small series of posts where I cover the basics of getting a WebGL context up and running, and building the basics of a rendering pipeline in a browser.

Everything starts somewhere... and usually with desktop graphics coding you start by building a window and clearing it to a colour (or I do at least) just to prove that you have taken over the rendering environment correctly and that the basics of interacting with the GPU are working as expected, and so the most simple WebGL example is probably therefore to take a blank HTML canvas, grab a WebGL context from it and clear it to a colour, which is what I'll do here.

For anyone not at all familiar with HTML, we first need to create a canvas. Adding this to any HTML document gives you the blank canvas we need for WebGL rendering. In this case the canvas is 256 pixels tall and 256 pixels wide. For now we don't need it to do anything more the exist, and to have a name we can use to reference it.

<canvas id="gl_greenSquare" width="256" height="256"></canvas>

Then to complete our simple example, adding this small snippet of javascript adds our WebGL code.

var loadIntoCanvas = function(canvas, client_onLoad, client_onRender) {

  var initialWidth = 0
  var initialHeight = 0;

  function setCanvasSize() {

    /* match the canvas size to the containing div */
    var parentSize = canvas.parentNode.parentNode.getBoundingClientRect();
    if (parentSize.width < initialWidth) {
      canvas.width = initialWidth / 2;
      canvas.height = initialHeight / 2;
    } else {
      canvas.width = initialWidth;
      canvas.height = initialHeight;
    }
	    
  }

  /* called when the canvas needs to be redrawn */
  function onRender(gl) {

    /* update the canvas size */
    setCanvasSize();

    /* redraw the canvas */
    client_onRender(gl, canvas);
  }

  /* called when the canvas is first loaded */  
  function onLoad() {

    /* initialise WebGL */	
    var gl = canvas.getContext("webgl"); 
    if (gl == null)
      gl = canvas.getContext("experimental-webgl"); 

    /* initial update of the canvas size */
    initialWidth = canvas.width;
    initialHeight = canvas.height;
    setCanvasSize();

    /* give the client a chance to initialise against the gl context */
    client_onLoad(gl, canvas);

    /* trigger a redraw (and a canvas resize whenever a resize occurs */
    window.addEventListener('resize', function() { onRender(gl); }, false);

    /* initial render of the canvas */
    onRender(gl);
  }  

  onLoad();

};

loadIntoCanvas(document.getElementById("gl_greenSquare"),
  function(gl, canvas) {
    /* setup would go here */
  },
  function(gl, canvas) {
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0.0, 1.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);      
  });

This could have been implemented using less code than is shown here, but I'm trying to do things "properly" and to make sure things are re-usable. What I've done here is to build a small number of functions that manage the canvas and the GL context for me and wrapped those in a function. This allows an instance of that function to be bound to the canvas and the instance can hold it's own state meaning I can bind against multiple contents and set different parameters if needed.

The code first records the initial canvas size, before attempting to fit it to it's parent container. Then it hooks a listener to the resize event so that we can redraw the canvas when the window is resized, and then finally it does the initial render. The resize handler also attempts to choose a new size for the canvas such that it hopefully can be made to fit into it's container, so maybe on mobile phones or other devices with small screens we get a canvas thats half the size.

We use a pair of callbacks to allow the client code to load or setup any GL assets (e.g textures, etc) though we don't need any of that here, and to execute the redraw, which in this cases just needs to clear the cavas to solid green.

Javascript code can be embedded into a HTML document directly by wrapping in a script block, like so:

<script type="text/javascript">
  ... insert WebGL code here
</script>