How do I lerp between values that loop (such as hue or rotation)?

View Demo

I'm trying to make the joint rotate smoothly around the center of the canvas, toward the angle of mouse pointer. What I have works, but I want it to animate the shortest distance possible to get to the mouse angle. The problem occurs when the value loops around at the horizontal line (3.14 and -3.14). Mouseover that area to see how the direction switches and it takes the long way back around.

Relevant Code

// ease the current angle to the target angle
joint.angle += ( joint.targetAngle - joint.angle ) * 0.1;

// get angle from joint to mouse
var dx = e.clientX - joint.x,
dy = e.clientY - joint.y;
joint.targetAngle = Math.atan2( dy, dx );


How can I make it rotate the shortest distance, even "across the gap"?

It’s not always the best method, and it can be more computationally expensive (though this ultimately depends on how you store your data), but I will make the argument that lerping 2D values works reasonably well in the majority of cases. Instead of lerping a desired angle, you can lerp the desired normalised direction vector.

One advantage of this method over the “pick the shortest route to angle” method is that it works when you need to interpolate between more than two values.

When lerping hue values, you can replace hue with a [cos(hue), sin(hue)] vector.

In your case, lerping the normalised joint direction:

// get normalised direction from joint to mouse
var dx = e.clientX - joint.x,
dy = e.clientY - joint.y;
var len = Math.sqrt(dx * dx + dy * dy);
dx /= len ? len : 1.0; dy /= len ? len : 1.0;
// get current direction
var dirx = cos(joint.angle),
diry = sin(joint.angle);
// ease the current direction to the target direction
dirx += (dx - dirx) * 0.1;
diry += (dy - diry) * 0.1;

joint.angle = Math.atan2(diry, dirx);


The code can be shorter if you can use a 2D vector class. For instance:

// get normalised direction from joint to mouse
var desired_dir = normalize(vec2(e.clientX, e.clientY) - joint);
// get current direction
var current_dir = vec2(cos(joint.angle), sin(joint.angle));
// ease the current direction to the target direction
current_dir += (desired_dir - current_dir) * 0.1;

joint.angle = Math.atan2(current_dir.y, current_dir.x);

• Thank you, this is working great here: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f I understand most of what you are doing, but can you elaborate a little more on line 5 of your code during the normalization? Not sure what is happening there exactly dx /= len... – jackrugile Mar 24 '14 at 5:25
• Dividing a vector by its length is called normalization. It ensures it has length 1. The len ? len : 1.0 part just avoids a division by zero, in the rare case that the mouse is placed exactly at the joint position. It could have been written: if (len != 0) dx /= len;. – sam hocevar Mar 24 '14 at 7:36
• -1. This answer is far from optimal in most cases. What if you're interpolating between 0° and 180°? In vector form: [1, 0] and [-1, 0]. Interpolating vectors will give you either 0°, 180°, or a divide by 0 error, in case of t=0.5. – Gustavo Maciel Jul 24 '15 at 0:41
• @GustavoMaciel that’s not “most cases”, that’s one very specific corner case that just never happens in practice. Also, there is no division by zero, check the code. – sam hocevar Jul 24 '15 at 8:02
• @GustavoMaciel having checked the code again, it’s actually extremely safe and works exactly as it should even in the corner case you describe. – sam hocevar Jul 24 '15 at 8:25

The trick is to remember that angles (at least in Euclidean space) are periodic by 2*pi. If the difference between the current angle and the target angle is too large (i.e. the cursor has crossed the boundary), just adjust the current angle by adding or subtracting 2*pi accordingly.

In this case, you can try the following: (I've never programmed in Javascript before, so forgive my coding style.)

  var dtheta = joint.targetAngle - joint.angle;
if (dtheta > Math.PI) joint.angle += 2*Math.PI;
else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;
joint.angle += ( joint.targetAngle - joint.angle ) * joint.easing;


EDIT: In this implementation, moving the cursor too quickly around the center of the joint causes it to jerk. This is the intended behavior, since the angular velocity of the joint is always proportional to dtheta. If this behavior is undesired, the problem can easily be fixed by placing a cap on the angular acceleration of the joint.

To do this, we'll need to keep track of the joint's velocity and impose a maximum acceleration:

  joint = {
// snip
velocity: 0,
maxAccel: 0.01
},


Then, for our convenience, we'll introduce a clipping function:

function clip(x, min, max) {
return x < min ? min : x > max ? max : x
}


Now, our movement code looks like this. First, we calculate dtheta as before, adjusting joint.angle as necessary:

  var dtheta = joint.targetAngle - joint.angle;
if (dtheta > Math.PI) joint.angle += 2*Math.PI;
else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;


Then, instead of moving the joint immediately, we compute a target velocity and use clip to force it within our acceptable range.

  var targetVel = ( joint.targetAngle - joint.angle ) * joint.easing;
joint.velocity = clip(targetVel,
joint.velocity - joint.maxAccel,
joint.velocity + joint.maxAccel);
joint.angle += joint.velocity;


This produces smooth motion, even when switching directions, while performing calculations in only one dimension. Furthermore, it allows the velocity and acceleration of the joint to be adjusted independently. See demo here: http://codepen.io/anon/pen/HGnDF/

• This method comes really close, but if I my mouse too fast, it starts to jump the wrong way slightly. Demo here, let me know if I didn't implement that correctly: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98 – jackrugile Mar 24 '14 at 5:20
• I'm not sure what you mean by jumping the wrong way slightly; for me, the joint always moves in the correct direction. Of course, if you move your mouse around the center too quickly, you out-run the joint, and it jerks as it switches from moving in one direction to the other. This is the intended behavior, since the speed of rotation is always proportional to dtheta. Were you intending for the joint to have some momentum? – David Zhang Mar 24 '14 at 5:49
• See my last edit for a way of eliminating the jerky motion created by outrunning the joint. – David Zhang Mar 24 '14 at 16:00
• Hey, tried your demo link, but it looks like it is just pointing to my original one. Did you save a new one? If not that's fine, I should be able to implement this later tonight and see how it performs. I think what you described in your edit is more what I was looking for. Will get back to you, thanks! – jackrugile Mar 24 '14 at 20:59
• Oh, yeah, you're right. Sorry, my mistake. I'll fix that. – David Zhang Mar 24 '14 at 22:05

I love the other answers given. Very technical!

If you want, I have a very simple method to accomplish this. We'll assume angles for these examples. The concept can be extrapolated to other value types, such as colors.

double MAX_ANGLE = 360.0;
double startAngle = 300.0;
double endAngle = 15.0;
double distanceForward = 0.0;  // Clockwise
double distanceBackward = 0.0; // Counter-Clockwise

// Calculate both distances, forward and backward:
distanceForward = endAngle - startAngle;    // -285.00
distanceBackward = startAngle - endAngle;   // +285.00

// Which direction is shortest?
// Forward? (normalized to 75)
if (NormalizeAngle(distanceForward) < NormalizeAngle(distanceBackward)) {
// Adjust for 360/0 degree wrap
if (endAngle < startAngle) endAngle += MAX_ANGLE; // Will be above 360
}

// Backward? (normalized to 285)
else {
// Adjust for 360/0 degree wrap
if (endAngle > startAngle) endAngle -= MAX_ANGLE; // Will be below 0
}

// Now, Lerp between startAngle and endAngle.

// EndAngle can be above 360 if wrapping clockwise past 0, or
// EndAngle can be below 0 if wrapping counter-clockwise before 0.
// Normalize each returned Lerp value to bring angle in range of 0 to 360 if required.  Most engines will automatically do this for you.

double NormalizeAngle(double angle) {
while (angle < 0)
angle += MAX_ANGLE;
while (angle >= MAX_ANGLE)
angle -= MAX_ANGLE;
return angle;
}


I just created this in the browser and has never been tested. I hope I got the logic right the first try.

 2017/06/02 - Clarified the logic a bit.

Start by calculating distanceForward and distanceBackwards, and allow the results to extend beyond the range (0-360).

Normalizing angles brings those values back into the range of (0-360). To do this, you add 360 until the value is above zero, and subtract 360 while the value is above 360. The resulting start / end angles will be the equivalent (-285 is the same as 75).

You next find the smallest Normalized angle of either distanceForward or distanceBackward. distanceForward in the example becomes 75, which is smaller than the normalized value of distanceBackward (300).

If distanceForward is the smallest AND endAngle < startAngle, extend endAngle beyond 360 by adding 360. (it becomes 375 in the example).

If distanceBackward is the smallest AND endAngle > startAngle, extend endAngle to below 0 by subtracting 360.

You would now lerp from startAngle (300) to the new endAngle (375). The engine should automatically adjust values above 360 by subtracting 360 for you. Otherwise you would have to lerp from 300 to 360, THEN lerp from 0 to 15 if the engine doesn't normalize the values for you.

• Well, although the atan2 based idea is straightforward, this one could save some space and a bit of performance (no need to store x and y, nor to compute sin, cos and atan2 too often). I think it's worth expanding a bit on why this solution is correct (i.e. choosing the shortest path on a circle or sphere, just like SLERP does for quaterions). This question and answers should be put in the community wiki as this is a really common issue most gameplay programmers face. – teodron Mar 26 '14 at 17:52
• Great answer. I also like the other answer given by @David Zhang. However I always get a strange jitter using his edited solution when I make a swift turn. But your answer fits perfectly in my game. I am interested in the mathematics theory behind your answer. Although it looks simple but it's not obvious why we should compare the normalized angle distance of different directions. – newguy Jun 2 '17 at 13:58
• I'm glad that my code works. As mentioned, this was just typed into the browser, not tested in an actual project. I can't even remember answering this question (three years ago)! But, looking it over, it looks like I was just extending the range (0-360) to allow the test values to go beyond / before this range in order to compare total degrees difference, and take the shortest difference. Normalizing just brings those values into the range of (0-360) again. So distanceForward becomes 75 (-285 + 360), which is smaller than distanceBackward (285), so it is the shortest distance. – Doug.McFarlane Jun 2 '17 at 18:29
• Since distanceForward is the shortest distance, the first part of the IF clause is used. Since endAngle (15) is less than startAngle (300), we add 360 to get 375. So you would lerp from startAngle (300) to the new endAngle (375). The engine should automatically adjust values above 360 by subtracting 360 for you. – Doug.McFarlane Jun 2 '17 at 18:36
• I edited the answer to add more clarity. – Doug.McFarlane Jun 2 '17 at 19:12