D
D
Dmitry Makarov2015-12-07 15:19:22
Mathematics
Dmitry Makarov, 2015-12-07 15:19:22

How to implement scaling relative to an arbitrary point?

I am implementing a specialized 2D editor in .NET. I want to implement scaling relative to the cursor.
In GDI+, the Graphics class has a wonderful Transform property that allows me to implement what I need using the affine transformation apparatus in matrix form. In all books they write that in order to scale relative to an arbitrary point, it is necessary:

  1. perform an offset transformation so that the point relative to which we are scaling is shifted to the origin
  2. scale directly with respect to the origin
  3. perform an offset transformation so that the origin is shifted to the origin.

That is, within the framework of my application, it is enough to correctly change the Transform matrix when the mouse wheel is rotated.
I do it like this:
public class DrawingAreaControl : UserControl
    {
        public DrawingAreaControl()
        {
            DoubleBuffered = true;
            transform = new Matrix();
        }

        public Matrix transform { get; set; }
    
        protected override void OnPaint(PaintEventArgs e)
        {
            e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

            e.Graphics.Transform = transform;

            base.OnPaint(e);
        }

        const float SCALE_MUL = 1.05f;
        protected override void OnMouseWheel(MouseEventArgs e)
        {
            base.OnMouseWheel(e);

            transform.Multiply(new Matrix(1, 0, 0, 1, e.Location.X, e.Location.Y));
            transform.Multiply(e.Delta > 0 ?
                                new Matrix(SCALE_MUL, 0, 0, SCALE_MUL, 0, 0) :
                                new Matrix(1 / SCALE_MUL, 0, 0, 1 / SCALE_MUL, 0, 0));
            transform.Multiply(new Matrix(1, 0, 0, 1, -e.Location.X, -e.Location.Y));

            Invalidate();
        }
    }

but it doesn't work as it should. After several scalings, the drawing under the mouse starts to run away and there is no longer the necessary scaling relative to the cursor.
For anyone, this is due to incorrect calculation of transformations to the origin and back.
How to calculate them so that it works correctly?

Answer the question

In order to leave comments, you need to log in

2 answer(s)
D
Dmitry Makarov, 2015-12-09
@DmitryITWorksMakarov

I found the solution myself.
I carefully painted the matrices, manually multiplied them, got a ready-made scaling matrix relative to the cursor, substituted it into the program and everything works.
I began to look at what was wrong with my program.
It turned out that I misunderstood the order of matrix multiplication when applying the Multiply(Matrix) method. Fortunately, Multiply has an overloaded version with an arbitrary choice of operand order.
It should be like this:

public class DrawingAreaControl : UserControl
    {
        public DrawingAreaControl()
        {
            DoubleBuffered = true;
            transform = new Matrix();
        }

        public Matrix transform { get; set; }
    
        protected override void OnPaint(PaintEventArgs e)
        {
            e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

            e.Graphics.Transform = transform;

            base.OnPaint(e);
        }

        const float SCALE_MUL = 1.05f;
        protected override void OnMouseWheel(MouseEventArgs e)
        {
            base.OnMouseWheel(e);

            var matrixOrder = MatrixOrder.Append;
            var K = e.Delta > 0 ? SCALE_MUL : 1 / SCALE_MUL;
            transform.Multiply(new Matrix(1, 0, 0, 1, -e.Location.X, -e.Location.Y), matrixOrder);
            transform.Multiply(new Matrix(K, 0, 0, K, 0, 0), matrixOrder);
            transform.Multiply(new Matrix(1, 0, 0, 1, e.Location.X, e.Location.Y), matrixOrder);

            Invalidate();
        }
    }

Perhaps my version with a manually prepared matrix is ​​somewhat more computationally efficient, but the presented version is more readable.

A
Anton Zhukov, 2015-12-07
@MrCheater

I had the same task on js - here is the solution code (as is)
onScroll = ({dy, x, y}) => {
let border = this.props.border * this.state.containerWidth / 100;
let originalDeltaX = x - this.state.x ;
let originalDeltaY = y - this.state.y ;
let deltaX = originalDeltaX;
let deltaY = originalDeltaY;
let minWidth = this.state.containerWidth - border * 2;
let minHeight = this.state.containerHeight - border * 2;
let prevWidth = this.state.width;
this.state.width -= dy / 5;
this.state.height *= this.state.width / prevWidth;
deltaX *= this.state.width / prevWidth;
deltaY *= this.state.width / prevWidth; let newX = this.state.x - deltaX + originalDeltaX;
if(this.state.width < minWidth) {
let scale = minWidth / this.state.width;
this.state.width *= scale;
this.state.height *= scale;
deltaX *= scale;
delta Y *= scale;
}
if(this.state.height < minHeight) {
let scale = minHeight / this.state.height;
this.state.width *= scale;
this.state.height *= scale;
deltaX *= scale;
delta Y *= scale;
} let newY = this.state.y - deltaY + originalDeltaY; if(newX > border) { newX = border; }
if(newY > border) {
newY = border;
}
if(newX + this.state.width < this.state.containerWidth - border) {
newX = this.state.containerWidth - border - this.state.width;
}
if(newY + this.state.height < this.state.containerHeight - border) {
newY = this.state.containerHeight - border - this.state.height;
}
this.setState({
width : this.state.width,
height : this.state.height,
x : newX,
y : newY
});
};

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question