Построение правильных многоугольников и неправильных звезд

Многоугольники и звезды

Скоро Новый Год! Давайте создавать уже праздничное настроение и запасаться шампанским! Самое время порисовать праздничные звезды )))

Правильный многоугольник

Правильный многоугольник — выпуклый многоугольник, у которого равны все стороны и все углы между смежными сторонами.

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

Рис.1. Правильный пятиугольник (шестиугольник, треугольник)

В рисовании многоугольника нет ничего сложного. Рассчитать координаты вершин многоугольника – вот достойная задача. Конечно, при подсчете учитываем угол смещения для стартового угла, потому что хотим вращать фигуру.

Расчет координат вершин многоугольника
//**********************************************************************
//  Посчитать координаты вершин многоугольника
//  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 находится нужная фукнция:

Найти координату точки на эллипсе по углу
//***********************************************************************
//   Найти координату точки на эллипсе по 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. Но они настолько совпадают, что приведение одного к другому ни сложностей, ни вопросов у компилятора не вызывает.

Используемые типы
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 градусов на число вершин. Иными словами, половину угла между смежными вершинами. И последовательно соединим все получившиеся вершины.

Получается вполне себе симпатичная звезда.

Рис.2. Правильная пятиконечная звезда

Найдем массив вершин для правильной звезды:

Расчет координат вершин правильной звезды
//**********************************************************************
//  Посчитать координаты вершин звезды
//  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;
[свернуть]

Играя с размером внутреннего эллипса и количеством вершин, можно получить разные симпатичные фигуры.

Рис.3. Правильные новогодние звезды

Для таких по новогоднему красивых звезд используем GDI+. А вернее TxIPCanvas, который реализован в рамках GDIPCanvas (включен в состав демонстрационного проекта). Мы просто инициализируем текстурную кисть, а заливка происходит как ни в чем не бывало – текущей кистью. Если это просто цвет, значит просто цвет. Если картинка, зальет картинкой.

Инициализация текстурной кисти GDI+
//*******************************************************************
//  Инициализация текстурной кисти 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. Правильная звезда имени Стэтхэма

Теперь надо разобраться, что с чем пересекается. Смотрим на рис.4. Координата P0 получается как пересечение линий (V0, V2) и (V1,V4). Обобщая, можем записать так:

Latex formula

Где F – функция нахождения пересечения векторов, заданных двумя точками. Теме пересечения прямых посвящена целая статья. Функция расчета координаты пересечения, как обычно, находится в модуле xIPTrig.

Посчитать координаты вершин правильной "брутальной" звезды
//**********************************************************************
//  Посчитать координаты вершин "брутальной" звезды
//  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:

Пересечения прямых (p1,p2) и (p3,p4)
//*******************************************************
//  Нахождение точки пересечения прямых (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%. Получилась какая-то робкая звезда диско.

Рис.5. Неправильная робкая звезда диско

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

Рис.6. Эволюция праздника…
Рис.7. Звезда-ромб. Ковер-самолет
Рис.8. Равнобедренный треугольник. Звезда-бумеранг
Рис.9. «Брутальная» звезда ромб-бабочка или цветочек
Рис.10. Хочется верить, что солнышко…

Хотя, конечно, больше смахивает на бактерию, которая появляется в новогоднем оставшемся оливье.

Рисование

Чуть не забыл про сами функции рисования. Алгоритм простой. Вначале находим массивы координат вершин. Функции для этого все указаны выше. Затем рисуем одним из методов – Polygon или Curve. Для Curve есть дополнительный параметр «натяжения» струны. Если этот параметр равен 0.0, то выглядеть будет так, будто применили Polygon вместо Curve.

 Все функции используют текущие настройки ACanvas. Т.е. цвет, толщину линий, кисть нужно настроить до вызова функции. Также, нет подсчета квадрата. Это уже дело программиста, что передавать – заранее посчитанный квадрат, либо эллипс. В случае эллипса (не круга), ни многоугольник, ни звезда правильными не будут. Но мало ли какие художественные замыслы у автора.

Параметры функций повторяют параметры при подсчете координат вершин, поэтому расписывать не стал.

Рисуем многоугольник
//**********************************************************************
//  Нарисовать многоугольник
//  Используются текущие настройки 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;
[свернуть]
Рисуем звезду через Polygon
//**********************************************************************
//  Нарисовать звезду
//  Используются текущие настройки 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;
[свернуть]
Рисуем звезду через Curve
//**********************************************************************
//  Нарисовать звезду через кривые
//  Используются текущие настройки 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;
[свернуть]
Рисуем "брутальную" звезду через Curve
//**********************************************************************
//  Нарисовать "брутальную" звезду через кривые
//  Используются текущие настройки 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 Мб): Исполняемый файл


0 0 голос
Рейтинг статьи
Подписаться
Уведомить о
guest
2 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Иван
Иван
3 месяцев назад

Весьма позитивно. За прогу спасибо

2
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
()
x