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 next?() 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 .attr('points') .replace(/\r

(\t+)/g, '') .replace(/\ /g, 'L') .replace(/L(\t*)$/, 'z') else if $el.attr('d') # this is a path shape.path = $el .attr('d') .replace(/[

\t\r ]/g, '') if $grandparent.length shape.attrs = _.extend getAttrs($el), getAttrs($grandparent) else 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 animations[name] .layers[layer_index] .frames[frame_index] = frameData complete()

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.