I’ve long enjoyed the user interface designs shown in anime like Ghost In The Shell, and realized this weekend I could recreate the visual design in SVG. You can see the finished example above, and the complete code in the associated CodePen demo; this article breaks down my approach to the UI piece by piece.
The Markup
The basic markup is a series of <circle>
elements with increasing radii, all centered on the same point. There’s also a <text>
element:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<circle cx="50" cy="50" r="22"></circle>
<circle cx="50" cy="50" r="24"></circle>
<circle cx="50" cy="50" r="26"> </circle>
<circle cx="50" cy="50" r="30"></circle>
<circle cx="50" cy="50" r="34"></circle>
<circle cx="50" cy="50" r="34"></circle>
<text x="49" y="54">0</text>
</svg>
Since the transformation and animation of elements in CSS and SVG is not yet the same, the circles are animated with SMIL. The markup is enhanced by placing <animate>
elements inside each <circle>
:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="gits">
<circle cx="50" cy="50" r="22">
<animateTransform attributeName="transform"
attributeType="XML" type="rotate" from="0 50 50"
to="360 50 50" dur="10s" repeatCount="indefinite" />
</circle>
<circle cx="50" cy="50" r="24">
<animateTransform attributeName="transform"
attributeType="XML" type="rotate" from="0 50 50"
to="360 50 50" dur="8s" repeatCount="indefinite" />
</circle>
<circle cx="50" cy="50" r="26">
<animateTransform attributeName="transform"
attributeType="XML" type="rotate" from="0 50 50"
to="-360 50 50" dur="8s" repeatCount="indefinite" />
</circle>
<circle cx="50" cy="50" r="30">
<animateTransform attributeName="transform"
attributeType="XML" type="rotate" from="0 50 50"
to="360 50 50" dur="14s" repeatCount="indefinite" />
</circle>
<circle cx="50" cy="50" r="34">
<animateTransform attributeName="transform"
attributeType="XML" type="rotate" from="0 50 50"
to="360 50 50" dur="18s" repeatCount="indefinite" />
</circle>
<circle cx="50" cy="50" r="34">
<animateTransform attributeName="transform"
attributeType="XML" type="rotate" from="0 50 50"
to="-360 50 50" dur="20s" repeatCount="indefinite" />
</circle>
<text x="49" y="54">0</text>
</svg>
Some of the circles are rotating clockwise, some counter-clockwise, with different rotation speeds and indefinite (i.e. infinite) repetition. Note that the circles are 4 units apart.
CSS
Since the the circles are solid, and animated on their centres, no animation is yet visible, even when we add CSS:
circle {
stroke: #000;
fill: none;
stroke-width: 4px;
transition: .2s;
stroke-dashArray: 0 600;
}
text {
font-family: Titillium Web, sans-serif;
font-size: 12px;
text-anchor: middle;
}
The stroke-width
is just enough to make the strokes of each circle meet, but not overlap; while stroke-dashArray
is set height enough on the circles to not see any stroke at all, at least initially. The text-anchor
places the text in the center; the transition
will ease the animation to come.
JavaScript
So now the circles are spinning. They’re turned into circular segments with JavaScript:
var circles = document.getElementsByTagName("circle"),
progress = document.getElementsByTagName("text")[0];
We also need some random values, which can be derived from a function:
function getRandomInRange(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
The script uses a loop to determine the circumference of each circle. A random number between 20 and 80 is used to define the initial stroke-dasharray
of each circle, with the circumference of the circle stored as a property:
for (var j = 0; j < circles.length; ++j) {
var radius = parseInt(circles[j].getAttribute('r'), 10);
circles[j].circumference = 2 * radius * Math.PI;
circles[j].init = getRandomInRange(20,80);
circles[j].style.strokeDasharray = circles[j].init + " " + circles[j].circumference;
}
This forms each circle segment, allowing the animation to be seen.
var i = 0;
var timer = setInterval(function() {
progress.textContent = i;
if (i == 100) {
clearInterval(timer);
} else {
i++;
for (var j = 0; j < circles.length; ++j) {
circles[j].style.strokeDasharray = circles[j].init + i + " " + circles[j].circumference;
}
}}, 500)
The <text>
element is filled with the value of i
inside a timer every 500 milliseconds (i.e. every half second). The same value is used to increase the first value in the dashArray
of each circle, slowly completing them.
Improvements
There are several ways this could be improved:
- Not all of the circle segments complete when the timer finishes; the first
stroke-dashArray
value is large enough to complete the small inner circles, but not enough to finish the larger. I prefer the way it turned out, but you could alter the script to make the final state of the counter circles solid. - ideally, the entire SVG would be generated by JavaScript, and the result used to replace an HTML5
<progress>
element, building the UI progressively. - You could also alter the completion of the segments so that they expanded at different rates, completing to a solid outline for each circle at the moment the timer finished.
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/rrOZGK