Taking Device Orientation for a Spin

When The Police sang “Don’t Stand So Close To Me” they weren’t talking about using a smartphone to view a panoramic image on Facebook, but they could have been. For years, technology has driven relentlessly towards devices we can carry around in our pockets, and now that we’re there, we’re expected to take the thing out of our pocket and wave it around in front of our faces like a psychotic donkey in search of its own dangly carrot.

But if you can’t beat them, join them.

A brave new world

A couple of years back all sorts of specs for new HTML5 APIs sprang up much to our collective glee. Emboldened, we ran a few tests and found they basically didn’t work in anything and went off disheartened into the corner for a bit of a sob.

Turns out, while we were all busy boohooing, those browser boffins have actually being doing some work, and lo and behold, some of these APIs are even half usable. Mostly literally half usable—we’re still talking about browsers, after all.

Now, of course they’re all a bit JavaScripty and are going to involve complex methods and maths and science and probably about a thousand dependancies from Github that will fall out of fashion while we’re still trying to locate the documentation, right? Well, no!

So what if we actually wanted to use one of these APIs, say to impress our friends with our ability to make them wave their phones in front of their faces (because no one enjoys looking hapless more than the easily-technologically-impressed), how could we do something like that? Let’s find out.

The Device Orientation API

The phone-wavy API is more formally known as the DeviceOrientation Event Specification. It does a bunch of stuff that basically doesn’t work, but also gives us three values that represent orientation of a device (a phone, a tablet, probably not a desktop computer) around its x, y and z axes. You might think of it as pitch, roll and yaw if you like to spend your weekends wearing goggles and a leather hat.

The main way we access these values is through an event listener, which can inform our code every time the value changes. Which is constantly, because you try and hold a phone still and then try and hold the Earth still too.

The API calls those pitch, roll and yaw values alpha, beta and gamma. Chocks away:

window.addEventListener('deviceorientation', function(e) {
    console.log(e.alpha);
    console.log(e.beta);
    console.log(e.gamma);
});

If you look at this test page on your phone, you should be able to see the numbers change as you twirl the thing around your body like the dance partner you never had. Wrist strap recommended.

One important note

Like may of these newfangled APIs, Device Orientation is only available over HTTPS. We’re not allowed to have too much fun without protection, so make sure that you’re working on a secure line. I’ve found a quick and easy way to share my local dev environment over TLS with my devices is to use an ngrok tunnel.

ngrok http -host-header=rewrite mylocaldevsite.dev:80

ngrok will then set up a tunnel to your dev site with both HTTP and HTTPS URL options. You, of course, want the HTTPS option.

Right, where were we?

Make something to look at

It’s all well and good having a bunch of numbers, but they’re no use unless we do something with them. Something creative. Something to inspire the generations. Or we could just build that Facebook panoramic image viewer thing (because most of us are familiar with it and we’re not trying to be too clever here). Yeah, let’s just build one of those.

Our basic framework is going to be similar to that used for an image carousel. We have a container, constrained in size, and CSS overflow property set to hidden. Into this we place our wide content and use positioning to move the content back and forth behind the ‘window’ so that the part we want to show is visible.

Here it is mocked up with a slider to set the position. When you release the slider, the position updates. (This actually tests best on desktop with your window slightly narrowed.)

The details of the slider aren’t important (we’re about to replace it with phone-wavy goodness) but the crucial part is that moving the slider results in a function call to position the image. This takes a percentage value (0-100) with 0 being far left and 100 being far right (or ‘alt-nazi’ or whatever).

var position_image = function(percent) {
    var pos = (img_W / 100)*percent;
    img.style.transform = 'translate(-'+pos+'px)'; 
};

All this does is figure out what that percentage means in terms of the image width, and set the transform: translate(…); CSS property to move the image. (We use translate because it might be a bit faster to animate than left/right positioning.)

Ok. We can now read the orientation values from our device, and we can programatically position the image. What we need to do is figure out how to convert those raw orientation values into a nice tidy percentage to pass to our function and we’re done. (We’re so not done.)

The maths bit

If we go back to our raw values test page and make-believe that we have a fascinating panoramic image of some far-off beach or historic monument to look at, you’ll note that the main value that is changing as we swing back and forth is the ‘alpha’ value. That’s the one we want to track.

As our goal here is hey, these APIs are interesting and fun and not let’s build the world’s best panoramic image viewer, we’ll start by making a few assumptions and simplifications:

  • When the image loads, we’ll centre the image and take the current nose-forward orientation reading as the middle.
  • Moving left, we’ll track to the left of the image (lower percentage).
  • Moving right, we’ll track to the right (higher percentage).
  • If the user spins round, does cartwheels or loads the page then hops on a plane and switches earthly hemispheres, they’re on their own.

Nose-forward

When the page loads, the initial value of alpha gives us our nose-forward position. In Safari on iOS, this is normalised to always be 0, whereas most everywhere else it tends to be bound to pointy-uppy north. That doesn’t really matter to us, as we don’t know which direction the user might be facing in anyway — we just need to record that initial state and then use it to compare any new readings.

var initial_position = null;

window.addEventListener('deviceorientation', function(e) {
    if (initial_position === null) {
        initial_position = Math.floor(e.alpha);
    };

    var current_position = initial_position - Math.floor(e.alpha);
});

(I’m rounding down the values with Math.floor() to make debugging easier - we’ll take out the rounding later.)

We get our initial position if it’s not yet been set, and then calculate the current position as a difference between the new value and the stored one.

These values are weird

One thing you need to know about these values, is that they range from 0 to 360 but then you also get weird left-of-zero values like -2 and whatever. And they wrap past 360 back to zero as you’d expect if you do a forward roll.

What I’m interested in is working out my rotation. If 0 is my nose-forward position, I want a positive value as I turn right, and a negative value as I turn left. That puts the awkward 360-tipping point right behind the user where they can’t see it.

var rotation  = current_position;
if (current_position > 180) rotation = current_position-360;

Which way up?

Since we’re talking about orientation, we need to remember that the values are going to be different if the device is held in portrait on landscape mode. See for yourself - wiggle it like a steering wheel and you get different values. That’s easy to account for when you know which way up the device is, but in true browser style, the API for that bit isn’t well supported. The best I can come up with is:

var screen_portrait = false;
if (window.innerWidth < window.innerHeight) {
    screen_portrait = true;
}

It works. Then we can use screen_portrait to branch our code:

if (screen_portrait) {
        if (current_position > 180) rotation = current_position-360;
} else {
        if (current_position < -180) rotation = 360+current_position;
}

Here’s the code in action so you can see the values for yourself. If you change screen orientation you’ll need to refresh the page (it’s a demo!).

Limiting rotation

Now, while the youth of today are rarely seen without a phone in their hands, it would still be unreasonable to ask them to spin through 360° to view a photo. Instead, we need to limit the range of movement to something like 60°-from-nose in either direction and normalise our values to pan the entire image across that 120° range. -60 would be full-left (0%) and 60 would be full-right (100%).

If we set max_rotation = 60, that code ends up looking like this:

if (rotation > max_rotation) rotation = max_rotation;
if (rotation < (0-max_rotation)) rotation = 0-max_rotation;

var percent = Math.floor(((rotation + max_rotation)/(max_rotation*2))*100);

We should now be able to get a rotation from -60° to +60° expressed as a percentage. Try it for yourself.

The big reveal

All that’s left to do is pass that percentage to our image positioning function and would you believe it, it might actually work.

position_image(percent);

You can see the final result and take it for a spin. Literally.

So what have we made here? Have we built some highly technical panoramic image viewer to aid surgeons during life-saving operations using only JavaScript and some slightly questionable mathematics? No, my friends, we have not. Far from it.

What we have made is progress. We’ve taken a relatively newly available hardware API and a bit of simple JavaScript and paired it with existing CSS knowledge and made something that we didn’t have this morning. Something we probably didn’t even want this morning. Something that if you take a couple of steps back and squint a bit might be a prototype for something vaguely interesting. But more importantly, we’ve learned that our browsers are just a little bit more capable than we thought.

The web platform is maturing rapidly. There are new, relatively unexplored APIs for doing all sorts of crazy thing that are often dismissed as the preserve of native apps. Like some sort of app marmalade. Poppycock.

The web is an amazing, exciting place to create things. All it takes is some base knowledge of the fundamentals, a creative mind and a willingness to learn. We have those! So let’s create things.

About the author

Drew McLellan is lead developer on your favourite content management systems, Perch and Perch Runway. He is Director and Senior Developer at edgeofmyseat.com in Bristol, England, and is formerly Group Lead at the Web Standards Project. When not publishing 24 ways, Drew keeps a personal site covering web development issues and themes, takes photos, tweets a lot and tries to stay upright on his bicycle.

More articles by Drew

Comments