The Polygon PlayStation 4 Review and Xbox One Review involved an unprecedented level of coordination between the editorial and product teams at Vox Media. The goal was to create a pair of extremely high touch features to highlight the talents of our writers and video team, while pushing the envelope on longform design.
There were a lot of lessons learned, but the final results speak for themselves.
Why SVG?
Initially, we did not consider SVG when approaching the design of the reviews. But we soon realized that the SVG format offers the ability to have delicate line art due to its vector and path capabilities, making it a great fit for our needs. And, as it turns out, not just our aesthetic needs, but our technical needs as well.
Polygon, as a site, is designed to be responsive. However, standard image formats like jpg
, gif
, or png
don’t don’t always perform well when asked to enlarge or shrink dimensions as dictated by the user’s browser size. SVGs, on the other hand, take on responsive properties perfectly: vectors can increase or shrink to arbitrary sizes without any loss of fidelity, and animations and operations done on SVG elements adjust relative to its size without any additional work.
But before we could use SVGs, they needed to be created. There were no easy preexisting SVGs of the two consoles that we could grab. They needed to be designed from scratch.
From Illustrator to SVG
Polygon’s designer, Tyson Whiting, painstakingly traced paths to create line art from real life photos of the two consoles. Doing this in Adobe Illustrator is relatively straightforward, though it is somewhat mind-numbing.
After exporting a test line art tracing done in Illustrator for the first time, however, there were a couple issues that needed to be addressed:
-
Many of the colors, fills, and stroke widths Tyson applied inside Illustrator were exported as inline attributes and styles on the SVG elements.
-
The actual SVG tags used to replicate Illustrator objects (eg,
path
vsline
vspolygon
vspolyline
vsrectangle
) were not clear, and many times not the one we wanted.
The solution was remarkably simple in its directness, if not in its cleverness: just manually massage Illustrator’s exported SVG files.
Inline styles were moved to a centralized stylesheet that affected all SVGs on the page, and we learned through trial and error what attributes were used by Illustrator to determine which SVG tags were chosen.
A factory line was developed: Tyson, Polygon’s designer, would create and export the SVGs in Illustrator; Ally Palanzi, an intern at Vox Media, would manually go into the SVG and add group
tags and comments to clearly label which elements did what; and finally, I would place and animate SVGs wherever needed.
It wasn’t the fastest process, but ultimately we had a set of well-documented and clean SVGs to use.
Visually controlling SVG paths
The act of "drawing" in an SVG is optical trick caused by manipulating two SVG path
properties.
An SVG path
is a single, continuous line described by coordinates contained inside the element. It looks something like this:
Animating that path to draw itself is fundamentally an illusion created by adjusting that path’s stroke-dasharray
and stroke-dashoffset
.
The stroke-dasharray
property is a whitespace or comma delimited list that controls the dashes and gaps that make up the path. When set to "10 10"
, for example, the path
's stroke alternates in a dash of 10
and then a gap of 10
. Extrapolating this idea, if the total length of the path
was 10, then that would mean the first "dash" is as long as the path
itself.
Specifically, this means that there is no visual difference between a path that has no stroke-dasharray
set, and a path with a stroke-dasharray
set where the dash is equal to the path
's total length. Conversely, this also means that the gap after the dash is also the length of path
.
Why is this important?
A second property, stroke-dashoffset
controls the offset of the dashes in the path
. The offset, in this case, means where along the path the first "dash" of the dotted dash created by stroke-dasharray
starts. The default value for stroke-dashoffset
is 1 — meaning, it starts immediately.
What does it mean when stroke-dashoffset
is set to the length of the path
?
It means that the dotted dash starts an entire path
's width away. Or put another way, the gap between the dashes defined by stroke-dasharray
fills the entire length of the path
. Or put in even simpler terms, the path
is visually invisible.
Combining these two properties means that we can control how much of the path
is showing, in a visual sense.
The browser is still drawing and rendering the entire path
. However, due to the placement and length of the dashes and gaps, a stroke-dasharray
equal to the length of the path
combined with stroke-dashoffset
's of varying lengths will create the visual illusion of a partial stroke.
SVG path animation
There are several ways to animate the two properties above, with CSS transitions and JavaScript-based interval animation being the most practical and widely supported. Each has its pros and cons, but we chose JavaScript over CSS transitions for one primary reason:
Timing.
As of this post, there is not a reliable way to determine the specific moment when a CSS transition has finished. It’s possible to fake it by using a setTimeout
equal to the duration of the transition, but by doing so, it’s taken on faith that the transition wouldn’t be slightly slower or faster than the desired duration.
In any case, animating the property with either the CSS or the interval approach works something like the following pseudocode:
for every path in an svg:
store the results of getTotalLength()
set stroke-dasharray equal to totalLength + " " + totalLength
set stroke-dashoffset equal to totalLength
animate()
The animate
method will differ based on the approach used to change the values of stroke-dasharray
and stroke-dashoffset
. With a CSS transition approach, animate
would set transition
to something like:
stroke-dashoffset 2s ease-in-out
Then, after setting stroke-dashoffset
to 0 for each path
, the browser will automatically animate that value change via the specified CSS transition.
As mentioned above, this a great and performant way of doing things if the specific timing of when the transition finishes is not important. This gets tricky, however, if you want to chain together animations — or have things happen only after specific animations are finished.
For that, you’ll need to use JavaScript to manually set the values that the CSS transition tweened.
A naive approach might look like something like this for the JavaScript version of animate
:
requestAnimationFrame
But, there’s one major problem with this approach: the draw
method fires in ignorance of the browser’s render cycle, resulting in needless calls. In practical terms, the browser is doing more work than it needs to, thereby possibly lowering the FPS of the browser’s rendering, and making the animation appear slow.
The fix?
Use requestAnimationFrame
. There are great resources out around the web already explaining the hows and whys of requestAnimationFrame
, but suffice to say that if you are doing frame-based JavaScript animation, you should be using it. It ensures that you are rendering precisely the frames you need to render at exactly the time that the browser requires them to be rendered.
Using requestAnimationFrame, the above code now looks something like this:
Duration based vs frame-count based animation
There are two primary ways to determining how long an animation should run; an explicit duration, or a flexible frame count. There are benefits to each approach. The SVG animations on Polygon’s reviews primarily used the frame-count approach rather than setting an explicit animation duration.
Duration-based animation is the approach the code chunk above uses to animate a path. Given a desired duration (in the above case, 2000ms), the animation should run for exactly that duration. "Frames" are calculated by the proportion of elapsed time as compared to the total duration.
This has one big drawback: on slower computers, FPS dips result in skipped frames.
Imagine that in an ideal 60 FPS scenario, requestAnimationFrame
is called 60 times per second. This is fine. That means the animated SVG path’s stroke-dashoffset
is being changed 60 times a second — more than enough for it to appear smoothly animated to the human eye. However, if there’s a dip in FPS — say your computer’s hard drive suddenly starts thrashing and everything slows — then suddenly requestAnimationFrame
is only being called 15 times a second. And because we use elapsed time as the determining factor for stroke-dashoffset
, the visual result is one of dropped frames.
The animation appears to skip around.
The alternate approach, then, is to stop using elapsed time and use a different metric. In a frame-count strategy, we rely on the fact that the browser will self-impose a FPS cap, and simply animate stroke-dashoffset
based on the amount of frames elapsed. The above code chunk would look something like this:
Done this way, if there is ever an FPS dip, the visual impact on the animation is that of a slowdown, rather than a frame skip.
And in the case of Polygon’s PS4 and Xbox One SVG animations, it was deemed that the smoothness of the animations was more important than the specific amount of time that the animations took to run.
Introducing Metronome
As part of the development efforts for both reviews, we created an internal JavaScript library called Metronome that we hope to open source soon. It provides an easy interface to hook callbacks on top of requestAnimationFrame, with support for various easing methods.
Stay tuned for more news on Metronome!
Lastly: Come work with Vox Product! We are hiring.