Скоро Новый Год! Давайте создавать уже праздничное настроение и запасаться шампанским! Самое время порисовать праздничные звезды )))
Правильный многоугольник
Правильный многоугольник — выпуклый многоугольник, у которого равны все стороны и все углы между смежными сторонами.
Проще говоря, для построения многоугольника нам нужен круг. Разделив 360 градусов на нужное число вершин, получим угол смещения лучей, который будет одинаков для всех смежных сторон. Построив хорды между точками пересечения лучей и окружности, получим искомый правильный N-угольник. Т.к. имеем круг, иными словами, окружность вписанную в квадрат, можем с уверенностью сказать, что стороны многоугольника будут также равны между собой.

В рисовании многоугольника нет ничего сложного. Рассчитать координаты вершин многоугольника – вот достойная задача. Конечно, при подсчете учитываем угол смещения для стартового угла, потому что хотим вращать фигуру.
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 |
//********************************************************************** // Посчитать координаты вершин многоугольника // ARect: TRect - область, задающая эллипс // ACount: Integer - кол-во вершин многоугольника // AStartAngle: single - отклонение/поворот в радианах //********************************************************************** function CalcPolyVertex(const ARect: TRect; const ACount: Integer; const AStartAngle: single = 0.0): TPointFArray; var rct: TxRect;// вещественный прямоугольник, область круга (эллипса) dA: Single; // угол смещения sA: Single; // стартовый угол i: Integer; // счетчик цикла begin rct := xRect(ARect); // Если число вершин меньше 3, то это не многоугольник if ACount < 3 then raise Exception.CreateFmt(ERR_UnpossibleVertexCount,[ACount]); // Угол между лучами, 2*pi = 360 dA := 2*pi/ACount; // Стартовый угол. -pi/2 - хотим, чтобы начало отсчета было // строго вертикально, т.е. вычитаем 90 град. против часовой // и добавляем смещение sA := -pi/2 + AStartAngle; SetLength(Result, ACount); // В цикле считаем координаты пересечения эллипса и прямой // из центра окружности for i := 0 to ACount - 1 do begin // Координаты пересечния прямой и окружности Result[i] := CalcEllipsePointCoord(rct,sA); // Смещаемся на следующий угол sA := sA + dA; end; end; |
Самое сложное – посчитать координаты пересечения прямой и окружности. Этому посвящена целая статья. В модуле xIPTrig находится нужная фукнция:
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 |
//*********************************************************************** // Найти координату точки на эллипсе по Angle углу отклонения // Алгоритм раскрыт в статье: "Координаты точки эллипса по углу" // ip76.ru/theory-and-practice/ellipse-point-coord/ //*********************************************************************** function GetEllipseAngleParam(a,b : Extended; Angle : Extended) : Extended; var sn,cs : Extended; // синус/косинус begin SinCos(Angle,sn,cs); result := ArcTan2(a*sn, b*cs); end; function CalcEllipsePointCoord(a,b : Extended; Angle : extended) : TxPointEx; var sn,cs : Extended; begin //-- считаем синус/косинус для параметра уравнения ---- SinCos (GetEllipseAngleParam(a,b,Angle), sn, cs); //-- считаем результат по параметрическому уравнению -- result.X := a * cs; result.Y := b * sn; end; function CalcEllipsePointCoord(ARect : TxRect; Angle : extended) : TxPoint; var a,b : Extended; // полуоси по X/Y pnt : TxPointEx; begin //-- инициализация полуосей --------------------------- a := xWidthRect(Arect)/2; b := xHeightRect(Arect)/2; //-- считаем результат по параметрическому уравнению -- pnt := CalcEllipsePointCoord(a,b,Angle); //-- находим реальные координаты ---------------------- result.X := Arect.Left + a + pnt.X; result.Y := Arect.Top + b + pnt.Y; end; |
Ниже описание используемых типов. Вместо TxPoint вполне можно использовать TPointF. Как и для других используемых типов есть аналоги в современных версиях Delphi. Но они настолько совпадают, что приведение одного к другому ни сложностей, ни вопросов у компилятора не вызывает.
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 |
Type //-- вещественная точка ---------------------- PxPoint = ^TxPoint; TxPoint = packed record X : single; Y : single; end; //-- вещественный прямоугольник -------------- PxRect = ^TxRect; TxRect = packed record case Integer of 0: (Left, Top, Right, Bottom: single); 1: (TopLeft, BottomRight: TxPoint); end; //-- массив вещественных точек --------------- PPointFArray = ^TPointFArray; TPointFArray = array of TxPoint; //-- точная вещественная точка ----------------- PxPointEx = ^TxPointEx; TxPointEx = packed record X : Extended; Y : Extended; end; //-- точный вещественный прямоугольник --------- PxRectEx = ^TxRectEx; TxRectEx = packed record case Integer of 0: (Left, Top, Right, Bottom: Extended); 1: (TopLeft, BottomRight: TxPointEx); end; |
Правильная звезда
Давайте поместим внутрь построенного многоугольника еще один, меньший по размерам и повернутый на половину угла, полученного от деления 360 градусов на число вершин. Иными словами, половину угла между смежными вершинами. И последовательно соединим все получившиеся вершины.
Получается вполне себе симпатичная звезда.

Найдем массив вершин для правильной звезды:
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 |
//********************************************************************** // Посчитать координаты вершин звезды // AOutRect: TRect - область, задающая внешний эллипс // AInnRect: TRect - область, задающая внутренний эллипс // ACount: Integer - кол-во вершин многоугольника // AStartAngle: single - отклонение/поворот в радианах // AStarOffset: single - процент смещения внутри угла между // смежными вершинами для расчета координат "впадин" звезды 0..1 //********************************************************************** function CalcStarVertex(const AOutRect, AInnRect: TRect; const ACount: Integer; const AStartAngle: single = 0.0; const AStarOffset: single = 0.5): TPointFArray; var ORect, IRect: TxRect; Len: Integer; dA: Single; // угол смещения sA: Single; // стартовый угол i: Integer; begin if ACount < 3 then raise Exception.CreateFmt(ERR_UnpossibleVertexCount,[ACount]); // Внешний вещественный прямоугольник эллипса для вершин ORect := xRect(AOutRect); // Внутренний вещественный прямоугольник эллипса для "впадин" IRect := xRect(AInnRect); // Точек в массиве в 2 раза больше чем вершин Len := ACount * 2; // Угол между смежными вершинами dA := 2*pi/ACount; // Стартовать от угла sA := -pi/2 + AStartAngle; // Массив вершин имеет такую размерность SetLength(Result, Len); for i := 0 to ACount - 1 do begin // считаем координаты вершины Result[i*2] := CalcEllipsePointCoord(ORect,sA); // считаем координаты "впадины" с учетом процента смещения Result[i*2+1] := CalcEllipsePointCoord(IRect,sA + dA*AStarOffset); // смещаемся на следующий угол sA := sA + dA; end; end; |
Играя с размером внутреннего эллипса и количеством вершин, можно получить разные симпатичные фигуры.

Для таких по новогоднему красивых звезд используем GDI+. А вернее TxIPCanvas, который реализован в рамках GDIPCanvas (включен в состав демонстрационного проекта). Мы просто инициализируем текстурную кисть, а заливка происходит как ни в чем не бывало – текущей кистью. Если это просто цвет, значит просто цвет. Если картинка, зальет картинкой.
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 |
//******************************************************************* // Инициализация текстурной кисти GDI+ // ACanvas: TCanvas - если это TxIPCanvas, магия состоится // ARect: TRect - область вывода. Для корректного смещения // AImage: TGraphic - текстура // AStretch: Boolean - текстуру надо масштабировать под ARect // ARotate: Single - поворот текстуры в градусах //******************************************************************* procedure InitBrush(const ACanvas: TCanvas; const ARect: TRect; const AImage: TGraphic; const AStretch: Boolean = False; const ARotate: Single = 0.0); var rct: TRect; begin if ((ACanvas is TxIPCanvas) and Assigned(AImage)) then with TxIPCanvas(ACanvas) do begin IPImage.LoadFromGraphic(AImage); IPBrush.BrushType := xbtTextureFill; IPBrush.OffsetXY := GPPointF(ARect.Left, ARect.Top); IPBrush.RotateAngle := ARotate; if AStretch then begin rct := GetProportRect(GetImageRect(AImage), ARect.Width, ARect.Height); IPBrush.ScaleXY := GPPointF(rct.Width/ AImage.Width, rct.Height/ AImage.Height); end; IPBrush.GPImage := IPImage.GPImage; if AImage is TBitmap then Pen.Color := TBitmap(AImage).Canvas.Pixels[10,10]; end else with Acanvas do begin Brush.Color := Lighter(clIP76Color,90); Brush.Style := bsSolid; end; end; |
Правильная «брутальная» звезда
Именно «брутальные» звезды красуются на китайском флаге и флаге США. Именно такая звезда является символом армии РФ. Звезда, которая строится без всяких там внутренних эллипсов. Строится на пересечении линий, связывающих вершины. Строгая геометрия. Прямая и брутальная, как Джейсон Стэтхэм.

Теперь надо разобраться, что с чем пересекается. Смотрим на рис.4. Координата P0 получается как пересечение линий (V0, V2) и (V1,V4). Обобщая, можем записать так:
Где F – функция нахождения пересечения векторов, заданных двумя точками. Теме пересечения прямых посвящена целая статья. Функция расчета координаты пересечения, как обычно, находится в модуле xIPTrig.
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 |
//********************************************************************** // Посчитать координаты вершин "брутальной" звезды // ARect: TRect - область, задающая эллипс // ACount: Integer - кол-во вершин многоугольника // AStartAngle: single - отклонение/поворот в радианах //********************************************************************** function GetPoint(const APoints: Array of TxPoint; const AIndex: Integer): TxPoint; var Len: Integer; idx: Integer; begin Len := Length(APoints); idx := AIndex; if idx < 0 then idx := Len + idx; if idx >= Len then idx := idx - Len; Result := APoints[idx]; end; function CalcCrossStarVertex(const ARect: TRect; const ACount: Integer; const AStartAngle: single = 0.0): TPointFArray; var rct: TxRect; Len: Integer; dA: Single; // угол смещения sA: Single; // стартовый угол i: Integer; begin if ACount < 3 then raise Exception.CreateFmt(ERR_UnpossibleVertexCount,[ACount]); rct := xRect(ARect); Len := ACount * 2; dA := 2*pi/ACount; sA := -pi/2 + AStartAngle; SetLength(Result, Len); // Считаем вершины звезды for i := 0 to ACount - 1 do begin Result[i*2] := CalcEllipsePointCoord(rct,sA); sA := sA + dA; end; // Считаем перекрестия линий между вершинами for i := 0 to ACount - 1 do begin // Pi = F((Vi,Vi+2);(Vi+1,Vi-1)) CrossLines( GetPoint(Result,(i)*2), GetPoint(Result,(i+2)*2), GetPoint(Result,(i+1)*2), GetPoint(Result,(i-1)*2), Result[i*2+1]); end; end; |
Сама функция из модуля xIPTrig:
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 |
//******************************************************* // Нахождение точки пересечения прямых (p1,p2) и (p3,p4) // Результат - факт пересечения // https://ip76.ru/cross-lines/ //******************************************************* function CrossLines(const p1,p2,p3,p4: TxPoint; var res: TxPoint): Boolean; const Prec = 0.0001; var a1, a2: Extended; b1, b2: Extended; c1, c2: Extended; v: Extended; begin a1 := p2.y - p1.y; a2 := p4.y - p3.y; b1 := p1.x - p2.x; b2 := p3.x - p4.x; v := a1*b2 - a2*b1; Result := (abs(v) > Prec); if Result then begin c1 := p2.x*p1.y - p1.x*p2.y; c2 := p4.x*p3.y - p3.x*p4.y; res.X := -(c1*b2 - c2*b1)/v; res.Y := -(a1*c2 - a2*c1)/v; end; end; |
Неправильные звезды
Вернемся к звездам небрутальным, праздничным. У нас есть параметр смещения внутри угла между смежными вершинами. Ему совсем не обязательно делить угол пополам. Зададим другое значение, скажем, 90%. Получилась какая-то робкая звезда диско.

Если откажемся от полигонов в пользу кривых, получаем еще более интересные эффекты. В GDI+ это функция Curve. Существует исключительно для ленивцев, которым невмоготу считать направляющие кривых Безье. Параметр Tension (0..1) определяет «натяжение» струны кривой. Это лучше поэкспериментировать, так словами и не объяснишь.





Хотя, конечно, больше смахивает на бактерию, которая появляется в новогоднем оставшемся оливье.
Рисование
Чуть не забыл про сами функции рисования. Алгоритм простой. Вначале находим массивы координат вершин. Функции для этого все указаны выше. Затем рисуем одним из методов – Polygon или Curve. Для Curve есть дополнительный параметр «натяжения» струны. Если этот параметр равен 0.0, то выглядеть будет так, будто применили Polygon вместо Curve.
Все функции используют текущие настройки ACanvas. Т.е. цвет, толщину линий, кисть нужно настроить до вызова функции. Также, нет подсчета квадрата. Это уже дело программиста, что передавать – заранее посчитанный квадрат, либо эллипс. В случае эллипса (не круга), ни многоугольник, ни звезда правильными не будут. Но мало ли какие художественные замыслы у автора.
Параметры функций повторяют параметры при подсчете координат вершин, поэтому расписывать не стал.
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 |
//********************************************************************** // Нарисовать многоугольник // Используются текущие настройки ACanvas //********************************************************************** function DrawNPolygone(const ACanvas: TCanvas; const ARect: TRect; const ACount: Integer; const AStartAngle: single = 0.0): Boolean; var Vertices: TPointFArray; Points: Array of TPoint; i: Integer; begin Result := CheckParamsValid(ACanvas, ARect, nil, False); if not Result then Exit; Vertices := CalcPolyVertex(ARect, ACount, AStartAngle); if ACanvas is TxIPCanvas then TxIPCanvas(ACanvas).Polygon(Vertices, nil) else begin SetLength(Points, ACount); for i := 0 to ACount-1 do Points[i] := xPointToPoint(TxPoint(Vertices[i])); ACanvas.Polygon(Points); 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 |
//********************************************************************** // Нарисовать звезду // Используются текущие настройки ACanvas //********************************************************************** function DrawNPolyStar(const ACanvas: TCanvas; const AOutRect, AInnRect: TRect; const ACount: Integer; const AStartAngle: single = 0.0; const AStarOffset: single = 0.5): Boolean; var Vertices: TPointFArray; Points: Array of TPoint; Len: Integer; i: Integer; begin Result := CheckParamsValid(ACanvas, AOutRect, nil, False); if not Result then Exit; Vertices := CalcStarVertex(AOutRect, AInnRect, ACount, AStartAngle, AStarOffset); Len := Length(Vertices); if ACanvas is TxIPCanvas then TxIPCanvas(ACanvas).Polygon(Vertices, nil) else begin SetLength(Points, Len); for i := 0 to Len-1 do Points[i] := xPointToPoint(TxPoint(Vertices[i])); ACanvas.Polygon(Points); 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 |
//********************************************************************** // Нарисовать звезду через кривые // Используются текущие настройки ACanvas //********************************************************************** function DrawNPolyStarCurve(const ACanvas: TCanvas; const AOutRect, AInnRect: TRect; const ACount: Integer; const ATension: Single = 0.5; const AStartAngle: single = 0.0; const AStarOffset: single = 0.5): Boolean; var Vertices: TPointFArray; Points: Array of TPoint; Len: Integer; i: Integer; begin Result := CheckParamsValid(ACanvas, AOutRect, nil, False); if not Result then Exit; Vertices := CalcStarVertex(AOutRect, AInnRect, ACount, AStartAngle, AStarOffset); Len := Length(Vertices); if ACanvas is TxIPCanvas then TxIPCanvas(ACanvas).Curve(Vertices, ATension) else begin SetLength(Points, Len); for i := 0 to Len-1 do Points[i] := xPointToPoint(TxPoint(Vertices[i])); ACanvas.Polygon(Points); 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 |
//********************************************************************** // Нарисовать "брутальную" звезду через кривые // Используются текущие настройки ACanvas //********************************************************************** function DrawCroccNPolyStar(const ACanvas: TCanvas; const ARect: TRect; const ACount: Integer; const ATension: Single = 0.5; const AStartAngle: single = 0.0): Boolean; var Vertices: TPointFArray; Points: Array of TPoint; Len: Integer; i: Integer; begin Result := CheckParamsValid(ACanvas, ARect, nil, False); if not Result then Exit; Vertices := CalcCrossStarVertex(ARect, ACount, AStartAngle); Len := Length(Vertices); if ACanvas is TxIPCanvas then TxIPCanvas(ACanvas).Curve(Vertices, ATension) else begin SetLength(Points, Len); for i := 0 to Len-1 do Points[i] := xPointToPoint(TxPoint(Vertices[i])); ACanvas.Polygon(Points); end; end; |
В интерфейсе проекта есть галочка «Rotate Image». Ее наличие указывает, что надо повернуть текстуру на угол поворота всей фигуры. Но особенности реализации не позволяют «намертво» прилепить текстуру к повороту фигуры, зато смотрится весьма динамично.
Если использовать «бесшовную» текстуру и делать смещение по её центру, то поворот будет идеален. Однако, для настоящего поворота любой фигуры, надо использовать поворот всего «мирового пространства». Разговор об этом обязательно состоится, очень интересная тема.
Друзья, спасибо за внимание!
С наступающим Новым Годом!!!
Информация о новых статьях смотрим в телеграм-канале.
Не забываем комментировать и подписываться )))
Скачать (1.24 Мб): Исходники (Delphi XE 7-10)
Скачать (2.02 Мб): Исполняемый файл
Весьма позитивно. За прогу спасибо
Не за что ))) С Новым Годом!