Smooth compass rotation in HTML5

Here is how you can implement a 'compass bearing responsive' part of a web page.

Consider a page that has an element that you want to follow a compass bearing:

<img src="/images/rotation_compass.png" height="237" width="237" alt="compass" style="background-color:#ff0000;height:237px;width:237px;margin-left:auto;margin-right:auto;display:block"/>

compass

We can use the css rotation transformation to change its orientation:

<img src="/images/rotation_compass.png" height="237" width="237" alt="compass" style="background-color:#ff0000;height:237px;width:237px;margin-left:auto;margin-right:auto;display:block;transform:rotate(15deg);"/>
compass

Set the rotation centre

Depending upon the browser (or other styling), the element may not rotate around the center of the image. For example, this one rotates around the top-left corner:

compass

transform:rotate(15deg);

We suggest that you always set the rotation center to the center of the image with transform-origin:

<img src="/images/rotation_compass.png" height="237" width="237" alt="compass" style="background-color:#ff0000;height:237px;width:237px;margin-left:auto;margin-right:auto;display:block;transform-origin: 50% 50%;transform:rotate(15deg);"/>
compass

transform:rotate(15deg);transform-origin: 50% 50%;

Those buttons

The buttons above allow you to change the rotation of the element by +/- 1°

We use a simple jQuery function to do that, which reads the current rotation of the element, calculates the new rotation and finally sets that.

This is the routine:

function rotate(id,by)
{
  var r = parseInt($('#'+id).attr("rotation"));
  r+=by;
  $('#'+id).attr("rotation",r);
  $('#'+id).css("transform","rotate("+r+"deg)");
  $('#'+id+'_caption').html(r);
}

For convenience, we are using a bespoke attribute on the element, "rotation", to store the current rotation value. This means we don't have to parse out the transform style.

Set a viewport around it

When the image is being rotated, the amount of screen space used for it changes.

To keep a fixed space, put the image into a viewport and hide any overflow:

<div style="width:237px;height:237px;display:block;margin-left:auto;margin-right:auto;border:1px solid #000;overflow:hidden">
<img src="/images/rotation_compass.png" height="237" width="237" alt="compass" style="background-color:#ff0000;height:237px;width:237px;margin-left:auto;margin-right:auto;display:block;transform-origin: 50% 50%;transform:rotate(15deg);"/>
</div>
compass

15deg

Overcome the corner problem

You will notice the white background showing in the corner as the image rotates which isn't what you want on your page.

The solution is to make the element being rotated larger. It should have a height and width at least as big as the diagonal of the viewport.

You can either increase the size of the image itself, or nest it inside another div that becomes the target for the rotation.

Using pythagoras' theorem, the length of the viewport diagonal is easily calculated:

diagonal = sqrt((viewportWidth * viewportWidth) + (viewportHeight * viewportHeight))

So in our example, the viewport width and height are both 237px

Therefore the diagonal length is sqrt((237*237)+(237*237))

So 335.168, which we will round up to 337, to make (later) calculations easier:

<div style="width:237px;height:237px;display:block;margin-left:auto;margin-right:auto;border:1px solid #000;overflow:hidden">
<div style="width:337px;height:337px;display:block;background-color:#ff0000;transform-origin: 50% 50%;transform:rotate(15deg);">
<img src="/images/rotation_compass.png" height="237" width="237" alt="compass" style="margin-left:50px;margin-top:50px;" />
</div>
</div>
compass

15deg

Since the size of the rotating element is 337x337px but the viewport is 237x237px, we need to shift the image left and up by half the difference (50px) to centre it again:

<div style="width:237px;height:237px;display:block;margin-left:auto;margin-right:auto;border:1px solid #000;overflow:hidden">
<div style="margin-left:-50px;margin-top:-50px;width:337px;height:337px;display:block;background-color:#ff0000;transform-origin: 50% 50%;transform:rotate(15deg);">
<img src="/images/rotation_compass.png" height="237" width="237" alt="compass" style="margin-left:50px;margin-top:50px;" />
</div>
</div>
compass

15deg

Now we have the image rotating properly, we can relate it to a compass heading/bearing

If we are heading North East, we want NE on the image to be pointing up If we are heading South East, we want SE on the image to be pointing up If we are heading South West, we want SW on the image to be pointing up If we are heading North West, we want NW on the image to be pointing up
compass compass compass compass
NE bearing = 45° SE bearing = 135° SW bearing = 225° NW bearing = 315°
NE rotation = 315° SE rotation = 225° SW rotation = 135° NW rotation = 45°

In general

rotation = 360 - bearing

And that a bearing lies between 0° and 360°

compass

Bearing 15deg

And here is the updated jQuery function that deals with converting from bearing to rotation:

function bearing(id,by)
{
  var b = parseInt($('#'+id).attr("bearing"));
  b+=by;
  if(b<0) b+=360;
  if(b>360) b-=360;
  var r = 360 - b;
  $('#'+id).attr("bearing",b);
  $('#'+id).css("transform","rotate("+r+"deg)");
  $('#'+id+'_caption').html(b);
}

Again, we use the bespoke attribute "bearing" to hold the current bearing value for the element.

Smoothing it out

You will have noticed that changing the bearing causes a jumpy change as the image is redrawn, which doesn't make a very nice UI to look at.

Fortunately we can use a CSS transition to animate the change in rotation:

<div style="width:237px;height:237px;display:block;margin-left:auto;margin-right:auto;margin-top:10px;border:1px solid #000;overflow:hidden">
<div style="margin-left:-50px;margin-top:-50px;width:337px;height:337px;display:block;background-color:#ff0000;transform-origin: 50% 50%;transform:rotate(0deg);transition: 1s linear;">
<img src="/images/rotation_compass.png" height="237" width="237" alt="compass" style="margin-left:50px;margin-top:50px;" />
</div>
</div>
compass

Bearing 45deg

Fixing the spin

If you change the bearing so that it crosses the 0/360 line, you will see that the transition now causes the image to spin.

This is due to the linear nature of the transition. Changing from a rotation of 359° to 1° is not a 2° anti-clockwise change (what we want to see) but a 358° change.

Fortunately, while a bearing will only ever be between 0° and 360°, rotation can go beyond 360.

That is to say, a rotation of 370° is equivalent to a rotation of 10°.

compass

Bearing 45deg

The final jQuery function

function bearingNoSpin(id,by)
{
  var oldH = parseInt($('#'+id).attr("heading"));
  var oldB = parseInt($('#'+id).attr("bearing"));
  var newB = oldB + by;
  if(newB<0) newB+=360;
  if(newB>360) newB-=360;

  var delta = oldB - newB;
  if(delta>180) delta = delta - 360;
  if(delta<-180) delta = 360 + delta;

  var newH = oldH +delta;

  $('#'+id).attr("bearing",newB);
  $('#'+id).attr("heading",newH);
  $('#'+id).css("transform","rotate("+newH+"deg)");
  $('#'+id+'_caption').html(newB);
}