1

I'm trying to rotate a svg polygon using javascript with Math.sin and Math.cos. I have created a codepen here that illustrates what I am trying to do and the problem. I am using Math.sin and Math.cos rather than transform because I need to access the coordinates of the rotated points. Essentially my code:

var r = (Math.PI / 180.0) * a, 
    cos = Math.cos(r), 
    sin = Math.sin(r), 
    cx=350, cy=250;
for(var p of rectangle){
   p[0]=cos*(p[0]-cx)-sin*(p[1]-cy)+cx;
   p[1]=cos*(p[1]-cy)+sin*(p[0]-cx)+cy;
}

does not work correctly, with the rotated points not quite right. They should be rotated anti-clockwise in 2D about cx=350, cy=250. Instead the rectangle is distorted, seemingly doing a figure-8 in 3D.

iPadDeveloper2011
  • 4,560
  • 1
  • 25
  • 36

3 Answers3

3

rotatePoint function returns the point clockwise-rotated around the center. The angle is given in radians:

const rotatePoint = (point, center, angle) => {
  const dx = point.x - center.x;
  const dy = point.y - center.y;
  const fromAngle = Math.atan2(dy, dx);
  const toAngle = fromAngle + angle;
  const radius = Math.hypot(dx, dy);
  const x = center.x + radius * Math.cos(toAngle);
  const y = center.y + radius * Math.sin(toAngle);
  return {x, y};
};

const rotatePoint = (point, center, angle) => {
  const dx = point.x - center.x;
  const dy = point.y - center.y;
  const fromAngle = Math.atan2(dy, dx);
  const toAngle = fromAngle + angle;
  const radius = Math.hypot(dx, dy);
  const x = center.x + radius * Math.cos(toAngle);
  const y = center.y + radius * Math.sin(toAngle);
  return {x, y};
};

const svg = d3.select('svg');
const drawPath = points => points.forEach((p, i) => {
  const another = i === 0 ? points[points.length - 1] : points[i - 1];
  svg.append('line')
    .attr('x1', p.x)
    .attr('y1', p.y)
    .attr('x2', another.x)
    .attr('y2', another.y)
    .style('fill', 'none')
    .style('stroke', 'black');
});

const centerP = {x: 100, y: 100};
svg.append('circle')
  .attr('cx', centerP.x)
  .attr('cy', centerP.y)
  .attr('r', 5)
  .style('fill', 'red')

let rect = [
  {x: 110, y: 20}, 
  {x: 150, y: 20}, 
  {x: 150, y: 80}, 
  {x: 110, y: 80}
];
drawPath(rect);

for (let i = 0; i < 6; i++) {
  rect = rect.map(p => rotatePoint(p, centerP, Math.PI / 3));
  drawPath(rect);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="250" height="200" />
Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
  • Thanks for that Michael. I will see if I can get it working with your code. I'm curious to know what exactly is wrong with my code though. According to other stack answers it should work and be more efficient than what you have as it uses simplifying trig identities. – iPadDeveloper2011 Jul 28 '21 at 09:01
  • 1
    Great, @Michael with some minor tweaks to make it consistent with my code I got your function to work. I especially like the use of `Math.atan2` which calculates the angle in the correct quadrant, unlike `Math.atan` which doesn't work if, for example both `x` and `y` are negative. I'd still like to know what is wrong with my code though. As far as I can see it *should* work. See [this stackoverflow question](https://stackoverflow.com/questions/17410809/how-to-calculate-rotation-in-2d-in-javascript) for example. – iPadDeveloper2011 Jul 28 '21 at 09:51
  • @iPadDeveloper2011, the funtction in another answer works as well, but it rotates the point counter-clockwise: https://jsfiddle.net/mrovinsky/17ybe0hg/ – Michael Rovinsky Jul 28 '21 at 11:17
2

The problem with your solution is that you're updating the x-coordinate of the rotated point (p[0]), and then using this updated value, rather than the original, in calculating the y-coordinate - been there, done that :)

p[0]=cos*(p[0]-cx)-sin*(p[1]-cy)+cx;
p[1]=cos*(p[1]-cy)+sin*(p[0]-cx)+cy;

Should be something like:

var rotX = cos*(p[0]-cx)-sin*(p[1]-cy)+cx;
var rotY = cos*(p[1]-cy)+sin*(p[0]-cx)+cy;
p[0] = rotX;
p[1] = rotY;

Or, better yet, define a function

const rotatePoint = (point, center, angle) => {
  const dx = point.x - center.x;
  const dy = point.y - center.y;
  const ca = Math.cos(angle);
  const sa = Math.sin(angle);
  const x = center.x + dx * ca - dy * sa;
  const y = center.y + dx * sa + dy * ca;
  return {x, y};
};

I don't see any need to be using atan2 and hypot for this.

iPadDeveloper2011
  • 4,560
  • 1
  • 25
  • 36
RaffleBuffle
  • 5,396
  • 1
  • 9
  • 16
  • OMG! In the end it was all so simple!`p0=cos*(p[0]-cx)-sin*(p[1]-cy)+cx; p[1]=cos*(p[1]-cy)+sin*(p[0]-cx)+cy; p[0]=p0;` – iPadDeveloper2011 Jul 29 '21 at 06:11
  • I knew it would be something 'obvious' and 'simple' like that, but I just couldn't find it! Thanks so much @RaffleBuffle you have saved my sanity. Yes, `atan2` and `hypot` are only needed if you don't know the trig identities. – iPadDeveloper2011 Jul 29 '21 at 06:14
0

Just to put this completely to bed, the answer, as posted by @RaffleBaffle, was to not use the updated x coordinate to update my y coordinate. Here is the working code after making the required change:

var r = (Math.PI / 180.0) * a, 
    cos = Math.cos(r), 
    sin = Math.sin(r), 
    cx=350, cy=250;
for(var p of rectangle){
   var p0=cos*(p[0]-cx)-sin*(p[1]-cy)+cx;
   p[1]=cos*(p[1]-cy)+sin*(p[0]-cx)+cy;
   p[0]=p0;
}

You see children, in the end it was sooooo simple and easy. There was no need for those hours of fretting. And that is the story of computer programming. Goodnight now, pleasant dreams. xxx

iPadDeveloper2011
  • 4,560
  • 1
  • 25
  • 36