Рисование сплайнами и кривыми Безье

Постановка задачи. Требуется рисовать объекты 2d-графики, используя плавные кривые.

Обсуждение. В примере с корабликом мы уже рисовали окружности и сектора, хотя очевидно они не исчерпывают все необходимые нам графические объекты. В этом примере мы продемонстрируем другие варианты решения этой задачи. Ключевыми понятиями будут сплайн и кривая Безье.

Подготовительные действия:

На форме размером 900х400 поместим надпись (label1) «Упругость сплайна» и окно для ввода textBox1. Создадим меню с кнопками : «Выбор кривой» и «Очистка экрана». Задайте семь типов кривой (подменю «Выбор кривой»):
1) Незамкнутая ломаная,
2) Замкнутая ломаная,
3) Незамкнутый сплайн,
4) Замкнутый сплайн,
5) Одна кривая Безье, 6) Незамкнутая кривая Безье,
7) Замкнутая кривая Безье.

Начнем постепенно с ними разбираться. Нам понадобятся объекты классов Graphics, SolidBrush, Pen, Point и Random. Каждой позиции меню зададим соответствующие обработчики событий, например, для незамкнутой ломаной будет задан метод незамкнутаяЛоманаяToolStripMenuItem_Click(),
а для очистки экрана — метод
очисткаЭкранаToolStripMenuItem_Click().
Добавим два метода класса Form1:
private Point[] star5(int x, int y, int r),
возвращающий массив 10 точек «Звезды», и
public Color RandomColor()
(смотри пример «Парапланы»).

Будем использовать свойства холста g: SmoothingMode и FillMode, добавленные в библиотеке System.Drawing.Drawing2D; а также методы класса Graphics: FromHwnd(), Clear(), DrawLine(), DrawLines(), DrawCurve(), DrawCloseCurve(), DrawBezier(), DrawBeziers(), Fillellipse().

 Немного теории

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

Для того, чтобы нарисовать отрезок прямой, надо указать координаты начала (Point start) и конца (Point end) отрезка, выбрать перо (pen) для рисования  и применить метод объекта g.DrawLine(pen, start, end).

Для рисования незамкнутой ломаной, состоящей из отрезков, нужно задать массив последовательных точек  Point[] pm  и применить метод  g.DrawLines(pen, pm). Если в качестве последней точки массива указать координаты первой точки, то контур станет замкнутым.

Для рисования кривых следует использовать методы DrawCurve() или  DrawCloseCurve() для рисования сплайном, или же DrawBezier() и DrawBeziers() для рисования кривых Безье.  

Справка

Сплайн (основной или фундаментальный сплайн, аналог лекала в черчении) — это последовательность отдельных кривых, объединенных в одну большую кривую. Сплайн задается массивом точек и параметром упругости. Сплайн обязательно проходит через заданные массивом pm точки на плоскости, а параметр упругости  (elasticity — вещественное число типа float) определяет плавность их соединения. Если упругость = 0.0f  (бесконечная физическая упругость),  то соединение точек будет выполнено прямыми отрезками. Если упругость = 1.0f  (отсутствие физической упругости), то кривая имеет  наименьший суммарный изгиб. Если упругость > 1.0f, то кривая напоминает сдавленный берегами ручей, стремящийся увеличить изгиб своих излучин и течь по более длинному пути. Лучше всего исследовать влияние упругости, меняя ее и рисуя сплайн заново. Для вызова метода достаточно написать
g.DrawCurve(pen, pm, elasticity, FillMode.Alternate);
— незамкнутый сплайн или
g.DrawClosedCurve(pen, pm, elasticity, FillMode.Alternate);
— замкнутый сплайн.
Отличие второго метода от первого в том, что последняя точка массива добавляется от первой (eё можно не добавлять к массиву).

Кривой Безье называется кривая, задаваемая полиномом 3 порядка: y=ax3+bx2+cx+d. То есть для ее определения нужно задать четыре параметра (a,b,c,d).
Кривые Безье используются очень часто в 2d- и 3d-графике. Они позволяют задавать любой тип соединений («сшивания») кривых, в этом они являются более универсальными, чем рассмотренные выше сплайны (для которых кроме заданных точек дополнительно вводится только один параметр — упругость).
Для полного определения кривой Безье, проходящей через начальную (start) и конечную (finish) точки необходимо задать еще две управляющие точки (control1 и control2), задающих касательные к этим точкам и радиусы кривизны. Безье1 Отдаляя управляющую точку control1 от точки start, мы увеличиваем радиус кривизны, смещая ее относительно точки start, мы изменяем направление касательной к кривой в точке start. Все это справедливо и для пары finish-control2.
Эти четыре точки полностью определяют коэффициенты (a,b,c,d) кривой Безье.  Полезно самостоятельно исследовать, как меняется форма кривой Безье при перемещении управляющих точек. Теперь это можно сделать в программе, описанной в статье «Кривая Безье и метод де Кастельжо«.

Остальное понятно из комментариев в тексте программы (файл Form1.cs):

using System;
using System.Drawing;
using System.Drawing.Drawing2D; // добавлено
using System.Windows.Forms;
namespace сплайны_и_кривые_Безье
{
   public partial class Form1 : Form
   {
      Graphics g;

      public Form1()
      {
         InitializeComponent();
         g = Graphics.FromHwnd(this.Handle);  // холст
         // сглаживание: enum SmoothingMode 
         g.SmoothingMode = SmoothingMode.AntiAlias;
      }

      private void очисткаЭкранаToolStripMenuItem_Click(object sender, EventArgs e)
      {
         g.Clear(BackColor);
      }

      private Point[] star5(int x, int y, int r)  // звезда
      {
          Point point1 = new Point(x, y-6*r);
          Point point2 = new Point(x+2*r, y-3*r);
          Point point3 = new Point(x+6*r, y-2*r);
          Point point4 = new Point(x+3*r, y+r);
          Point point5 = new Point(x+4*r, y+5*r);
          Point point6 = new Point(x, y+3*r);
          Point point7 = new Point(x - 4 * r, y + 5 * r);
          Point point8 = new Point(x - 3 * r, y + r);
          Point point9 = new Point(x - 6 * r, y - 2 * r);
          Point point10 = new Point(x - 2 * r, y - 3 * r);
          Point[] pm =
          {
             point1,
             point2,
             point3,
             point4,
             point5,
             point6,
             point7,
             point8,
             point9,
             point10
         };
         return pm;
      }
 
      private void незамкнутаяЛоманаяToolStripMenuItem_Click(object sender, EventArgs e)
      {
         Pen redPen = new Pen(Color.Red, 2f);
      // Задание 10 точек, определяющих незамкнутую ломаную
         Point[] pm = star5(200, 200, 25);
      // Рисование незамкнутой ломаной из отрезков (Line)
         g.DrawLines(redPen, pm); // для замыкания добавьте первую точку
      }

      private void замкнутаяЛоманаяToolStripMenuItem_Click(object sender, EventArgs e)
      {
          Pen greenPen = new Pen(Color.Green, 2f);
       // Задание 10 точек, определяющих замкнутую область "звезда"
          Point[] pm = star5(250, 220, 25);
       // Рисование замкнутой ломаной из отрезков
          g.DrawClosedCurve(greenPen, pm, 0f, FillMode.Winding);  
       // Упругость: 0f - беск.физ.упругость - прямыми;
      }
 
      private void сплайнToolStripMenuItem_Click(object sender, EventArgs e)
      {
         Pen pen = new Pen(RandomColor(), 2f);
      // Задание точек, определяющих кривую
         Point[] pm = star5(500, 200, 25);
         float elasticity = Convert.ToSingle(textBox1.Text); // - упругость
      // Рисование замкнутой кривой через 10 точек сплайном
      // g.DrawClosedCurve(bluePen, pm, elasticity, FillMode.Alternate);
      // Упругость:   0f - беск.физ.упругость - прямыми;
      // 1f - отсутствие физ.упругости, наименьший суммарный изгиб
      // >1 - сдавленный берегами ручей, стремящийся увеличить изгиб своих излучин и течь по более длинному пути.
         g.DrawClosedCurve(pen, pm, elasticity, FillMode.Alternate);
      }

      private void незамкнутыйСплайнToolStripMenuItem_Click(object sender, EventArgs e)
      {
         Pen pen = new Pen(RandomColor(), 3f);
         // Задание точек, определяющих кривую
         Point[] pm = star5(400, 200, 25);
         float elasticity = Convert.ToSingle(textBox1.Text);  
         // Рисование незамкнутой кривой через 10 точек сплайном
         g.DrawCurve(pen, pm, elasticity);
      } 
  
      public Color RandomColor()
      {
          int r, g, b;
          byte[] bytes1 = new byte[3];     // массив 3 цветов
          Random rnd1 = new Random();
          rnd1.NextBytes(bytes1);          // генерация в массив
          r = Convert.ToInt16(bytes1[0]);
          g = Convert.ToInt16(bytes1[1]);
          b = Convert.ToInt16(bytes1[2]);
          return Color.FromArgb(r, g, b);  // возврат цвета
      }

      private void однаКриваяБезьеToolStripMenuItem_Click(object sender, EventArgs e)
      {
      // кривая Безье: 4 точки (x,y) - старт,
      // управление1, управление2, end
         Point start = new Point(500, 100);
         Point control1 = new Point( 500, 300);
         Point control2 = new Point( 700, 300 );
         Point finish = new Point( 800, 100);
         g.DrawBezier(new Pen(Color.Black, 3), start, control1, control2, finish);  // это кривая Безье, дальше - для наглядности
         SolidBrush br = new SolidBrush(Color.Green);
         Pen pen = new Pen(Color.Green, 1);
         g.FillEllipse(br, start.X-5, start.Y-5, 11, 11);
         g.FillEllipse(br, control1.X - 5, control1.Y - 5, 11, 11);
         g.FillEllipse(br, control2.X - 5, control2.Y - 5, 11, 11);
         g.FillEllipse(br, finish.X - 5, finish.Y - 5, 11, 11);
         g.DrawLine(pen, start, control1);
         g.DrawLine(pen, finish, control2);
      }

      private void незамкнутаяКриваяБезьеToolStripMenuItem_Click(object sender, EventArgs e)
      {
      // Зададим точки для 2-х кривых Безье - всего 7.
         Point start = new Point(500, 100);
         Point control1 = new Point(750, 50);
         Point control2 = new Point(510, 310);
         Point end1 = new Point(500, 300);
         Point control3 = new Point(490, 310);
         Point control4 = new Point(240, 50);
         Point end2 = new Point(490, 100);
         Point[] bezierPoints =
         {
            start, control1, control2, end1, control3, control4, end2
         };
      // Рисуем
         g.DrawBeziers(new Pen(Color.Red, 3), bezierPoints);
      }

      private void замкнутаяКриваяБезьеToolStripMenuItem_Click(object sender, EventArgs e)
      {
      // Зададим точки для 2-х кривых Безье - всего 7.
         Point start = new Point(600, 150);
         Point control1 = new Point(850, 100);
         Point control2 = new Point(610, 360);
         Point end1 = new Point(600, 350);
         Point control3 = new Point(590, 360);
         Point control4 = new Point(350, 100);
         Point end2 = start;     // первая и третья точки совпадают
         Point[] bezierPoints =
         {
            start, control1, control2, end1, control3, control4, end2
         };
      // Рисуем
         g.DrawBeziers(new Pen(Color.Orange, 4), bezierPoints);
     }
  }
}

Внешний вид формы при выборе другой позиции меню после рисования двух фигур (Незамкнутый сплайн и Замкнутая кривая Безье) выглядит так:

СплайнБезье
Если после очистки экрана  задать 1-4 пункты подменю «Выбор кривой», то мы увидим четыре сплайна типа «Звезда»:

СплайнБезье2
Если после очистки экрана  задать 5-7 пункты подменю «Выбор кривой», то мы увидим 3 кривых Безье, где красный (незамкнутый) и желтый (замкнутый) контуры состоят всего из двух кривых Безье.

СплайнБезье3
Поэкспериментировав с параметрами контуров, задаваемых сплайнами и кривыми Безье, вы сможете глубже понять принципы векторной графики.

Перейдем к рассмотрению примера рисования и защиты произвольных областей на холсте.

Оставьте комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *