Search code examples
scalingvectormath

Non-axis aligned scaling


Finding a good way to do this has stumped me for a while now: assume I have a selection box with a set of points in it. By dragging the corners you can scale the (distance between) points in the box. Now for an axis aligned box this is easy. Take a corner as an anchor point (subtract this corner from each point, scale it, then add it to the point again) and multiply each points x and y by the factor with which the box has gotten bigger.

But now take a box that is not aligned with the x and y axis. How do you scale the points inside this box when you drag its corners?


Solution

  • You pick one corner of the rectangle as the origin. The two edges connected to it will be the basis (u and v, which should be perpendicular to each other). You would need to normalize them first.

    Subtract the origin from the coordinates and calculate the dot-product with the scaling vector (u), and with the other vector (v). This would give you how much u and v contributes to the coordinate.

    Then you scale the component you want. To get the final coordinate, you just multiply the the (now scaled) components with their respective vector, and add them together.

    For example:

    Points: p1 = (3,5) and p2 = (6,4)
    
    Selection corners: (0,2),(8,0),(9,4),(1,6)
    selected origin = (8,0)
    
    u = ((0,2)-(8,0))/|(0,2)-(8,0)| = <-0.970, 0.242>
    v = <-0.242, -0.970>
    

    (v is u, but with flipped coordinates, and one of them negated)

    p1´ = p1 - origin = (-5, 5)
    p2´ = p2 - origin = (-2, 4)
    
    p1_u = p1´ . u = -0.970 * (-5) + 0.242 * 5 = 6.063
    p1_v = p1´ . v = -0.242 * (-5) - 0.970 * 5 = -3.638
    
    Scale p1_u by 0.5: 3.038
    
    p1_u * u + p1_v * v + origin = <5.941, 4.265>
    
    Same for p2: <7.412, 3.647>
    

    As you maybe can see, they have moved towards the line (8,0)-(9,4), since we scaled by 0.5, with (0,8) as the origin.

    Edit: This turned out to be a little harder to explain than I anticipated.

    In python code, it could look something like this:

    def scale(points, origin, u, scale):
        # normalize
        len_u = (u[0]**2 + u[1]**2) ** 0.5
        u = (u[0]/len_u, u[1]/len_u)
        # create v
        v = (-u[1],u[0])
        ret = []
        for x,y in points:
            # subtract origin
            x, y = x - origin[0], y - origin[1]
            # calculate dot product
            pu = x * u[0] + y * u[1]
            pv = x * v[0] + y * v[1]
            # scale
            pu = pu * scale
            # transform back to normal space
            x = pu * u[0] + pv * v[0] + origin[0]
            y = pu * u[1] + pv * v[1] + origin[1]
            ret.append((x,y))
        return ret
    
    >>> scale([(3,5),(6,4)],(8,0),(-8,2),0.5)
    [(5.9411764705882355, 4.2647058823529411), (7.4117647058823533, 3.6470588235294117)]