Главная | Эксперименты | Утилиты | Компоненты | Что почитать | Контакты |
|
Базовые сведенияКак GDI, так и GDI+ предоставляют функции для рисования кривых Безье. При этом используются кубические кривые, для построения которых нужны четыре опорные точки. Простой пример для GDI+ построения кривой и ее опорных точек:pascal
var g : TGPGraphics; pen : TGPPen; brush : TGPBrush; P : array [1..4] of TGPPointF; i : integer; begin g := TGPGraphics.Create(image1.Canvas.Handle); pen := TGPPen.Create(aclBlack, 2); brush := TGPSolidBrush.Create(aclGreen); try g.SetSmoothingMode(SmoothingModeAntiAlias); P[1] := makePoint( 10.0, 100); P[2] := makePoint( 60.0, 20); P[3] := makePoint( 110.0, 180); P[4] := makePoint( 160.0, 100); g.DrawBezier(pen, P[1], P[2], P[3], P[4]); pen.SetWidth(1); pen.SetColor(aclBlue); g.DrawLine(pen, P[1], P[2]); g.DrawLine(pen, P[3], P[4]); for i := 1 to 4 do g.FillEllipse(brush, P[i].x-3, P[i].y-3, 6, 6); finally brush.free; pen.free; g.free; end; ![]() Вычисление точек кривой БезьеКривая Безье описывается простым параметрическим уравнением:
pascal
var g : TGPGraphics; pen : TGPPen; brush : TGPBrush; P : array [1..4] of TGPPointF; t, x, y : Single; R1, R2:TGPPointF; begin g := TGPGraphics.Create(image1.Canvas.Handle); pen := TGPPen.Create(aclBlack, 2); brush := TGPSolidBrush.Create(aclGreen); try g.SetSmoothingMode(SmoothingModeAntiAlias); P[1] := makePoint( 10.0, 100); P[2] := makePoint( 60.0, 20); P[3] := makePoint( 110.0, 180); P[4] := makePoint( 160.0, 100); g.DrawBezier(pen, P[1], P[2], P[3], P[4]); R1 := P[1]; R2 := P[4]; t := 0.125; while t < 1.0 do begin x := BCoord(P[1].x, P[2].x, P[3].x, P[4].x, t); y := BCoord(P[1].y, P[2].y, P[3].y, P[4].y, t); g.FillEllipse(brush, x-3, y-3, 6, 6); if x < R1.x then R1.x := x; if y < R1.y then R1.y := y; if x > R2.x then R2.x := x; if y > R2.y then R2.y := y; t := t + 0.125; end; pen.SetWidth(1); pen.SetColor(aclBlue); g.DrawRectangle(pen, R1.x-2, R1.y-2, R2.x-R1.x+4, R2.Y-R1.Y+4); //... ![]() Вычисление опорных точек.Пусть изместно, что кривая выходит из точки A, проходит через точку B при t = 1/3, через точку C при t = 2/3 и заканчивается в точке D. Подставив эти значения в параметрическое уравнение кривой Безье получим выражения для вычисления координат опорных точек:Оформим эти вычисления отдельной процедурой. Она принимает четыре точки, через которые должна пройти кривая. В этих же переменных возвращаются начальная, конечная и две опорные точки кривой.
pascal
var g : TGPGraphics; pen : TGPPen; brush : TGPSolidBrush; P : array [1..4] of TGPPointF; i : integer; begin g := TGPGraphics.Create(image1.Canvas.Handle); pen := TGPPen.Create(aclBlack, 2); brush := TGPSolidBrush.Create(aclGreen); try g.SetSmoothingMode(SmoothingModeAntiAlias); P[1] := makePoint( 10.0, 100); P[2] := makePoint( 100.0, 150); P[3] := makePoint( 180.0, 120); P[4] := makePoint( 190.0, 20); for i := 1 to 4 do g.FillEllipse(brush, P[i].x-3, P[i].y-3, 6, 6); ToBezier(P[1], P[2], P[3], P[4]); g.DrawBezier(pen, P[1], P[2], P[3], P[4]); brush.SetColor(aclRed); for i := 2 to 3 do g.FillEllipse(brush, P[i].x-3, P[i].y-3, 6, 6); pen.SetWidth(1); pen.SetColor(aclBlue); for i := 1 to 3 do g.DrawLine(pen, P[i], P[i+1]); ![]() Аппроксимация дуги эллипса кривыми БезьеПусть задан эллипс с центром в точке (0, 0) и радиусами a, b (по оси X и по оси Y соответственно). Требуется построить дугу между заданными углами.Прежде всего нужно найти точки начала и конца дуги - они же будут начальной и конечной точками кривой. Для заданного угла ![]() ![]()
pascal
var g : TGPGraphics; pen : TGPPen; brush : TGPSolidBrush; Dt : TGPPointF; RX, RY : Single; A, B, C, D:TGPoint; begin g := TGPGraphics.Create(image1.Canvas.Handle); pen := TGPPen.Create(aclBlack, 2); brush := TGPSolidBrush.Create(aclGreen); try g.SetSmoothingMode(SmoothingModeAntiAlias); RX := 180; RY := 80; Dt := makePoint(190.0, 90); // Исходный эллипс для сравнения pen.SetWidth(2); pen.SetColor(aclRed); g.DrawEllipse(pen, 10, 10, RX*2, RY*2); pen.SetWidth(3); pen.SetColor(aclBlack); GetArcBezier(rx, ry, 30, 60, A, B, C, D); g.DrawBezier(pen, A.x+Dt.x, A.y+Dt.y, B.x+Dt.x, B.y+Dt.y, C.x+Dt.x, C.y+Dt.y, D.x+Dt.x, D.y+Dt.y ); pen.SetWidth(3); pen.SetColor(aclBlue); GetArcBezier(rx, ry, -30, -200, A, B, C, D); g.DrawBezier(pen, A.x+Dt.x, A.y+Dt.y, B.x+Dt.x, B.y+Dt.y, C.x+Dt.x, C.y+Dt.y, D.x+Dt.x, D.y+Dt.y ); ![]() Здесь красный эллипс нарисован средствами GDI+ для сравнения. Видно, что аппроксимация маленьких дуг (черным) совпадает с эталоном. Большие дуги сильно отклоняются от эталонной кривой. Для точной аппроксимации разобъем дугу на две - три части и для каждой построим свою кривую Безье: pascal
function GetArcBezier(rx, ry, start, sweep:Single; var Beziers : array of TGPoint):integer; overload; var f:Single; i:integer; begin result := 0; if abs(sweep) > 360 then sweep := 360; if abs(sweep) < 1 then exit; f := abs(sweep); if f > 270 then result := 4 else if f > 180 then result := 3 else if f > 90 then result := 2 else result := 1; f := sweep / result; for i := 0 to result - 1 do begin GetArcBezier(rx, ry, start+f*i, f, Beziers[i*4], Beziers[i*4+1], Beziers[i*4+2], Beziers[i*4+3]); end; end; ![]() Аппроксимация синусоиды кривыми БезьеОграничимся интервалом от 0 до pi/2. Будем задавать синусоиду на этом интервале вектором. Начальная точка синусоиды в центре координат, конечную указывает заданный вектор. Для вычисления опорных точек можно использовать заранее вычисленные коэффициенты:pascal
function GetSinBezier(V:TGPoint; sin:boolean; var A, B, C, D:TGPoint):boolean; begin result := false; if (abs(V.x) < 0.001) or (abs(V.y) < 0.001) then exit; result := true; A.x := 0; A.y := 0; if sin then begin B.x := 1/3 * abs(V.x); B.y := 0.534295 * abs(V.y); C.x := 2/3 * abs(V.x); C.y := abs(V.y); end else begin B.x := 1/3 * abs(V.x); B.y := 0; C.x := 2/3 * abs(V.x); C.y := (1-0.534295) * abs(V.y) end; D.x := abs(V.x); D.y := abs(V.y); if V.x > 0 then begin if V.y > 0 then begin end else begin B.y := -B.y; C.y := -C.y; D.y := -D.y; end end else begin if V.y > 0 then begin B.x := -B.x; C.x := -C.x; D.x := -D.x; end else begin B.x := -B.x; B.y := -B.y; C.x := -C.x; C.y := -C.y; D.x := -D.x; D.y := -D.y; end end; end; ![]() Аппроксимация параболы кривыми БезьеКак известно, параболу можно построить по трем точкам. Возьмем, например, точку перегиба параболы и любую другую точку кривой. Тогда третью точку можно получить простым отражением. Мы приходим к той-же схеме, что и для синусоиды, т.е. используем вектор для задания двух точек.pascal
function GetParabolaBezier(V:TGPoint; right:boolean; var A, B, C, D:TGPoint):boolean; var x1, x2, x3, y1, y2, y3 : Single; A1, B1, C1:Single; begin result := false; if (abs(V.x) < 0.001) or (abs(V.y) < 0.001) then exit; result := true; // Для определения коэффициентов параболы нужны две точки // Третью получаем отражением относительно оси OY if right then begin // Первая точка - точка изгиба x2 := 0; y2 := 0; x3 := V.x; y3 := V.y; x1 := -V.x; y1 := V.y; end else begin // Точку изгиба указывает вектор x1 := 0; y1 := 0; x2 := V.x; y2 := V.y; x3 := V.x*2; y3 := 0; end; // Определяем коэффициенты параболы y = A*x^2 + B*x + C A1 := (y3 - (x3*(y2-y1)+x2*y1-x1*y2)/(x2-x1)) / (x3*(x3-x1-x2)+x1*x2); B1 := (y2-y1)/(x2-x1)-A1*(x1+x2); C1 := (x2*y1-x1*y2)/(x2-x1)+A1*(x1*x2); // Первая и последняя точки известны if right then begin A := makePoint(x2, y2); D := makePoint(x3, y3); end else begin A := makePoint(x1, y1); D := makePoint(x2, y2); end; // Вычисляем еще две точки B.x := A.x + (D.x-A.x)/3; C.x := A.x + (D.x-A.x)/3*2; B.y := A1*B.x*B.x+B1*B.x+C1; C.y := A1*C.x*C.x+B1*C.x+C1; ToBezier(A, B, C, D); end; ![]() Соединение точек плавной кривой в GDI+Естественно, GDI+ имеет средства для соединения точек плавной кривой. Можно строить как замкнутые, так и не замкнутые кривые.![]() Если интересен пример, смотрите код в прикрепленном архиве. Нас же больше интересует сам принцип построения таких кривых. ![]() - Пусть, например, плавная кривая должна пройти через точки P0, P1, P2, т.е. кривая будет состоять из двух кривых Безье. Для плавного соединения этих кривых в точке P1 нужно чтобы три последовательных опорных точки A, P1, и B лежали на одной прямой. По сути для получения опорных точек нам нужно найти касательную к кривой в точке P1. Зная касательную и выбрав на ней точки A и B мы получим опорные точки для построения кривой Безье. Повторив этот расчет для каждой заданной точки мы получим серию опорных точек по которым сможем построить несколько кривых Безье. Вместе эти кривые называются сплайном. Т.к. мы рассматриваем кубические кривые Безье, то и сплайн называется кубическим Безье сплайном (cubic Bezier spline). Соответственно из квадратичных кривых Безье можно построить квадратичный Безье сплайн (quadratic Bezier spline). Возможны несколько различных подходов для построения таких касательных. В каждом варианте будет получаться несколько отличающаяся кривая. Кубические сплайны ЭрмитаКубические сплайны Эрмита (Cubic Hermite Splines) - это почти то-же самое, что и кубические сплайны Безье. Можно рассматривать их просто как другой способ задания кривых Безье. Вместо указания опорных точек мы в каждой точке кривой задаем вектор. Направление и величина этого вектора определяют скорость с которой кривая будет отклоняться к заданной точке.Например, на рисунке слева вид кривой определяется векторами U и V, заданными для точек A и D, а на рисунке справа та-же кривая задана опорными точками B и C. Для преобразования векторов к опорным точкам кривой Безье существуют простые формулы:
Сплайны Катмулла-РомаСплайны Катмулла-Рома (Catmull-Rom spline) можно рассматривать как вариант построения кубических сплайнов Эрмита.![]() - Открытым остается вопрос что делать на концах последовательности. Если мы строим замкнутую кривую, то для первой точки списка соседней будет последняя и наоборот. Когда же мы строим не замкнутую кривую, то возможны варианты:
pascal
function toCMSlines(Points:array of TGPoint; closed:boolean=true; asClosed:boolean=true):ArrayOfPoint; var C, i, CE, N1, N2, k:integer; P1, P2, P0, C0, B0, P0C:TGPoint; begin k := 0; C := High(Points) - Low(Points) + 1; CE := C; if not closed then dec(CE); SetLength(result, CE * 3+1); for i := 0 to C-1 do begin P0 := Points[i]; // Получаем предыдущую и следующую точки N1 := i-1; if N1 < 0 then if closed or asClosed then N1 := C-1 else N1 := i; N2 := i+1; if N2 > C-1 then if closed or asClosed then N2 := 0 else N2 := i; P1 := Points[N1]; P2 := Points[N2]; // Половина вектора (P1,P2) P2.X := (P2.X - P1.X)/2; P2.Y := (P2.Y - P1.Y)/2; // Опорные точки двух соседних кривых Безье C0.X := P0.X - P2.X/3; C0.Y := P0.Y - P2.Y/3; B0.X := P0.X + P2.X/3; B0.Y := P0.Y + P2.Y/3; if i = 0 then P0C := C0 else begin result[k] := C0; inc(k); end; result[k] := P0; inc(k); if (i < C-1) or closed then begin result[k] := B0; inc(k); end; end; if closed then begin result[k] := P0C; inc(k); result[k] := Points[0]; end; end; Пример замкнутой и незамкнутой кривой: ![]() ![]() Сплайны Кочанека-БартельсаСплайны Кочанека-Бартельса (Kochanek-Bartels splines) также являются кубическими сплайнами Эрмита, но дополнены тремя параметрами. Параметры называются tension, bias и continuity (натяжение, скос/склонение, непрерывность) и изменяют вид касательной и, следовательно, всей кривой.Формулы для получения двух векторов в каждой точке: Если параметры равны нулю, мы получаем сплайн Катмулла-Рома. Метод для вычисления точек кривых Безье по этим формулам (полный пример в архиве):
Литература
|