Year round learning for product, design and engineering professionals

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.

animated gif of the parallax effect on the homescreen
Gizmodo

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

looking down on the scene from above
looking down on the scene

The vertical arrow is the line of sight. Now, let’s rotate the phone to the left

rotating the phone to the left
looking down on the scene, having rotated 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?

the original and new lines of sight make a right triangle
the original and new lines of sight make a right triangle

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.

the trigonometry of the triangle created by the two lines of sight
the trigonometry of the triangle created by the two lines of sight

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.

the homescreen
the homescreen

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.

the homescreen with image moved to right
the homescreen with image moved to right

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.

the background image is scaled to 150% of the width of the element, but as it is wider than this inherently, the height it reduced.
the background image is scaled to 150% of the width of the element, but as it is wider than this inherently, the height is reduced.

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.

positioning the background image.
Positioning the background image.

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.

the x, z, and z axis, with alpha, beta and gamma rotation
the x, z, and z axis, with alpha, beta and gamma rotation. Device probably doesn’t actually support deviceOrientation.

Here’s how we’ll use this information

  1. we’ll add an event listener for devicemotion events to the Window
  2. this function will receive an event, which includes information about the current rotation of the device around its x, y and z axes
  3. We’ll use this to determine the distance our background image should moved to achieve the parallax effect
  4. 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.

screenOrientation values
screenOrientation values. Cell Phone designed by Marwa Boukarim from The Noun Project

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.

delivering year round learning for front end and full stack professionals

Learn more about us

Three days of talks, two of them in the engineering room. Web Directions you have broken my brain.

Cheryl Gledhill Product Manager, BlueChilli