Animating Keyboard Events in d3

d3 can do some serious data, but it is also a pretty sweet tool to animate user input.

I'll give a little run through here on how to capture keyboard events to run a little animation. Well, to drive a little triangle around the screen using the arrow keys.


Set Up SVG

First, we need an element to hold our d3 goodness, so we'll append an svg element onto the body and set its default dimensions.

var svg = d3.select('body')  
  .append('svg')
  .attr('height', 600)
  .attr('width', 1000);

Next, we'll add in a triangle polygon that will eventually be controlled by the arrow keys.

var triangle = svg.append('polygon')  
  .attr('fill', 'yellow')
  .attr('stroke', 'blue')
  .attr('stroke-width', 2)
  .attr('points', "0, 30 0, 0 40, 15 ");

We have a little yellow triangle, but it's stuck way up in the corner. Let's start it out in the center of the svg element instead. To make storing and referencing the triangles position easier, we'll add some relevant properties to the triangle object using underscore's extend method.

_.extend(triangle, {  
  x: 500,
  y: 300,
  angle: 0,
  _speed: 10
});

This sets the triangle's x and y values 500 and 300 respectively, or what is the middle of our current svg. These coordinates are meaningless properties until we actually place the triangle using the transform attribute provided to us by d3.

triangle.attr('transform', function() {  
  return 'translate(' + triangle.x + ',' + triangle.y + ')';
})

At this point, we have a pretty little triangle that is still tragically static. Let's get our little tripartite friend moving!


Catching Key Events

The first thing we're going to do to get some movement, is to set up capturing keyboard events. d3 has a very straightforward method to do so. Unfortunately, it is only useful for catching single keys being pressed at a time. We're going to expand it a little so we can react to multiple keys pressed at once, in this case to model diagonal movement.

To do so, we're going to put watchers on both the keydown and keyup events. To store which keys are being pressed, we'll create an object keyPressed.

var keyPressed = {};

d3.select('body')  
  .on('keydown', function() {
    keyPressed[d3.event.keyIdentifier] = true;
  })
  .on('keyup', function() {
    keyPressed[d3.event.keyIdentifier] = false;
  });

With this in place, every keydown event will store that event's keyIdentifier as the property name in the keyPressed object and set its value to true. On a keydown event, the corresponding keyIdentifier will reset it to false. With this setup, we can monitor all keys that are being pressed simultaneously.


Gettin' Our Move On

So far we have a stationary triangle and some code that tells me what buttons I'm pushing. Great. Not too exciting yet, but we'll get there, I promise.

In order to set and manipulate our triangle's position, we're going to use the transform/translate property of svg polygons.

We'll create a moveTriangle function to monitor our keypresses and to change the triangle's x and y properties to appropriately respond to the arrow keys. moveTriangle will reference the keyPressed object to find out what keys are currently being pressed. We'll also add a helper function isInBounds to make sure no one is flying off the screen. (You may wonder how we're going to reference keyPressed in real time, but don't worry, d3 will come through in the end.)

var moveTriangle = function() {

  var x = triangle.x;
  var y = triangle.y;

  if (keyPressed['Left']) {
    triangle.x = isInBounds(x - triangle._speed, 'width');
  }
  if (keyPressed['Up']) {
    triangle.y = isInBounds(y - triangle._speed, 'height');
  }
  if (keyPressed['Right']) {
    triangle.x = isInBounds(x + triangle._speed, 'width');
  }
  if (keyPressed['Down']) {
    triangle.y = isInBounds(y + triangle._speed, 'height');
  }
  triangle.move(x, y);
};

var isInBounds = function(n, dimension) {  
  if (n < 0) {
    return 0;
  } else if (n > svg.attr(dimension)) {
    return svg.attr(dimension);
  } else {
    return n;
  }
}

The moveTriangle effectively translates the arrow keys into new x and y coordinates on the triangle object, but again, we need to make use of translate to actually move the triangle on the screen. On the last line in moveTriangle we reference a move method on triangle. Let's write that now.

triangle.move = function(x, y) {  
  var dx = this.x - x;
  var dy = this.y - y;
  if (dx !== 0 || dy !== 0) {
    this.angle = 360 * (Math.atan2(dy, dx) / (Math.PI * 2));
  }
  triangle.attr('transform', function() {
    return 'rotate(' + [this.angle, this.x + 20, this.y + 15].join() + ')' +
      'translate(' + [this.x, this.y].join() + ')';
  }.bind(this));
};

Under the fancy trigonometry, this function's main purpose is to call d3's attr method on our triangle object and relay the appropriate position to the translate attribute. We also run some fancy math to keep track of where our triangle is pointing just to keep things interesting.


Animate!
All this code is well and good, but it needs a final line of d3 magic to work. As it stands, we keep track of what keys are pressed, but the moveTriangle function isn't being called on! How will it know when the arrows are being pressed?

d3.timer(moveTriangle);  

Toss that little line in there, and have fun! d3's timer method will tie our moveTriangle function smoothly into the internal gears and cogs of it's time keeping mechanisms. It will now be run continuously and keep referencing the keyPressed object for an updated look at which keys the user is pressing and then appropriately telling our little yellow triangle where to go.