Making <canvas> Art


Got <canvas>? Focus your attention, if you will, to the top of this web document. The header area of my website includes animated “northern lights” of a sort, generated using HTML5’s new JavaScript rendering engine called <canvas>. Click anywhere on the page to generate new lights, each with randomized colors and trajectories that alter the appearance of the site header as they slowly move across the page.

If you’re not familiar with <canvas>, the best way to learn it is to see a simple example. The following HTML and JavaScript will generate an orange rectangle within the <canvas> area.

<canvas id="example1" width="400" height="300"></canvas>
// get the canvas

var canvas = document.getElementById('example1');

// get the "2d" context

var context = canvas.getContext('2d');

// change fill style to orange

context.fillStyle = '#ff7700';

// draw an orange rectangle

context.fillRect(0, 0, 400, 300);

You always start a canvas drawing by finding the element in the web document and then selecting a “context.” In this case, the context is 2d, because we want to make two-dimensional shapes within our canvas. After selecting a context, the canvas API contains tons of useful drawing features like fill styles, shapes, strokes, shadows, and a plethora of other features that allow us to make fancy alterations to our image area. The result of our script looks something like this:

The great thing about <canvas> is that it’s a JavaScript API, meaning we can take advantage of all the great features of JavaScript like variables, events, loops, and the like. Let’s adapt our script to create a grid of smaller squares across our canvas area using a simple JavaScript loop.

<canvas id="example2" width="400" height="300"></canvas>
// get the canvas and context

var canvas = document.getElementById('example2');
var context = canvas.getContext('2d');

// orange fill style

context.fillStyle = '#ff7700';

// create squares in a loop

for(var x = 0; x < canvas.width; x += 25){
  for(var y = 0; y < canvas.height; y += 25){
    context.fillRect(x, y, 20, 20);
  }
}

In just a few lines of JavaScript, we were able to generate 192 squares in our canvas. This, in my opinion, is the true value of <canvas>. It allows us to leverage the processing power of the web browser to generate math-based geometry and drawings. With a bit of work and a lot of creativity, we can even harness this power for artistic effect.

Animation

We’ll need to understand how to create an animation using <canvas> before we can continue. This is a little more difficult to do. First of all, please understand that it’s very easy to use the canvas API in a way that hinders the performance of your web browser. Drawing in a canvas is very processor-intensive, especially if you are constantly updating the drawing for things like animation. To help alleviate any performance issues, I’m going to use a new feature called requestAnimationFrame, that allows our web browser to decide how often to update the canvas, while maintaining optimal performance in our web document. This isn’t available in every browser, but fortunately Paul Irish has written an excellent poly-fill to add this capability to older web browsers. He writes about his script and this feature of the web browser in his blog post, here.

For the sake of brevity, I’m not going to include Paul’s script in my code examples, but you should use it in your own code. Using requestAnimationFrame, we can create a basic “animation loop” in our script. It looks something like this:

<canvas id="example3" width="400" height="300"></canvas>
// get the canvas

var canvas = document.getElementById('example3');
var context = canvas.getContext('2d');
context.fillStyle = '#ff7700';
var time = false;

// box position

var x = -100;

// animation loop

var loop = function(){

  // get time since last frame

  var now = new Date().getTime();
  var d = now - (time || now);
  time = now;

  // clear previously drawn rectangles

  context.clearRect(0, 0, canvas.width, canvas.height);

  // draw new rectangle

  context.fillRect(x, 100, 100, 100);

  // advance horizontal position

  x += 0.1 * d;

  // reset horizontal position

  if(x > canvas.width){
    x = -100;
  }

  // request next frame

  requestAnimationFrame(loop);

};

// first frame

loop();

When using animations in your canvas element, all drawing should be performed by a repeatable function. In our example, we’re using the loop() function to draw a square in our canvas. The requestAnimationFrame function tells the browser to automatically choose when to next draw the frame, based on the available processing power. The result is our loop() running over and over, advancing our orange box from left to right. Notice that we use the d variable (delta) to determine the time between frames in milliseconds. This is an important addition to normalize the speed of our animation. Without it, our animation would play much faster on computers with better processors, and in a few years when computers gain even more processing power, our animation could be so fast as to confuse or disorient users. Using the delta variable, we can specify a speed per millisecond. In our example, the position of the square advances by 0.1*d, or 0.1 pixels every millisecond, which translates into 100 pixels per second. No matter the speed of your processor, the animation will always take the same amount of time to complete.

The artistic element

Now that we understand the canvas element and how to animate it, we can put this together with some artistic creativity to create something more intriguing. In this next example, we’ll randomly generate colored circles and give them random trajectories within our canvas. By drawing the circles using a gradient rather than a solid color, our “northern lights” script will come to life.

<canvas id="example4" width="400" height="300" style="background-color: #0e74a2;"></canvas>
// get the canvas

var canvas = document.getElementById('example4');
var context = canvas.getContext('2d');
var time = false;

// create an empty array of "circles"

var circles = [];

// animation loop

var loop = function(){

  // get time since last frame

  var now = new Date().getTime();
  var d = now - (time || now);
  time = now;

  // clear the canvas

  context.clearRect(0, 0, canvas.width, canvas.height);

  // draw circles

  for(var i = 0; i < circles.length; i++){
    circles[i].update(d);
    circles[i].draw();
  }

  // request next frame

  requestAnimationFrame(loop);

};

// circle object

var circle = function(options){

  // configuration

  var circle = this;
  circle.settings = {
    x: 0,
    y: 0,
    radius: 20,
    orientation: 0,
    vector: { x: 0, y: 0 },
    speed: 1,
    color: { red: 0, green: 0, blue: 0, alpha: 1 }
  };

  // merge options into settings

  var newsettings = {};
  for(var attrname in circle.settings){ newsettings[attrname] = circle.settings[attrname]; }
  for(var attrname in options){ newsettings[attrname] = options[attrname]; }
  circle.settings = newsettings;

  // update circle

  circle.update = function(d){

    // update position

    circle.settings.x += circle.settings.vector.x * circle.settings.speed * d;
    circle.settings.y += circle.settings.vector.y * circle.settings.speed * d;

    // bounce

    if(circle.settings.x < 0 && circle.settings.vector.x < 0 || circle.settings.x > canvas.width && circle.settings.vector.x > 0){
      circle.settings.vector.x = circle.settings.vector.x * -1;
    }
    if(circle.settings.y < 0 && circle.settings.vector.y < 0 || circle.settings.y > canvas.height && circle.settings.vector.y > 0){
      circle.settings.vector.y = circle.settings.vector.y * -1;
    }

  };

  // draw circle

  circle.draw = function(){

    // gradient fill

    var gradient = context.createRadialGradient(circle.settings.x, circle.settings.y, circle.settings.radius / 10, circle.settings.x, circle.settings.y, circle.settings.radius);
    gradient.addColorStop(0, 'rgba(' + circle.settings.color.red + ', ' + circle.settings.color.green + ', ' + circle.settings.color.blue + ', ' + circle.settings.color.alpha + ')');
    gradient.addColorStop(1, 'rgba(' + circle.settings.color.red + ', ' + circle.settings.color.green + ', ' + circle.settings.color.blue + ', ' + circle.settings.color.alpha / 50 + ')');
    context.fillStyle = gradient;

    // draw

    context.beginPath();
    context.arc(circle.settings.x, circle.settings.y, circle.settings.radius, 0, 2 * Math.PI, false);
    context.fill();

  };

};

// create new circles

var newcircles = function(){

  // remove old circles

  circles = [];

  // create 5 new circles

  for(var i = 0; i < 5; i++){

    // create a new circle

    var newcircle = new circle({
      x: Math.floor(Math.random() * canvas.width),
      y: Math.floor(Math.random() * canvas.height),
      radius: Math.floor(Math.random() * canvas.width),
      orientation: Math.floor(Math.random() * 360),
      vector: {
        x: Math.random() / 40,
        y: Math.random() / 40
      },
      speed: Math.random(),
      color: {
        red: 100 + Math.floor(Math.random() * 155),
        green: 100 + Math.floor(Math.random() * 155),
        blue: 100 + Math.floor(Math.random() * 155),
        alpha: 0.1 + Math.random()
      }
    });

    // add new circle to circles array

    circles.push(newcircle);

  }

};

// generate new circles

window.addEventListener('click', newcircles, false);
window.addEventListener('touchend', newcircles, false);
newcircles();

// first frame

loop();

New randomized circles will appear every time you click in this window. Try it out!

We’re only beginning to understand and utilize the power of <canvas>. I’m eager to see how the industry adopts it as well as technologies like SVG to build amazing and artistic web content. In my next post, I’ll show how to adopt this code to use keyboard shortcuts and animations to create a simple canvas-based game that you can play right in your web browser.