Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

joint animation example

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

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

3 Answers 3

up vote 7 down vote accepted

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);
share|improve this answer
    
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 at 5:25
1  
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 at 7:36

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/

share|improve this answer
    
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 at 5:20
1  
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 at 5:49
    
See my last edit for a way of eliminating the jerky motion created by outrunning the joint. –  David Zhang Mar 24 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 at 20:59
1  
Oh, yeah, you're right. Sorry, my mistake. I'll fix that. –  David Zhang Mar 24 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.

share|improve this answer
    
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 at 17:52

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.