Lea Verou has a wonderful talk about various techniques we can use to create a pie chart purely in HTML/CSS. The techniques are clever and interesting, but pie charts are a little boring. So let's modify one of her ideas and create Pacman instead.
Waka-waka-waka-waka... And away we go...
Pacman at his core: the SVG circle
The main idea we'll use from her talk is to manipulate the SVG spec for dashed borders -- because half of all web development is taking a spec and manipulating it beyond recognition to achieve something it was never meant to do. Lea Verou discusses this SVG technique in her talk about 27 minutes in.
You don't need to be familiar with SVG to follow this post. But if it's new and you want to learn more, Mozilla has a tutorial. For our purposes, we only need it for creating a circle. So, let's start with that:
HTML:
<svg id="pacman" viewbox="0 0 100 100">
<circle cx="50%" cy="50%" r="25%">
</svg>
Things to note:
viewbox
: creates a 100x100 area for creating our circle. These are drawing dimensions, but the element's size on the page is set later in CSS.cx
andcy
: place the circle's center coordinate (x, y) in the exact middle of the viewboxr
: sets the circle's radius to be a quarter of the viewbox size
That's really all the HTML we need. The rest can be done in CSS, since CSS can include SVG-specific styles. So, let's add some CSS to see what this looks like:
CSS:
#pacman {
background: black;
width: 200px;
}
#pacman circle {
stroke: yellow;
fill: none;
}
Things to note:
- The black background isn't important, but allows us to see the viewbox dimensions
stroke
: sets the color of the line of the circlefill
: we don't want the circle filled with any color- Using anything other than traditional Pacman yellow for
stroke
would be blasphemy
So, why make the radius 25%? Don't we want the circle to completely fill the viewbox? Yes. Stay tuned. And don't we want the circle filled in? Again, yes. Geez, I said stay tuned.
Pacman isn't Pacman without a mouth
The first useful SVG property we can abuse in our CSS is stroke-dasharray
, which
creates a dashed line around the circle. Of course, CSS provides border-style: dashed
to do the same thing, but the SVG version is super-charged. It allows a degree of fine-tuning
that would make a micro-manager salivate. Skipping over the details
(read more here),
we will set two numbers. The first sets the dash length, and the second sets the gap
length between dashes. For example, dashes of length 20 with gap length 5 look like this:
#pacman circle {
stroke: yellow;
stroke-dasharray: 20,5;
fill: none;
}
Let's bastardize this to create the outline for Pacman's open mouth. We'll create a dash
length that goes 80% of the way around the circle to depict Pacman, and a large enough gap
length for the remaining way around to depict the mouth. Unfortunately, we can't simply plug
in 80% into stroke-dasharray
because the spec doesn't calculate that percent
based on the circle's circumference. Instead, it's time for a tiny geometry excursion:
If you survive the math, you get cake
We can find the circle's circumference as pi * diameter. We've set the SVG viewbox to 100x100, and the radius to 25% of that, which makes the radius 25 and the diameter 50. The circumference must be pi * 50, or about 157. Taking 80% of 157 we get 126. So, we want to set the dash length to 126 to go 80% around the circle. The gap length must at least fill the remaining 20% of the circle, but it doesn't matter how large it is beyond that minimum. We'll pick an ample 100. It looks like this:
#pacman circle {
stroke: yellow;
stroke-dasharray: 126,100;
fill: none;
}
Sobering up Pacman with dashoffset
Alright, that is one lazy-looking Pacman. Like, he's laid back hoping ghosts will fall out of
the sky into his mouth. The dash we created begins at the right-most point of the circle and
proceeds clockwise. To straighten him out so he's facing full right, let's abuse a second
useful SVG property called stroke-dashoffset
. It sets where to begin within the
dash-gap pattern when drawing. Since the circle starts from the far right, we want the
pattern to start within the gap portion, and the gap portion should last half the mouth's
length before the dash begins. That means the pattern offset needs to be negative in order to
start before the dash, and the value should be half the mouth length.
Since the circumference is 157, and the dash is length 126, the mouth must have length class="nowrap" 157 - 126 = 31. Half of that is about 15, so -15 is the correct offset. Setting it looks like this:
#pacman circle {
stroke: yellow;
stroke-dasharray: 126,100;
stroke-dashoffset: -15;
fill: none;
}
There are other ways to accomplish that, for example you might use the CSS property
transform: rotate()
.
Pacman's empty existence
Great, so now we have a hollow Pacman. Time to make him the solid chomper he really is. The
fourth useful SVG property we can Macgyver to our advantage is stroke-width
, which
sets the width of our yellow line. For example, here is a stroke-width of 10%:
#pacman circle {
stroke: yellow;
stroke-dasharray: 126,100;
stroke-dashoffset: -15;
stroke-width: 10%;
fill: none;
}
Notice how the line expands in both directions equally (towards and away from the circle's
center). This means a stroke-width
of 50% will expand the line all the way in to
the center, but also all the way out to another full radius length. Let's do that, and now it
should be clear why we set the radius to 25%. We had to accomodate this expansion in both
directions.
#pacman circle {
stroke: yellow;
stroke-dasharray: 126,100;
stroke-dashoffset: -15;
stroke-width: 50%;
fill: none;
}
And yet it moves
Pacman still looks rather catatonic. Like a ghost needs to go poke him with a stick. Or like
your MAME emulator just froze. Luckily, we can animate CSS properties. If that's a new
concept,
Mozilla's got your back again.
The closed mouth position is simply a dash of length 157 with an offset of 0. Those
properties can be moved into a keyframe
to animate between the two states, like this:
#pacman circle {
stroke: yellow;
stroke-width: 50%;
fill: none;
animation: chomp 0.15s linear infinite alternate;
}
@keyframes chomp {
from {
stroke-dasharray: 157,100;
stroke-dashoffset: 0;
}
to {
stroke-dasharray: 126,100;
stroke-dashoffset: -15;
}
}
Waka-waka-waka
Pacman still needs an environment to run around in. The following explores that idea a bit.
<!-- HTML -->
<div class="pacman-environment">
<div class="border"></div>
<div class="path">
<div class="dots">
<svg class="pacman" viewbox="0 0 100 100">
<circle cx="50%" cy="50%" r="25%"></circle>
</svg>
</div>
</div>
<div class="border"></div>
</div>
/* CSS */
@keyframes chomp {
from {
stroke-dasharray: 157,100;
stroke-dashoffset: 0;
}
to {
stroke-dasharray: 126,100;
stroke-dashoffset: -15;
}
}
@keyframes dots {
from {
width: 95%;
}
to {
width: 5%;
}
}
.pacman-environment {
box-sizing: border-box;
background-color: black;
width: 100%;
overflow: hidden;
}
.border {
height: 12px;
width: 100%;
border-radius: 12px;
border: 3px solid #2121de;
}
.path {
position: relative;
margin: 10px -200px;
height: 100px;
}
.pacman {
height: 100%;
}
circle {
stroke: yellow;
stroke-width: 50%;
fill: none;
animation: chomp .15s linear infinite alternate;
}
.dots {
float: right;
width: 100%;
height: 100%;
background-image: linear-gradient(to left, #ddd 20%, transparent 0%);
background-position: center right;
background-size: 50px 10px;
background-repeat: repeat-x;
animation: dots 5s linear infinite;
}
Producing this:
Conclusion
The fact that this all stemmed from a video on pie charts proves just how questionably I use my free time.