Move your mouse or touch to re-orient pointer image

Using CSS transforms, transitions and animation, we can rotate any element on the page, but CSS won’t allow us to do that dynamically, in response to user input. To make that happen, we need JavaScript, combined with the lessons I’ve shown to this point regarding CSS rotation.

Setting the Origin

In many use-cases, an element will rotate around its center point. In CSS, the center of rotation for an element is defined by it’s transform-origin property, which has a default value of center center. However, it’s important to note that the mathematical center of the element - as represented by the default value - may not necessarily be the visual center of an element. This is especially true if the element is asymmetric: for example, a circle with a pointer, as shown in Figure 1.

Figure 1: Mathematical center (left) vs. visual center (right). CSS will always use the mathematical center as the transform-origin value by default

So, before rotating an element dynamically with JavaScript, it’s important to set the transform-origin correctly, followed by testing it by using different values for transform: rotate(). For the element shown in Figure 1, the CSS is:

#pointer {
    width: 150px;
    height: auto;
    position: relative;
    transform-origin: center 110px;
    display: block;
    margin: 0 auto;
    touch-action: none;
}

We need to find the centering information in JavaScript, since our script will also need to know the transform-origin of the element in order to do the calculations that follow. Added to the bottom of the page:

var pointer = document.getElementById("pointer"),
pointerBox = pointer.getBoundingClientRect(),
centerPoint = window.getComputedStyle(pointer).transformOrigin,
centers = centerPoint.split(" "),
centerY = pointerBox.top + parseInt(centers[1]) - window.pageYOffset,
centerX = pointerBox.left + parseInt(centers[0]) - window.pageXOffset,

This provides is with the center of the element relative to the page, rather than relative to to the element’s own top left corner. pageXOffset and pageYOffset are subtracted in case the page has been scrolled.

Next we need to add an event listener to track the mouse. The simplest is to listen for the event in the window:

window.addEventListener("mousemove", function(e) {
    …
});

Inside this function, we do some trigonometry to determine the angle of the mouse, relative to the origin-center of the element:

var radians = Math.atan2(e.clientX - centerX, e.clientY - centerY);

We could use this value in CSS, with a little wrangling, since CSS transforms also accept rads as a unit, but it’s usually easier to convert the result to degrees:

var degree = (radians * (180 / Math.PI) * -1) + 180; 

The result is then applied as in inline style to the element, via JavaScript:

pointer.style.transform = "rotate("+degree+"deg)";

Incorporating Touch

Since we should also be tracking touch for mobile devices, we should integrate the needed changes with some of the above code into its own function:

function rotatePointer(e) {
    var pointerEvent = e;
       if (e.targetTouches && e.targetTouches[0]) {
          e.preventDefault(); 
          pointerEvent = e.targetTouches[0];
          mouseX = pointerEvent.pageX;
          mouseY = pointerEvent.pageY;
    } else {
          mouseX = e.clientX,
          mouseY = e.clientY;
    }
	var centerY = pointerBox.top + parseInt(centers[1]) - window.pageYOffset,
	centerX = pointerBox.left + parseInt(centers[0]) - window.pageXOffset,
 radians = Math.atan2(mouseX - centerX, mouseY - centerY),
 degrees = (radians * (180 / Math.PI) * -1) + 180; 
 pointer.style.transform = 'rotate('+degrees+'deg)';
}

And then call this function from event listeners. Note that normally the touch area would be limited to the element around the pointer; otherwise, the user may have a very hard time scrolling the page with gestures.


window.addEventListener('mousemove', rotatePointer);
window.addEventListener('touchmove', rotatePointer);
window.addEventListener('touchstart', rotatePointer);

Holding Steady

If you are building on a complex page, you may want to wait on the page to load before measuring the position of elements, since trying to take measurements before the page has fully loaded may yeild incorrect values. If so, the complete code would become something like:

document.addEventListener("DOMContentLoaded", function() {
	var pointer = document.getElementById("pointer"),
	pointerBox = pointer.getBoundingClientRect(),
	centerPoint = window.getComputedStyle(pointer).transformOrigin,
	centers = centerPoint.split(" ");

	function rotatePointer(e) {
		var pointerEvent = e;
       	if (e.targetTouches && e.targetTouches[0]) {
          		e.preventDefault(); 
          		pointerEvent = e.targetTouches[0];
          		mouseX = pointerEvent.pageX;
          		mouseY = pointerEvent.pageY;
    		} else {
          		mouseX = e.clientX,
          		mouseY = e.clientY;
    		}

 var centerY = pointerBox.top + parseInt(centers[1]) - window.pageYOffset,
 centerX = pointerBox.left + parseInt(centers[0]) - window.pageXOffset,
 radians = Math.atan2(mouseX - centerX, mouseY - centerY),
 degrees = (radians * (180 / Math.PI) * -1) + 180; 
 pointer.style.transform = 'rotate('+degrees+'deg)';
}

window.addEventListener('mousemove', rotatePointer);
window.addEventListener('touchmove', rotatePointer);
window.addEventListener('touchstart', rotatePointer);
})

You can also apply the same logic to rotate multiple elements simultaneously, as I will show in the next article.

I’d like to thank Blake Brown for his advice and assistance on IE/Edge compatibility with this code.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/jARmWV