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"?

-
Use modulo. :D en.wikipedia.org/wiki/Modular_arithmetic – Vaughan Hilts Mar 24 '14 at 1:47
@VaughanHilts Not sure how I'd use it my situation. Can you elaborate further? – jackrugile Mar 24 '14 at 5:22

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

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.

-
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

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