The Playground Vector Animation Process

The Idea

Let’s be clear. We’ve got opinions. We’ve got values. And we feel strongly about them.

We’ve been around for enough time to have had some amazing as well as some trying experiences. These experiences have helped us to really understand who we are and what we stand for.

Re-launching our brand and our website presented a chance to take the time to reflect and to codify our beliefs and values. We managed to distill these into eight core values. To show our commitment to these tenets we wanted to put them front and centre on our site.

We decided that animations would be a good way to convey our message. But not just any old animations. Something crafted. Something unique. Our original idea was to have hand-drawn animations that looked like they were pencil sketches. But how could we display these animations? As .PNG flip books? As .GIFs 1? As plain old video files?


We were building a responsive website. We wanted to create a good experience for all of our users: mobile, tablet, desktop, Hi-DPI. Scratch that. We wanted to create a jaw-dropping experience users had never seen and we didn’t want to have to compromise for technical reasons. This meant we had a few things to take into consideration:

Platform independence

Whatever solution we came up with, it needed to work on all platforms. That includes touch, desktop, IE. As an example, you can embed video within a page on a mobile device, but playing that video has to happen fullscreen. We weren’t willing to compromise the mobile experience by breaking the story-telling flow to interject full-screen video.

Bandwidth & load times

Serving eight high-resolution MP4 files on our homepage was going to bring our page load easily into the 30-40 MB range. PNG flipbooks would be just as bad. Lossy encoding might help, but sacrificing the rendering quality was not looking like a particularly appealing option.


Our ideal showcase was one that included the user. We wanted more than just passive content consumption. We weren’t sure how just yet, but we knew we wanted to use some sort of input from the user and we wanted to use the web to its full capacity. We wanted to bring the animations to life.


The key requirement was that at no point could we let technological restrictions compromise the quality and integrity of the animations. The art was the star of this project and any implementation was to facilitate—never to hinder.

Let’s do something really fucking hard

Part of our culture is to want to try new things, to find new possibilities and to push boundaries. We had seen a lot of interesting things happening with animations and graphics on the web. The state of the art was split between two schools:

  1. WebGL/canvas-based implementations in either 2D or 3D which were essentially worlds unto themselves, and
  2. DOM-based CSS animations that used transforms and tweens reminiscent of an inexperienced flash animator from the early 2000s 2.

We liked that things were moving forward but we felt the pace wasn’t ambitious enough. We thought the course could use some redirection.

Defining the look

We had our initial ideas for the art and we had figured out the technical requirements and evaluated the current landscape. It was time to make some key choices: to define a visual style and art direction.

Animating for the web might on the surface seem like any old animation, but the reality is rather different. In traditional animation you know the resolution and aspect ratio of your field. In our case we were embedding animations into pages, the layout of which was so far undetermined and, in fact, would actually change depending on devices, screens, browsers, etc.

While some of our initial ideas involved scenes of characters acting out the values on drawn backgrounds or giant vertical panoramas going from value to value, the diverse landscape of the web meant these were infeasible. We could easily see giant white bars or unwanted crops in our future and these were clearly symptoms of using the wrong approach.

We decided to drop the background and started playing with a sketchy “hand-made” feel in Chris’ own personal style. We realized, however, that we needed something that better fit the Playground brand, our own personality. We needed something more digitally native. Maybe something polygonal.

Quentin Baillieux's work was hugely influential in defining the style.

We found our inspiration with a French artist whose work Chris has admired for years: Quentin Baillieux. His overly simplified geometric style really resonated with us and we loved the landscape of possibility that this minimal approach opened.

And we liked that this approach suggested a vector-based implementation. We could use something native to the browser, something that’s still kind of new and fairly uncommon and it would work just as well for mobile devices as Hi-DPI laptops 3.

Some early concept work

Stepping out into the unknown

We had an idea of how we wanted things to look and feel and we were excited about using vectors. We knew we were being ambitious and we knew there was a high risk of failure. We needed to start prototyping and get something into the browser as soon as possible.

Chris put together twelve frames of a simple animation for us to test with.
We quickly traced over the hand-drawn sketches and created some SVGs.


Naturally, since we knew we wanted to work with vector graphics, the first thing to try was the toolkit built for animating SVGs: SMIL. SMIL (or Synchronized Multimedia Integration Language) theoretically allows for animating the numeric attributes of elements (e.g. x, y, or path), transform attributes (translation, rotation), color attribute (fill, stroke) and moving elements along a path. So far so good.

We quickly learned, however, that there were drawbacks to this approach. If the number of points in a path changed, the transition would be instant, but if the number of points was consistent, the transition would tween. It wasn’t especially clear how to animate multiple frames—whereas transition from one state to another was easy, transitioning between several seemed unlikely without some sort of javascript injection.

Worst of all, SMIL support proved to be spotty. IE did not support it at all, support in Safari was broken when embedded in an HTML document and our test, as far as it did work, only worked in Firefox and not Chrome.

SVG flipbooks

Although SMIL support is spotty, vanilla SVG support is quite good. We could, theoretically, embed a single frame of the animation within the HTML page and use a JavaScript timer to swap in the next frames. We put this solution down as Plan B. It would work but it would be a bit of a naive approach that wouldn’t allow for the kind of interactivity we dreamed of including.

Javascript libraries to the rescue

Because solutions native to the browser weren’t quite where we needed them, we decided to evaluate Javascript drawing libraries. We had a look at Paper.js, Processing.js, Raphaël and SVG.js.

Paper.js and Processing.js are quite powerful but their implementations target canvas. We wanted seamless animations both visually and in interaction and that meant using the same DOM as the rest of the page. We ended up choosing Raphaël for its maturity and the fact that it was DOM-based. We liked the idea of attaching JavaScript event handlers to our animated elements and we were impressed that, theoretically, a Raphaël-based solution would work even in IE6 4.

Upping the ante

We had a working test and a promising technological solution. It was time to try out a proper animation. By this time Chris had completed the first animation (the “Be Fearless” knight). He’d hand-drawn the major poses, spread those out on a timeline to establish a sense of rhythm and then drawn the remaining frames in between.

The frames, however, were drawn in Photoshop as a series of raster layers. Ryan bravely volunteered to trace over the 72 frames in Illustrator to produce something vector-based we could begin to work with. We exported those 72 frames to an equivalent number of SVG files.

All 72 frames of the original knight animation were converted to vector by hand tracing in Ilustrator.

We wrote some code to translate the SVG into something we could send to Raphael’s API 5 and we were in business.

Refining the Process

It was obvious that tracing over every layer in every frame of every PSD was not a good long-term solution. Even if Ryan had the stamina to complete everything one time, what if we needed to change something? Clearly we needed an automated solution.

The first and obvious step was for Chris to produce the animations in vector format so that we wouldn’t need to do any tracing. Simple enough. But how to get that data into SVG? We tried just dropping our animation PSD into Illustrator with the hope that it would ‘just work’ but Illustrator took every folder in the PSD and merged it into one layer. If we wanted to convert our animations to SVG we would have to have each frame be its own numbered PSD.

We tightened our belts and trudged on 6, setting out onto a journey that took us deeper into the bowels of Photoshop than we had ever dared tread.

Photoshop Actions

We’re not proud of this, but the fact is you usually try to work with what you know. Ryan had done some work with Photoshop actions before—for doing things like resizing and cropping multiple images, though, never anything with this much complexity.

We needed to write an action that would make all layers invisible except for the ones in a selected folder, save the contents out as a PSD, turn off that folder’s visibility, select the next folder down and repeat the process until all folders in the PSD had been processed. This was a lot more difficult than it sounds.

After trying longer than we care to admit to create an action that, firstly, worked at all consistently and secondly, was efficient, we were ready to move on.

Photoshop Layer Comps

We knew there was a feature where all Layer Comps in a PSD could be saved out each as their own PSD. But this process didn’t work very well—it still required us to go through each frame each time and generate a Layer Comp for the output to work. It was just messy. Why not make a Action to generate the Layer Comps you ask? You’re just sick 7.

Photoshop Scripts

Unable to find a built-in Photoshop feature that fulfilled our needs, we turned to the internet and found ourselves knee-deep in comment threads on Adobe forums. We’d heard rumours that you could script Photoshop but to be honest, we had about zero interest in acquiring the domain knowledge to do so.

Fortunately, we didn’t have to. Paul Riggott, we owe you one. We modified one of his scripts and found the missing piece to our puzzle. You can download that puzzle piece here or you can check out his impressive collection of scripts.

Using Illustrator to translate between PSD and SVG

Okay, so we had one PSD for each frame. All we needed to do now was take all those files and convert them into SVGs. Photoshop wasn’t up to the task so we enlisted Illustrator and created a handy batch action we called “Process PSD to SVG”. Figuring this out also took longer than we’re maybe willing to admit — however, after a great deal of experimentation and just as much frustration and confusion, we had a streamlined processing workflow.

Parsing and tidying Adobe-generated SVGs

Unfortunately, the SVG files generated by Illustrator were overly verbose and a little arbitrary. Shapes were sometimes defined as polygons, sometimes paths and sometimes polylines. It wasn’t exactly clear what factors were responsible here—it all looked the same in Photoshop.

In any case, developing with Raphaël means using their API and that API happens to differ slightly from SVG. Paths are supported, but polygons and polylines are not. We needed to introduce some consistency and meet Raphaël’s needs.

Because the site already had a build process (using Docpad we were compiling Stylus down to CSS and Jade templates down to HTML) it was pretty straightforward to hook in a custom plugin that would parse the SVG data (using cheerio) and save-to-file a JSON data structure that contained all the essential data: paths, fills, opacity and hierarchy. We were able to extract the timing information in our parser by having the frame numbers included in the Photoshop layer group names.

getAttrs: ($el) ->
  ... # parse out fill and opacity attributes
  return attrs
writeAfter: (opts, next) ->
  ... # include dependencies
  files = docpad.getFilesAtPath(sourcePath)
  complete = _.after files.length, () ->
    ... # write to file
  files.forEach (file) ->
    # filepaths provide some useful data that we want to extract
    # they are of the format:
    # vector/animation-key/M - layer/filename N.svg
    # where M is the index of the layer
    # and N is the index of the frame
    # note that the filename could be just "N.svg" without
    # any other text
    url = file.get('url')
    filedata = url.replace(/^\//, '').split '/'
    name = filedata[1]
    layer_index = filedata[2].match(/(\d+).+/)[1]
    frame_index = parseInt filedata[3].match(/.*?(\d+).*?\.svg/)[1]
    ... # build datastructure skeleton if none exists
    fs.readFile file.get('fullPath'), 'utf8', (err, data) ->
      frameData = []
      $ = cheerio.load data
      $root = $('svg')
      # get the canvas dimensions if we don't already have them
      if !_.has animations[name], 'canvas'
        animations[name].canvas = {
          width: $root.attr('width')
          height: $root.attr('height')
          viewBox: $root.attr('viewBox')
      # the SVGs we get from the photoshop -> illustrator pipeline
      # have elements that look like this:
      # <g id="R_hand" style="opacity:0.702;">
      #   <g>
      #     <polygon style="fill:#C0BEBE;" points="282,341.76 278.4,346.561 293.28,346.32                    "/>
      #   </g>
      # </g>
      # so we need to identify the polygons and paths 
      # and then merge in attributes from their grandparents
      $('polygon, polyline, path').each (i, el) ->
        $el = $(el)
        $grandparent = $el.parent().parent()
        shape = {}
        if $el.attr('points') # this is a polygon or polyline
          shape.path = 'M' + $el
                                .replace(/\r\n(\t+)/g, '')
                                .replace(/\ /g, 'L')
                                .replace(/L(\t*)$/, 'z')
        else if $el.attr('d') # this is a path
          shape.path = $el
                          .replace(/[\n\t\r ]/g, '') 
        if $grandparent.length
          shape.attrs = _.extend getAttrs($el),
          shape.attrs = getAttrs($el)
        # default fill on a polygon is black,
        # but default fill on a path is transparent
        # since we're converting polygons to paths
        # we need to be explicit with black fills
        if !_.has shape.attrs, 'fill'
          shape.attrs.fill = '#000000'
        frameData.push shape 
                      .frames[frame_index] = frameData

Putting it all together

Once we had all the data, it wasn’t hard at all to start working with Raphaël. For each animation we defined a canvas and created a simple timer (using requestAnimationFrame) that would draw out each frame when the time came.

Happy accidents

Creating this automated process to convert PSD animations into data that could be plugged into Raphaël opened up worlds of possibility. We didn’t have to worry if we wanted to change some of the art and we were able to introduce optimizations and dynamic magic in the browser.

Colour rotating

Part of the magic of the last few Playground websites has come from introducing an element of randomness. We actually cycle through a small group of brand colours as the user navigates from page to page. This means the site is mostly grayscale with a single spot colour. We wanted to make the animations share in this magic.

Because we were programmatically drawing each frame we were able to check for the current brand colour and adjust the spot colours in the animations accordingly.

Optimizing repeated frames

In a traditional filmstrip animation, frames are shown at a constant rate (typically 24 fps) and timing is handled by “holding” frames—i.e. repeating them. In our case, we weren’t just shining a light through a film, though, we were actually drawing out each frame. To conserve processing power we only clear the canvas and draw a new frame if the frame’s data is different from its predecessor. This means being smart both with bandwidth and with drawing.

Splitting an animation into layers

Not having to redraw repeated frames provided a huge boost in efficiency, but in some cases it wasn’t enough. Our knitting friend in the “Make the web a better place” animation contained a great deal of shapes and many of these were being redrawn from frame to frame unnecessarily.

We decided to split her up into four layers, each with a corresponding Raphaël canvas: background, lady, armrest, arm. Her chair and the table beside her are drawn once in the background. Above this the lady herself animates throughout all the frames. Above this we draw (once only) the armrest which obscures part of her body. And finally, her arm which is animated throughout rests atop the armrest.

Wobble wobble wiggle

Ideally, we wanted some more user interaction with the animations. We talked about introducing physics: gravity, collisions, joints… Maybe the steam above the lady’s coffee cup would swirl around as the user’s cursor passed through it. Maybe a user could grab the knights arm and throw him around the page 8. We realized, however, that we still had a whole website left to build. So we ended up going with something simple: CSS animations that added some wiggle. This was kind of cute, kind of fun and it provided a hint that we weren’t just using video or GIFs.

Data compression

The nice thing about having all of the animation data saved in a text format like JSON meant that we could take advantage of Gzip compression over HTTP. Apache has had mod_gzip since the year 2000 and this is well supported in all kinds of browsers.

What this all means, of course, is that we were able to create 8 infinitely scalable vector animations and have them cost ~170k in bandwidth. That’s 8 gorgeous, crisp animations totalling the size of a single lossy JPEG.

Lessons Learned

Animating hair is hard

The lady in “Make the web a better place” has pretty great hair. This took Chris several days to complete as her hair needed to sway and bunch up without bubbling or looking weird. It’s no coincidence that all the other characters are either bald or have sharp angular hair.

Stand on the shoulders of giants

We owe so much to so many people. Quentin BaillieuxDmitry Baranovskiy, the Node family, Matthew MuellerBenjamin Lupton, everyone involved with SVG, Jeremy Ashkenas and the DocumentCloud team, the jQuery family, the aforementioned Paul Riggott… the list goes on. We wrote this article and we deliberately left our code unminimized on our site, not because we think it’s particularly good, but because we’re happy to share what we’ve learned so that others can take it that much further 9.

Great artists steal
  1. Pronounced with a hard “G”, of course. 
  2. We mean that in the nicest possible way, honest. We’ve all been there. 
  3. In theory, anyway. 
  4. Not that it was ever our intention to support an 11-year old browser. 
  5. After we finished this project we did discover that Dmitry had already written such a translator. Had we known, we probably would have used it, but doing it ourselves actually ended up creating some exciting possibilities for manipulating the SVG data. 
  6. We had no interest in being made into supper
  7. We tried that too. 
  8. Chat heads? 
  9. In fact, we’re thrilled to see that we’ve already inspired a few people