Вывести текст в перспективе

Текст в перспективе

Нарисовать текст в перспективе особой сложности нет. Более проблемно заставить такой текст быть текстурным. Рассмотрим в статье, как нарисовать текст в перспективной проекции с использованием текстуры в качестве заливки. Также разрешим ряд часто задаваемых вопросов.

В аффинных преобразованиях нет перспективной трансформации. Это явно следует из определения таких преобразований:

Аффинное преобразование … отображение плоскости …, при котором параллельные прямые переходят в параллельные прямые, пересекающиеся — в пересекающиеся, скрещивающиеся — в скрещивающиеся

Википедия

А восприятие перспективы в первую очередь строится на схождении параллельных прямых в горизонте. Поэтому здесь будет идти речь о деформации.

Подготовка

Нам нужен GDI+ для Delphi 7. По прежнему продолжаю тему «универсального исходника», который без проблем скомпилируется и в Delphi 7, и в XE 7,10,11. В статье «Как подключить GDI+ для Delphi 7 и не иметь проблем в XE» подробно расписано, как это сделать.

Нам нужно изображение для заливки текста. Из статьи «Как вставить изображение из буфера обмена» берем код для вставки изображения и загрузки из файла.

Здесь будет применена трансформация по четырем точкам, задающих четырехугольник, по которому будет деформироваться текст. Для хранения данных используем в приватной секции формы два массива:

Чтобы деформировать что либо, надо иметь представление «этого» в наборе вершин, линий и кривых. За хранение подобного набора, что в GDI, что в GDI+ отвечает объект траектории. Траектория в GDI+ представлена классом TGPGraphicsPath.

Преобразовать текст в траекторию

Текст в траекторию можно добавить методом AddString. В обойме GDIPlus для метода целых 4 перегруженных реализаций. По сути, они различаются только в типе координат — целочисленные или вещественные, и способом задания координат — рисовать либо от точки, либо в прямоугольнике. Поэтому рассмотрим один метод:

Параметры метода AddString

string_: WideString
Текст, который требуется нарисовать. Может включать в себя переносы строк.
length: Integer
Длина текста, которые отображается. Если задать -1, будет отображен весь текст.
family: TGPFontFamily
Множество шрифтов одного семейства. Указатель на объект определяющий семейство шрифтов для строки.
style: Integer
Стиль шрифта. Результат побитового ИЛИ, примененного к двум или более из этих элементов: 
FontStyleRegular = Integer(0);
FontStyleBold = Integer(1);
FontStyleItalic = Integer(2);
FontStyleBoldItalic = Integer(3);
FontStyleUnderline = Integer(4);
FontStyleStrikeout = Integer(8);
emSize: Single
Размер строковых символов в мировых единицах.
origin: TGPPointF
Координаты начала строки, указанные мировых единицах.
format: TGPStringFormat
Указатель на объект TGPStringFormat, который определяет информацию о формате вывода (выравнивание, обрезка, позиции табуляции и т. п.) для строки.

Про TGPStringFormat можно целую статью написать, поэтому останавливаться на нем смысла нет. Тем более здесь он нам не нужен.

Как получить или создать TGPFontFamily?

TGPFontFamily — важный параметр, без которого работать с текстом не получится. Это объект, у него есть специальный конструктор. Его можно инициализировать вручную. А можно получить очень простым способом — из объекта gfont типа TGPFont. Как создать gfont смотрим тут.

Нарисовать текст

Чтобы нарисовать текст, надо нарисовать траекторию. Для рисования рамки траектории используется метод DrawPath. Для заливки траектории используется метод FillPath.

Таким образом, вывод текста будет выглядеть так:

gpath: TGPGraphicsPath
Траектория GDI+.
gpath := TGPGraphicsPath.Create;
gpen: TGPPen
Перо GDI+. Конструктор принимает цвет и толщину.
gpen := TGPPen.Create($FFBBBBBB, 0.5);
gbrush: TGPBrush
Кисть GDI+. В нашем случае используем текстурную кисть, которая определяется классом TGPTextureBrush, который является наследником класса TGPBrush.
gbrush := TGPTextureBrush.Create(gbmp);

Где gbmp — экземпляр TGPBitmap. В нем хранится текстура.
gpg: TGPGraphics
Холст GDI+.
gpg := TGPGraphics.Create(bmp.Canvas.Handle);

Где bmp — экземпляр TBitmap, который создается как буфер при отрисовке в PaintBox’е.

Шрифт GDI+

Не будем останавливаться на всех нюансах работы со шрифтом в GDI+. Здесь только то, что нужно сейчас.

В тексте выше есть такая переменная gfont: TGPFont. Создать шрифт GDI+ можно кучей разных способов. Но мы сейчас рассмотрим способ, который Д. Осипов назвал чисто теоретическим, не видя в нем практического применения. Это использование конструктора:

У конструктора есть вполне практическое применение. Например, сейчас у меня есть PaintBox, в нем есть шрифт, который я настроил на этапе дизайна. Это удобно и быстро. Рисую на обычном TBitmap. У которого есть Canvas, в котором есть свойство Font. Этот Font инициализирую шрифтом из PaintBox’а.

И теперь я хочу иметь точно такой же шрифт для GDI+. И вместо процедуры инициализации шрифта(1) имею одну строку (2).

1) Инициализация TGPFont из TFont

Более полный вариант с использованием коллекции TGPFontCollection и пример использования TGPStringFormat можно посмотреть тут.

2) Одна строка создания TGPFont из существующего TFont:

В исходниках есть оба метода создания шрифта. Можно поэкспериментировать.

Как узнать размер текста в GDI+?

Для того, чтобы узнать размер текста, нужен экземпляр TGPFont. Размер или прямоугольник текста нам нужен, чтобы получить начальные параметры четырехугольника деформации текста.

Результат работы метода помещается в переменную box: TGPRectF. Это прямоугольник, который занимает выводимый текст с заданным шрифтом. Чтобы отцентрировать его и проинициализировать массив вершин прямоугольника, дополнительно напишем следующее:

Это тот самый box, который фигурирует в вызове функции AddString выше:

В итоге, пока у нас нет кисти для заливки, получаем рамку текста.

Рис.1. Текст с рамкой

Текстурная кисть TGPTextureBrush

В GDI+ кисть текстурная представлена классом TGPTextureBrush. Выбираем самый простой конструктор:

А конструкторов у текстурной кисти целых 8 штук! Вообще, программисты GDIP — большие любители перегружать конструкторы и методы.

В любой другой ситуации пригодился бы конструктор с параметром dstRect: TGPRectF, но из специфики задачи он не нужен. Возможно, при случае расскажу. Не пропустите, подписывайтесь на телеграм-канал!

Второй параметр нас устраивает значением по умолчанию. TWrapMode имеет следующие значения:

WrapModeTile
0 — картинка повторяется (плитка)
WrapModeTileFlipX
1 — зеркалка относительно вертикальной оси
WrapModeTileFlipY
2 — зеркалка относительно горизонтальной оси
WrapModeTileFlipXY
3 — зеркалка по обеим осям
WrapModeClamp
4 — нет заливки

Как создать TGPBitmap из TBitmap

Почему TGPBitmap, а не TGPImage, как указано в параметрах конструктора текстурной кисти? Потому что: 1) TGPBitmap — наследник TGPImage, 2) битмап — это то, с чем на самом деле работает Windows, 3) и его очень просто создать из TBitmap.

Теперь можно создать кисть:

Рис.2. Текст с рамкой и заливкой

На рисунке 2 видно, что в заливке участвует только верхняя часть картинки. Сама картинка размером 800х600. Именно в таком размере она и выступает в качестве заливки. Кисть не умеет масштабировать текстуру под нужный размер выводимого примитива, потому что это вообще не ее дело. Перед тем, как создать кисть, надо вначале определенным образом поработать с текстурой или использовать другой конструктор.

Текст в перспективе

Ну вот, собственно, добрались и до сабжа. Деформацией в TGPGraphicsPath занимается метод, который так и называется — Warp. Это почти единичный случай, когда метод GDIP не перегружен и существует в единственном экземпляре:

Метод Warp, помимо деформации, преобразует кривые, из которых состоит траектория, в набор отрезков. За качество аппроксимации отвечает параметр flatness. По умолчанию, он равен 0.25. Это официальная версия.

Но лично я никаких изменений от этого параметра не обнаружил. Чтобы реально ощутить мощь параметра, необходимо использовать метод Flatten, о котором в ближайшем будущем будет спето немало песен. Если тема аппроксимации кривых интересна, подписывайтесь на телегу!

Параметры метода Warp

destPoints: PGPPointF
Массив из 4 точек (или из трех). Задает вершины четырехугольника, по которому будет происходить деформация. Если в массиве содержится три точки, четвертая рассчитывается автоматически и четырехугольник становится параллелограммом.
count: Integer
Указывает, что в массиве либо 4 точки — и это произвольный четырехугольник, либо 3 — и это параллелограмм.
srcRect: TGPRectF
Исходный прямоугольник, из которого будет происходить перенос в четырехугольник деформации. В нашем случае, это box, про который говорилось выше.
matrix: TGPMatrix = nil
Можно также наложить аффинное преобразование поверх деформации. Но этого сейчас мы делать не станем.
warpMode: TWarpMode = WarpModePerspective
Режим деформации. Имеет два значения: WarpModePerspective и WarpModeBilinear. Используем значение по умолчанию WarpModePerspective. WarpModeBilinear задает билинейную деформацию. Реализация которой в GDI+ сделана настолько коряво, что надо сделать ряд телодвижений и написать статью, чтобы ею воспользоваться.
flatness: Single = FlatnessDefault
Параметр, определяющий качество аппроксимации. Чем он меньше, тем отрезки, из которых состоит получившаяся траектория, короче, их количество больше, качество лучше. Возрастает нагрузка на печень ресурсы машины. Как говорилось выше, особой реакции не наблюдается, что 0.1, что 5 — картина одна и та же.

Порядок вершин в параметре destPoints

Для параметра destPoints важен индекс. В нулевом индексе содержится координата верхней левой вершины четырехугольника, в первом — верхней правой, во 2-м — левая нижняя вершина и в 3-м — правая нижняя.

Рис.3. Порядок вершин в массиве destPoints

Нарисовать текст в перспективе

Сейчас будет представлен фрагмент кода. Этот код промежуточный, призван показать все вышесказанное в связке. Окончательный вариант можно посмотреть в конце статьи или в исходниках.

Урезанный вариант обработчика OnPaint

[свернуть]

Добавлена возможность присоединить к траектории еще прямоугольник. В этом случае символы текста будут как-бы вырезаны в фоне. В таком режиме лучше видно, что происходит с картинкой при деформации.

Обратите внимание на заполнение массива вершин FPoints — оно происходит согласно этому правилу.

Рис.4. Перспективная деформация на кисть не действует никак

На рисунке 4 видим, что вывод текста в перспективе происходит замечательно, а вот текстура остается на месте и никакие деформации на нее не действуют.

Текстура в перспективе

Очевидно, что необходимо подвергнуть текстуру такой же деформации, что и текст в траектории. Для этого надо вспомнить хорошо забытое старое — Perspective Transformation. У нас есть готовая функция для таких дел. Подключим модуль IP76ProjectiveTransform и заменим создание битмапа tmp на следующий код:

Обращаю внимание на порядок вершин в параметрах функции. Он отличается от нумерации в массиве destPoints.

Рис.5. Теперь и картинка, и текст в перспективе работают синхронно

Поэтому нас и не интересовали другие конструкторы для текстурной кисти. Мы формируем картинку, как раз под текущий вывод текста в перспективе. С нужными размерами, с нужным искажением.

Откуда берутся точки для деформации?

Из работы с мышью )))) Есть в интерфейсе кнопка — «Сброс деформации», ее обработчик таков:

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

Мышиная работа

[свернуть]

А как из TGPBitmap получить TBitmap?

Вопрос связан с тем, что пламя, которое (конечно) присутствует в исходнике, работает с TGPBitmap. А функция перспективной трансформации работает с TBitmap. Следовательно, надо как-то получить TBitmap из TGPBitmap.

Проще всего это сделать с помощью метода TGPBitmap.GetHBITMAP:

Полный текст отрисовки

Он не такой большой, как кажется. Уберите все комментарии, и ужмется раза в два.

Практическое применение

Можно использовать, когда требуется растянуть или сжать текст в границах заданного прямоугольника. Речь не идет о нахождении оптимальных параметров шрифта. Это именно о том, чтоб «влезло». Аналогичного эффекта можно добиться, если использовать аффинное преобразование масштабирования.

Можно применять для замены аффинного сдвига, либо поворота. Как известно, с помощью сдвига можно осуществить и поворот. Только смысла в этом особого не вижу. Все равно придется рассчитывать координаты по формулам аффинных преобразований, отчего бы этим не заняться специализированному движку?

Конечно, это имитация 3D. Если взять аффинные преобразования для 3D, рассчитать координаты и вывести с помощью перспективной деформации, можно добиться эффекта пространства. Например, при создании проволочной модели будущей сцены для 3D-рендеринга. Или математических изысканий.

Безусловно, самое интересное применение — это расположить текст по кругу, эллипсу и произвольной траектории. Уже год планирую про это написать. Напишу. Надо сесть, оформить исходник, взять музу в заложники. Ну и как мантру повторю — подписывайтесь на канал, оставляйте комментарии, регистрируйтесь на сайте, это мотивирует продолжать мое безнадежное дело.

Ну и чисто декоративное применение, конечно. Для заставок, инфографики и прочей попсы. Все картинки кликабельны.


Скачать

Друзья, спасибо за внимание!

Исходник (zip) 347 Кб. Delphi 7, XE 7, XE 10, XE 11

Для XE открываем файл .dpr и спокойно build’им. Путь ..\GDIPlus\ из Search Path можно убрать, а можно и не убирать, модули из него никак не задействованы в XE из-за директивного условия.

Пустой подкаталог _dcu в архиве — для Delphi 7. Он указан в настройках проекта, как Unit output directory. Если его не окажется, XE просто молча создаст, а Delphi 7 выругается. Поэтому присутствует, чтобы никто не ругался )

Исполняемый файл (zip) 447 Кб.

Исполняемый файл, скомпилированный в XE 11 (zip) 1.22 Мб.

Для XE 11 исполняемый файл скомпилирован в release на том же самом исходнике. Но если в распакованном виде для Delphi 7, exe весит 717 Кб, то для XE 11 уже 2.82 Мб. Прогресс — он ведь в размерах измеряется.


5 6 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x
()
x