Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ken Auenson
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ken Auenson ( @KenAuenson )

Using jQuery's Animate() Step Callback Function To Create Custom Animations

By on

When I was reading the jQuery 1.4 Reference Guide over the weekend, it mentioned that jQuery's animate() method has a step callback function that gets called after each step of the animation has completed. Unfortunately, it didn't say anything more than this. I tried looking at the online documentation and it said the exact same thing. As such, I thought the step callback function would be a good feature to experiment with; I wanted to see what it did and how I might use it.

After running a number of animations and logging the step callback arguments to the console, it appears that the step callback receives two arguments: the current value of the property being animated and an object containing information about the animation. Apparently, the step callback gets invoked for step of each property being animated; while this might not be what you expected, it makes sense since each animating property value changes independently.

To get a sense of what I'm talking about, take a look at this small demo:

<!DOCTYPE HTML>
<html>
<head>
	<title>jQuery Animate() Step Demo</title>
	<script type="text/javascript" src="../jquery-1.4.1.js"></script>
	<script type="text/javascript">

		// When the DOM is ready, initialize.
		jQuery(function( $ ){

			$( "div" )
				.css({
					position: "fixed",
					left: "500px"
				})
				.animate(
					{
						left: 0
					},
					{
						duration: 50,
						step: function( currentLeft ){
							console.log( "Left: ", currentLeft );
						}
					}
				)
			;

		});

	</script>
</head>
<body>

	<div>Hello</div>

</body>
</html>

As you can see, I am taking a DIV with a left of 500px and I'm animating it down to a left of 0px. The step callback of the animate() method simply logs the current left to the console. When we run the above code, we get the following output:

Left: 500
Left: 500
Left: 265.69762988232833
Left: 250.00000000000003
Left: 187.82752820878636
Left: 187.82752820878636
Left: 143.5551771087318
Left: 103.05368692688177
Left: 57.37168930605276
Left: 47.74575140626314
Left: 17.555878527937182
Left: 7.854209717842252
Left: 0
Left: 0

As you can see, the step callback simply reports the current value of the left property as it is being animated. The number of times that the step callback gets invoked is determined by the animation duration and the selected easing.

I said above that the step callback function receives two arguments, the second of which contains information about the animation. After inspecting this second argument, I simply couldn't find anything useful about it other than that it tells you what property is being animated; none of the other values contained within it seem to be all that relevant.

One of the things that I thought would be interesting would be to use the step callback function to override the current animation. Unfortunately, this cannot be done directly; not only is the step function called after each animation step (not before), should you override the property value, the animation itself will simply override your setting in the next step. But, we can do something a bit tricky! We can use the animate() function to animate irrelevant properties such that we can use the step callback function to power our own animation logic.

jQuery's animate() function is controlled by the initial property value, the duration, and the selected easing method. As such, the step callback reflects the relative position of the current state of the given property within the greater animation. While this is typically based on somewhat arbitrary values, if we pick specific start and end values for a given property, we can being to leverage the step callback in a very custom way.

Image that we had a property that we wanted to animate in a way that the animate() function didn't really provide for. If we chose to animate a completely irrelevant property from 100 to 0 (ex. text-indent), we could think of the value being passed to our step callback as the "percentage" completed in the given animation. The percentage would be determined internally by the animation duration and easing function; but, we could then use that percentage inside of the step callback to power our own animation of the contextual element.

In the following demo, that's exactly what I'm going to do. I'm going to create an image that I can drag with my mouse; then, upon releasing the mouse, the image will drift off in the given direction. As the image drifts, if it hits the sides of the window, it will ricochet off the side, moving back in the opposite direction. This change of direction mid-animation is not something that jQuery's animate() function can do inherently. But, by animating the irrelevant property, text-indent, from 100 to zero, we can use the step callback function to calculate the position of the image manually while still reaping the benefits of the native easing and duration management.

<!DOCTYPE HTML>
<html>
<head>
	<title>Using The jQuery Animate() Step Callback</title>
	<style type="text/css">

		html,
		body {
			height: 100% ;
			margin: 0px 0px 0px 0px ;
			overflow: hidden ;
			padding: 0px 0px 0px 0px ;
			width: 100% ;
			}

		img {
			border: 1px solid #000000 ;
			cursor: pointer ;
			height: 100px ;
			overflow: hidden ;
			position: fixed ;
			width: 100px ;
			}

		img.initial {
			left: 50% ;
			margin: -51px 0px 0px -51px ;
			top: 50% ;
			}

	</style>
	<script type="text/javascript" src="../jquery-1.4.1.js"></script>
	<script type="text/javascript">

		// When the DOM is ready, initialize.
		jQuery(function( $ ){

			// Get a reference to our test image.
			var image = $( "img" );

			// Get the dimensions of the image.
			var imageWidth = image.outerWidth();
			var imageHeight = image.outerHeight();

			// Get the dimentions of the window.
			var windowWidth = $( window ).width();
			var windowHeight = $( window ).height();

			// Get the min and max positions.
			var minLeft = 0;
			var maxLeft = (windowWidth - imageWidth);
			var minTop = 0;
			var maxTop = (windowHeight - imageHeight);

			// Let's set some caps on the max speed.
			var maxSpeed = 5;


			// Bind the window resize to update the dimensions.
			$( window ).resize(
				function(){
					// Update the window size.
					windowWidth = $( window ).width();
					windowHeight = $( window ).height();

					// Update max positional coordinates.
					maxLeft = (windowWidth - imageWidth);
					maxTop = (windowHeight - imageHeight);
				}
			);


			// Define the function for the mouse move. We don't
			// want to listen to this all the time - only once
			// the user has clicked on the image.
			var onMouseMove = function( event ){
				// Get the mouse offset.
				var mouseOffset = image.data( "mouseOffset" );

				// Calculate the position of the image baesd on
				// the mouse position and the mouse offset.
				var position = {
					left: (event.pageX - mouseOffset.left),
					top: (event.pageY - mouseOffset.top)
				};

				// Check to make sure the image is in bounds
				// of the top/left screen.
				position.left = Math.max( position.left, minLeft );
				position.top = Math.max( position.top, minTop );

				// Check to make sure the image is in bounds
				// of the bottom/right screen.
				position.left = Math.min( position.left, maxLeft );
				position.top = Math.min( position.top, maxTop );

				// Update the position of the image.
				image.css({
					left: (position.left + "px"),
					top: (position.top + "px")
				});

				// Get the mouse events collection.
				var mouseEvents = image.data( "mouseEvents" );

				// Check to see if enough time has passed since
				// the last mouse event capture. If it has, then
				// let's store this one.
				if ((event.timeStamp - mouseEvents[ mouseEvents.length - 1 ].timeStamp ) > 40){

					// Push the current mouse event.
					mouseEvents.push( event );

					// Check the number of mouse events. If there
					// are too many, lets remove the oldest one.
					if (mouseEvents.length > 2){

						// Remove the oldest event.
						mouseEvents.shift();

					}

				}
			};


			// Define the handler for the mouse up since we don't
			// want to bind this until the image has been clicked.
			var onMouseUp = function( event ){
				// Unbind the mouse events.
				$( document ).unbind( "mousemove mouseup" );

				// Get the last stored mouse event.
				var lastEvent = image.data( "mouseEvents" ).shift();

				// Check to see if we have a mouse move event.
				// If we don't we can exit out.
				if (!lastEvent){
					return;
				}

				// Figure out the delta X and Y of the mouse
				// movement bewteen the release and the last
				// recorded time.
				var deltaX = (event.pageX - lastEvent.pageX);
				var deltaY = (event.pageY - lastEvent.pageY);

				// Figure out the time over which this change
				// occurred. We are using MAX to make sure we
				// don't run into division problems.
				var deltaMS = Math.max(
					(event.timeStamp - lastEvent.timeStamp),
					1
				);

				// Calculate the directional speed X using a
				// simple (distance / time) forumla.
				var speedX = Math.max(
					Math.min( (deltaX / deltaMS), maxSpeed ),
					-maxSpeed
				);

				// Calculate the directional speed Y using a
				// simple (distance / time) forumla.
				var speedY = Math.max(
					Math.min( (deltaY / deltaMS), maxSpeed ),
					-maxSpeed
				);

				// Our speed is cacluated in the change in pixels
				// over a given millisecond. In order to use this,
				// we will need to capture the number of
				// milliseconds between each step of the
				// animation. For that, we'll need to stamp each
				// step with a time.
				var lastStepTime = new Date();

				// Here, we are overriding the meaning of text-
				// indent. We are going to animate our text-indent
				// property from 100 to zero so that we can use it
				// as a psueod-percentage of the completion of the
				// animation.
				image.css( "text-indent", 100 );

				// Animate the image based on the change in
				// position in the given change in timeframe. We
				// are using the textIndex CSS property to levarage
				// the Easing of our "speed" calculations.
				//
				// NOTE: For the duration, we are picking a fairly
				// arbitrary value to multiply by. Just something
				// enough to see the animation in a fun way.
				image.animate(
					{
						textIndent: 0
					},
					{
						duration: (
							Math.max(
								Math.abs( speedX ),
								Math.abs( speedY )
							) * 3000
						),
						step: function( currentStep ){
							// Update the speed based on the
							// easing. Since we know our step is
							// going to be between 100 and zero,
							// we can use this as an "Eased" per-
							// centage of our speed.
							speedX *= (currentStep / 100);
							speedY *= (currentStep / 100);

							// Get the current time.
							var now = new Date();

							// Get the duration of this step in
							// milliseconds so that we can apply
							// our speed updates.
							var stepDuration = (now.getTime() - lastStepTime.getTime());

							// Store the last step time.
							lastStepTime = now;

							// Get the position of the image.
							var position = image.position();

							// Update the image position left.
							var newLeft = (position.left + (speedX * stepDuration));

							// Update the image position top.
							var newTop = (position.top + (speedY * stepDuration));

							// Check to see if we have gone out of
							// bounds. If the image goes out of
							// bounds at any point, we need to
							// ajdust the speed to allow the image
							// to "bounce" off the bounds.

							// Min left.
							if (newLeft < minLeft){
								newLeft = minLeft;
								speedX *= -1;
							}

							// Min top.
							if (newTop < minTop){
								newTop = minTop;
								speedY *= -1;
							}

							// Max left.
							if (newLeft > maxLeft){
								newLeft = maxLeft;
								speedX *= -1;
							}

							// Max top.
							if (newTop > maxTop){
								newTop = maxTop;
								speedY *= -1;
							}

							// Updat the position of the image.
							image.css({
								left: (newLeft + "px"),
								top: (newTop + "px")
							});
						}
					}
				);

			};


			// Bind the mouse down event.
			image.mousedown(
				function( event ){

					// Prevent the default action.
					event.preventDefault();

					// Check to see if the image is still in its
					// initial state.
					if (image.is( ".initial" )){

						// Get the current position.
						var position = image.position();

						// Remove the initial class.
						image.removeClass( "initial" );

						// Set the position (so that the image
						// doesn't visibily jump to the top / left
						// of the screen. NOTE: We are using the
						// 51px to make up for visible margin.
						image.css({
							left: ((position.left - 51) + "px"),
							top: ((position.top - 51) + "px")
						});

					}

					// Since we are using the animate() method to
					// actually move the image, we can use the
					// stop() method to hault any animation.
					image.stop();

					// Get the current position of the image.
					var position = image.position();

					// Get the relative position of the mouse (in
					// the context of the image). The extra pixel
					// is for the image border.
					var mouseOffset = {
						left: (event.pageX - position.left + 1),
						top: (event.pageY - position.top + 1)
					};

					// Store the mouse offset with the image so
					// that the mouse move event can figure out
					// how to move the mouse.
					image.data( "mouseOffset", mouseOffset );

					// Store the current mouse event so that we
					// can figure out the change in mouse up event
					// once the click is released.
					image.data( "mouseEvents", [ event ] );

					// Bind the mouse move listener. Make sure to
					// bind this to the document itself so that
					// the image doesn't have to keep up with the
					// mouse perfectly as it moves.
					$( document ).mousemove( onMouseMove );

					// Bind the mouse up listener. Make sure to
					// bind this to the document itself so that
					// the image doesn't have to keep up with the
					// mouse perfectly as it moves.
					$( document ).mouseup( onMouseUp );
				}
			);

		});

	</script>
</head>
<body>

	<img src="./face.jpg" class="initial" />

</body>
</html>

While I know this is a lot of code to look at, if you review the animate() method call, you'll see that it is animating the text-indent property from 100 to zero. While text-indent has no visual bearing on our UI, its animation will give the step callback function an understanding of the percentage animation completed. Within the step callback, I am then using that percentage, implicitly adjusted by easing, to decrease the speed of the drifting image over time. To see this in action, you really need to take a look at the video, or try it for yourself here.

jQuery's animate method is very powerful, especially for more "linear" animations. Not only does it do all of the calculations for us, it allows us to use the stop() method to cancel current animations. While this is great in most cases, we can go a bit further and leverage the step callback function to really start building complex animations that are not inherently supported by the jQuery library.

Want to use code from this post? Check out the license.

Reader Comments

198 Comments

You made the statement:

One of the things that I thought would be interesting would be to use the step callback function to override the current animation. Unfortunately, this cannot be done directly; not only is the step function called after each animation step (not before), should you override the property value, the animation itself will simply override your setting in the next step.

While you're can't take override the current animation settings, you can call the stop() method (which will also allow you to clear the queue) and then define a new animation rule.

This would be pretty straightforward (aka not "tricky" :]) to implement.

On a non-related note, don't forget that you can also animate to right and bottom--which allows you to skip doing the whole "if left position + image width > window width" calculation. You could just detecting the direction of the mouse and then animation left/right/top/bottom based on the mouse direction.

15,640 Comments

@Dan,

Flicking her head is kind of fun :)

I totally forgot about using right / bottom. I guess when I explicitly set the left / top, it never occurred to me that the bottom / right could be read (although I can think of a reason why not).

I had considered using the animation queuing to handle the change in direction; but, I wasn't sure how to integrate the easing of the speed across the animations. By keeping this all part of a single animation, I thought it was the most elegant way to use the implicit duration / easing management provided by the jQuery animate() function.

15,640 Comments

@Jack,

I think it should; I am not exactly sure when the step callback function was added. I don't think it's a new feature.

5 Comments

@Ben
This is badass! I'm a jQuery newb so I've been slowly gathering experience and using it on client projects (and my own skunkworx projects) over the past 2 months. At any rate, I love reading your jQuery posts!

Keep up the awesome work man.

11 Comments

Very nice work! I love these kind of experiments!

It seems that you're using the "animate()" method as a pure looping mechanism - i.e. one that will run for the specified duration every 30 or so milliseconds. Since you're not using an easing function, this is just a straight animation loop with equal durations between "frames" (well, almost, as it defaults to "swing").

I like the inventive text-indent thing, although, as I hope you'll agree, it's a tad hacky.. jQuery's animate() properties don't have to be CSS properties - you could achieve the same percentage based animation with something like this:

jQuery({percent:100}).animate({percent:0}, {step:function(){console.log(this.percent);}});

Also, since it's only a straight loop, I'd argue that the good ol' setInterval would suffice better, although you'd have to do a little more arithmetic.

I've tried to do similar things before, and I'm always left wondering whether jQuery is the right tool.

15,640 Comments

@James,

Excellent point! I suppose in order to be continually cross-browser compatible, the animate() method would have to support any value that is numerc; I hadn't thought about that. The idea of using "precent" is quite nice.

1 Comments

This is great - Thank you!
Is there a way to have this animation begin onload and loop (not be controlled by offclick)?

1 Comments

That's a great piece of code!

Would it be possible to add droppable areas so that if the object reaches that droppable, it stops and a callback is called?

1 Comments

I'm using Jquery instead of flash becouse it's better for SEO, search engines and downloading speed for user.
You can place your images on different domain without cookies to speed up loading (parallel downloads).
You can see implementation of this technique on my site http://eshopes.com

1 Comments

This is brilliant.

Any way to make the object interact with another object? For instance, if I were to "throw" the animated object, could it "bounce off of" a static div on the same z-index as the "thrown" object?

3 Comments

Wow Ben, this is ingenious! Thanks so much for taking the time to post this article and share your source!!!

I just used it on a recent project and thought I'd share some of my findings with your readers in case they run into the same issues.

In my project, I was creating an iPhone emulation, where you can drag and throw a large image within a container div.

1.) I started out using the current jQuery library (1.7.2) and my script wasn't working in IE7 & 8 - was throwing an error deep within the jQuery library. I switched over to 1.4.1 and that did the trick.

2.) Since my image is much larger than yours it was taking a few seconds to load. Therefore, image.outerWidth() and image.outerHeight() were consoling as "0" in Chrome and Safari. I moved most of your script inside an image.load function and that resolved the issue.

Thanks again and hope this helps!

Warm regards,
Miki

3 Comments

Actually per my last post... I thought I had it working in IE 7 & 8 but now my mouseup event isn't firing. Instead it get's stuck on the mousemove event.

If you look at your demo, it doesn't work there either. I have looked at this for a few hours without success. Anyone know how to get this fling effect working in IE 7 & 8?

3 Comments

Sorry for so many comments!

I fixed the IE7/8 issue by adding:

event.preventDefault();

to the onMouseMove function.

Cheers :)

1 Comments

I explored the second object generated during jQuery's "step" callback and found the percentage of animation.

Behold:

$('yourdiv').animate({
	top: 1593 //some random pixel distance to animate over
},{
	duration: 500, //some duration in millis
	step: function(cssValue, animProperties){
		console.log(animProperties.state);
	}
});

animProperties.state will be a float from 0 to 1 over the course of duration (in my case 1/2 second).

Hope this helps!

1 Comments

Thanks a lot sir, the same thing for me as also eager to know about the step callback function, and now get satisfied answer,,

2 Comments

hello everyone,

I am trying to make an animation using the animate function in a numeric way (that I found in the web) and moreover the code is part of an AdobeEdge document.

This is the code inside a „mouseMove" event.

var x=sym.$("Koordinatenfeld").offset();
var xpos = x.left;
var ges = 3*(e.pageX - xpos);

jQuery({someValue: 0}).animate({someValue: ges}, {

duration: 500,
easing:'swing',
step: function() {
sym.stop(Math.ceil(this.someValue)+14250);
}
})

Basically what I tried is to send the animation in a point on the timeline depending on the position of the user mouse over a div called „Koordinatenfeld".
This worked perfect so long with:

var x=sym.$("Koordinatenfeld").offset();
var xpos = x.left;
var ges = e.pageX - xpos;

sym.stop(14250+(ges*3));

Yet this catapults it once at where it should move to
BUT with a slight movement towards it so it would be less abrupt.
So thats what I tried with the animate code above.
Problem is for me to get into it the proper change and renew of the variable „ges".
So that it looks every time newly where the mouse is and from there animates to the new position in timeline.

I know this seems very complicate but there has to ne a way to do it, isn't there?

Thanks guys for your interest

Stefan

1 Comments

Hello Ben,

Nice example. Have you already done the same including the gravity also (the image always come back to the ground) ? And with rotation, too ? Like a rolling ball.

1 Comments

Hi Ben,

Thank you so much for this example, I've been using it for over a year now and recently had to upgrade to jQuery 1.8.3 to fix other issues but now the animation is broken. Would a new version be available?

1 Comments

This is a great animation.

Is there a way to add an audible when the image bounces off the boudaries?

Thanks,

JLO

1 Comments

Really appreciated your Code above was really helpful, I kinda working on something where I need to trigger the step callback function on every pixel movement of animation, Can you help me how would I do that..?
Thank you.. :)

1 Comments

This is really useful. I found out that you don't actually have to use a dummy css property (surprisingly). To animate a property in a linear-gradient for instance I did this

this.css('someLinearGradientParam', 0);

var that = this;
this.animate({ 'someLinearGradientParam' : 100 }, { 'step' : function(val)
{
var style = 'linear-gradient(...stuff...' + val)';
that.css('background', style);
});

This makes it really easy to implement own animation plugins while still using jQuery as it's core

1 Comments

This is really a Awesome JQuery Effects. You can find few more Jquery effects at

http://e-weddingcardswala.in/e_cardscollection/840/

http://e-weddingcardswala.in/e_cardscollection/837/

http://e-weddingcardswala.in/e_cardscollection/834/

2 Comments

I'm just playing about introducing myself to JQuery and came upon this excellent code.
I'm wondering what is involved in turning this into an extension method so that I can decide what elements I can throw about the place......

Like..... $('#someElement').throwMe();

?

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel