Антон Григорьев дата публикации 01-12-2003 11:32 Использование кривых Безье
Более полный вариант этой статьи вошёл в книгу "О чём не пишут в книгах по Delphi"
Программа Canvas2 предназначена для демонстрации возможностей Windows по рисованию
кривых Безье. Программа включает в себя следующие возможности:
- Рисование "резиновой" линии Безье
- Аппроксимация кривой Безье ломаной линией
- Рисование ломаной линии нестандартным стилем
Программа также может служить для иллюстрации базовых принципов реализации анимации
без мерцания.
При подготовке программы основным источником информации по кривым Безье была книга
Фень Юань "Программирование графики для Windows" - СПб.: Питер, 2002
Главное окно программы позволяет пользователю рисовать кривые Безье в интерактивном
режиме. При нажатии и удерживании левой кнопки мыши на форме за курсором начинает
тянуться прямая линия. После отпускания кнопки на линии появляются четыре красных
квадратика. Два из них обозначают начало и конец линии, два - опорные или промежуточные
точки. Пользователь может перетаскивать эти квадратики, ухватив за них мышью. Также
можно менять стиль линии с помощью группы зависимых кнопок, расположенной в левом
верхнем углу окна. Ниже находится другая группа кнопок, указывающая, как будут
интерпретироваться дополнительные точки: как опорные или как промежуточные. Опорные
точки вместе с концевыми задают касательные к кривой в её концах. В общем случае эти
точки не принадлежат кривой. Промежуточные точки принадлежат кривой. По двум концевым
и двум промежуточным или по двум опорным точкам можно однозначно построить кривую
Безье. Кнопка "Завершить" "впечатывает" текущую кривую в картинку в том виде, в каком
она в данный момент представлена на экране. После этого кривую больше нельзя изменять,
но можно нарисовать новую кривую.
Теоретические основы изображения кривых Безье |
Теорию кривых Безье разработал П. де Кастело в 1959 году и, независимо от него,
П. Безье в 1962 году. Для построения кривой Безье N-ого порядка необходимо N+1
точек, две из которых определяют концы кривой, а остальные N-1 называются опорными.
В компьютерной графике наибольшее распространение получили квадратичные кривые
Безье, строящиеся по трём точкам, и кубические кривые Безье, строящиеся по
четырём точкам. Квадратичные кривые Безье используются, например, в шрифтах
TrueType при определении контуров символов. API Windows позволяет строить только
кубические кривые Безье.
Кубические кривые Безье задаются следующей формулой:
P(t)=A*(1-t)3+3*B*t*(1-t)2+3*C*(1-t)*t2+D*t3, (1)
где A - начало кривой, D - её конец, а B и C - первая и вторая опорные точки. Прямая
AB является касательной к кривой в точке A, прямая CD - в точке D. Параметр t
изменяется от 0 до 1. При t=0 P(t)=A, при t=1 P(t)=D
Одним из важнейших свойств кривой Безье является её делимость. Если кривую разделить
на две кривых в точке t=0.5, каждая из полученных кривых также будет являться кривой
Безье. На этом свойстве основывается алгоритм рисования кривых Безье: если кривая
может быть достаточно точно аппроксимирована прямой, рисуется отрезок прямой, если
нет - она разбивается на две кривых Безье, к каждой из которых вновь применяется
этот алгоритм.
В Windows поддерживается два типа кривых: кубические кривые Безье и эллиптические
дуги. В Windows 9x/Me дуги рисуются независимо от кривых Безье. В Windows NT/2000/XP
дуги аппроксимируются кривыми Безье.
Для рисования кривых Безье используются функции PolyBezier, PolyBezierTo и PolyDraw.
В некоторых случаях удобно строить кривую Безье не по опорным точкам, а по точкам,
через которые она должна пройти. Пусть кривая начинается в точке A, при t=1/3 проходит
через точку B`, при t=2/3 - через точку C`, и заканчивается в точке D. Подставляя эти
точки в уравнение (1), получаем систему, связывающую B` и C` с B и C. Решая систему,
получаем:
B=(-5*A+18*B`-9*C`+2*D)/6 (2)
C=(2*A-9*B`+18*C`-5*D)/6
Из этих уравнений, в частности, следует, что для любых четырёх точек плоскости
существует, и притом единственная, кривая Безье, которая начинается в первой точке,
проходит при t=1/3 через вторую точку, при t=2/3 - через третью и завершается в
четвёртой точке. Аналогичным образом можно вычислить опорные точки для кривой,
которая должна проходить через заданные точки при других значениях t.
API Windows реализует поддержку специфических объектов, называемых траекториями (path).
Траектория представляет собой запись движения пера и состоит из одного или нескольких
замкнутых контуров. Каждый контур состоит из отрезков прямых и кривых Безье. Для
построения траектории в Windows NT/2000/XP могут быть использованы все графические
функции рисования прямых, кривых и замкнутых контуров, а также функции вывода текста
(в этом случае замкнутые контуры будут совпадать с контурами символов). В Windows
9x/Me могут быть использованы только функции рисования прямых, ломаных, многоугольников
(за исключением PolyDraw и Rectangle), кривых Безье и функций вывода текста. Для
создания траектории используются функции BeginPath и EndPath. Все вызовы графических
функций, расположенные между BeginPath и EndPath, вместо вывода в контекст устройства
будут создавать в нём траекторию.
После того как траектория построена, её можно отобразить или преобразовать. Мы не будем
здесь перечислять все возможные операции с траекториями, остановимся только на
преобразовании траектории в ломаную. Как уже отмечалось выше, все контуры траектории
представляют собой набор отрезков прямых и кривых Безье. С другой стороны, при построении
кривой Безье она аппроксимируется ломаной. Следовательно, вся траектория может быть
аппроксимирована набором отрезков прямой. Функция FlattenPath преобразует кривые Безье,
входящие в состав траектории, в ломаные линии. Таким образом, после вызова этой функции
траектория будет состоять из отрезков прямой.
Отметим также некоторые другие преобразования траектории, полезные для создания
графических редакторов и подобных им программ. Функция PathToRegion позволяет преобразовать
траекторию в регион. Это может понадобиться, в частности, при определении, попадает ли
курсор мыши в область объекта, представляемого сложной фигурой. Функция WidenPath
превращает каждый контур траектории в два контура - внутренний и внешний. Расстояние
между ними определяется толщиной текущего пера. Таким образом, траектория как бы
утолщается. После преобразования утолщённой траектории в регион можно определять, попадает
ли курсор мыши на кривую с учётом погрешности, определяемой толщиной пера.
Поучить информацию о точках текущей траектории можно с помощью функции GetPath. Для каждой
точки траектории эта функция возвращает координаты и тип точки (начальная линии,
замыкающая точка отрезка, точка кривой Безье, конец контура).
Таким образом, создав траекторию из кривой Безье (BeginPath/PolyBezier/EndPath), мы можем
преобразовать эту траекторию в ломаную (FlattenPath), а затем получить координаты узлов
этой ломаной (GetPath).
Получение координат точек прямой |
Для рисования прямых линий в Windows используется алгоритм GIQ (Grid Intersection
Quantization). Каждый пиксель окружается воображаемым ромбом из четырёх пикселей. Если
прямая имеет общие точки с этим ромбом, пиксель рисуется.
Для нахождения координат всех пикселей, составляющих заданную прямую, используется
функция LineDDA. Эта функция в качестве параметра принимает координаты начала и конца
линии, а также указатель на функцию, которой будут передаваться координаты пикселей.
Данная функция должна быть реализована в программе. За время выполнения LineDDA эта
функция будет вызвана столько раз, сколько пикселей содержит линия (как обычно в
Windows, последний пиксель не считается принадлежащим прямой). Каждый раз при вызове
ей будут передаваться координаты очередного пикселя, причём пиксели будут упорядочены
от начала к концу прямой. Используя эту функцию, можно получить координаты всех
пикселей прямой и нарисовать их каким-либо оригинальным способом, получая нестандартные
стили прямых.
Так как любую кривую Безье можно разбить на отрезки прямых, её также можно нарисовать
нестандартным стилем. Достаточно для каждого из этих отрезков вызвать LineDDA.
Анимация заключается в последовательной смене картинок. При этом главная проблема -
устранение мерцания изображения. В программе Canvas2 мерцание "резиновой" линии
в целом незаметно, так как эта линия постоянно изменяет своё положение, а вот мерцание
подложки, на которой нарисованы уже "впечатанные" в неё линии, было бы заметно, поэтому
пришлось принимать специальные меры по её устранению.
Чтобы не было мерцания при обновлении изображения, необходимо выполнение двух условий:
- Быстрая смена одного изображения другим
- Отсутствие между старым и новым изображением промежуточных полустёртых изображений
Максимальную скорость вывода при использовании средств GDI даёт вывод изображения на
поверхность растра (bitmap) с последующим перенесением на экран. Этим обеспечивается то,
что все элементы рисунка выводятся на экран одновременно, а не по очереди, что позволяет
избежать промежуточных изображений. Поэтому программа Canvas2 хранит растр, на котором
отображаются уже завершённые кривые, и при обработке события OnPaint рисует растр, а
сверху - редактируемую в данный момент кривую с дополнительными элементами, облегчающими
редактирование. Тем не менее, это не устраняет мерцание полностью, если для обновления
окна использовать метод TWinControl.Refresh или TWinControl.Invalidate. Это связано с
особенностями рисования окон в Windows и с тем, как VCL использует эти особенности.
Для перерисовки сначала с помощью функций InvalidateRect или InvalidateRgn отмечается
область окна, нуждающаяся в обновлении. Можно последовательно отметить несколько
областей - система будет добавлять новую область к уже существующей. Затем с помощью
функции UpdateWindow в очередь сообщений помещается WM_Paint. Рисование окна
происходит при обработке этого сообщения. Для начала рисования вызывается функция
BeginPaint. Эта функция анализирует область, нуждающуюся в обновлении, и, если при
вызове функций InvalidateRect/Rgn был установлен флаг обновления фона, посылает окну
сообщение WM_EraseBknd. В ответ на это сообщение окно закрашивает свою клиентскую
часть заданной кистью. В частности, для форм Delphi это будет сплошная кисть с
цветом, определяемым свойством Color формы. Поэтому сначала будет стёрто старое
изображение, и лишь затем будет нарисовано новое. Это приводит к появлению мерцания.
Существует три способа избавиться от мерцания:
- Не указывать флаг обновления фона при вызове InvalidateRect(Rgn)
- Перекрыть обработчик WM_EraseBkgnd и ничего не делать при получении этого сообщения
- Обновлять окно напрямую, без сообщения WM_Paint.
В Delphi самым простым является третий способ: достаточно вызвать процедуру обработки
OnPaint напрямую. Для реализации первого способа придётся вручную вызывать функцию API
InvalidateRect, потому что TWinControl.Invalidate не позволяет сбрасывать флаг
обновления фона. Второй способ не очень удобен, если анимированная картинка занимает
не всё окно. Поэтому в программе Canvas2 выбран третий способ.
Если вы честно прочитали всё, что написано выше, и не поленились посмотреть в MSDN'е
описание упомянутых в тексте функций, вы уже можете самостоятельно написать программу,
подобную Canvas2. Правда, новичков нередко ставит в тупик задача создания "резиновой"
линии, особенно кривой, которую потом можно изменять. Однако эта задача не требует ни
каких-либо специальных знаний, ни особого напряжения интеллекта. Достаточно просто
набраться терпения, просчитать все возможные состояния процесса и описать реакцию на
все эти состояния в программе.
Начиная с этого момента я предполагаю, что вы уже успели попробовать Canvas2 и поэтому
хорошо представляете процесс рисования кривых с её помощью.
Итак, у нас есть два основных состояния: когда рисование кривой ещё не начато, и нажатие
левой кнопки мыши приведёт к появлению "резиновой" прямой, которая затем станет основой
для кривой, и когда кривая уже нарисована, но ещё не "впечатана" в рисунок, т.е. её
крайние и промежуточные точки можно передвигать. Переменная TCurveForm.NewLine типа
Boolean указывает, в каком из двух состояний находится программа: в первом (True) или
во втором (False).
Когда пользователь схватил точку и тащит её, возможно шесть вариантов: схвачена одна из
четырёх контрольных точек редактируемой кривой, рисуется "резиновая" прямая или при
редактировании кривой пользователь промахнулся и не схватил ни одной из контрольных
точек. Для описания того, какая точка сейчас перемещается пользователем, объявлена
переменная TCurveForm.DragPoint специально созданного типа TDragPoint. Эта переменная
может иметь следующие значения:
- ptNone - пользователь пытается тянуть несуществующую точку
- ptFirst - пользователь перемещает вторую точку "резиновой" прямой
- ptBegin - пользователь перемещает начало кривой
- ptInter1, ptInter2 - пользователь перемещает промежуточные точки
- ptEnd - пользователь перемещает конец кривой
Для хранения координат кривой используется массив TCurveForm.Curve. Его нулевой
элемент хранит начало прямой, третий - её конец, а первый и второй - промежуточные
точки. В режиме "резиновой" прямой первый и второй элементы не используются, поэтому
они могут иметь произвольные значения.
Процедура OnPaint должна учитывать состояние программы: до редактирования кривой ещё
не дошло (NewLine=True), нужно проверить, не перемещает ли пользователь концевую точку
прямой, и если да, нарисовать эту прямую. Если редактирование кривой уже начато, надо
отобразить кривую и дополнительные элементы для редактирования (касательные и маркеры
контрольных точек).
Когда пользователь нажимает кнопку мыши, программа должна проверить, в каком состоянии
находится программа. Если редактирование кривой ещё не начато, это нажатие означает начало
рисования "резиновой" прямой. Для перехода в этот режим значение DragPoint устанавливается
в dpFirst. Если редактирование кривой уже начато, необходимо проверить, попадает ли
позиция курсора мыши в окрестность какой-либо контрольной точки, и на основании результатов
проверки присвоить соответствующее значение переменной DragPoint. Для проверки определена
функция PtNearPt. Строго говоря, необходимо также запоминать, насколько отстоят координаты
курсора мыши от координат контрольной точки, чтобы при первом перемещении не было скачка.
Но так как окрестность точки очень мала, этот прыжок практически незаметен, и в данном
случае этим можно пренебречь, чтобы не усложнять программу.
При перемещении мыши нужно проверить, нажата ли левая кнопка и перемещает ли пользователь
какую-либо точку. Если да, требуется обновить координаты этой точки и перерисовать окно.
При отпускании пользователем кнопки мыши какие-либо действия требуются, только если до
этого был установлен режим "резиновой" прямой. В этом случае нужно вычислить координаты
промежуточных точек (они выбираются на прямой) и перейти в режим редактирования кривой.
Нажатие кнопки "Завершить" осуществляет выход из режима редактирования кривой. Кривая
переносится на растр TCurveForm.Back, а значение NewLine снова устанавливается в True.
Функция рисования кривой достаточно проста. Сначала рассчитываются координаты опорных
точек. Если включен режим рисования по опорным точкам, координаты этих точек хранятся
в первом и втором элементах массива Curve. Если включен режим рисования по промежуточным
точкам, координаты опорных точек вычисляются по формулам (2). Затем на основе кривой
создаётся траектория, затем она преобразуется в ломаную, и координаты её узлов записываются
в массив PtBuf. В массив TpBuf записываются типы точек, но в данном случае они нам
неинтересны: траектория содержит только один контур, состоящий только из отрезков прямых.
Далее последовательно вызывается функция LineDDA для каждого из отрезков. При этом
вычисляется длина отрезка и смещения координат DX и DY. Это нужно для построения поперечных
линий. Как показала практика, начало и конец отрезка иногда совпадают, и его длина равна
нулю, поэтому нужна дополнительная проверка, позволяющая избежать деления на ноль.
Функция LineDDA передаёт в вызываемую ею LineDrawFunc один дополнительный параметр,
который использован для передачи объекта холста (Canvas), на котором следует рисовать
отрезок. Этот параметр в соответствии с описанием функции является целым числом, но
контроль типов здесь отсутствует, поэтому можно использовать любую 32-разрядную величину.
Так как все переменные объектов в Delphi являются 32-разрядными указателями, объект
TCanvas может быть передан в качестве этого параметра. Функция LineDDA не считает точки,
поэтому это приходится делать самостоятельно с помощью переменной TCurveForm.Counter.
Так как значение этой переменной между рисованием отдельных ломаных не меняется, кривая
имеет целостный вид.
Функция LineDrawFunc достаточно проста для понимания. В некотором комментарии нуждается
только выбор толщины пера при рисовании стилем "плакатное перо". Предположим, некоторая
точка прямой имеет координаты (X,Y), а соседняя с ней - координаты (X+1,Y-1). Тогда при
проведении через эти точки наклонной линии одинарной ширины между ними останутся
незаполненные точки, как на шахматной доске. Поэтому потребовалось увеличить толщину пера.
Надеюсь, изложенный здесь материал оказался вам полезным
С пожеланиями творческих успехов
Григорев Антон
Специально для Королевства Delphi
К материалу прилагаются файлы:
[Линии Безье] [Мерцание при перерисовке] [GDI, рисование на канве] [Регионы и траектории (Paths)]
Обсуждение материала [ 23-03-2007 05:43 ] 3 сообщения |