Maintain Aspect Ratio While Resizing

I am trying to maintain the aspect ratio of a DIV while I’m resizing it. So far what I’ve tried was first I determine the aspect ratio of the DIV I’m trying to resize by dividing its width by its height. Then I multiply both the width and the height by the aspect ratio as the DIV is being resized. It is not working though, I expected the height to grow or shrink in relation to the size of the width as the DIV is being resized but that didn’t happen. Instead, the height doesn’t increase or decrease in the way that I expected and sometimes the DIV seems to jump from one location to another when I try to resize it. Please see my code here. To resize the DIV doubleclick on it and then drag one of the handles that appear.

(TLDR: The bottom line of this post. But i’m going to show you how I investigated this, so you might be able to do so on your own.)

So, standard debugging methods. Stick console logs in everywhere, figure out where its going wrong.

I went for examining Width/Left first.

        function resize(e, draggable, drgObj, handle) {
            console.log(drgObj);
            console.log("PX: "+e.pageX);
            console.log("PY: "+e.pageY);
            const minDim = 20;
            const aspRatio = drgObj.oldWidth / drgObj.oldHeight;
            if (handle.classList.contains("nw")) {

                const width = drgObj.oldWidth - (e.pageX - drgObj.oldMouseX)
                const height = drgObj.oldHeight - (e.pageY - drgObj.oldMouseY)
                console.log("W: "+width)
                
                if (width > minDim) {
                    draggable.style.width = width*aspRatio + 'px'
                    console.log("NL: "+(drgObj.oldX + (e.pageX - drgObj.oldMouseX))*aspRatio);
                    draggable.style.left = (drgObj.oldX + (e.pageX - drgObj.oldMouseX))*aspRatio + 'px'
                }

this spits out into my console the values the function received: drgObj, the pageX, the pageY, and then the values it calculates: the width and the New Left.

My first result when moving the handle was:

{
  oldHeight: 80,
  oldMouseX: 202,
  oldMouseY: 152,
  oldWidth: 160,
  oldX: 200,
  oldY: 150
}
"PX: 201"
"PY: 152"
"W: 161"
"NL: 398"

So, based on oldMouseX and oldMouseY, my first registered movement was to move the handle 1 pixel to the left. (I dont actually remember what i did, but its irrelevant, because maths is maths; when it goes wrong in one direction, it goes wrong in all of them.)

Width is 161; which makes sense; I dragged the box 1 pixel wider to the left, so 161 width feels correct.
You’ll see my new Left value is… 398 pixels. That doesnt seem right, does it, if we started at 200 (oldX)?

So something went wrong in the Left calculations. Lets look closer at the values.

draggable.style.left = (drgObj.oldX + (e.pageX - drgObj.oldMouseX))*aspRatio + 'px'
Okay, well, lets plug some numbers in, because we’ve got them all;
drgObj.oldX is 200;
e.pageX is 201;
drgObj.oldMouseX is 202;
aspRatio (not specifically mentioned, but its oldWidth/oldHeight) would be 160/80 or 2.
so…
(200 + (201 - 202))*2 should be199 * 2 = 398… so the math checks out… our logic must be wrong.

But wait… why are we multiplying a static value like the X coordinate by the aspect ratio? The aspect ratio has nothing to do with our positioning of the box, only its size…

If I delete the * aspRatio from your left/top calculations… suddenly the box starts moving with the mouse again. So problem #1 solved. Problem #2 is that the value applied to the width of the box is wrong. See if you can figure out why, and how your maths need to change. (It’s very similar, but can you tell me why?) Problem #3 will be re-constraining the box, but that will be an entirely separate post, because that’s a different logic problem.

3 Likes

Assuming you are starting with an element of a specific aspect ratio could you not output that part to css and then just use js to adjust the width and top, left positions only?

e.g.

 #test1 {
            z-index: 0;
            top: 150px;
            left: 200px;
            width: 160px;
            aspect-ratio:2 / 1;           
            border: solid purple 2px;
        }

The height will take care of itself.

Just a thought :slight_smile:

that would enforce the ratio; the calculations on width, left, and top are still up in the air, as there are multiple ways you can go about it from a given reference point (the mouse), and will still need the aspect ratio to calculate/determine (at which point, you’ve also inherently calculated the height, even if you dont set it)

Basically, it will come down to:

  • Where do you anchor the new box
  • How do you handle imperfect aspected changes (though your solution answers this for the OP: Your proposal is to always use the width)
1 Like

I’m using the following formula to obtain the aspect ratio:
(width/height) = aspectRatio

I can express height in terms of width as follows:
height = width/aspectRatio

I can also express width in terms of height as follows:
width = aspectRatio * height

What this means is if I’m dragging the resize handle horizontally then the value of width can be obtained using the following expression:
width = drgObj.oldWidth - (e.pageX - drgObj.oldMouseX)

Then the height can be obtained simply by dividing the width by the aspectRatio.

If I drag the resize handle vertically I can obtain the value of height as follows:
height = drgObj.oldHeight - (e.pageY - drgObj.oldMouseY)

Then the width can be obtained simply by multiplying aspectRatio by height.

As for the situation where I’m dealing with fractional aspect ratio I think if I use 6 or more decimal places it should be close enough to the actual value.

Although it looks like my new code some what works, it’s buggy and the resize doesn’t work in all direction, I can only drag the handlers horizontally. The entire DIV seems to move when I move the mouse instead of just the sides increase or decreasing size. And sometimes when I try to hold down the mouse on one of the resize handles it disappears. Please see my new code.

Hi, Thanks for your input. However, I think you solution is not ideal for how my application needs to work. Users need to be able to upload their images for example and in that situation I will not know ahead of time what their aspect ratio will be.

1 Like

So… lets talk theory for a minute. I’m gonna get mathsy, so I understand if you want to skip this post :wink:

A box can be defined by 2 points. Specifically, in a web browser when defining a box, you define two points in abstraction: the top left corner (x,y), and the bottom right corner (x+width, y+height). The browser can then extrapolate the rest of the box’s sides from that information (x,y,w,h). Here’s my demonstration box. It’s a square, but it doesnt matter what rectangle it is, the following holds true.
image

You define aspect ratio as width/height; I’m actually going to define is as height/width; those are effectively equivalent statements: w/h = 1/(h/w) … you get a number either way around. So why have I defined it height/width?
If you learned maths in an american system, the next bit should ring some bells: I’m going to call my height the rise, and my width the run. I’m defining a slope of a line. And that makes sense, in our picture. If i define my line to go through the top left corner of the box, the slope of the line h/w corresponds to drawing a line from the top left corner of the box, through the bottom right corner of the box:
image

Now here’s where resizing comes in. If you want to maintain your aspect ratio, what you are saying is the slope of that line cannot change. You’re going to move one corner of the box (for demonstration purposes, lets say you’re moving the bottom right corner). We now have two ways of resizing the box, because there’s actually 3 well-defined points on our line.

(“Three? where’s the third?”)

If the line bisects the box, from top left to bottom right, the line must also pass through the exact center of the box; the center point can be found from the top left corner by going half your height and half your width. (This is where using Paint somewhat fails me, as i’m… going to have to approximate the center of the box. But you get the point.)

image

For the purposes of concrete demonstration, I am going to give my box some numbers:
The top left corner of my box is at 40,40. The bottom right corner of my box is at 100,100; my aspect ratio is 1/1 (a square), and so my midpoint must be at 70,70.

Anchoring

If we’re moving the bottom-right corner of the box, we are redefining our box; the bottom-right corner must be part of the box. There are now two (commonly used) ways of redefining the box: Anchoring from the Middle, and Anchoring from the Top Left (or more generally speaking: Anchoring from the opposite corner from which you’re moving). Which you choose is up to you.

Anchoring from the Middle

You’ve probably seen this one in drawing applications; when you anchor in the middle, the middle of the box stays still. We’re moving the bottom right corner, and we want to keep the aspect ratio the same.
If I move my bottom right corner outwards along the line, in order for the middle point to stay fixed, the top left corner must move the same distance in the opposite direction; if my bottom-right corner goes from 100,100 to 120,120, then my top-left corner which started at 40,40 must move to 20,20, in order for the midpoint to remain at 70,70.
borrows GIF from the internet for demonstration. Demo does not retain aspect ratio, but it shows the anchoring from the middle.
Sketch tutorial: What's new with group resizing in Sketch 44

Anchoring from Top Left (Opposite Corner)

You’ve definitely seen this one - resize your browser window, and you’ll see an example. If i move one corner of the box, the opposite corner is going to be my anchor; that point will not move. So if I move my bottom right corner to 120,120, the top left corner stays at 40,40, and the middle has to move; it’ll end up at 80,80. My aspect ratio is maintained, and the world’s happy.
(this doesnt show maintaining an aspect ratio either, but it demonstrates the anchoring type)
canvas - Correct algorithm to resize a rectangle on mouse drag - Stack Overflow

So step 1 will be figuring out which type of anchoring you want to do, as it changes the code and the maths you need to do. Then, we talk about something we’ve taken for granted…

Maintaining the Aspect Ratio

So we’ve so far just generally breezed over the statement “we’ll keep the aspect ratio”. If you go back to our paint drawing:
image
If we are moving the bottom right corner, as long as we put it down on the green line anywhere, we’re good.
But the mouse isnt limited to the green line. The mouse can go anywhere. How do we handle it when the user puts the mouse somewhere that ISNT on the green line? What if it’s somewhere over here?
image

Well, we cant put the corner there; it won’t have the right aspect ratio. So we’re going to have to change one or both of the dimensions to get it back on the green line.

image

There’s a couple of options here. Paul’s suggestion above would say “ignore the height. take the new width, put the corner on the green line so that the width is where the mouse pointer is; that’s your new box.” (That’s the black dot)
You can of course do the opposite; ignore the width and use the height; effectively the same code, but using the other two variables. (That’s the cyan dot.)
You can try and be clever; if the mouse is above the line, use the width; if it’s below the line, use the height; this is “using the longest dimension”. (In my example above, you would use the width, and put the point at the black dot)
You could also try and do some maths and figure out the closest point on the line to the cursor, but that feels a bit excessive. Or maybe it doesn’t. That’s up to you, again. (That’s the orange dot.)

So… the questions now return to you. Have a think about what you want the code to do; where you want to anchor, and where you want to end up when the mouse isnt on the aspect ratio line. We can help from there.

2 Likes

Images have a natural aspect ratio so you only have to set their width anyway and then let height be auto,

Here’s a css only resize demo to show it in action (no js needed). The resize handle is in the bottom right corner.

However I only posted the above to show what I meant and I think you should still follow the post above by @m_hutley . My post is just a bit of a distraction to illustrate my point :wink:

2 Likes

Just posting a link to Paul’s CSS resize, because it was a new one to me too (or one i’ve forgotten) :slight_smile:

2 Likes

Hi m_hutley, I’ve decided to anchor the DIV at its corners. So when I drag on one of the corner handles the opposite side of the DIV stays still and only the side being dragged and the top or bottom of the DIV decreases or increases in size.

I’m able to achieve part of that, my DIV’s width and height increase proportionally and maintain the aspect ratio. However I’m noticing two unexpected problems, when I drag the handlers I notice that sometimes the entire DIV moves and not just its sides. Also I cannot drag the left top corner handle vertically, only horizontally. Please see my new code here.

Okay, so we’re anchoring opposite, and… from the looks of that code, we’ve decided to prioritize the height when dealing with mouse sillyness on the bottom right handle. Cool.

I havent been able to trigger

on the bottom-right handle, only the top left. I can drag the top left corner horizontally just fine; its when we add any verticality that we have problems. So that obviously is where we need to look at your code.

Lets look at the vertical component of your move:

                    draggable.style.height = width*aspRatio + 'px'
                    draggable.style.top = drgObj.oldY + (e.pageY - drgObj.oldMouseY) + 'px'

okay so we’re defining height in terms of width * aspRatio; on the top-left handle that’s cool, it will keep the aspect ratio. Whether you want to do width on top-left and height on bottom-right, thats totally a decision you can make.

Top, however, is NOT being defined in terms of the width times the aspect ratio. There’s your problem.

So, if width is:
draggable.style.width = width + 'px'
and height is:
draggable.style.height = width*aspRatio + 'px'
so, width * aspRatio,

then if left is:
draggable.style.left = drgObj.oldX + (e.pageX - drgObj.oldMouseX) + 'px'
then top should be left * aspRatio, right?
draggable.style.top = drgObj.oldY + (e.pageY - drgObj.oldMouseY) + 'px'
=>
draggable.style.top = (drgObj.oldX + (e.pageX - drgObj.oldMouseX))*aspRatio + 'px'

(well… not really, because top and left wernt originally the same aspect ratio! So we need to deal with that…)

1 Like

(apologies for the double-post, but i was getting carried away there in the first one, figured the “correct” answer should get its own post)

So how do we fix top.
If you put my code fix in to scale top by the left measurement, you’ll find that the box “jumps” initially. That’s because it’s scaled completely from Left; but left and top dont have the same initial measurement, so when you start moving the box, it immediately jumps from being at top,left to being at left,left. That wont do.

What we need to figure out is, from the initial position (top), where does the box need to be now?

Remember the anchor? It hasnt moved, right? That’s why it’s the anchor.

Where should the top left of our box be, if we’ve defined a width and a height, and we know the bottom right corner? We dont need any aspect ratios for that. Lets do some simple geometry that we spelled out back in the theory.

well, if the bottom right corner is at (oldX+oldWidth , oldY+oldHeight), we know that our topleft corner should now be (oldX+oldWidth-newWidth, oldX+oldHeight-newHeight) (think that one through for a minute, to make sure i’m right and that makes sense to you). I’m drawing a box from bottom right to top left)

Now, keep in mind that our newHeight isn’t height. We’ve changed that definition because you’re scaling with width on this corner; so newHeight is width*aspRatio.

So, lets plug it in:

 draggable.style.top = (drgObj.oldY + drgObj.oldHeight - (width*aspRatio)) + 'px'

annnd… suddenly the box starts resizing itself appropriately.

1 Like

Thanks for your input. I will definitely check out this solution as well as the javascript solution.

1 Like

Thank you so much for your help. Your explanation made the problem easier to understand and everything works as expected.

2 Likes