DmitryITWorksMakarov
@DmitryITWorksMakarov

Как реализовать масштабирование относительно произвольной точки?

Реализую специализированный 2D-редактор в .NET. Хочу реализовать масштабирование относительно курсора.
В GDI+ у класса Graphics есть замечательное свойство Transform, которое позволяет с помощью аппарата аффинных преобразований в матричной форме реализовать то, что мне нужно. Во всех книжках пишут, что для масштабирования относительно произвольной точки необходимо:
  1. выполнить преобразование смещения так, чтобы точка относительно которой масштабируем сместилась в начало координат
  2. выполнить непосредственно масштабирование относительно начала координат
  3. выполнить преобразование смещение так, чтобы начало координат сместилось в исходную точку.


То есть в рамках моего приложения достаточно правильным образом менять матрицу Transform при вращении колесика мыши.

Делаю так:
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();
        }
    }

но это не работает так как надо. После нескольких масштабирований, рисунок под мышкой начинает убегать и уже нет нужного масштабирования относительно курсора.

По любому это из-за неправильного расчета преобразований в начало координат и обратно.
Как вычислить их, чтобы работало правильно?
  • Вопрос задан
  • 2832 просмотра
Решения вопроса 1
DmitryITWorksMakarov
@DmitryITWorksMakarov Автор вопроса
Сам нашел решение.

Расписал аккуратно матрицы, вручную перемножил, получил готовую матрицу масштабирования относительно курсора, подставил в программу и все работает.

Стал смотреть в чем дело у меня в программе.
Оказалось: я неправильно понимал порядок перемножения матриц при применении метода Multiply(Matrix). К счастью у Multiply есть перегруженная версия с произвольным выбором порядка операндов.

Должно быть так:

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();
        }
    }


Возможно, тот мой вариант с вручную подготовленной матрицей несколько эффективнее в вычислительном плане, но представленный вариант - читабельнее.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
@MrCheater
Full-Stack JS. В прошлом программист-олимпиадник
у меня на js была такая же задача - вот код решения (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;
if(this.state.width < minWidth) {
let scale = minWidth / this.state.width;
this.state.width *= scale;
this.state.height *= scale;
deltaX *= scale;
deltaY *= scale;
}
if(this.state.height < minHeight) {
let scale = minHeight / this.state.height;
this.state.width *= scale;
this.state.height *= scale;
deltaX *= scale;
deltaY *= scale;
}
let newX = this.state.x - deltaX + originalDeltaX;
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
});
};
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы