Konva – using a shape’s transform to rotate points

The Konva HTML5 canvas lib has a very useful feature that can spit out the transform applied to a shape, and which we can borrow to rotate points without math. Th example below illustrates the use of rect’s transform to position the corner circles. Codepen here.

Example of using the rect’s transform to position the corner circles. Codepen here.

What is a transform ?

I couldn’t find a simple explanation for this on the web – there is much info available but it gets complication quickly, so in a bid to avoid TLDR on that here is my attempt at explaining vectors to my gandma in 2 paragraphs.

When we are using an HTML canvas we are asking the computer to draw on a 2-D surface – think of it like the top of a table. We lay out a set of squares like a chess board on the table and number them from one corner – usually the top left corner as it happens. Now, to draw a line we say ‘pick up a pen; starting at the corner, move 3 squares right and 2 squares down and put your pen there; draw a line to the point 2 squares to the right and 4 squares down. The result is a line starting at {x: 3, y: 2} and extending to {x:5, y: 6}, in whatever color pen granny picked up. So that’s the basis of vector drawing on my granny’s best coffee table.

The name ‘vector’ is borrowed from its origin in mathematics but the software guys borrowed the term to explain the approach without sticking entirely to the full maths. It is used mainly to differentiate it from the opposite and older approach of managing the individual dots of an image – known as bitmaps. The advantage of the vector approach is that because the instructions are about drawing on a grid, we can make the grid smaller or larger and the instructions will work without the need to change them. So scaling our line from the table top to the size of a building is simple. Also, the instructions for the drawing stay the same regardless of scale meaning we only need to transmit a fel lines of instructions to get a huge image. If we tried that with a bitmap that was, say 100 x 100 pixels to something say 10M x 10M pixels, we would have to do a lot dot filling, consume a very large amount of memory, and send a vast chunk of data to have the picture drawn on some device – all together a v e r y – s l o w – p r o c e s s.

Conclusion – vector good, bitmap bad. But vectors will not be much help if we can only draw straight lines! Good news then, the HTML5 canvas gives us plenty of other drawing functions for lines, curves, rectangles, fills, etc. In fact there is enough to be able to produce drawings with vectors that are indistinguishable from bitmaps but have the infinite scalability advantage. See the excellent MDN intro to the canvas here.

As an aside, it should not come as any kind of surprise that the formidable PDF format is vector based.

Working with the individual vector drawing functions is possible but tediously slow for devs to produce useful output . To speed up productivity, we have libs like my current favourite Konva , which means rather than working with individual drawing commands we can elevate ourselves to thinking about fully composed shapes like circles, rectangles, paths and images, so improving our productivity and capability massively.

So what about transforms

Ok – lets think about a rectangle drawn somewhere on the canvas. Vector drawing commands enable us to have the rectangle slide to a point on the canvas – this is named a vector ‘translation’. We can also have the rectangle rotated around some canvas point (vector rotation), and scaled from another (vector scaling).

In Konva, all these individual changes are collected together as the rectangle’s ‘transform’. For example, in the new rect command below we are effectively creating the following transform:

  1. Make a rectangle of size 50 x 40 at point (0, 0)
  2. Translate the rectangle to point (10, 20)
  3. Rotate the rectangle by 45 degrees clockwise around its top-left corner (default rotation point for a rect).
let rect = new Konva.Rect({
  x: 10,
  y: 20,
  width: 50,
  height: 40,
  rotation: 45,
  stroke: 'black',
  strokeWidth: 1
});    

We treat the transform as a black box – we don’t need to understand anything more about it other than if we give it a point then it will apply the same set of actions on that point as was applied to the shape that owns the transform.

You explained the transform – what can I use it for ?

Here’s the good stuff. Say I wanted to put a circle around each corner of the rectangle. Before it was rotated that would be easy – the corners are

  • Top-left = (x: x, y: y)
  • Top-right = (x: x + width, y: y)
  • Bottom-right = (x: x + width, y: y + height)
  • Bottom-left = (x: x, y: y + height)

but when rotated they obviously move! I can use basic trigonometry math to calculate their positions which is not a massive challenge but more work than necessary, because the other option is to ask the rect for its transform then apply that transform to each corner point which will return its rotated position.

The built-in Konva node.getTransform and its close relation node.getAbsoluteTransform methods will retrieve the transform applied to the rectanlge shape. The absolute version gets the shape’s transform including the parent transform, while the plain getTransform gets the transform relative to the shape’s parent. The parent transform is important if the shape is part of a group that is also transformed.

Both return a Konva.Trasform object, which itself has the point() method that will take a given {x, y} object and apply the transform to it.

Using the transform applied to the shape means that we do not have to be concerned with how to mimic the steps of that transform – we just ask for the same transform to be applied to our points.

Which means that we can do this…

// assuming we have a Konva rect already...
let rect = new Konva.Rect({
  x: 100,
  y: 80,
  width: 60,
  height: 20,
  fill: 'cyan'
  rotation: 45    
})

let corners = [],
    size = rect.size();

// Now get the 4 corner points
corners[0] = {x: 0, y: 0 }; // top left
corners[1] = {x: size.width, y: 0 }; // top right
corners[2] = {x: size.width, y: size.height }; // bottom right
corners[4] = {x: 0, y: size.height }; // bottom left

// And rotate the corners using the same transform as the rect.
for (let i = 0; i < 4; i++){
  // Here be the magic
  corners[i] = rect.getAbsoluteTransform().point(corners[i]); 
}

// At this point we have the rotated positions of the corners.

IMPORTANT NOTE

You will have seen that the corners of the rect in the above code are set relative to the origin and not the rect position. In other words the top-left corner is {x: 0, y: 0} and not {x: rect.x(), y: rect.y()}, and the bottom-right is {x: rect.width, y: rect.height}. This is because the rect’s transform is:

  1. moveto(x, y)
  2. rotate(angle)

If we do not negate the moveto when deciding our unrotated corner points then they will appear to have experienced 2 times the moveto transform.

The moveto transform is not obvious – it is the effect of setting the shape.x() and shape.y() in the initial declaration of the shape.

See the sample codepen for the demo below here.

Summary

We’ve had a simple explanation of vector drawing and the meaning of the ‘transform’ in the context of a shape drawn by the Konva canvas library. In the code demo we used the getAbsoluteTransform() method to get the rect’s transform and applied it to the corners of the rect to rotate them perfectly following the position and rotation of the rectangle.

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: