Using SVG and CSS to create Pacman (out of pie charts)

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 and cy: place the circle's center coordinate (x, y) in the exact middle of the viewbox
  • r: 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 circle
  • fill: 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.

Posted 5/23/2017