Rotating a line to look at the mouse with linear interpolation causes jumping

74 views Asked by At

Earlier, I was trying to find out how to rotate a line around a pivot using p5.js, and I played with the code a little bit to make the line point towards the mouse using the atan2 function. It worked fine, and I decided to see what it would look like if I used linear interpolation (lerp) to make the animation look smoother. What was weird is that it seemed once the line passed a certain point, it jumped to the other side instead of just moving to that point. example image of issue

Here's the code I'm having the issue with:

let angle = 0;

let rot = 0;

function setup() {
  createCanvas(600, 300);
}

function draw() {
  let v1 = createVector(width / 2 - 50, height / 2);
  let v2 = createVector(width / 2 + 50, height / 2);

  background(255);
  stroke(0);
  strokeWeight(4);

  rot = lerp(rot, atan2(mouseY - v1.y, mouseX - v1.x), 0.1);

  push();
  translate(v1.x, v1.y);
  rotate(rot);
  translate(-v1.x, -v1.y);
  let r0 = line(v1.x, v1.y, v2.x, v2.y);
  strokeWeight(10);
  let p1 = point(v1.x, v1.y);
  let p2 = point(v2.x, v2.y);
  pop();
}

How can I make this animation look smooth, without the weird jumping?

2

There are 2 answers

2
IT goldman On BEST ANSWER

The issue is because atan2 returns value between -Math.PI to +Math.PI so there is a jump where the linear interpolation fails for the expression target_angle = atan2(mouseY - v1.y, mouseX - v1.x).

That's why, in order to make the shortest path between current rot to target_angle we should normalize the difference between so that the distance to cover its abs value should be less than Math.PI (half a circle)

let angle = 0;

let rot = 0;

function setup() {
  createCanvas(600, 300);
}

function draw() {
  let v1 = createVector(width / 2 - 50, height / 2);
  let v2 = createVector(width / 2 + 50, height / 2);

  background(255);
  stroke(0);
  strokeWeight(4);


  let target = atan2(mouseY - v1.y, mouseX - v1.x);

  while (target - rot > Math.PI) {
    target -= 2 * Math.PI;
  }

  while (target - rot < -Math.PI) {
    target += 2 * Math.PI;
  }

  rot = lerp(rot, target, 0.1);

  push();
  translate(v1.x, v1.y);
  rotate(rot);
  translate(-v1.x, -v1.y);
  let r0 = line(v1.x, v1.y, v2.x, v2.y);
  strokeWeight(10);
  let p1 = point(v1.x, v1.y);
  let p2 = point(v2.x, v2.y);
  pop();
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/p5.js"></script>

0
George Profenza On

This is a variation on IT goldman's nice answer(+1) with the same principle @ggorlen metioned. (I was familiar with the idea from actionscript 3 days from GreenSock's ShortRotationPlugin). Credits go to them.

This is the mixture of the above: the key difference is avoiding the blocking while loops (which I don't generally recommend).

let angle = 0;

let rot = 0;

function setup() {
  createCanvas(600, 300);
}

function draw() {
  let v1 = createVector(width / 2 - 50, height / 2);
  let v2 = createVector(width / 2 + 50, height / 2);

  background(255);
  stroke(0);
  strokeWeight(4);


  let target = atan2(mouseY - v1.y, mouseX - v1.x);
  // check remainder wrt to full 360 degrees (2 * PI)
  let diff = (target - rot) % TWO_PI;
  // if the difference is not on this half of the circle (needs short rotation) wrap around the circle (add or remove 2*PI) depending on the sign of the difference
  if (diff != diff % PI) {
    diff = (diff < 0) ? diff + TWO_PI : diff - TWO_PI;
  }
  // ease towards the rotation using short rotation angle difference
  rot = lerp(rot, rot + diff, 0.1);

  push();
  translate(v1.x, v1.y);
  rotate(rot);
  translate(-v1.x, -v1.y);
  let r0 = line(v1.x, v1.y, v2.x, v2.y);
  strokeWeight(10);
  let p1 = point(v1.x, v1.y);
  let p2 = point(v2.x, v2.y);
  pop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>