= Inputs.range([1,25], {value: 5, step:1, label: 'Dot radius'}) viewof dotRadius
Birthday animation
I turned 32 this week. I had a fun idea of how I could mark the occasion, given that 32 is an important number. I’ve been making plots using observable and D3, and under the hood, these use the <svg>
element, so I thought it would be a good excuse to have a look at how these work.
My idea is just a bunch of moving dots again. You draw a circle in an svg by adding a <circle />
and providing the position with cx
and cy
, a r
adius, and a fill
colour.
<svg width="500" height="100">
<circle cx="25" cy="50" r="5" fill="black"/>
</svg>
You can use D3 to do the same thing. The advantage here is that you get a nice way to change the attributes of your svg.
{const simpleDotSvg = d3.create('svg')
.attr('width', 500)
.attr('height', 100);
.append('circle')
simpleDotSvg.attr('cx', 25)
.attr('cy', 50)
.attr('r', dotRadius)
.attr('fill', 'black');
return simpleDotSvg.node();
}
I did say there were moving dots, so here’s a way to make them move. You can add an animateMotion
element to your circle to get it to move. Here, I tell it to repeat indefinitely, take 2 seconds to run, and to move from (50, 25) to (150, 25), then return to the beginning with Z
.
{const movingDotSvg = d3.create('svg')
.attr('width', 500)
.attr('height', 100)
.append('circle')
movingDotSvg.attr('cx', 25)
.attr('cy', 50)
.attr('r', dotRadius)
.attr('fill', 'black')
.append('animateMotion')
.attr('repeatCount', 'indefinite')
.attr('dur', '2s')
.attr('path', 'M 50 25 L 150 25 Z');
return movingDotSvg.node();
}
The idea is to have multiple dots moving around regular polygons. We’ll need a way to know what path they need to take. I remember being shown how to construct a hexagon with a compass as a child, and this is more or less the same!
function getPolygonPoints(originX, originY, radius, edges) {
const angleStep = (2 * Math.PI) / edges;
const points = [];
for (let i = 0; i < edges; i++) {
const angle = i * angleStep;
const x = originX + radius * Math.cos(angle);
const y = originY + radius * Math.sin(angle);
.push({ x, y });
points
}return points;
}
This function takes an origin, a radius, and the number of edges you want and returns an array of points where the corners are.
The next bit of code is a little more complicated.
function animatePoly(parent, originX, originY, radius, edges, duration, colour) {
const points = getPolygonPoints(originX, originY, radius, edges);
let pathString = `M ${points[0].x} ${points[0].y} `; // Start at the first point
for (let i = 1; i < points.length; i++){
+= `L ${points[i].x} ${points[i].y} `; // Add line segments
pathString
}+= 'Z'
pathString
.append('circle')
parent.attr('cx', originX)
.attr('cy', originY)
.attr('r', 5)
.attr('fill', colour)
.append('animateMotion')
.attr('dur', `${duration}s`)
.attr('repeatCount', 'indefinite')
.attr('path', pathString);
}
The first thing it does is to use our previous function to calculate the points of your desired polygon. Then, it uses them to create a path string like we used to make the moving dot above. It then appends a new shape to the parent
element specified, taking duration
and colour
arguments.
Let’s use it to make a dot moving around a square.
{const squareSvg = d3.create('svg')
.attr('width', 200)
.attr('height', 200);
animatePoly(squareSvg, 50, 50, 80, 4, 4, 'black');
return squareSvg.node();
}
Now, what does this have to do with being 32? Well, 32, is 25, so if we have polygons for the powers of two…
{const birthdaySvg = d3.create('svg')
.attr('width', 400)
.attr('height', 400);
animatePoly(birthdaySvg, 50, 50, 80, 2, 2, 'black');
animatePoly(birthdaySvg, 50, 50, 80, 4, 4, 'darkslategray');
animatePoly(birthdaySvg, 50, 50, 80, 8, 8, 'dimgray');
animatePoly(birthdaySvg, 50, 50, 80, 16, 16, 'slategray');
animatePoly(birthdaySvg, 50, 50, 80, 32, 32, 'gray');
return birthdaySvg.node();
}
I don’t know about you, but I find that very satisfying.
Having powers of two for this is a lot of fun, but not everyone is 32. As long as your birthday isn’t a prime number, you can get some satisfaction too!
Code
= Inputs.range([2, 100], {value: 24, step: 1, label: 'Select your age'}) viewof userAge
Code
{const factors = number => [...Array(number + 1).keys()].filter(i=>number % i === 0).slice(1);
const ageFactors = factors(userAge);
const svg = d3.create('svg')
.attr('width', 600)
.attr('height', 600);
.forEach((factor) => animatePoly(svg, 50, 90, 80, factor, factor, 'black'))
ageFactors
return svg.node();
}
This is kind of silly and pointless, but I can justify it to myself as good practice for whenever I want to go lower-level building my own visualisations.