Konva – rotate a shape around any point with simple math.

Shape rotation starts conceptually simple, but gets frustratingly hard quite quickly once you move away from very simple cases. This article discusses the basics and provides working code to rotate any shape around any arbitrary point on the stage. This article mentions the Konva graphics library but the math in the function can easily be used on any 2D rotation problem. Handy stuff to have in your kit bag.

Konva is a 2D drawing library that wraps the HTML5 canvas element. It makes development of sophisticated visual apps and games much less difficult than it would otherwise be, and apps built with Konva can include animation of many thousands of shapes without performance degradation.

Using Konva, we talk about drawing shapes on the stage – with these shapes being rectangles, circles, lines, etc. When we talk about rotating shapes, we have to consider two broadly different classes of shape. Anything rectangular will naturally rotate around its top-left corner – so that is Rectangle, Text, Image, etc. Meanwhile shapes derived from the ellipse – so that includes Circle, Ellipse, Wedge, Star etc, will rotate around the center of the circle that the shape scribes. 

Simple rotation

Simple rotation is what you get if you draw a shape and immediately modify the shapes rotation() property. The rotation property gets or sets the rotation angle in degrees. This is perfectly usable if you need to rotate around this point, but how to rotate around some other point ?

Complex rotation

Konvas onboard solution for complex rotation uses a concept called the shapes ‘origin’. which is manipulated using the offset() property. When we draw a shape on the Konva stage, we are effectively saying:

  1. Go to the shapes position (gives the origin)
  2. Apply the offset (gives drawing position)
  3. Draw the shape.

If you’ve used Konva shapes you might never have needed to know about the offset or origin.

Offset is an x, y adjustment vector. It works by being added to the position. But note that a positive offset actually translates the position of the shape in the negative direction.

Confused yet? Lets take a look at a sandbox app to explore these effects. Its here if you prefer to open at CodePen. The sandbox has two rectangles and two circles, stacked up. They are all initially drawn at the same point {x: 80, y: 60}. As we use the buttons to change the position, origin and rotation, one rectangle will stay put to show where we started while the other one moves. The circles are slightly different – the center of the red circle represents the origin point and the center of the black circle shows the rectangles position point.

The gif below shows four experiments.

Experiment 1: Simple rotation.

  • Click the reset button.
  • Click the ‘Rotate + 10 degrees’ button a few times.

We see the rotation angle started at 0 degrees rotation, position is {x: 80, y: 60}, and offset {x: 0, y: 0}. As the rectangle is rotated we see the rotation angle climbing as the rectangle rotates around the red circle. The position and offset do not change. This is the most simple form of rotation and it’s what everyone starts with as soon as they reach to apply rotation for the first time.

Here’s the first takeaway though – the rectangle rotates around the red circle, remember this red circle is centered on the origin. That just so happens to coincide with the position of the rectangle, but we will vary that next and see what happens.

Experiment 2: Moving the origin

  • Click the reset button.
  • Click ‘Move origin +10 on x-axis’ four times – pay attention to the rectangle.
  • Click the ‘Rotate + 10 degrees’ button a few times.

This is a bit of a shell-and-pea game. You have to pay attention to the movement of the rectangle and you might have trouble accepting what you are seeing for the first few times.

So what happened ? We started with a position of {x: 80, y: 60} and added an offset of {x: 40, y: 0}. Looking at the x components alone, you would expect 80 + 40 to give 120, producing a movement of the rectangles top-left corner of +40 to the right. However, offset works as a negative, so we get 80 + -40 gives 40 and see the rectangle hop 40 pixels to the left. BUT the position property of the shape remains at {x: 80, y: 60}. Confusing!

Looking at the rotation – you might want to click the rotate button a few more times now – shows clearly that the rotation is around the origin (red circle).

Experiment 3: Rotate around the center

For this one

  • Click the reset button.
  • Click ‘Move origin to center’
  • Click the ‘Rotate + 10 degrees’ button a few times.

What we see here is that, whilst we get what we asked for, the position remains unchanged again.

Experiment 4: Simple rotation

For the final experiment do the following

  • Click the reset button.
  • Click ‘Move position+10 on x-axis’ four of five times.
  • Click the ‘Rotate + 10 degrees’ button a few times.

What we witness here is exactly what we would expect because there is no offset change involved. We see the shape advance 40 pixels to the right, and we see it rotate around its natural rotation point.

Conclusions of experiments

What I don’t like about this is that it is non-intuitive / confusing because

  1. Though I change the visual position of the shape, its properties for position remain the same.
  2. To save and restore the canvas contents I need to store not only the position but also the offset.
  3. The positive values I apply for offset are interpreted as negative visual movement of the shape.

Why would I use this approach ?

Good question – so far I have only seen origin used as a low-code mechanism to move the rotation point for a shape.

What other issues can we forsee ?

In the sandbox experiments we were looking at rotation around points within the shape itself. What will we do when the rotation point is somewhere else on the canvas. We will be setting the offset() property to large values potentially leading to further cognitive overload, for me at least.

Care to suggest an alternative ?

Yes! An alternative is to wrap rotation in a function that is all about rotation, leaves the shape’s position unchanged and does not require the brain space that the use of the offset() property demands. How would that work – simple trigonometry. Don’t be put off if you’re not a math pro – that’s the point of wrapping this in a function, we do it once and forget it.

Here’s an recorded example of the use of this type of function in rotation sandbox available here. You can see a variety of shapes being rotated around arbitrary points on the canvas.

The concept within our rotation function has two steps:

  1. re-position the shape so that the rotation point is where it needs to be after the rotation,
  2. then rotate the shape around its standard rotation point.

We know the initial position of the shape, the point on the canvas that we have to rotate around, and the angle of the rotation. We therefore produce the function as below.

/* Rotate a shape around any point.
   shape is a Konva shape
   angleDegrees is the angle to rotate by, in degrees
   point is an object {x: posX, y: posY}
*/
function rotateAroundPoint(shape, angleDegrees, point) {
  // sin + cos require radians
  let angleRadians = angleDegrees * Math.PI / 180; 
  
  const x =
    point.x +
    (shape.x() - point.x) * Math.cos(angleRadians) -
    (shape.y() - point.y) * Math.sin(angleRadians);
  const y =
    point.y +
    (shape.x() - point.x) * Math.sin(angleRadians) +
    (shape.y() - point.y) * Math.cos(angleRadians);
   
  // move the rotated shape in relation to the rotation point.
  shape.position({x: x, y: y});

  // rotate the shape in place around its natural rotation point 
  shape.rotation(shape.rotation() + angleDegrees); 
}

It turns out that the math we need is quite simple. At lines 10 & 14 we are computing the new coordinates of the point to which we have to relocate the shape. We then apply that as the shapes position, and finally rotate the shape by the given angle around its natural rotation point. When I say ‘natural rotation point’ I mean there is no need to change the offset!

Here is the code sandbox for you to try.

Summary

We’ve seen the mechanism for rotation that Konva has baked-in – the rotation() property. We’ve also seen that to rotate around a point other than the natural rotation point of a shape requires manipulation of the shapes offset() property which, whilst it works, can be confusing. Finally we’ve seen an alternative function that achieves rotation around any point on the canvas without the potential confusion of the offset() method.

Thanks for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: