Dragging, Resizing and Rotating HTML elements: Fun with Web Components and Math
March 3, 2020
A bit of background: for the last year, David Dal Busco and I have been developing DeckDeckGo, a tool for creating and sharing presentations. There was one particular feature we wanted: being able to add elements to any slide and be able to drag it, resize it and rotate it. Here it is in action (feel free to use the open source Web Component):
(check out David’s article on how the component itself works)
Say you have an HTML element (like a
<div>). The browser draws a nice
rectangle. Then you rotate it. The browser draws a nice, rotated, rectangle.
Now you want it to be resized when dragged by a corner. How hard can that be?
left, which are the CSS attributes for specifying the
position of the un-rotated rectangle. We call the top-left corner of the
rectangle. We also know , the angle of the rotation. Here we’ll focus
on one case: the user drags the bottom-right corner, . That is, the
bottom-right of the rotated rectangle. We can easily recover its position:
that’s where the user’s cursor is when they start dragging!
Here’s the million dollar question: when the user moves around, how do we make sure that (the top-left, rotated corner) stays in place?
Because that, really, is our goal! You might think that for each pixels that is moved to the right, we increase the rectangle’s width by pixels. That would work… if there was no rotation! As soon as the rectangle is rotated, everything gets ugly. One thing in particular that may not be straightforward is that the unrotated’s rectangle top-left corner () will move as well…
Enter: The Matrix
Let’s recap. All CSS values,
under our control: we can read and write them. Moreover we know where the
cursor is when the user starts dragging: that’s , the corner being dragged.
One other thing we know: should not move while is being dragged about. So let’s start by figuring out what the position of is! The browser knows it (because it is displaying it) but won’t tell us. So we have to compute it ourselves. How?
The browser starts with a rectangle with top-left corner at (, ) and then rotates it around the rectangle’s center . In order for us to compute that, we’ll use the matrix representation of Affine transformations. The beauty of those matrices is that you can compose them: you want to rotate by angle , and then translate it by some ? Fine! That’s \( t(v) \cdot r(\theta) \cdot p \) (because of boring reason we do this right-to-left), where \( r(\theta) \) is the matrix of rotation by (around the origin), and \( t(v) \) is the matrix of translation by .
If you’re curious, this is what the matrices actually look like:
But… that’s the rotation around the origin! We want to be like the browser, we want to rotate around !!! I learned matrices in vaaaaaaain!
Don’t worry, remember how we can compose those transformations? In order to rotate around a point that is not the origin, we just have to pretend that point is actually the origin for the duration of the rotation. Basically, move that point (and everything else) to the origin, rotate, and then move back.
That means a rotation around a point is something like this:
and the center of the rectangle is simply
(left + width/2, top + height/2).
Congrats, we have , the-point-that-should-never-move!
Believe it or not, the rest is just a variation on the above. In order to
calculate (which means
left) we can simply rotate back,
but this time around the new center of rotation. New? Yes! Because when
was dragged, the center of the rectangle moved!
This new center is simply the point halfway between the new, dragged and the original, never-to-be-moved top-left corner . Now the new top-left corner that we should give the browser is:
What’s left to compute? The new width and height. They’re kind of tricky to get in the rotated rectangle, but if we unrotate the resized rectangle, then the width is the horizontal distance between and , and the height is the vertical distance between and ! We just computed , so let’s now figure out (bottom-right corner in the unrotated, resized rectangle):
And that’s it. We computed the new
left values (), which we can
specify when redrawing the resized rectangle, alongside its new width and
Where Is The Code?
Deriving the corresponding code is left as an exercise to the reader! Better yet, why don’t you use our open-source Web Component instead? There’s a lot we didn’t cover in this article, in particular how to handle minimum widths and heights and how to make all corners draggable. Instead I invite you to study DeckDeckGo’s code!