The iOS 7 homescreen parallax effect in the browser
A couple of weeks ago we started a series on how you might implement some of the more notable design effects in iOS 7 using purely web technologies. In the meantime, it’s been noted elsewhere that this may be difficult and perhaps impossible to do. I’m here today to tell you otherwise! Well, at the least the impossible part.
Today we are going to look at one of the features that got some attention, the use of parallax on the homescreen to create a sense of depth This is one of the words that Apple used to describe their new, supposedly “flat” design.
If you’ve not seen this in action, Gizmodo took Apple’s video and made an animated GIF version, which you can see below.
It got me thinking, how did they do that? How easy would it be to replicate using web technologies?
Before we jump in and build it, you can take a look at the finished product on a smartphone, or emulated in your browser to get a sense of what we’re going to be doing.
We’ve all seen parallax effects before. They’re a staple of 2D, and “2.5D” games, to add the perception of distance between objects. For the last couple of years we’ve also seen the use, and abuse, of parallax and scrolling in web page design.
So, what is going on to create what appears at a glance to be a 3rd dimension? Well I’ll let you in on a little secret that anyone who has installed the iOS 7 beta will have already noticed. The effect is particularly impressive when viewing a 2D rendering (such as a video) of the effect. When you see the same effect on a device, and then watch a video of the effect on the device, I think you’ll find the video more compelling.
What is parallax?
Look out the window. Objects at different distances move at different relative speeds. The closer an object, the more quickly it appears to be moving, and the further away it is, the more slowly it appears to be moving.
Documentary film makers, who often have only photographs or paintings to use as their primary visual material, commonly make use of this to trick our eyes into seeing a 3rd dimension. It’s called “the Kid Stays in the Picture” effect. It tricks our brain that we are looking at a 3D scene, by using different relative motions.
In the iOS 7 homescreen situation, we have two levels of depth. The surface of the screen, and the background image. If you pay attention to the animated version, you’ll see that the icons don’t move relative to the surface of the screen. The effect is created purely by moving the background image relative to the icons. Notice how as we tilt the phone from right to left (on the screen) the background image moves toward the right, and as the phone tilts from left to right, the image moves back to the left. Why does this create a sense of depth? For the moment, to keep things simple let’s just worry about rotation around a line running from the top to the bottom of the screen through its middle as the device is facing toward us. That is, when we tilte the phone left and right.
Imagine we were looking down on the scene from above
The vertical arrow is the line of sight. Now, let’s rotate the phone to the left
I cheated a bit here, to make the maths a little bit simpler, but the idea is more or less the same. Instead of the device rotating, I’ve moved the observer, but their line of site is still the same – directly between the two icons in the middle of the screen. So, the icons don’t appear to move relative to the screen, but the background figures do. But just how far are the figures apparently shifted?
I’m making the observation that the original, and adjusted lines of sight of the viewer make a right-angled triangle. Which is excellent, because the trigonometry (don’t be put off!) of right-angled triangle is simpler than arbitrary triangles.
OK, so if your high school maths is a little rusty (don’t worry, I have a degree in mathematics and I had to look this up!) here’s what we have.
- We have the original line of sight (we’ve labelled that a, which you might remember as being the adjacent side.
- We have our new line of sight, labelled h for hypotenuse
- we have the angle between the old and new lines of sight, θ (pronounced theta, for some reason, angles are usually labelled using Greek letters).
- We have the distance at right angles from the hypotenuse to the adjacent side, labelled o for opposite (because it is the side opposite the angle we know, θ)
And we have enough information to calculate o, should we know the length of a, using our high school trig
o = a*tan(θ)
We know θ, it’s the angle we’ve rotated the screen (and we’ll see in a moment how we’ll use the deviceOrientation event to get this angle as it changes). But how do we know a? Well, here’s the cool part, we just make it up. The larger the value of a, the further ‘away’ the background figures will appear to be (so the larger o will be.) In particular what this means is the relative movement of the background figures behind the icons is not linear, but goes something like this, where the background is 50px away
degrees | apparent movement (px) |
---|---|
0 | 0 |
5 | 4 |
10 | 9 |
15 | 13 |
20 | 18 |
25 | 23 |
30 | 29 |
35 | 35 |
40 | 42 |
45 | 50 |
50 | 60 |
55 | 71 |
60 | 87 |
65 | 107 |
70 | 137 |
75 | 187 |
80 | 284 |
85 | 572 |
90 | ∞ |
Note how the movement left or right increases dramatically as we get closer to θ = 90 degrees. Of course it can never be 90 degrees.
In short, the more we rotate the device, the more the background image appears to move.
Creating the effect
So, now we have a basic model for how parallax works, how can we use this to emulate the parallax effect? Here’s my idea
- we’ll have an element that emulates the homescreen of the device
- the homescreen element contains the application icons
- we’ll have a background image for that element, like the one in Apple’s animated example.
Now we have our basic content
- when the device rotates to the right, we calculate how far to the left our background image should move, which will change depending on how far “away” we want the background to be
- when the device rotates to the left, we calculate how far to the right the background should move
- we then use
background-position
to move the background image relative to the element.
There are other ways we could do this, with their own advantages. We could have the background as a separate img
element, then use CSS3 translate3D
to move this element to the left and right. This would have the advantage of enabling the device’s GPU to take care of moving the background. But we’ll stick to background-position
, as it’s the simplest approach.
So, here’s our basic HTML
<section id="homescreen"> <figure id="app1"> <img src="images/app1.png"> <figcaption>App 1</figcaption> </figure> <figure id="app2"> <img src="images/app2.png"> <figcaption>App 2</figcaption> </figure> <figure id="app3"> <img src="images/app3.png"> <figcaption>App 3</figcaption> </figure> <!-- and so on --> </section>
and some very basic style
#homescreen { width: 100%; height: 100%; position: absolute; left: 0; top: 0; background-image: url('images/wds12.jpg'); background-repeat: no-repeat; } #homescreen figure { width: 64px; text-align: center; float: left; margin: 20px; text-shadow: 0 1px 0 #555; } #homescreen img { width: 100%; border-radius: 10px; box-shadow: 0 4px 4px -2px rgba(0, 0, 0, .6); }
Which gives use something like this.
Yes, not particularly interesting, yet. Now let’s think about moving the background image.
By default, a background image is positioned with its top left hand corner in the element’s top left hand corner. But this isn’t what we’ll want, because as soon as we rotate to the left, the image moves to the right, and so we’ll see “through” the element on the left of the background image like so.
So we’ll need to make sure that our image is wider than our element, and positioned not in the top left hand corner. We could use the CSS3 property background-size
to achieve the first, but we need to be careful. Suppose we decided to make the width of any background image 150% the width of the element. We’d use background-size: 150%
to do this, but this then scales the height of the element to maintain the image’s proportions. For an image that is inherently narrower than the element’s current size, then the height will increase. But if the image is inherently wider than 150% of the width of the element in pixels, then the height of the image will decrease, and we may end up with this situation.
Better to have an image that will in all likelihood be taller and wider than the element’s width and height (yes, in a responsive world, this will present its own challenges).
The second part is a bit more straightforward. Surely, we’ll just use background-position: center
? Sadly, not quite so simple. Because we’ll be changing the background-position
to achieve the effect. So, what we do is position the background image using JavaScript and the DOM, so that its top is half the difference between its intrinsic height and the current height of the homescreen element above the top of the element, and similarly, its left is half the difference between its width height and the current width of the homescreen element to the left of the element. Essentially, the center of the image will be in the center of the element.
We’ll do this by adjusting the background-position
of the element once the page has loaded (as is often the case, this is the most complicated part of the process, even though it’s only peripheral.)
function setupBackgroundImage(element) { //set up the background image for the element //call this when the page loads var imgURL = window.getComputedStyle(element).backgroundImage //get the current background-image URL //bg image format is url(' + url + ') so we strip the url() part imgURL = imgURL.replace(/"/g,"").replace(/url\(|\)$/ig, ""); //now we make a new image element and set this as its source var theImage = new Image(); theImage.src = imgURL; //we'll set an onload listener, so that when the image loads, we position the background image of the element theImage.onload = function() { positionBackgroundImage(element, this.width, this.height) } } function positionBackgroundImage(element, imageWidth, imageHeight) { //this is called when a backgroundImage loads var elRect = element.getBoundingClientRect(); xOffset = -1 * (imageWidth - elRect.width)/2 yOffset = -1 * (imageHeight - elRect.height)/2 //these are global variables as we want to remember the offsets for later //ideally we'd not use global vars, but done like this for simplicity element.style.backgroundPosition = xOffset + "px " + yOffset + "px" }
Now we’ve set up our elements, our homescreen background image, and we’ve worked out our algorithm, we’re ready to go.
Adjusting the background image
OK, at this point, we’ll pretend we already know the current rotation of the device (we’ll cover how we get that next). Let’s assume that each time the device changes its orientation we get an event. Which in fact is exactly what happens. We’ll create an event handler for this event. As we’re responding to a change in the orientation, we’ll call this ‘orientationChanged
‘. Here’s what this will need to do (again we’ll concentrate on just the rotation around the Y axis of the device to keep things simple)
- calculate the tan of the rotation around the Y axis (in radians, not degrees) (this was θ in our earlier discussion). More on radians in the notes
- calculate the relative movement of the background image (
a*tan(θ)
) - adjust for the xOffset we calculated earlier based on the width of the background image and the element
- set the
background-position
of the element to this value
And here’s that in JavaScript
function orientationChanged (xOrientation, yOrientation) { var rotYTan = Math.tan(yOrientation*(Math.PI/180)) //calculate the tan of the rotation around the Y axis //Math.tan takes radians, not degrees as the argument var backgroundDistance = 50 //set the distance of the background from the foreground //the smaller, the 'closer' an object appears var xImagePosition = (-1 * rotYTan * backgroundDistance) + xOffset + "px" //calculate the distance to shift the background image horizontally //the X and Y seem swapped, but X in device rotation terms is around a line through the middle of of the screen from left to right, so its correct homescreen.style.backgroundPosition = xImagePosition + " " + 0; //set the backgroundimage position to xImagePosition }
So, now we are moving the background image left or right depending on the rotation of the device. But how well does this work? Well, you can test it out yourself. Here’s an emulation, which uses CSS 3D transforms, as well as a version you can run directly in a device. What do you think? In the emulated version, you might think the 3D effect is simply coming from the use of CSS 3D, but if you set the distance to zero, and then rotate the device, you’ll see that there is no parallax effect. The greater the distance you set, the more pronounced the effect.
The main event
But so far, we’ve not discussed how we actually get this mystical orientation information. As we mentioned briefly we’ll be using the deviceOrientation event widely supported in mobile devices in particular.
I covered DeviceMotion and DeviceOrientation in some detail recently when I built a motion activated security camera in the browser, so we’ll not go into the details of DeviceOrientation here. In short though
- alpha is the rotation around the z-axis (an imaginary line coming directly out of the screen). Positive alpha is rotated to the left, negative alpha is rotate to the right. This seems counter intuitive, but our frame of reference is looking upwards, so to the left is clockwise in this frame of reference, and clockwise is positive, anti-clockwise negative.
- beta is rotation around the x-axis, a line left to right across the device when it is laid flat on its back. Positive beta is when the device is tilted away from you, from 0 degrees to 180 degrees (screen facing downwards). Negative alpha is when the device is tilted toward you (again, from 0 degress to -180 degrees, which is facing downward)
- gamma is rotation around the y-axis, a line through the middle of the device away from the user. Positive (from 0 to 180 degrees) is tilted to the left. Negative, 0 to -180 degrees, is tilted to right
Which is all a lot easier to understand with a picture.
Here’s how we’ll use this information
- we’ll add an event listener for
devicemotion
events to theWindow
- this function will receive an event, which includes information about the current rotation of the device around its x, y and z axes
- We’ll use this to determine the distance our background image should moved to achieve the parallax effect
- We’ll then move our image based on this calculation
Here’s our event handler, which is almost identical to the code above, just adapted to take the orientation event as its argument.
function orientationChanged (orientationEvent) { var gamma = orientationEven.gamma; //get the rotation around the y-axis from the orientation event var tanOfGamma = Math.tan(gamma*(Math.PI/180)) //calculate the tan of the rotation around the Y axis (gamma) //Math.tan takes radians, not degrees as the argument var backgroundDistance = 50 //set the distance of the background from the foreground //the smaller, the 'closer' an object appears var xImagePosition = (-1 * tanOfGamma * backgroundDistance) + xOffset + "px" //calculate the distance to shift the background image horizontally homescreen.style.backgroundPosition = xImagePosition + " " + 0; //set the backgroundimage position to xImagePosition 0 }
What the X?
Ok, so there’s a reason we’ve still only worried about movement left to right. When you look at a screen, the neutral position in terms of rotation around the y axis (that is, tilted to left or right) is clearly with the screen perpendicular to the user’s line of sight. But, what’s the neutral position for the tilt forward and backwards? Lying on its back? Probably not. Perpendicular to the surface of the earth? Also probably not. When you hold a phone or tablet, it’s likely to be somewhere between these two.
To simplify matters, let’s say that the user will typically hold a device at around 45 degrees. So, we’ll make this our “neutral” position for the parallax effect. Which means, we’ll subtract 45 degrees from the current rotation when calculating the position of the background image up and down.
function orientationChanged (orientationEvent) { var beta = orientationEvent.beta; var gamma = orientationEvent.gamma; //get the rotation around the x and y axes from the orientation event var tanOfGamma = Math.tan(gamma*(Math.PI/180)) var tanOfBeta = Math.tan((beta -45)*(Math.PI/180)) //calculate the tan of the rotation around the X and Y axes //we treat beta = 45 degrees as neutral //Math.tan takes radians, not degrees, as the argument var backgroundDistance = 50 //set the distance of the background from the foreground //the smaller, the 'closer' an object appears var xImagePosition = (-1 * tanOfGamma * backgroundDistance) + xOffset + "px" var yImagePosition = (tanOfBeta * backgroundDistance) + yOffset + "px" //calculate the distance to shift the background image horizontally homescreen.style.backgroundPosition = xImagePosition + " " + yImagePosition; //set the backgroundimage position to xImagePosition yImagePosition }
Dis-orientation
What’s also interesting is that if we change the orientation of the screen, from landscape to portrait, the beta and gamma values from the orientation event are still relative to the device. So, we need to now take into account whether the screen has been flipped (maybe even upside down) before doing our calculations.
First, we need to know what the current screen orientation is. We do this with the screenOrientation
property of the window
. This is the current rotation of the screen, where 90 degrees is rotated to the right, 180 degrees is turned upside down, and -90 degrees is rotated to the left.
Let’s think about each of these three ‘new’ possible states in turn.
When turned upside down (screenOrientation === 180
), we’ll reverse the beta and gamma values, by multiplying them by -1. Effectively, we’re treating up as down, and down as up, left as right, and right as left.
if (screenOrientation === 180) { beta = -1 * orientationEvent.beta gamma = -1 * orientationEvent.gamma }
When rotated left, screenOrientation === -90
, we’ll swap gamma for beta, and the reverse of beta (-1 * beta) for gamma. Essentially, swapping left and right for forward and back, back for right, and forward for left.
if (screenOrientation === -90) { beta = orientationEvent.gamma gamma = -1 * orientationEvent.beta }
Lastly, when the screen is rotated to the right (screenOrientation === 90
), we’ll do the opposite. So we swap left for forward, right for backward, forward for left, and backwards for right.
if (screenOrientation === 90) { beta = -1 * orientationEvent.gamma gamma = orientationEvent.beta }
Then we continue as before, and now regardless of the screen’s orientation, we still have our parallax effect. We’ve actually gone one step further than the real iOS homescreen, at least on the iPhone, as on the iPhone device rotations don’t affect the orientation of the homescreen.
Getting the Code
So, very little code really to achieve the effect, particularly after we’ve set up our background image. If you’d like to grab the code, it’s all up on Github. And here’s the finished product if you want to visit it in your tablet or phone of choice. Provided they support DeviceOrientation events, the effect should work. Enjoy!
Technologies covered in this post
Browser Support via Can I Use
Notes
In school, you most likely did trigonometry and geometry with angles in degrees. Grown up math is typically done in radians, where 2π radians is 360 degrees. Since device orientation gives us rotation in degrees, we covert to radians by multiplying the number of degrees we have by π and then dividing by 180.
Great reading, every weekend.
We round up the best writing about the web and send it your way each Friday.