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.