In the last two posts, we saw how to draw an American flag with HTML5. Now let's see what it looks like in SVG, using D3. If you have an SVG-compatible browser, you should see the flag below.
Mouse over the stars and see what happens.
This sort of animation is natural with SVG and JavaScript because SVG maintains a model of what you drew, so your script is able to address individual elements. Beautiful animations are also possible with HTML5 and CSS3, but the techniques are quite different.
Now use the zoom feature of your browser to magnify the flag. The edges of the stars remain crisp. If you try that with the HTML5 canvas version in the last post, the edges will become fuzzy. An HTML5 canvas is essentially a fancy bitmap and it behaves like one when you zoom.
I'll have more to say after this source listing.
<body>
<h1>American Flag with D3</h1>
<svg id="flag" width="570" height="300" style="border:1px solid grey" />
<script>
drawAmericanFlag("flag", 0, 0, 300);
// Draw an American Flag
// svgId - The id of the HTML SVG element on which to draw the flag.
// x - The x coordinate of the flag's upper left corner, on the SVG area.
// y - The y coordinate of the flag's upper left corner, on the SVG area.
// height - The height of the flag. (Its width will be computed based
// on the standard dimensions of an American flag.)
function drawAmericanFlag(svgId, x, y, height) {
// From the height, derive other measurements.
var width = height * 1.9;
var xStarSeparation = height * 0.063;
var yStarSeparation = height * 0.054;
var svg = d3.select("#"+svgId);
var g = svg.append("g");
// Make the white background.
g.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", width)
.attr("height", height)
.attr("fill", "white");
// Make the red stripes.
d3.range(7).forEach(function (d) {
g.append("rect")
.attr("x", x)
.attr("y", y + d * 2 * height / 13)
.attr("width", width)
.attr("height", height / 13)
.attr("fill", "#C40043");
});
// Make the blue field.
g.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", 0.76 * height)
.attr("height", height * 7 / 13)
.attr("fill", "#002654");
// Draw the stars.
var outerRadius = 0.0616 * height / 2;
var innerRadius = outerRadius * Math.sin(Math.PI / 10) / Math.sin(7 * Math.PI / 10);
for (var row = 1; row <= 9; ++row) {
for (var col = 1; col <= 11; ++col) {
if ((row + col) % 2 == 0) {
var xCenter = x + xStarSeparation * col;
var yCenter = y + yStarSeparation * row;
drawStar(g, xCenter, yCenter, 5, outerRadius, innerRadius)
.attr("fill", "white")
.on("mouseover", function (d) {
var boundingBox = d3.select(this).node().getBBox();
var xCtr = boundingBox.x + outerRadius;
var yCtr = boundingBox.y + outerRadius;
var star = d3.select(this);
for (var angle = 5; angle <= 360*5; angle+=5) {
var xform = "rotate(" + angle + "," + xCtr + "," + yCtr + ")";
star.transition().delay(angle).duration(0).attr("transform", xform);
}
});
}
}
}
}
// Draw a star. This function just creates the SVG <path> element and wraps
// it in a <g>. It is up to the caller to set the fill and/or stroke on the
// path after this function returns.
// parent - The element to which the returned <g> will be appended.
// xCenter - The x coordinate of the center of the star.
// yCenter - The y coordinate of the center of the star.
// nPoints - The number of points the star should have.
// outerRadius - The radius of a circle that would tightly fit the star's outer vertexes.
// innerRadius - The radius of a circle that would tightly fit the star's inner vertexes.
function drawStar(parent, xCenter, yCenter, nPoints, outerRadius, innerRadius) {
var lineGenerator = d3.svg.line()
.x(function (d,i) {
var angle = i * Math.PI / nPoints - Math.PI / 2;
var radius = i % 2 == 0 ? outerRadius : innerRadius;
return xCenter + radius * Math.cos(angle);
})
.y(function (d,i) {
var angle = i * Math.PI / nPoints - Math.PI / 2;
var radius = i % 2 == 0 ? outerRadius : innerRadius;
return yCenter + radius * Math.sin(angle);
});
return parent.append("g").attr("class", "star")
.append("path")
.datum(d3.range(nPoints * 2))
.attr("d", lineGenerator);
}
</script>
</body>
In the previous series on D3, I emphasized the
enter() and exit() sets as sources of new graphical elements. This time, I'm showing a more brute-force way of introducing them. On line 32, we see the D3
range() function, which creates an array of 7 values (0 through 6). They feed into a
forEach that makes a red stripe that corresponds to each, appending an SVG <rect> to the <g>. I'm not sure if
range() or
enter() is considered better form, but either will work.
You may also be interested in two aspects of the mouseover event-handler starting on line 59, where we make the stars spin.
- If we rotate the star unthinkingly, we'll end up rotating it around the origin of the SVG area. It will make a big arc rather than spinning in place. We want to rotate about the center of the star instead. To find the center, we need the bounding box of the star, and we can get it in two steps. First, d3.select(this) on line 63 obtains the SVG path element that contains the current datum, d. More precisely, it retrieves a "selection" with one element, which the node() call zeroes in on. The second step is to call getBBox() on the node and access the x and y attributes of the return value. They are the coordinates of the upper-left corner of the box that bounds the SVG path for the star. Add the radius of the star, and we're at the center.
- We make the star spin by setting up many consecutive rotations. The rotations are instantaneous (duration(0)) but the calls to delay(angle) stagger them over time. (Sorry for the cheat of making the angle double as a number of milliseconds, but it just worked out.) You would think that we could spin the star by doing one slow rotation, but that doesn't work. Strangely, it makes the star rotate through a loop rather than in place. An in-place rotation is really a translation (shift), followed by a rotation, followed by another translation. When you slow things down, the translations become apparent, hence the loopiness.
The drawStar function differs drastically from its HTML5 cousin. In HTML5, we just drew lines from here to there and we were done. With D3, we create a line generator whose x and y attributes are functions that return the coordinates of points in a series. We use d3.range() again (line 97) to obtain the points. Their values and indexes become the d and i parameters of the x and y functions. (It turns out that we only need the indexes, i.)
If you want to tinker, here's the complete source: D3flag.html (4.99 kb)
That's it! Now how many stars can you keep spinning at once?