Despite being named after a terribly awkward developmental stage, tween
functions can let you do some really cool stuff in d3.
The tween function is run on an element during a d3 transition. It allows you to register a custom function that will be run at every increment between the two poles of that transition and get reference to the precise attributes of the element each step of the way.
I came across tween functions to model collision detection when I was trying to implement a rudimentary Asteroids clone. For this example, I'll boil it down a little more, and use a custom tween function to determine when to randomly moving circles intersect each other.
Set Up d3
To get things going we're going to set up the svg
element and append it. We'll also add some data for d3 to join for some sweet circles.
var svg = d3.select('#container')
.append('svg')
.attr('height', 600)
.attr('width', 1000);
var data = ['blue', 'purple'];
Set Up Animation
Next up, we're going to set up a simple animation to have our circles bounce around between random points on the screen. We'll chain the animations by calling .each('end', move)
to re-call the move
function when the original transition is completed.
var move = function() {
var circles = svg.selectAll('circle')
.data(data);
circles.enter()
.append('circle')
var circleAttrs = circles
.transition()
.duration(1000)
.attr('cx', function(d) {
return Math.floor(Math.random() * 1000);
})
.attr('cy', function(d) {
return Math.floor(Math.random() * 600);
})
.attr('r', 100)
.attr('fill', function(d) {
return d;
})
.tween('collision', collisionDetection)
.each('end', move);
};
move();
Enter Tween Function
Notice right before the final call to .each
, we add our tween function. The first argument is the name the tween function is registered for, and the second argument is a factory function that will return a function to be run on all the increments between the start and the end of the transition.
The factory function passed to tween
must return a function. It's also possible to take advantage of the closure nature of the factory function to preserve and reference any information from the start of the transition.
Our factory function, collisionDetection
will return a function that will grab all other present circles using d3.select
, and using some slick 8th grade Pythagorean algebra, determine the distance between our circle and any others. If the calculated distance is less than the sum of their two radii, we'll consider them collided.
var collisionDetection = function() {
return function() {
var thisCircle = d3.select(this);
d3.select('circle').each(function() {
var otherCircle = d3.select(this);
if (thisCircle.datum() !== otherCircle.datum()) {
dx = thisCircle.attr('cx') - otherCircle.attr('cx'),
dy = thisCircle.attr('cy') - otherCircle.attr('cy'),
distance = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
if (distance < +thisCircle.attr('r') + +otherCircle.attr('r')) {
collision(thisCircle, otherCircle);
}
}
});
};
};
In this example, on a collision it will call a collision function passing in the two offending circles. Throw in a quick console.log
to see what's going on.
var collision = function(thisCircle, otherCircle) {
console.log('Collision at: ' + thisCircle.attr('cx') + ',' + thisCircle.attr('cy'));
}
tween
functions give you a lot of room to play around when using transitions in d3. Check out the documentation and make some sweet stuff.