This is a demonstration of a visual effect that combines radial perspective (like Apple's QuickTime Virtual Reality or Google's Street View) with the parallax effect that has become so popular in Web design in recent years. Unlike the QTVR, Street View or parallax scrolling, Radial Parallax does not require JavaScript or plugins and involves a total download of about 50KB or less. In this post I'll explain what it does, how it works and how you can implement it yourself.

About the Demo, "Autumn in Cupertino"

The demo I created to introduce Radial Parallax is an intersection I pass through on my way to Whole Foods. One day while stopped at the light, it occurred to me that there were evergreen trees all around me except for one corner that had deciduous trees. As the Bay Area is known for having only two seasons ("Rain" and "No Rain"), I thought these trees might be the only indication a person might have that summer has become autumn. This intersection also happens to be the middle of Apple's campus. There are some stories that can only be told by panning around a scene, like how people in Cupertino mark seasonal changes. For all we know, these trees are what tell them it's time to release new iOS devices.


In the mid 1990s I saw a QTVR demo on a CD ROM that allowed me to pan 360 degrees around a photographic scene to see the environment as if I was actually there with the photographer. It was the coolest thing I had ever seen. Period. I thought, "this is the future."

It never became as widely-used as I expected. The QTVR concept became popular among hotels and realtors looking to convince prospective customers of the value of a space. Google used the concept for the Street View component of its map service, and that's where most people encounter it today. Beyond that, the concept was largely forgotten.

The file sizes were prohibitively large, and the camera equipment required to make it work well was expensive. You could easily spend over $2000 on an SLR, fisheye lens, special tripod mount and software only to discover that the software doesn't know how to correct for the distortion created by your particular $400 lens. I could see this creating barriers to adoption by the masses.

A few years ago a company named Occipital introduced an app for iOS and Android that made the task wonderfully easy. The panoramas it produced weren't perfect, but they were very impressive considering you were using your phone's camera, compositing and publishing all from one device that wasn't a computer in a few minutes. You could upload your panoramas to Occipital's Web site, but there was no way to host them on your own site. The most you could do was download the flattened composite.

So I've long felt that the 360-degree panoramas was a technology in waiting. It waited for consumer bandwidth to improve and it waited for software to enable the easy production and consumption of content. Today, every modern browser (except Internet Explorer, because it does not support SMIL animation) has the capability to render these panoramas with a parallax effect.

What Radial Parallax Does

Older versions of QTVR seemed to operate on a fairly simple concept: by making a passing object smaller as it reaches the middle of your field of view, then larger as it passes our of your field of view, you can create the illusion that the object is orbiting around the viewer. Since smaller objects appear to be further away, we assume it has moved away and back toward us.

Imagine two movie screens side by side, each with its own projector. The left screen plays the left half of the movie while the right side plays the other. An object moving across one screen toward the other would disappear from the first screen as it appears on the second screen. If you rotate those two screens on their Y axes while keeping their edges together at the point furthest from the viewer, you would create the illusion of depth. Objects would shrink as they moved toward the vanishing point, then grow again while travelling across the second screen.

Radial Parallax operates on this same principle. A 3600-pixel-wide scene is created in SVG, but the view box for that SVG only shows half of what we want the viewer to see. A second SVG references the objects in the first SVG and displays the other half. Both SVGs are rotated like our two movie screens using a CSS perspective transform.

WHY TWO SVGS? The browser will not apply perspective transforms to elements inside an SVG. The transform has to be applied to the SVG itself. Since we have to rotate each half of the image in a different direction, we need to use two SVGs with different transforms applied. If at some point in the future it becomes possible to apply these transforms to groups inside an SVG, this effect will work more smoothly on devices with less processing power. Currently, the browser "paints" one SVG, then the other each time the scene moves, and if it can't repaint both SVGs faster than you notice, the motion appears choppy.

SMIL animation is used to move parts of the scene across the two SVGs at different rates, creating the parallax effect. The stars, which are farthest away, move at the slowest rate. The mountains that surround the valley move faster because they are closer, but still miles away. The buildings and trees move still faster, and swirling leaves in the foreground move even faster.

How Radial Parallax Works

Here is a simplified illustration of what is going on:

An SVG is created with one sine-wave-like path (id="wave") and a motion path (id="motion-path") for the wave to follow. The use tag allows us to reuse the wave and apply different effects to it:

  <use xlink:href="#wave" x="0" y="0" stroke="red" stroke-width="1"><animateMotion dur="36s" begin="-0.445s" repeatCount="indefinite"><mpath xlink:href="#motion-path"/></animateMotion></use>
<use xlink:href="#wave" x="0" y="0" stroke="green" stroke-width="2"><animateMotion dur="72s" begin="-0.939s" repeatCount="indefinite"><mpath xlink:href="#motion-path"/></animateMotion></use>
<use xlink:href="#wave" x="0" y="0" stroke="blue" stroke-width="3"><animateMotion dur="108s" begin="-1.381s" repeatCount="indefinite"><mpath xlink:href="#motion-path"/></animateMotion></use>

Each instance of the wave has a different color and stroke thickness applied so it stands out, and the dur attribute determines the speed of the movement. The red wave takes 36 seconds to travel along the same motion path as the others, but the green wave takes twice as long. So the red wave moves the fastest, twice as fast as green and three times as fast as blue. These same three use tags are pasted into the second SVG and reference the #wave and #motion-path we defined in the first one. The only difference is that the animations in one SVG are set to begin="0s" while the other will be offset to ensure the waves meet up at a point where one appears to be a continuation of the other. By setting the begin time to a negative number of seconds, the wave will start out at a position where it would be if it had already been in motion for that length of time.

If we replaced our use tags with rects, we could define each layer of our scene in a different pattern that is the same height as the rect and same width as the motion path and fill the rect with that pattern:

<path id="motion-path1" d="M0,0 -3600,0" stroke="none" fill="none"/>
<pattern id="mountains1" x="0" y="0" width="3600" height="460" patternUnits="userSpaceOnUse">
    <path fill="#2B2D42" d="M0,242.281 41.333,226.676 32.667,460.948 0,460.948z"/>
    <path fill="#3B3E59" d="M41.333,226.676 32.667,460.948 122,460.948 114.667,241.004z"/>
<rect x="0" y="0" fill="url(#mountains1)" width="4000" height="460"><animateMotion dur="72s" begin="0s" repeatCount="indefinite"><mpath xlink:href="#motion-path1"/></animateMotion></rect>

The pattern can be referenced by both SVGs without the need for duplicate code. Our pattern and motion path are 3600 pixels wide, but as the rect completes its first loop we will see a gap at the end, interrupting the animation. I have made the rect 400 pixels wider than the pattern to cover this. Because we have used a pattern, it will repeat in the rect after 3600 pixels, ensuring the viewer sees an uninterrupted image. To complete the scene, we create a rect for each layer and a pattern in the defs section to fill it. We can now set each instance of each layer of the scene to move at a different speed just by changing the dur attribute in that rect.

How to Implement Radial Parallax

You can resize a panoramic photograph to 3600 pixels and use the template below, or paste your own SVG scene in place of the image. Because the repainting operation is so expensive for the browser, I'm linking to my photographic panorama pens below instead of displaying them inline.

.leftpanel {
    -webkit-transform: translateY(-48px) translate3d(0, 0, 80px) perspective(80px) rotateX(0deg) rotateY(3deg);transform: translateY(-48px) perspective(80px) rotateX(0deg) rotateY(3deg);position:absolute;top:0px;left:0px;z-index:1;
.rightpanel {
    -webkit-transform: translateY(-48px) translate3d(0, 0, 80px) perspective(80px) rotateX(0deg) rotateY(-3deg);transform: translateY(-48px) perspective(80px) rotateX(0deg) rotateY(-3deg);position:absolute;top:0px;left:320px;z-index:2;
body {
    background-color: #333;
<svg x="0px" y="0px" width="360px" height="400" xml:space="preserve" class="leftpanel">
    <pattern id="image" x="0" y="0" patternUnits="userSpaceOnUse" height="1074px" width="3600px">
      <image xlink:href="BACKGROUND_IMAGE_URL" x="0" y="-400" height="1074px" width="3600px"></image>
    <pattern id="imagef" x="0" y="0" patternUnits="userSpaceOnUse" height="1074px" width="3600px">
      <image xlink:href="FOREGROUND_IMAGE_URL" x="0" y="-400" height="1074px" width="3600px"></image>
    <path id="motion-path1" d="M0,0 -3600,0" stroke="none" fill="none"/>
    <rect x="0" y="0" fill="url(#image)" width="4000" height="1074"><animateMotion dur="72s" begin="0s" repeatCount="indefinite"><mpath xlink:href="#motion-path1"/></animateMotion></rect>
    <rect x="0" y="0" fill="url(#imagef)" width="4000" height="1074"><animateMotion dur="18s" begin="0s" repeatCount="indefinite"><mpath xlink:href="#motion-path1"/></animateMotion></rect>
<svg x="0px" y="0px" width="360px" height="400" class="rightpanel">
    <rect x="0" y="0" fill="url(#image)" width="4000" height="1074"><animateMotion dur="72s" begin="-7.2s" repeatCount="indefinite"><mpath xlink:href="#motion-path1"/></animateMotion></rect>
    <rect x="0" y="0" fill="url(#imagef)" width="4000" height="1074"><animateMotion dur="18s" begin="-1.8s" repeatCount="indefinite"><mpath xlink:href="#motion-path1"/></animateMotion></rect>
<svg height="138px" width="711px" style="position:absolute;top:330px;left:0px;z-index:3;-webkit-transform: translate3d(0, 0, 800px);">
      <rect x="0" y="0" fill="#333" width="711px" height="138px"/>

One is a simple panorama taken from that street corner that served as the setting for "Autumn in Cupertino." The perspectives aren't exactly the same because I wasn't standing in the same exact spot. The other shows a basic parallax effect near Portland's City Hall. I copied selected leaves on the ground in Photoshop and pasted them into a 3600-pixel-wide transparent PNG which is layered on top of the background by the SVG code and moved at a different speed.

My goal is to make it easy for others to use this technique. If you have any questions (or if I've left something out), please let me know. If you do use it, please link to your pen in the comments below. I'd love to see what people come up with, whether it's photographic or illustrated!

6,028 1 18