🎨 Графическое программирование на C#

← Вернуться к модулю 8

Урок 8.7: Морфинг фигур

Модуль 8 ⏱️ ~1 час

Морфинг форм

Плавный переход фигур достигается интерполяцией каждой пары вершин. Это требует, чтобы фигуры имели одно и то же число точек.

private PointF[] Morph(PointF[] a, PointF[] b, float t)
{
    PointF[] result = new PointF[a.Length];
    for (int i = 0; i < a.Length; i++)
    {
        result[i] = new PointF(
            a[i].X + (b[i].X - a[i].X) * t,
            a[i].Y + (b[i].Y - a[i].Y) * t);
    }
    return result;
}

📝 Пример задания — морфинг фигур

Три фигуры (круг, квадрат, звезда) с 12 вершинами интерполируются циклически. Параметр morphT от 0 до 1 делит цикл на три фазы перехода:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace CSharpGraphicsApp
{
    public class Task27_MorphingShapes : Form
    {
        private float morphT = 0;
        private readonly Timer timer = new Timer();
        private const int ShapePoints = 12;

        public Task27_MorphingShapes()
        {
            Text = "Задание 27 — Морфинг фигур (Morphing Shapes)";
            Size = new Size(800, 600);
            BackColor = Color.White;
            DoubleBuffered = true;
            Paint += Form_Paint;

            timer.Interval = 16;
            timer.Tick += (s, e) => { morphT += 0.02f; if (morphT > 1) morphT -= 1; Invalidate(); };
            timer.Start();
            FormClosed += (s, e) => timer.Stop();
        }

        private PointF[] GetCirclePoints(float cx, float cy, float r)
        {
            var pts = new PointF[ShapePoints];
            for (int i = 0; i < ShapePoints; i++)
            {
                float a = (float)(i * 2 * Math.PI / ShapePoints);
                pts[i] = new PointF(cx + r * (float)Math.Cos(a), cy + r * (float)Math.Sin(a));
            }
            return pts;
        }

        private PointF[] GetSquarePoints(float cx, float cy, float size)
        {
            var pts = new PointF[ShapePoints];
            for (int i = 0; i < ShapePoints; i++)
            {
                float a = (float)(i * 2 * Math.PI / ShapePoints);
                float x = size * (float)Math.Cos(a);
                float y = size * (float)Math.Sin(a);
                float maxVal = Math.Max(Math.Abs(x), Math.Abs(y));
                pts[i] = new PointF(cx + (x / maxVal) * size, cy + (y / maxVal) * size);
            }
            return pts;
        }

        private PointF[] GetStarPoints(float cx, float cy, float r)
        {
            var pts = new PointF[ShapePoints];
            for (int i = 0; i < ShapePoints; i++)
            {
                float a = (float)(i * 2 * Math.PI / ShapePoints);
                float rad = (i % 2 == 0) ? r : r * 0.4f;
                pts[i] = new PointF(cx + rad * (float)Math.Cos(a), cy + rad * (float)Math.Sin(a));
            }
            return pts;
        }

        private PointF[] MorphShapes(PointF[] s1, PointF[] s2, float t)
        {
            var result = new PointF[s1.Length];
            for (int i = 0; i < s1.Length; i++)
                result[i] = new PointF(
                    s1[i].X + (s2[i].X - s1[i].X) * t,
                    s1[i].Y + (s2[i].Y - s1[i].Y) * t);
            return result;
        }

        private void Form_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            e.Graphics.Clear(Color.White);

            float cx = ClientSize.Width / 2f;
            float cy = ClientSize.Height / 2f;
            float r = 100;

            var circle = GetCirclePoints(cx, cy, r);
            var square = GetSquarePoints(cx, cy, r);
            var star   = GetStarPoints(cx, cy, r);

            PointF[] current;
            string name;
            if (morphT < 0.33f)
            {
                float t = morphT / 0.33f;
                current = MorphShapes(circle, square, t);
                name = $"Circle → Square ({(int)(t * 100)}%)";
            }
            else if (morphT < 0.66f)
            {
                float t = (morphT - 0.33f) / 0.33f;
                current = MorphShapes(square, star, t);
                name = $"Square → Star ({(int)(t * 100)}%)";
            }
            else
            {
                float t = (morphT - 0.66f) / 0.34f;
                current = MorphShapes(star, circle, t);
                name = $"Star → Circle ({(int)(t * 100)}%)";
            }

            using (var brush = new SolidBrush(Color.FromArgb(150, Color.Blue)))
            using (var pen = new Pen(Color.DarkBlue, 3))
            {
                e.Graphics.FillPolygon(brush, current);
                e.Graphics.DrawPolygon(pen, current);
            }
            using (var brush = new SolidBrush(Color.Red))
                foreach (var pt in current)
                    e.Graphics.FillEllipse(brush, pt.X - 4, pt.Y - 4, 8, 8);

            e.Graphics.DrawString("Морфинг фигур",
                new Font("Segoe UI", 12, FontStyle.Bold), Brushes.Black, 10, 10);
            e.Graphics.DrawString(name,
                new Font("Segoe UI", 11), Brushes.DarkBlue, 10, 40);
        }
    }
}

Практическое задание

  • Создайте последовательность фигур с одинаковым числом точек.
  • Реализуйте интерполяцию между ними.
  • Анимируйте переключение форм во времени.
  • Добавьте подпись текущего перехода.

Задание по аналогии: Морфинг фигур

По аналогии с Task27_MorphingShapes создайте анимацию плавного перехода между тремя фигурами.

  1. Объявите константу const int ShapePoints = 12 и поле float morphT = 0. В Timer_Tick увеличивайте morphT += 0.02f и сбрасывайте: if (morphT > 1) morphT -= 1.
  2. Реализуйте методы GetCirclePoints, GetSquarePoints, GetStarPoints — каждый возвращает массив из ShapePoints точек типа PointF[]. Все три фигуры должны иметь одинаковое число вершин.
  3. Реализуйте метод MorphShapes(PointF[] s1, PointF[] s2, float t): для каждой пары вершин вычислите result[i].X = s1[i].X + (s2[i].X - s1[i].X) * t.
  4. В Form_Paint определите текущую фазу по morphT (0..0.33 = circle→square, 0.33..0.66 = square→star, 0.66..1.0 = star→circle). Нарисуйте текущую фигуру через FillPolygon и DrawPolygon.
  5. Дополнительно: отмечайте вершины красными точками через FillEllipse — это помогает увидеть, как каждый узел перемещается при морфинге.

Пример результата выполнения задания:

Результат Task27

Скриншот программы (Task27)

💡 Совет: Для квадрата вычислите точки через cos/sin на окружности, а затем нормализуйте на max(|x|, |y|) — это даёт скруглённый квадрат. Увеличьте ShapePoints до 24 для более плавного морфинга.

Тест

Почему фигуры должны иметь один и тот же набор вершин? Что происходит при разном количестве точек?