Magnus
Magnus

Reputation: 7829

Spinable turn dial: Calculate angle between two vectors, moving the reference line each time

I have made the below spin dial, using popmotion.

const {
  listen,
  styler,
  pointer,
  value,
  transform,
  spring,
  inertia,
  calc
} = window.popmotion;
const { pipe } = transform;

const dial = document.querySelector(".dial");
const dialStyler = styler(dial);
const dialRotate = value(0, dialStyler.set('rotate'));

const dialRect = dial.getBoundingClientRect();
const dialY = dialRect.top + window.scrollY + (dialRect.height / 2);
const dialX = dialRect.left + window.scrollX + (dialRect.width / 2);
// console.log(dialX, dialY);

const pointA = {x: dialX, y: dialY};
// let pointB = {x: 0, y: 0};
// let angle = 0;
// let prevAngle = 90;
// Angle between origo and pointer
const pointerAngle = o => pointer( o ).pipe(v => {
  const pointB = {x: v.x, y: v.y};
  const angle = calc.angle(pointA, pointB) + 90;
  // console.log('pointA: ', pointA);
  // console.log('pointB: ', pointB);
  // console.log('angle: ', angle);
  // console.log('prevAngle: ', prevAngle);
  // console.log('angle - prevAngle: ', angle - prevAngle);
  return angle;
});

listen(dial, "mousedown touchstart").start(e => {
  e.preventDefault();
  // prevAngle = angle;
  pointerAngle().start(dialRotate);
});

listen(document, "mouseup touchend").start(() => {
  dialRotate.stop();
});
img {
  width: 200px;
}
<script src="https://unpkg.com/popmotion/dist/popmotion.global.min.js"></script>

<img class="dial" src="https://greensock.com/wp-content/uploads/custom/draggable/img/knob.png">

How can i get the dial to start at the same location each time i let go and re-click?

The way it works now, it moves to the cursor/finger position when clicking/touching. I would like it to instead start from the exact place it is currently at, and calculate the angle from there.

This is probably trigonometry related, but I have not been able to figure it out.

Upvotes: 0

Views: 121

Answers (1)

Magnus
Magnus

Reputation: 7829

I came up with the following solution:

Normally, atan2 (angle from popmotion in my case) calculates the angle between two points by assuming that a horizontal x-axis runs through the first point. It then calculates the angle between the x-axis and the vector running through the two points. After all, it does not make sense to talk about an angle between two points, an angle only exists between two vectors / lines.

In our case, we wanted the hypothetical x-axis to move to wherever we click/touch, so we start at 0 angle each time, before spinning the dial. The obvious solution was then to calculate two atan2, one from origin of the dial to the point when we first click, and one from origin of the dial to whatever point our pointer/finger moves to. Then we just subtract the first standstill atan2 from the one that moves.

The above will ensure we always start at a 0 degree angle, when placing our pointer / finger down.

Finally, we simply add the old angle to the new, to start from wherever we previously left off.

Below is the final code.

PS: As a bonus effect, I added Inertia to the dial so it continues rotating at decreased speed until it stops, when we let it go:

const {
  listen,
  styler,
  pointer,
  value,
  transform,
  spring,
  inertia,
  calc
} = window.popmotion;
const {
  pipe
} = transform;

const dial = document.querySelector(".dial");
const dialStyler = styler(dial);
const dialRotate = value(0, dialStyler.set('rotate'));

// Get origin of dial graphic
const dialRect = dial.getBoundingClientRect();
const dialY = dialRect.top + window.scrollY + (dialRect.height / 2);
const dialX = dialRect.left + window.scrollX + (dialRect.width / 2);

// Angle between origo and pointer
const pointA = {
  x: dialX,
  y: dialY
};
let startSet = false;
let startPoint = {
  x: 0,
  y: 0
};
let combinedAngle = 0;
let prevAngle = 0;

const pointerAngle = o => pointer(o).pipe(v => {
  // Capture exact coordinate click/touch event happens
  // Used to calculate angle from that point and to where pointer is dragged
  // Also, capture last rotate position, to add to new angle
  // Ensures angle starts from where it previously stopped (not from 0 degrees)
  if (!startSet) {
    startPoint = {
      x: v.x,
      y: v.y
    };
    prevAngle = dialRotate.get();
    startSet = true;
  }

  const startAngle = calc.angle(pointA, startPoint) + 90;
  const pointB = {
    x: v.x,
    y: v.y
  };
  const mainAngle = calc.angle(pointA, pointB) + 90;
  const newAngle = mainAngle - startAngle;

  combinedAngle = newAngle + prevAngle;
  return combinedAngle;
});

listen(dial, "mousedown touchstart").start(e => {
  e.preventDefault();
  pointerAngle().start(dialRotate);
});

listen(document, "mouseup touchend").start(() => {
  startSet = false;
  const angle = dialRotate.get();
  inertia({
    velocity: dialRotate.getVelocity(),
    power: 0.8,
    from: angle,
  }).start(dialRotate);
});
img {
  width: 200px;
}
<script src="https://unpkg.com/popmotion/dist/popmotion.global.min.js"></script>
<img class="dial" src="https://greensock.com/wp-content/uploads/custom/draggable/img/knob.png">

Upvotes: 0

Related Questions