Аффинные преобразования на плоскости используются в машинной графике повсеместно. Придумал их Эйлер в 18 веке, развил Мёбиус в 19-ом, в 20-м они переместились в графический адаптер и теперь являются неотъемлемой частью нашей с вами жизни…
Аффинное преобразование — это изменение единичной матрицы с учетом матриц масштабирования, перемещения, сдвига и поворота (ниже, в разделе «Ссылки по теме» есть пункт об этих матрицах).
Не смотря на простоту метода, с его применением возникают какие-то сложности, связанные, видимо, с непониманием, почему именно такие матрицы, почему важен порядок, почему это должно работать.
Хотя, как мне кажется, больше отпугивает само понятие матрицы. Давайте рассмотрим, почему матрицы имеют такие коэффициенты и как это все работает.
Ссылки по теме
- Как повернуть изображение. GDI, GDI+, Direct2D, JavaScript
- Вращение прямоугольника вокруг произвольной точки
- Границы повернутого прямоугольника
- Матрицы аффинных преобразований на плоскости
Поворот
Предположим, стоит задача повернуть прямоугольник на некоторый угол относительно его центра. Очевидно, надо рассчитать 4 угловые точки и построить по ним полигон.
Имеем некий прямоугольник с вершинами в точках P1, P2, P3, P4. Рассмотрим точку P1(x,y). Она отстоит от оси абсцисс на угол α. Повернем ее на угол β. Очевидно, что вращение происходит по окружности с центром, находящимся в центре заданного прямоугольника O(x,y).
Рассчитаем координаты новой точки P1′( x′, y′).
Где R – радиус окружности на которой расположена точка P1, и равен (O, P1). Воспользуемся формулами сложения углов (1.1 и 2.1) из справочника:
или
Замечаем, что R × cos(α) это не что иное, как координата X точки P1, а R × sin(α) – координата Y. Таким образом, формулы расчета координат новой точки P1′ приобретают вид:
Чтобы выйти на правильную позицию на холсте, прибавим к полученным значениям координаты центра прямоугольника:
И мы только что получили матрицу поворота аффинного преобразования.
В общем виде формулы можно записать как:
В матричном виде:
Где: M11, M12, M21, M22, Dx, Dy – коэффициенты, определяющие преобразование.
Теперь представим себе, что прямоугольник – это на самом деле бесконечная плоскость. И к каждой точке этой плоскости применены одна и те же формула трансформации. Вот это и будет называться аффинным преобразованием на плоскости.
Таким образом, матрицей поворота будет следующая:
Немного кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
type TxPoint = packed record X : single; Y : single; end; //******************************************************************* // Посчитать координаты точки повернутой на Angle радиан //******************************************************************* function CalcAnglePoint(const ACenter, APoint: TxPoint; const Angle: Single): TxPoint; var sn,cs: single; begin SinCos(Angle, sn, cs); Result.X := (APoint.X-ACenter.X) * cs - (APoint.Y-ACenter.Y) * sn + ACenter.X; Result.Y := (APoint.X-ACenter.X) * sn + (APoint.Y-ACenter.Y) * cs + ACenter.Y; end; |
Сдвиг
Еще одно интересное преобразование. Состоит из вертикального сдвига, когда меняется только координата Y, и горизонтального, когда меняется только X.
Для определения величин сдвигов будем использовать углы отклонения от горизонтали или вертикали, т.к. это наиболее часто встречающаяся ситуация.
Снова рассматриваем прямоугольник, помня, что это на самом деле плоскость.
Для наглядности деформация будет происходить относительно левой нижней точки P3.
Вначале деформируем следующим образом:
При вертикальном сдвиге координаты X не меняются. Изменяются координаты Y. В данном случае координата Y точки P1 изменилась на расстояние (P2,P2′).
Из чего получим формулу преобразования для Y:
Теперь добавим горизонтальный сдвиг. Координата X точки P1 изменилась на расстояние (P4,P4′), которое рассчитывается аналогичным образом.
В итоге, формулы для трансформации сдвига выглядят следующим образом:
Или матрицей:
В позиции M12 находится коэффициент, на который нужно умножить расстояние X, чтобы получить величину вертикального смещения по Y. Аналогично, в позиции М21 находится коэффициент, на который умножается расстояние Y для определения горизонтального сдвига. Это на тот случай, если не нужны углы и заранее знаем, на какие величины хотим деформировать.
Немного кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//******************************************************************* // Посчитать координаты точки при сдвиге с углами A и B //******************************************************************* function CalcShearPoint(const ACenter, APoint: TxPoint; const A,B: single): TxPoint; var tA, tB: single; begin tA := Tan(A); tB := Tan(B); Result.X := (APoint.X - ACenter.X) + (APoint.Y - ACenter.Y)*tB + ACenter.X; Result.Y := (APoint.X - ACenter.X)*tA + (APoint.Y - ACenter.Y) + ACenter.Y; end; |
Перемещение и масштабирование
Это очень простые матрицы и в свете всего вышесказанного, думаю, понятные и останавливаться на них особого смысла нет. Поэтому, для полноты картины, просто приведу.
Пермещение | Масштабирование |
---|---|
Композиция
Давайте сейчас воспринимать композицию, как перемножение матриц в порядке слева направо. На самом деле порядок может быть и обратным — когда матрицы домножаются в начало матричного выражения. Об особенностях порядка перемножения рассказано тут.
Предположим, у нас есть следующее преобразование:
На которое следом идет такое преобразование:
Раскроем скобки и преобразуем:
или
где
Это ничто иное, как перемножение матриц коэффициентов:
Соответственно, расчет новых координат выглядит как:
Понятно, что можно умножить получившуюся матрицу на матрицу третьего преобразования, четвертого и т.д. Таким образом, чтобы получить коэффициенты сложного аффинного преобразования, нужно перемножить матрицы коэффициентов в порядке применения трансформаций.
Утверждение, прямо скажем, смелое, но правильное.
Проверим. Допустим, я хочу вначале применить деформацию, а потом повернуть на угол.
На рисунке 7 пока только деформация. Теперь к деформированному прямоугольнику (плоскости) применяю поворот на 80 градусов.
Расчет координат в точности воспроизводит приведенные выше выкладки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
type T4Point = array [0..3] of TxPoint; T4Matrix = array [0..3] of Single; //******************************************************************* // Комбинированный расчет деформации и поворота //******************************************************************* function CalcComboPoints (const APoints: T4Point; const ACenter: TxPoint; const Angle, A, B: single; var M: T4Matrix): T4Point; var A0,B0,C0,D0: Single; // матрица деформации М0 A1,B1,C1,D1: Single; // матрица поворота М1 A2,B2,C2,D2: Single; // матрица M0 * М1 sn, cs: Single; // синус косинус Angle tA, tB: Single; // тангенсы углов A, B i: Integer; begin SinCos(Angle,sn,cs); tA := Tan(A); tB := Tan(B); // Деформация A0 := 1; B0 := tB; C0 := tA; D0 := 1; // Поворот A1 := cs; B1 := -sn; C1 :=sn; D1 :=cs; // Перемножение матриц M0 * M1 A2 := A0 * A1 + C0 * B1; B2 := B0 * A1 + D0 * B1; C2 := A0 * C1 + C0 * D1; D2 := B0 * C1 + D0 * D1; M[0] := A2; M[1] := B2; M[2] := C2; M[3] := D2; // Расчет for i := 0 to Length(APoints)-1 do begin Result[i].X := A2*(APoints[i].X - ACenter.X) + B2*(APoints[i].Y - ACenter.Y) + ACenter.X; Result[i].Y := C2*(APoints[i].X - ACenter.X) + D2*(APoints[i].Y - ACenter.Y) + ACenter.Y; end; end; |
Применение
Аффинные преобразования изменяют геометрию плоскости, при этом сохраняя параллельность линий и соотношение расстояний.
Безусловно, это один из основных методов обработки изображений. Аффинные преобразования используются для исправления искажений и деформаций, возникающих при не самых идеальных ракурсах камеры. Широко используется в машинном обучении и компьютерном зрении.
Лично мне чаще всего попадались проблемы, которые обобщенно можно выразить двумя вопросами:
- как повернуть изображение?
- как нарисовать эллипс под углом?
Эти два вопроса объединяет одно. Изображение – набор точек, эллипс — геометрическое место точек. Решая вопрос в лоб, надо к каждой точке применить функции поворота. И если для изображения – ну, может быть, то к эллипсу, как фигуре векторной графики, это выглядит топорней некуда.
Аффинное преобразование – это преобразование плоскости. Применяя к плоскости некую трансформацию, мы понимаем, что эта трансформация применяется к каждой точке плоскости. Чтобы плоскость не содержала, это вытянется, сожмется, деформируется, повернется вместе с плоскостью.
Центр трансформации
Все что мы делали ранее, происходило относительно либо центра прямоугольника, либо одной из его вершин. Это центр трансформации. По сути – это начало координат той координатной системы, в которой происходит трансформация. Поэтому, перед всеми преобразованиями необходимо установить центр системы координат.
В Windows этим занимается функция:
1 2 |
function SetViewportOrgEx(DC: HDC; X, Y: Integer; Point: PPoint): BOOL; |
С этой функцией обычно идет еще набор функций, типа изменения окна вывода. Сейчас пока это все не нужно. После применения этой функции начало координат из верхнего левого угла переместиться в указанную точку. Все геометрические построения должны это учитывать. Поэтому лучше изначально писать код так, чтобы все рисовки были в относительных координатах.
Аффинное преобразование Windows
В основе работы с аффинным преобразованием лежит изменение матрицы с учетом масштабирования, перемещения, деформации или поворота. В Windows GDI за матрицу отвечает класс TXForm.
1 2 3 4 5 6 7 8 9 10 |
tagXFORM = record eM11: Single; eM12: Single; eM21: Single; eM22: Single; eDx: Single; eDy: Single; end; TXForm = tagXFORM; |
Знакомые обозначения. Поля имеют смысл ровно тот, который описан и в справочнике, и выше в статье. Это коэффициенты матрицы преобразования.
Необходимо проинициализировать поля записи и применить преобразование функцией Windows GDI:
1 |
function SetWorldTransform(DC: HDC; const p2: TXForm): BOOL; |
Однако, работать с мировыми координатами и аффинными преобразованиями в Windows GDI — удовольствие так себе. Поэтому рекомендуется это делать в GDI+.
В GDI+ есть масса вариантов, сильно облегчающих жизнь при трансформации плоскости. Но сейчас в плане статьи стоит задача работать именно через матрицы преобразования, поэтому работаем следующим образом.
Необходимо создать экземпляр класса TGPMatrix. Затем проинициализировать его методом:
1 |
function TGPMatrix.SetElements(m11, m12, m21, m22, dx, dy: Single): TStatus; |
Снова знакомые имена аргументов. Аргументы имеют тот же смысл, что и поля записи TXForm.
После инициализации применяем трансформацию методом TGPGraphics:
1 |
function TGPGraphics.SetTransform(matrix: TGPMatrix): TStatus; |
После этих манипуляций рисуем все, что хотим нарисовать, не заботясь о расчетах, углах и синусах.
Представим, что один человек рисует портрет, а второй стоит сбоку и смотрит на холст. Для него портрет выглядит деформированным, а для художника, который смотрит прямо, портрет вполне себе правильный, не трансформированный и пока нравится. Что-то похожее происходит при аффинных преобразованиях. Правда, в приведенном примере трансформация перспективная. Но сути дела это не меняет.
Рекомендован следующий порядок при работе с аффинными преобразованиями:
- Назначение нового центра координат (спорное утверждение)
- Инициализация матрицы преобразования
- Применение матрицы
- Рисуем эллипс
Композиция
Как выше говорилось, при сложном преобразовании, состоящим из нескольких трансформаций, необходимо перемножить матрицы в порядке применения преобразований. За это отвечает функция Windows GDI:
1 |
function CombineTransform(var p1: TXForm; const p2, p3: TXForm): BOOL; |
Здесь p2 и p3 — первая и вторая структуры TXForm, результат их комбинирования будет находиться в параметре p1. Т.е. надо понимать, что p3 – это преобразование, накладываемое на p2, т.е. идущее следом за ним. Порядок применения матриц очень важен.
Практика
Весь исходный код можно скачать по ссылке ниже. Чтобы не загромождать буду фрагментировать. Если приводить весь код в статье, до конца можно никогда не добраться.
Перед вызовом этой функции точка начала координат уже установлена в центр прямоугольника.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
//**************************************************************** // Рисование с использованием аффинных преобразований //**************************************************************** procedure TFmRRMain.DrawAffine(const ACanvas: TxIPCanvas; const ARect: TxRect; const ACenter: TxPoint); var val: TxRect; // отображаемый прямоугольник pnt: TxPoint; // центр вселенной mode: Integer; // текущий режим отображения Mx: TGPMatrix; // матрица GDI+ (аналог TXForm) RotaMx: TXForm; // матрица поворота SkewMx: TXForm; // матрица сдвига sn,cs: Single; // синус и косинус угла поворота tA,tB: Single; // тангенсы углов сдвига begin mode := GetCurrMode; if mode < 9 then exit; SinCos(-FAngle, sn, cs); tA := tan(FAngleA); tB := tan(FAngleB); Mx := TGPMatrix.Create; try with ACanvas do begin // прямоугольник фигуры val := ARect; // установили центр трансформации pnt := xPoint(0,0); // рисуем координатные оси DrawCoordSystem(ACanvas, val, pnt); // рисуем исходный прямоугольник DrawRectPoints(ACanvas, val, True, False, clBtnShadow); // матрица поворота RotaMx.eM11 := cs; RotaMx.eM12 := sn; RotaMx.eM21 := -sn; RotaMx.eM22 := cs; RotaMx.eDx := 0; RotaMx.eDy := 0; // матрица сдвига SkewMx.eM11 := 1; SkewMx.eM12 := tA; SkewMx.eM21 := tB; SkewMx.eM22 := 1; SkewMx.eDx := 0; SkewMx.eDy := 0; // композиция матриц if mode in [11] then CombineTransform(RotaMx, SkewMx, RotaMx); // если поворот, либо сдвиг + поворот if mode in [9,11] then MX.SetElements(RotaMx.eM11, RotaMx.eM12, RotaMx.eM21, RotaMx.eM22, RotaMx.eDx, RotaMx.eDy); // если сдвиг if mode in [10] then MX.SetElements(SkewMx.eM11, SkewMx.eM12, SkewMx.eM21, SkewMx.eM22, SkewMx.eDx, SkewMx.eDy); // Установить преобразование GPGraphics.SetTransform(MX); // Если есть галка - рисуем кота if chbAffineImage.Checked then StretchDraw(GPRectF(val), FImage); // Если есть галка - рисуем эллипс if chbAffineEllipse.Checked then DrawEllipse(ACanvas, val); // рисуем трансформированный прямоугольник DrawRectPoints(ACanvas, val, True, True, clIP76Color); // сбросили все трансформации GPGraphics.ResetTransform; end; finally FreeAndNil(MX); end; end; |
Аффинный поворот
Итак, у нас есть демонстрационной приложение, написанное для статьи. Внизу есть ряд кнопок 1..15. Это разные режимы демонстрации. Режимы 9..11 отвечают за демонстрацию аффинных преобразований с предварительным назначением нового центра координат . В режиме 9 видим такую картину:
Ну, это мы уже видели. Однако, если повернуть прямоугольник, за вершину P1 или в поле «Rotate Angle» увидим интересное изменение:
Помимо поворота непосредственно прямоугольника, повернулись и подписи вершин.
Преобразование применятся к плоскости, к каждой точке плоскости. Грубо говоря, преобразование применяется ко всему, что на плоскости.
Поэтому, можно предположить, что такой же фокус пройдет и с эллипсом:
То же самое касается и изображения:
В чем, собственно, прелесть аффинных преобразований и состоит. Не надо считать каждую точку, писать километры кода. Мы просто рисуем, как будто и нет никаких преобразований, все также прямоугольно, параллельно и перпендикулярно. Все расчеты берет на себя плоскость, к которой применили преобразование.
Фрагмент из вышеприведенного кода:
1 2 3 4 5 6 7 8 |
// Если есть галка - рисуем кота if chbAffineImage.Checked then StretchDraw(GPRectF(val),FImage); // Если есть галка - рисуем эллипс if chbAffineEllipse.Checked then DrawEllipse(ACanvas, val); |
В функции DrawEllipse нет ничего особенного, вынесена, чтобы не загромождать код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//****************************************************************** // Просто нарисовать "толстый" синий эллипс //****************************************************************** procedure DrawEllipse(const ACanvas: TxIPCanvas; const ARect: TxRect); begin with Acanvas do begin Pen.Color := Darker(clIP76Color,30); Pen.Width := 3; Brush.Style := bsClear; Ellipse(GPRectF(ARect)); Pen.Width := 1; end; end; |
Т.е. не взирая на все требуемые повороты, деформации и прочие издевательства, спокойно рисуем все в том же прямоугольнике val, все теми же обычными методами.
Аффинный сдвиг
Аналогичные чудеса в режиме деформации сдвига (кнопка 10):
Перед рисованием инициализируем матрицы TXForm и в зависимости, от того что требуется, применяем ту или иную матрицу. Фрагмент из кода выше:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// матрица поворота RotaMx.eM11 := cs; RotaMx.eM12 := sn; RotaMx.eM21 := -sn; RotaMx.eM22 := cs; RotaMx.eDx := 0; RotaMx.eDy := 0; // матрица сдвига SkewMx.eM11 := 1; SkewMx.eM12 := tA; SkewMx.eM21 := tB; SkewMx.eM22 := 1; SkewMx.eDx := 0; SkewMx.eDy := 0; // композиция матриц if mode in [11] then CombineTransform(RotaMx, SkewMx, RotaMx); // если поворот, либо сдвиг + поворот if mode in [9,11] then MX.SetElements(RotaMx.eM11, RotaMx.eM12, RotaMx.eM21, RotaMx.eM22, RotaMx.eDx, RotaMx.eDy); // если сдвиг if mode in [10] then MX.SetElements(SkewMx.eM11, SkewMx.eM12, SkewMx.eM21, SkewMx.eM22, SkewMx.eDx, SkewMx.eDy); // Установить преобразование GPGraphics.SetTransform(MX); |
Аффинная композиция
В коде выше присутствует композиция матриц — CombineTransform. Последовательность преобразований такая — вначале деформация, потом поворот. Проинспектируем кнопкой 11.
Сравним с деформацией на рис.8. Там мы делали композицию матриц руками (в интерфейсе – режим 8), теперь этим занимается Windows GDI.
Перед вызовом функции DrawAffine, необходимо перевести центр системы координат в центр прямоугольника. Напомню, начало системы координат – это центр трансформации. Именно вокруг него вертится вселенная.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
try // находим прямоугольник пропорциональный картинке val := xRect(0,0,FImage.Width,FImage.Height); val := GetProportRect(val, rct.Width * (FRect.Right-FRect.Left), rct.Height * (FRect.Bottom-FRect.Top)); // смещаем его, согласно настройкам FRect xOffsetRect(val, rct.Width*FRect.Left,rct.Height*FRect.Top); // находим центр прямоугольника - центр трансформации pnt := CenterRect(val); // работаем с аффинным преобразованием // устанавливаем центр прямоугольника как начало координат if mode in [9,10,11] then SetViewportOrgEx(bmp.Canvas.Handle, Trunc(pnt.X), Trunc(pnt.Y), nil); with cnv do begin ... rct := ClipRect; // Если центр посредине, надо сместить пространство if mode in [9,10,11] then OffsetRect(rct, -Trunc(pnt.X), -Trunc(pnt.y)); ... // Инициализация точек для мыши FZeroPoint := CalcAnglePoint (pnt, xPoint(val.Right, val.Top), -FAngle); FPointA := CalcShearPoint(pnt, xPoint(val.Right, val.Bottom),FAngleA,FAngleB); FPointB := CalcShearPoint(pnt, xPoint(val.Left, val.Top), FAngleA, FAngleB); FCenterPoint := pnt; if mode in [10,13] then // Режим, когда прямоугольник по центру и таскаем за p2 и p4 FPointС := CalcShearPoint(pnt, xPoint(val.Left, val.Bottom),FAngleA, FAngleB) else // Во всех остальных случаях, опорная точка - центр FPointС := FCenterPoint; if mode in [9,10,11] then // при аффинных преобразованиях со смещенным центром // сдвигаем прямоугольник вывода xOffsetRect(val, -Trunc(pnt.X), -Trunc(pnt.y)); FCurrRect := val; ... if mode in [9,10,11] then DrawAffine(cnv, FCurrRect, FCenterPoint) ... end; finally pbx.Canvas.Draw(0,0,bmp); FreeAndNil(cnv); FreeAndNil(bmp); UpdateMenu; end; |
Это фрагмент обработчика OnPaint того PaintBox, на котором рисуем . Вначале идет вызов SetViewportOrgEx, что переводит начало координат в центр прямоугольника. Потом происходит коррекция прямоугольника области рисования OffsetRect(rct, -Trunc(pnt.X), -Trunc(pnt.y)), иначе все координаты «уплывут» вправо на величину (pnt.x, pnt.y), затем вызов DrawAffine.
Если б мышь знала… Координаты вершин
В приложении есть возможность таскать за некоторые вершины. Тут возникает проблема. Если применяем аффинное преобразование, координаты точек, за которые можно «ухватиться» сильно отличаются от координат в «нормальном» состоянии координатной системы. Поэтому, перед тем как плоскость начнет изменяться, необходимо рассчитать нужные точки, т.е. координаты, понятные мышке.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Инициализация точек для мыши FZeroPoint := CalcAnglePoint (pnt, xPoint(val.Right, val.Top), -FAngle); FPointA := CalcShearPoint(pnt, xPoint(val.Right, val.Bottom),FAngleA,FAngleB); FPointB := CalcShearPoint(pnt, xPoint(val.Left, val.Top), FAngleA, FAngleB); FCenterPoint := pnt; if mode in [10,13] then // Режим, когда прямоугольник по центру и таскаем за p2 и p4 FPointС := CalcShearPoint(pnt, xPoint(val.Left, val.Bottom),FAngleA, FAngleB) else // Во всех остальных случаях, опорная точка - центр FPointС := FCenterPoint; |
И только потом
1 |
xOffsetRect(val, -Trunc(pnt.X), -Trunc(pnt.y)); |
И только после этого
1 |
DrawAffine(cnv, FCurrRect, FCenterPoint) |
Функции расчета достались в наследство от предыдущих этапов, когда экспериментировали над прямоугольником без применения аффинных преобразования. Не зря ж в конечном счете экспериментировали.
Конечно, можно сформировать и скомбинировать матрицы, взять коэффициенты и посчитать по формуле. Почти как в CalcComboPoints.
В GDI+ в TGPMatrix для этих целей существуют методы:
1 2 3 4 5 6 |
function TGPMatrix.TransformPoints(pts: PGPPointF; count: Integer = 1): TStatus; function TGPMatrix.TransformPoints(pts: PGPPoint; count: Integer = 1): TStatus; |
Здесь они не используются. Потом как-нибудь обязательно вернемся к ним.
Смещать ли начало координат?
Теоретически рекомендуется делать именно так, смещать и потом трансформировать. И, возможно, это правильно. Но можно обойтись и без этого. Просто привычней работать в «обычной» системе координат, когда точка (0,0) системы находится в левом верхнем углу.
По сути, как это делается, мы уже рассмотрели в самом начале статьи, когда вращали прямоугольник, считали координаты вершин и смещали в нужное место. Давайте это сделаем с помощью аффинных преобразований.
Порядок действий таков:
- Определяемся с прямоугольником, в котором что-то хотим нарисовать
- Определяемся с центром трансформации (pnt), т.е. точкой, вокруг которой будет происходить трансформация
- Смещаем прямоугольник на -pnt.x, -pnt.y, тем самым совмещая центр трансформации с началом координат
- Формируем матрицу трансформации
- Добавляем матрицу перемещения с параметрами (или просто инициализируем в текущей матрице) eDx=pnt.x, eDy=pnt.y
- Рисуем в получившемся смещенном прямоугольнике
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
//****************************************************************** // Нарисовать прямоугольник под углом, без перевода начала координат // Если указано, рисовать AImage или эллипс //****************************************************************** procedure DrawRectAffineRotate(const ACanvas: TxIPCanvas; const ARect: TxRect; const ACenter: TxPoint; const Angle: Single; const ADrawEllipse, ADrawPicture: Boolean; const AImage: TGPImage = nil); var MX: TGPMatrix; // матрица преобразования rct: TxRect; // прямоугольник вывода sn, cs: Single; // синус косинус заданного угла begin SinCos(Angle, sn, cs); Mx := TGPMatrix.Create; rct := ARect; try with ACanvas do begin // сместим прямоугольник, чтобы его центр совпал с началом xOffsetRect(rct, -ACenter.X, -ACenter.Y); // инициализация преобразования MX.SetElements(cs, sn, -sn, cs, ACenter.x, ACenter.y); // Установить преобразование GPGraphics.SetTransform(MX); // Если указано - рисуем изображение if ADrawPicture and Assigned(AImage) then StretchDraw(GPRectF(rct), AImage); // Если указано - рисуем эллипс if ADrawEllipse then DrawEllipse(ACanvas, rct); end; finally FreeAndNil(MX); end; end; |
Сильно сократил код за счет того, что не использую TXForm, а инициализирую сразу TGPMatrix. Что тут изменилось. В матрицу поворота добавил еще смещение в точку (ACenter.x, ACenter.y) – центр прямоугольника, или центр трансформации. Перед рисованием сместил прямоугольник отрисовки так, чтобы он оказался центрирован по началу координат, т.е. точки (0,0). Аффинное преобразование отработает поворот и сместиться на указанные величины по x и y.
Т.е. произойдет ровно тоже самое, что мы делали руками, когда прибавляли координаты центра к расчетным значениям вершин.
В демонстрационном примере это режимы 12..14, которые визуально будут совпадать с 9..11, с той лишь разницей, что перевода центра координат не происходит.
Чтобы продемонстрировать, работоспособность метода, существует последний режим – 15. Помимо основного прямоугольника, рисуется звезда и текст под углом, обратным текущему. Текст в дополнение к повороту также подвергается масштабированию.
Я грозился вернуться к теме поворота звезд. Говорил, что есть более правильные методы. Брутальная звезда имени Стетхама, помните? Так вот, наиболее правильным будет вращать звезды, и не только, через аффинное преобразование.
Процедура, отвечающая за рисовку всего этого хозяйства:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
procedure TFmRRMain.DrawAffineStars(const ACanvas: TxIPCanvas; const ARect: TxRect; const ACenter: TxPoint); var val: TxRect; // отображаемый прямоугольник pnt: TxPoint; // центр вселенной begin with ACanvas do begin // центр координат для прямоугольника val := ARect; pnt := ACenter; // рисуем координатные оси DrawCoordSystem(ACanvas, val, pnt); // рисуем исходный прямоугольник с точками DrawRectPoints(ACanvas, val, True, False, clBtnShadow); // Рисуем основной прямоугольник (картинка, эллипс) DrawRectAffineRotate(ACanvas, val, pnt, -FAngle, chbAffineEllipse.Checked, chbAffineImage.Checked, FImage.GPImage); // сместить прямоугольник на начало сист.координат xOffsetRect(val, -pnt.X, -pnt.Y); // рисуем описывающий прямоугольник DrawRectPoints(ACanvas, val, True, True, clIP76Color); // прямоугольник под звезду val := ARect; xOffsetRect(val, ARect.Right - 90, 0); val.Top := ClipRect.Top + 10; val.Right := ClipRect.Right; val.Bottom := ARect.Top + xHeightRect(ARect) / 2; // Рисуем звезду DrawStarAffineRotate(ACanvas, val, FAngle, FStarImage.GPImage); // прямоугольник под текст xOffsetRect(val, 0, xHeightRect(val)+10); val.Bottom := ClipRect.Bottom-10; // Рисуем текст DrawTextAffineRotate(ACanvas, val, FAngle * 1.5, abs(FAngle/(2*pi)) + 0.6); // сбросили все трансформации GPGraphics.ResetTransform; end; end; |
Как видно, не очень много, и в принципе, просто.
Процедура для рисовки текста, чуть посложнее. Добавил к повороту еще и преобразование масштаба.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
//****************************************************************** // Нарисовать текст под углом, без перевода начала координат //****************************************************************** procedure DrawTextAffineRotate(const ACanvas: TxIPCanvas; const ARect: TxRect; const Angle, Scale: Single); const ROTA_TEXT = 'Affine'+#13#10+'Transformation 2D'+#13#10+'IP76.RU'; var MX: TGPMatrix; // матрица преобразования MXS: TGPMatrix; // матрица преобразования - масштаб rct: TxRect; // прямоугольник вывода pnt: TxPoint; // центр sn, cs: Single; // синус косинус заданного угла begin SinCos(Angle, sn, cs); MX := TGPMatrix.Create; MXS := TGPMatrix.Create; rct := ARect; pnt := CenterRect(rct); try // инициализация вращения MX.SetElements(cs, sn, -sn, cs, pnt.x,pnt.y); // инициализация масштаба MXS.SetElements(Scale, 0, 0, Scale, 0, 0); // добавить вращение после масштаба MXS.Multiply(MX, MatrixOrderAppend); with ACanvas do begin // инициализация шрифта Font.Size := 18; Font.Style := [fsBold]; Font.Color := clIP76Color; IPFont.RenderingHint := trhAntiAlias; IPFont.Alignment := taCenter; // сбросили все трансформации GPGraphics.ResetTransform; // Установить преобразование GPGraphics.SetTransform(MXS); // посчитаем прямоугольник вывода для текста rct.Left := -TextWidth(ROTA_TEXT) / 2; rct.Top := -TextHeight(ROTA_TEXT) / 2; rct.Right := rct.Left + TextWidth(ROTA_TEXT) + 2; rct.Bottom := rct.Top + TextHeight(ROTA_TEXT); // инициализируем кисть Brush.Style := bsClear; // рисуем текст TextRect(GPRectF(rct), ROTA_TEXT); end; finally FreeAndNil(MX); FreeAndNil(MXS); end; end; |
Как видим, матрицы TXForm не используются. Чтобы добавить преобразование в GDI+ используется метод TGPMatrix:
1 2 |
function TGPMatrix.Multiply(matrix: TGPMatrix; order: TMatrixOrder = MatrixOrderPrepend): TStatus; |
Суть такая же, как CombineTransform для Windows GDI. В листинге вначале происходит масштаб, потом поворот.
Видео + скачать
На этом пока все.
Друзья, спасибо за внимание!
Подписывайтесь на телеграм-канал.
Оцените мой труд, жду ваших комментариев.
Скачать (1 036 Кб): Исходники (Delphi XE 7-10)
Скачать (1 843 Кб): Исполняемый файл
Небольшой комментарий к Affine2D. Если подвести курсор к верхнему правому углу поля отрисовки, появится знак меню. Нажмите его и настройте надписи, картинки, и т.д., по своему усмотрению.
Понравилась статья- ничего лишнего, все по делу. Спасибо большое за ваш труд. С нетерпением жду вашу следующую статью.