Animated Characters in the web world

I'm currently working on a solvable problem. In one of my side projects, I want to have characters that users can customize. Anyone who has played a few video games knows how common this is.

For some companies, it's a centeral part of the user experience. Xbox has an entire store for avatar clothing.

Anyways, so I want to do something similar on a web project I'm working on. The platform is primarily targeted for teachers and students, leading to these requirements:

  • Should use limited bandwidth
  • Should support rendering up to ~30 characters at once
  • Should be runnable on low-powered devices (such as Chromebooks)
  • Should allow animation of limbs and body

This lends itself to an interesting problem...

Naive Approach

I love to start with naive approaches. It's a good warmup exercise. Get's the earth moving.

So what's the naive approach here?

I started with considering using PNG images to represent the character body and each asset. PNGs allow transparency, which would work well as we stack items on top of each other. Ignoring the performance requirements, let's figure out how we would display and animate a character composed of multiple PNGs.

Well first, we'd start with a bunch of item PNGs. In production, I'd want to include @2x images for retina screens, but I'll skip those details for now.

So let's say we have a bunch of these images in some asset location. Our item database would look like this:

item_name item_path

Cool. If we list out the items, we can get all of the image locations. This is great for a store page and showing off the item by itself, but it's only partly useful to us on our quest to make a character. We need to be able to place the image assets relative to one another. We will take a hint from the how items are absolutely positioned in a document, and store the top/left locations for each item:

item_name item_path item_top item_left
body 0 0
hat -30 200
shirt 200 200

The numbers above are made up, but I think what we're storing is clear. For sanity and scaling, we would most likely want to store image sizes for each asset, but we'll leave that out.

So at the end of the day, we'd have some character rendered using HTML and these images:

<div id="character">  
  <div id="body" style="background-image:{asset-url}; top: 0; left:0;"></div>
  <div id="hat" style="background-image:{asset-url}; top: -30px; left: 200px;"></div>
  <div id="shirt" style="background-image:{asset-url}; top: 200px; left: 200px;"></div>

What about z-index? How do we handle that? We'd have to add another column to our items table. That would be the case for almost any per item detail we'd want to render.

Okay, so now we need to animate this character. To animate the body and it's position, we could just use CSS or JS animations on #character. You might notice that in order to move the hands and legs independently, we would actually need to make them their own separate images. The base body would actually consist of 5 parts (the torso, 2 hands, 2 feet). If we wanted to animate certain items with each part (like a sword in one of the hands) we would also need to store information about what group to render the item under. That way, when we animate #left-hand, we animate the items attached to #left-hand as well.

You might already be able to see that this is getting a bit out of hand. We're going to need to store the location, details, and ordering information for all of these items, including parent and children items in the case of our body, hands, and feet.

Now that we've harped on the implementation of this method, let's consider performance requirements.

If a character can have 7 items equipped, as well as 5 items for the base body, then that gives us a grand total of 12 items per character. If we have 30 characters, that's 360 images downloaded, rendered, and animated. Yikes. Good luck getting a Chromebook to run smoothly with that :)

Let's get serious

Alright, so we've done the mental exercise of going the naive route. It's not very performant, and it's also very hard to work with. That said, it wasn't a complete waste. We learned some important things:

  • Storing the positioning and other item metadata needs to be part of the equation.
  • Scaling the character and its items should be straight-forward (for the PNGs, we would need to store separate PNGs and metadata for each scale we planned to render in)
  • Character assets should minimize the number of requests necessary to render

We also need to think of the potential to scale our method as our use-cases grow. Do we want to have to add 10 more DB rows and a bunch of parent/child logic whenever our design team decides they want a 100x100 character to show in a section (and we only have a 50x50 and a 150x150)?

After a bit of research, I arrived at SVGs. SVGs are really powerful for a few reasons:

  • They are vector images, meaning a single SVG can scale to any resolution
  • They are just XML, meaning they can be modified and manipulated using any XML parser
  • They store positioning and style attributes
  • They can be animated using libraries like SnapSVG
  • If you group nodes under a g node, they all are affected by transforms to that node

Bonus points:

  • Simple images are really small
  • They can store animations

And for the negatives:

  • the z-index is based on render order

Wow, SVGs look great. They contain their own metadata, scale, and are easy to manipulate.

They also offer the hidden super power of being composable. We can take a bunch of seperate SVG images, merge them together, and store them as a single SVG asset. As long as that asset is tagged with group IDs, it would still be fully animatable.

I ran over to CodePen to see if I could verify this. Turns out, it's very straight forward...

See the Pen blank-skeleton-shirt-anim by Tyler Shaddix (@tshaddix) on CodePen.

So this is pretty exciting...

We can animate and scale these merged SVGs with just a few lines of JS. What's more, we can get an entire character asset in a single request. 30 students = 30 requests (that's 12 times less than the PNG method).

So that just leaves one question: How do we merge the SVGs? I'll leave that for another post :)