DM Blog

Animating Rotation With jQuery

Shortly after it was announced, I bought the Hype App in the Mac App Store which is a WYSIWYG editor that allows you to create beautiful animated and interactive web content without any coding required. It generates HTML5, CSS3 and JavaScript output — no flash. I didn’t have anything specific in mind when I bought it, I just bought it out of mere curiosity (at a promotional price), and I never really had a good look at it until recently.

I started creating a new version of a personal website using Hype, but I was quickly disappointed to see that all of the content was contained in a JavaScript file — not exactly SEO friendly. I didn’t critically need this interactivity and animation, I just wanted to experiment with it and hopefully learn something along the way. So I ended up scrapping the idea and decided to hand-code it myself using jQuery and CSS3.

I also wanted to make sure that the site looked/worked well even without JavaScript, (see “Progressive Enhancement” on Wikipedia), which is worth mentioning but never a huge obstacle.

Once I completed the HTML5 markup and styled the page with CSS, I moved onto the JavaScript (using jQuery) and had to read up on the .animate() method. All I wanted was to start with an image centred on the page (hiding all other content) and then shrink and rotate it to its final position on the page — in a single, smooth animation — and then unhide the rest of the content. Nothing too complicated; pretty simple and straightforward.

The only challenge was the rotation part. There were a couple of solutions I found online, most of which required the use of a rotation plugin, but nothing that I actually liked. I knew there had to be a simpler way, and there was.

Here’s what I came up with:

$(document).ready(function() {
	// begin our starting animation
	var photoPosition = $('#photo img').position();
	$('body').addClass('start');
	$('.start #photo img').centre().animate({
		left	: photoPosition.left,
		top		: photoPosition.top,
		width	: '242px',
		height	: '182px'
	},{
		duration : 3000,
		step	 : function(now, fx) {
			// SEE NOTE BELOW
			// var degree = 355 * fx.state;
			var degree = 360 - (5 * fx.state);
			var rotate = 'rotate(' + degree + 'deg)';
			$('#photo img').css({
				'-webkit-transform' : rotate,
				   '-moz-transform' : rotate,
				    '-ms-transform' : rotate,
				     '-o-transform' : rotate,
				        'transform' : rotate
			});
		},
		complete : function() {
			$('#photo img').css({
				position : 'relative',
				left	 : 0,
				right	 : 0
			});
			$('body').removeClass('start');
			$('#main-content').css('opacity', 0).animate({
				opacity	 : 1
			},1000);
		}
	});
});

You can see that I’m calling a .centre() method (which doesn’t natively exist in jQuery) before the .animate() method, and here’s the code for that:

// add a function to jQuery to centre our element
jQuery.fn.centre = function () {
	this.css('position', 'absolute');
	this.css('top', (($(window).height() - this.outerHeight()) / 2) + $(window).scrollTop() + 'px');
	this.css('left', (($(window).width() - this.outerWidth()) / 2) + $(window).scrollLeft() + 'px');
	return this;
}

There’s nothing complicated about the animation so the JavaScript snippet above should be very easy to follow. The first part is just a CSS map of the final destination for the photo, and the last part is just some CSS to apply once the animation is complete. The actual rotation (with jQuery) is in the middle section, using the step option. Here’s what the documentation says about this:

The second version of .animate() provides a step option — a callback function that is fired at each step of the animation. This function is useful for enabling custom animation types or altering the animation as it is occurring. It accepts two arguments (now and fx), and this is set to the DOM element being animated.

  • now: the numeric value of the property being animated at each step

  • fx: a reference to the jQuery.fx prototype object, which contains a number of properties such as elem for the animated element, start and end for the first and last value of the animated property, respectively, and prop for the property being animated.

I used the developer tools in Safari to get a list of all the properties of fx. Among many others, I found the fx.state property, which basically returns a decimal value representing the current state of the animation from 0.0 at the beginning, to 1.0 at the end. I’m using the value of fx.state to calculate how many degrees the element should be rotated and then apply this value using CSS transforms. Since the rotation is incrementally applied in many steps (and in fractional/decimal values), it results in a very smooth animation. Many of the other solutions/plugins I found online only applied the rotation in single-digit degrees at a time.

About the var degree = 360 - (5 * fx.state); line

I had to think about this for a while in order to get it right. I wanted my image to be rotated -5º (or 355º) so originally, I had var degree = 355 * fx.state; and while that did result in my image ending up at the correct angle at the end of the animation, it also made my image rotate in almost a complete circle — from 0º…90º…180º…270º and eventually to 355º — not the effect I was going for. What I wanted was for the image to rotate 5º counter-clockwise, not clockwise. So essentially 360º…359º…358º… all the way down to 355º. You can see the solution I came up with above. At the beginning of the animation, the image is rotated 360 - (5 * 0) = 360º degrees; at the halfway point, 360 - (5 * 0.5) = 357.5º; and ultimately 360 - (5 * 1) = 355º degrees at the very end. (But again, the degrees get calculated through many fine steps from 0 to 0.00001 to 0.1 to 0.8888, etc., so the animation appears very smooth.) If you want a clockwise rotation, then you want what I originally had: var degree = 355 * fx.state;

Not so complicated after all, no?


Update —

This post is more than sixteen months old and jQuery has gone from 1.7 to 1.10 in that time… Somewhere along the way, jQuery changed fx.state to fx.pos.