How to build a resizable box
In this article I'll show you how to build a resizable box. I'll go over the math of resizing first and will then show you how to turn it into code with javaScript. By the end you should be able to build something like this:
I wrote the example in Svelte but if you know Vue or React you should be able to read it. If you want, go check out the code.
The math
Let's start by establishing a coordinate system. If you forgot how coordinate systems work, here's a 6 minute catch up.
A common approach to resizing is to try to change the width or height of an element directly. This article shows such an approach and all the edge cases that arise from it.
By doing it in a mathematical way we avoid most of the edge cases and end up with code that's easier to read/debug. It's also easy to extend with features like resizing multiple elements at the same time or preserving the aspect ratio of a box.
The specific implementation for this doesn't matter. I use SVG (which has an actual coordinate system) but you can also turn your new coordinates back into width / height values after doing the math.
For our example, we can define our shape with 3 points. Each point has an x and a y coordinate. To scale our triangle we need to multiply each point by the same factor. So If we want a triangle twice the size we take the x and y of each point and multiply by 2. We call this number the "growth factor" (or growth rate).
- growth factor 2 -> double the size
- growth factor 0.5 -> half the size
The top point starts with the coordinates x = 1, y = 0. If we multiply both by 2 we get the new point x = 2, y = 0. Do that with every point and you scaled your element to twice the size!
Making it relative
Unfortunately, we have a problem. Our coordinates are defined in relation to the entire coordinate system. So when our shape is not at the origin (0 / 0) of the coordinate system, this happens:
Notice how the left side of the triangle also moves? We want to move the triangle in relation to itself, not in relation to the entire coordinate system
To do this we'll create a new point and we'll call this point the anchor.
We'll place the anchor opposite to where our mouse cursor is. So the x coordinate is equal to the point of the triangle that's the furthest left and the y coordinate is equal to the point that's furthest on top. The red box around the triangle shows the space the triangle takes up. We call this the bounding box (or bbox for short).
If you're confused at this point, don't worry. Have another look at the ilustration and the demo and check how the anchor and the bbox behave.
We defined our anchor point, next all we need to do is to define all of our coordinates relative to this anchor point (and in this case relative to the top left corner of the shape's bbox).
The green arrow shows the absolute distance which is our current x coordinate. We can get the distance relative to our anchor like this:
const pointRelativeToAnchor = {x: point.x - anchor.x,y: point.y - anchor.y}
Putting it all together
We've introduced all our variables. Now let's put it all together and build our resizing function.
What we're ultimately looking for are the coordinates of our point after our user resized the rect. We'll call this coordinate the "endPoint".
First let's start by getting all of our relative coordinates
// mouse coordinates at the start of the drag actionconst relativeStartMouseX = startMouseX - anchor.xconst relativeStartMouseY = startMouseY - anchor.y// mouse coordinates at the end of the drag actionconst relativeEndMouseX = endMouseX - anchor.xconst relativeEndMouseY = endMouseY - anchor.y// point coordinates at the start of the drag actionconst startPointRelativeToAnchor = {x: startPoint.x - anchor.xy: startPoint.x - anchor.y}
From the change in mouse coordinates we can get the growth factor our users want:
const growthFactor = {x: relativeStartMouseX / relativeEndMouseX,y: relativeStartMouseY / relativeEndMouseY,}
To calculate our new relative coordinates we multiply the start point by the growth factor
const endPointRelativeToAnchor = {x: startPointRelativeToAnchor.x * growthFactor.x,y: startPointRelativeToAnchor.y * growthFactor.y,}
then we can turn them back into absolute coordinates like this
const endPoint = {x: endPointRelativeToAnchor.x + anchor.x,y: endPointRelativeToAnchor.y * anchor.y,}
If we want to scale it into any other direction we can use the same math, all we need to do is move our anchor somewhere else:
The interactions
There are 3 actions a user can take, mousedown, mousemove and mouseup.
mousedown
We're going to attach a mousedown even to every edge and corner of our bbox.
function handleMouseDown(e, direction){// transform mouse coordinates into svg spacelet {x,y} = getSvgCoordinates(e, svg)// can have 2 directions at once (like ne)let directions = direction.split("");// move the anchor to the opposing edge(s) of bboxdirections.forEach(direction => {if (/[n]/.test(direction)) anchor.y = bbox.bottom;if (/[e]/.test(direction)) anchor.x = bbox.left;if (/[s]/.test(direction)) anchor.y = bbox.top;if (/[w]/.test(direction)) anchor.x = bbox.right;});// copy the start statestartState = { direction, shapePoints, x, y };};
the direction is a string like "n", "w", "ne" etc. A direction of "ne" means the point clicked is the top right (north-east) corner. In that case we would move the anchor all the way to the edge and all the way to the bottom. That way it's going to end up at the bottom left edge - opposite to our clicked corner.
the startState is a copy of the mousecoordinates and the points of our triangle.
mousemove
the mousemove should event happen when the user moves his mouse anywhere in our SVG.
function handleMouseMove(e) {if (!startState) return;let mousePosition = relativeToAnchor(getSvgCoordinates(e, svg));let startMousePosition = relativeToAnchor(startState);// can grow individually on each axis// to lock aspect ratio use the same growth rate for x and ylet xGrowthRate = mousePosition.x / startMousePosition.x;let yGrowthRate = mousePosition.y / startMousePosition.y;// recalculate every pointshapePoints = startState.shapePoints.map(({x,y}) => {// move sidewaysif (/[ew]/.test(startState.direction)) {let startX = x - anchor.x;let endX = startX * xGrowthRate;x = endX + anchor.x;}// move up and downif (/[ns]/.test(startState.direction)) {y = (y - anchor.y) * yGrowthRate + anchor.y;}return { x, y };});}
mouseup
The mouseup event can also happen anywhere. When it is triggered all we do is reset the startState. That way the mousemove event will do nothing.
🎉🎉 You did it!
That's it! You now have all the information to build your own implementation. If you're unsure about what to do next have another look at the code for the demo. If you have any questions or feedback tweet at me.