Журнал о программированнии на языках Blitz3D, BlitzPlus, BlitzMax

Редактор двумерных кривых

Материал из Blitz Et Cetera

Перейти к: навигация, поиск

Параметры кривой и ее построение

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

Напомню, для того, чтобы построить траекторию, были созданы две интерполяционные функции - x = f1(t) и y = f2(t), 0<=t<=1. Если Вы редактировали кривые в векторных редакторах, например в CorelDraw или Macromedia Flash, то могли заметить, что от каждой вершины отходят два отрезка с точками на концах. Если присмотреться, то можно увидеть, что эти отрезки - касательные к частям кривой, подходящим к этой точке. Когда отрезки параллельны - кривая плавно закругляется в вершине, в противном случае получается излом. Будем называть эти отрезки векторами (так как можно задать направление - от вершины).

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

Теперь можно создать процедуру рисования кривой. Как вычислить коэффициенты, было рассказано в предыдущем выпуске, а построить кривую легко можно в цикле отрезками по точкам. Разбиение кривой на 100 отрезков (шаг 1/100) быстро строит вполне качественную кривую. Но какие производные подставлять в формулы? Можно просто подставить приращения отрезков, умноженные на константу (скажем, 20), но удобнее для редактирования будет, если эта константа будет зависеть от расстояния между вершинами кривой. Теперь можно поэкспериментировать с векторами:

Type dot

 Field x,y,dx1,dy1,dx2,dy2
End Type

Const xres=800, yres=600, stp#=.01

Global seldot.dot, bi, sel
Dim a#(1),b#(1),c#(1),d#(1),oc(1)

Graphics xres,yres,32

;Задание начальных точек
dt.dot=New dot
dt\x=.5*xres
dt\y=.25*yres
dt\dx2=50
dt\dy2=50
dt.dot=New dot
dt\x=.5*xres
dt\y=.75*yres
dt\dx1=40
dt\dy1=-60


drawcurve First dot
WaitKey

Function drawcurve(dt1.dot)
dt2.dot=After dt1

;Вычисление расстояния между вершинами кривой для вычисления производных
r#=.05*Sqr((dt1\x-dt2\x)*(dt1\x-dt2\x)+(dt1\y-dt2\y)*(dt1\y-dt2\y))

;Вычисление коэффициентов кривой
For nn=0 To 1
 If nn Then
  v1=dt1\y
  v2=dt2\y
  c#(nn)=dt1\dy2*r#
  dy2#=dt2\dy1*r#
 Else
  v1=dt1\x
  v2=dt2\x
  c#(nn)=dt1\dx2*r#
  dy2#=dt2\dx1*r#
 End If
 d#(nn)=v1
 b#(nn)=3.0*v2-dy2#-2.0*c#(nn)-3.0*d#(nn)
 a#(nn)=(dy2#-2*b#(nn)-c#(nn))/3.0
Next

;Построение кривой
For t#=0 To 1 Step stp
 tt#=t#*t#
 For nn=0 To 1
  oc(nn)=a#(nn)*tt#*t#+b#(nn)*tt#+c#(nn)*t#+d#(nn)
 Next
 If t#>0 Then Line oc(0),oc(1),x,y
 x=oc(0)
 y=oc(1)
Next

;Отображение направляющих отрезков, относящихся к кривой, и ее вершин
Color 0,255,255
If dt1\dx2<>0 Or dt1\dy2<>0 Then
 Line dt1\x,dt1\y,dt1\x+dt1\dx2,dt1\y+dt1\dy2
 Oval dt1\x+dt1\dx2-1,dt1\y+dt1\dy2-1,3,3
End If
If dt2\dx1<>0 Or dt2\dy1<>0 Then
 Line dt2\x,dt2\y,dt2\x-dt2\dx1,dt2\y-dt2\dy1
 Oval dt2\x-dt2\dx1-1,dt2\y-dt2\dy1-1,3,3
End If
Color 255,255,255

End Function

Интерфейс

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

Для вершин существуют несколько методов проверки на касание курсором: проверка на совпадение координат (недостаток - сложно выделить объект, нужна большая точность), квадратная зона вокруг вершины (путем отсечения полуплоскостей) и круглая зона (вычисление расстояния между курсором и вершиной). Воспользуемся вторым методом.

При выделении кривой можно использовать подобные методы, проверяя промежуточные точки, но очевидным недостатком будут разрывы в области поиска при "разрежении" массива точек. Поэтому, будет лучше использовать другой метод - разбиение кривой на отрезки и проверка на пересечение ее зоны коллизии и курсора. Это довольно интересная задача: сначала нужно определить, находится ли курсор в прямоугольнике, который ограничивает отрезок (берем минимальные и максимальные значения x и y вершин отрезка и добавляем бордюр в 3 пиксела). Затем определяется расстояние до линии, которой принадлежит отрезок и если оно меньше или равно 3, значит можно выделять линию. Найти коэффициенты уравнения линии, проходящей через 2 точки отрезка можно следующим образом:

Теперь можно найти расстояние от точки до этой линии:

Переменная sel определяет текущий выбранный объект: 0 - ничего, 1 - вершина, 2 - кривая. Когда курсор проходит над объектом - объект выделяется, если курсор уходит от объекта - выделение снимается. Еще немного кода и можно перемещать вершины и делить кривую новой вершиной (ПКМ). Но для того, чтобы редактировать вектора нужно, чтобы определенная вершина была выделена независимо от положения курсора. Для этого введем новое значение - sel=3 (вершина выделяется ЛКМ). При этом, клавишами "1" и "2" можно изменять первый или второй, а клавишей "3" - оба вектора сразу. "0" обнуляет вектора, "Del" удаляет вершину. Добавим возможность загрузки и записи кривой в файл "data.bb".

Type dot

 Field x,y,dx1,dy1,dx2,dy2
End Type

Const xres=800, yres=600, stp#=.01

Global seldot.dot, sel, mx, my
Dim a#(1),b#(1),c#(1),d#(1),oc(1)

Graphics xres,yres,32,2

dt.dot=New dot
dt\x=.5*xres
dt\y=.25*yres
dt.dot=New dot
dt\x=.5*xres
dt\y=.75*yres

SetBuffer BackBuffer()
Repeat

 redraw

 mx=MouseX()
 my=MouseY()
 ;Эта переменная будет принимать значение 1, если нажата левая и 2, если правая
 ; кнопка мыши
 mb=MouseDown(1)+2*MouseDown(2)

 ;Выбор действия в зависимости от нажатой кнопки и выбранного объекта
 Select mb+sel*10
  Case 11
   ;Если нажата левая кнопка мыши и под курсором находится вершина - фиксируем ее
   sel=3
  Case 31
   ;Если нажата левая кнопка мыши при зафиксированной вершине - перепроверяем
   ; выделение
   sel=0
   redraw
   If sel=1 Then sel=3
  Case 12
   ;Нажата правая кнопка мыши при выбранной вершине - перемещаем вершину
   seldot\x=mx
   seldot\y=my
  Case 22
   ;Нажата правая кнопка мыши при выбранной линии - вставляем вершину
   dt.dot=New dot
   dt\x=mx
   dt\y=my
   Insert dt After seldot
   seldot=dt
   sel=0
 End Select

 ;Блок изменения векторов
 If sel=3 Then
  If KeyDown(3) Then
   ;Нажата клавиша "2" - изменяем второй вектор
   seldot\dx1=seldot\x-mx
   seldot\dy1=seldot\y-my
  End If
  If KeyDown(2) Or KeyDown(4) Then
   ;Изменяем первый вектор
   seldot\dx2=mx-seldot\x
   seldot\dy2=my-seldot\y
   If KeyDown(4) Then
    ;И второй, если нажата клавиша "3" а не "2"
    seldot\dx1=mx-seldot\x
    seldot\dy1=my-seldot\y
   End If
  End If
 End If

 ;Если выбрана вершина, то...
 If sel=1 Or sel=3 Then
  ;Удаление точки("Del")
  If KeyDown(211) And After First dot<>Last dot Then
   Delete seldot
   sel=0
  End If
  ;Обнуление векторов, если нажат "0"
  If KeyDown(11) Then
   seldot\dx1=0
   seldot\dy1=0
   seldot\dx2=0
   seldot\dy2=0
  End If
 End If

 ;Запись кривой
 If KeyHit(60) Then
  f=WriteFile("data.bb")
  For dt.dot=Each dot
   WriteLine f,"Data "+dt\x+","+dt\y+","+dt\dx1+","+dt\dy1+","+dt\dx2+","+dt\dy2  
  Next
  CloseFile f
 End If

 ;Загрузка кривой
 If KeyHit(61) Then
  Delete Each dot
  f=ReadFile("data.bb")
  While Not Eof(f)
   dt.dot=New dot
   m$=","+Mid$(ReadLine(f),6)
   For n=1 To 6
    m$=Mid$(m$,Instr(m$,",")+1)
    Select n
     Case 1:dt\x=m$
     Case 2:dt\y=m$
     Case 3:dt\dx1=m$
     Case 4:dt\dy1=m$
     Case 5:dt\dx2=m$
     Case 6:dt\dy2=m$
    End Select
   Next
  Wend
  CloseFile f
 End If

Until KeyHit(1)

Function redraw()
;Если выделение не зафиксировано, оно обновляется
If sel<3 Then sel=0

Cls
Color 255,255,255
;Проверка на коллизию с точкой
If sel=0 Then
 For dt.dot=Each dot
  If Abs(mx-dt\x)<=3 And Abs(my-dt\y)<=3 Then seldot=dt: sel=1
 Next
End If
For dt.dot=Each dot
 ;Выделение точки
 Oval dt\x-1,dt\y-1,3,3
 drawcurve dt
Next
If sel Then Oval seldot\x-3,seldot\y-3,7,7

Flip
End Function

Function drawcurve(dt1.dot)
dt2.dot=After dt1
If dt2=Null Then Return

r#=.05*Sqr((dt1\x-dt2\x)*(dt1\x-dt2\x)+(dt1\y-dt2\y)*(dt1\y-dt2\y))
For nn=0 To 1
 If nn Then
  v1=dt1\y
  v2=dt2\y
  c#(nn)=dt1\dy2*r#
  dy2#=dt2\dy1*r#
 Else
  v1=dt1\x
  v2=dt2\x
  c#(nn)=dt1\dx2*r#
  dy2#=dt2\dx1*r#
 End If
 d#(nn)=v1
 b#(nn)=3.0*v2-dy2#-2.0*c#(nn)-3.0*d#(nn)
 a#(nn)=(dy2#-2*b#(nn)-c#(nn))/3.0
Next

For t#=0 To 1 Step stp
 tt#=t#*t#
 For nn=0 To 1
  oc(nn)=a#(nn)*tt#*t#+b#(nn)*tt#+c#(nn)*t#+d#(nn)
 Next

 If t#>0 Then
  If sel=4 Then
   ;Выделение текущей кривой
   For xx=-1 To 1
    For yy=-1 To 1
     Line oc(0)+xx,oc(1)+yy,x+xx,y+yy
    Next
   Next
  Else
   Line oc(0),oc(1),x,y
  End If
 End If

 ;Проверка на коллизию с отрезком кривой
 If sel=0 Then
  ;Проверка нахождения курсора внутри ограничивающего прямоугольника
  If mx>=min(x,oc(0))-3 And mx<=max(x,oc(0))+3 Then
   If my>=min(y,oc(1))-3 And my<=max(y,oc(1))+3 Then
    aa#=y-oc(1)
    bb#=oc(0)-x
    ;Проверка расстояния от прямой
    If Abs(aa#*(mx-x)+bb#*(my-y))<=3.0*Sqr(aa#*aa#+bb#*bb#) Then
     seldot=dt1
     ;Это хитрый способ повторить построение кривой, выделив ее и заодно
     ; при этом избавиться от проверки
     sel=4
     t#=-stp
    End If
   End If
  End If
 End If

 x=oc(0)
 y=oc(1)

Next
;Восстанавливаем значение sel
If sel=4 Then sel=2

Color 0,255,255
If dt1\dx2<>0 Or dt1\dy2<>0 Then
 Line dt1\x,dt1\y,dt1\x+dt1\dx2,dt1\y+dt1\dy2
 Oval dt1\x+dt1\dx2-1,dt1\y+dt1\dy2-1,3,3
End If
If dt2\dx1<>0 Or dt2\dy1<>0 Then
 Line dt2\x,dt2\y,dt2\x-dt2\dx1,dt2\y-dt2\dy1
 Oval dt2\x-dt2\dx1-1,dt2\y-dt2\dy1-1,3,3
End If
Color 255,255,255

End Function

Function min(v1,v2)
If v1<v2 Then Return v1 Else Return v2
End Function

Function max(v1,v2)
If v1>v2 Then Return v1 Else Return v2

End Function

Автор: Матвей Меркулов (E-mail: MattMerkulov_sobaka_gmail.com, ICQ: 392-274-050)

Другие

Друзья