Между строк: Создание элементов интерфейса через VectorApi Unity UI Toolkit

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

А сегодня, мы будем создавать кастомные элементы для интерфейсов, но уже используя VectorApi в UI Toolkit'е движка Unity.

Прочитав эту статью вы узнаете:

  1. Что такое painter2D

  2. Как создавать элементы используя его

  3. Как удивить коллег своими знаниями в области создания интерфейсов

Глава 1: Линии!

Также как и в предыдущей части, мы будем писать свои классы, наследуюсь от класса VisualElement (является базовым классом для создания кастомного элемента интерфейса).

Давайте будем переходить от простого к сложному, на примере кода:

using UnityEngine;
using UnityEngine.UIElements;

namespace CustomElements
{
    public class EmojiIconElement : VisualElement
    {
        public new class UxmlFactory : UxmlFactory { }

        public EmojiIconElement()
        {
            generateVisualContent += GenerateVisualContent;
        }
        
        private void GenerateVisualContent(MeshGenerationContext mgc)
        {
            var top = 0;
            var left = 0f;
            var right = contentRect.width;
            var bottom = contentRect.height;
            
            var painter2D = mgc.painter2D;
            painter2D.lineWidth = 10.0f;
            painter2D.strokeColor = Color.white;
            painter2D.lineJoin = LineJoin.Bevel;
            painter2D.lineCap = LineCap.Round;
            

            painter2D.BeginPath();
            
            painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
            painter2D.LineTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(bottom * 0.7)));
            
            painter2D.MoveTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
            painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(bottom * 0.7)));
            
            painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.3)), (float)(bottom * 0.8)));
            painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.3)), (float)(bottom * 0.8)));
            
            painter2D.Stroke();

       }
    }
}

В данном случае, мы рассмотрим класс EmojiIconElement.

Да, это интерпретация вот этого смайла – |_|

Да, это интерпретация вот этого смайла — |_|

В рамках этой главы, будет рассмотрен метод GenerateVisualContent(...) и его внутренности, а про конструктор, базовый класс и UxmlFactory я уже рассказывал в предыдущей статье, в главе Mesh и треугольник!

Не буду тянуть больше со вступлением, рассмотрим наш код.

private void GenerateVisualContent(MeshGenerationContext mgc)
{
    var top = 0;
    var left = 0f;
    var right = contentRect.width;
    var bottom = contentRect.height;
            
    var painter2D = mgc.painter2D;
    painter2D.lineWidth = 10.0f;
    painter2D.strokeColor = Color.white;
    painter2D.lineJoin = LineJoin.Bevel;
    painter2D.lineCap = LineCap.Round;
            

    painter2D.BeginPath();
            
    painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
    painter2D.LineTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(bottom * 0.7)));
            
    painter2D.MoveTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
    painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(bottom * 0.7)));
            
    painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.3)), (float)(bottom * 0.8)));
    painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.3)), (float)(bottom * 0.8)));
            
    painter2D.Stroke();
}

Как я и говорил ранее, нас интересует метод GenerateVisualContent.

Если кратко, он активируется, когда нашему VisualElement'у будет необходимо отобразить наш элемент или перегенерировать себя (это, как правило, происходит, если были изменения в UI элементе).

Четыре переменные top, left, right, bottom нужны для упрощения работы с позициями в рамках нашего UI элемента.

Также важно отметить, что эти значения переменных будет меняться в зависимости от размеров UI элемента (в UI Builder / в билде), и благодаря этому наш UI элемент будет масштабироваться относительно размера экрана и самого элемента.

Именно на этом самом contentRect'e и будет происходить наша генерация.

Именно на этом самом contentRect’e и будет происходить наша генерация.

Дальше у нас идет описание нашего painter2D и здесь мы остановимся подробнее.

Если говорить официальным языком, то это класс, который позволяет рисовать векторную графику.

— А как именно рисовать векторую графику?

— Хороший вопрос!

Он предоставляет различные вызовы API используя которые можно рисовать линии, дуги, кривые.

Также, у него есть различные свойства, которые влияют на результат зарисовки:

  • lineWidth — отвечает за ширину линии

  • strokeColor — цвет обводки

  • fillColor — цвет заливки

  • lineJoin — как будет выглядеть линии при соединение

  • lineCap — как будут выглядеть концы линий

Более наглядно как это выглядит

Более наглядно как это выглядит

C подготовкой нашего painter2D определились, дальше разберем, как устроена работа с путями в графическом программировании.

В контексте графического программирования «путь» (path) представляет собой последовательность геометрических фигур, таких как линии, кривые, прямоугольники и окружности, которые определяют форму или контур объекта.

Путь может быть открытым или закрытым.

  • Открытый путь: Начинается и заканчивается без соединения конечных точек.

  • Закрытый путь: Конечные точки пути соединены, образуя замкнутую форму.

  1. Начало нового пути: Этот шаг определяет начало нового векторного пути. При вызове BeginPath() вы начинаете записывать команды рисования для нового пути.

  2. Добавление команд рисования: После начала нового пути вы добавляете команды рисования, такие как ArcTo(), LineTo(), и другие, для создания форм и геометрических объектов в вашем пути.

  3. Завершение пути: После того как вы нарисовали все необходимые фигуры и геометрические объекты для текущего пути, вы вызываете ClosePath() для завершения этого пути.
    Это указывает графическому движку на то, что вы закончили рисование этого пути, и что он должен его отрисовать.

Давайте теперь вернемся к нашему примеру и разберем, что мы делаем:

painter2D.BeginPath();
            
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(left + (contentRect.width * 0.2)), (float)(bottom * 0.7)));
            
painter2D.MoveTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(top + contentRect.height * 0.1)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.2)), (float)(bottom * 0.7)));
            
painter2D.MoveTo(new Vector2((float)(left + (contentRect.width * 0.3)), (float)(bottom * 0.8)));
painter2D.LineTo(new Vector2((float)(right - (contentRect.width * 0.3)), (float)(bottom * 0.8)));
            
painter2D.Stroke();

Мы начинаем с команды BeginPath(), после чего вызываем метод MoveTo(Vector2 pos) — который перемещает точку рисования на новую позицию, от которой будут выполняться следующие команды.

Следом за ней идет метод LineTo(Vector2 pos), как можно понять из названия, оно проводит прямую линию из текущей позицию painter2D до позиции заданный в аргументе метода.

Далее идет две пачки команды, которые перемещают курсор рисования и чертят линию.

В конце, мы можем заметить метод Stroke() — который непосредственно отрисовывает контур текущего пути, который мы определили ранее.

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

Поздравляю, теперь у вас есть кастомный UI элемент!

Глава 2: Кривые!

У нас есть два разных варианта возможности нарисовать кривые линии:

  1. Метод BezierCurveTo() генерирует кубическую кривую Безье по двум контрольным точкам и конечному положению кубической кривой Безье.

  2. Метод QuadraticCurveTo() генерирует квадратичную кривую Безье по контрольной точке и конечному положению квадратичной кривой Безье.

Рассмотрим их использование:

painter2D.BeginPath();
painter2D.MoveTo(new Vector2(100, 100));
painter2D.BezierCurveTo(new Vector2(150, 150), new Vector2(200, 50), new Vector2(250, 100));
painter2D.Stroke();

Кривая Безье

Кривая Безье

И также приведем код для второго примера:

painter2D.BeginPath();
painter2D.MoveTo(new Vector2(100, 100));
painter2D.QuadraticCurveTo(new Vector2(150, 150), new Vector2(250, 100));
painter2D.Stroke();

Квадратичная кривая Безье

Квадратичная кривая Безье

Для более глубокого понимания кривых Безье, читаем Wikipedia.

Глава 3: Дуги!

Для рисования дуг можно использовать следующие методы:

  1. Метод Arc() создает дугу на основе предоставленного центра дуги, радиуса, а также начального и конечного углов.

  2. Метод ArcTo()создает дугу между двумя прямыми сегментами.

В рамках рисования дуг, так же стоит рассказать про заливку пути.

Когда в конце пути мы получаем замкнутую фигуру, тогда мы можем ее покрасить в какой-то цвет, вызвав метод painter2D.Fill().

Рассмотрим пример построения дуги используя метод Arc() .

painter2D.lineWidth = 2.0f;
painter2D.strokeColor = Color.red;
painter2D.fillColor = Color.blue;

painter2D.BeginPath();

painter2D.MoveTo(new Vector2(100, 100));


painter2D.Arc(new Vector2(100, 100), 50.0f, 10.0f, 95.0f);
painter2D.ClosePath();


painter2D.Fill();
painter2D.Stroke();

И как раз, параметр painter2D.FillColor отвечает какой цвет будет у залитой области.

Красная обводка и синяя заливка

Красная обводка и синяя заливка

А используя метод painter2D.ArcTo(), можно нарисовать кривую:

painter2D.BeginPath();
painter2D.MoveTo(new Vector2(100, 100));
painter2D.ArcTo(new Vector2(150, 150), new Vector2(200, 100), 20.0f);
painter2D.LineTo(new Vector2(200, 100));
painter2D.Stroke();

Кривая через дугу

Кривая через дугу

Вы наверное могли задуматься, а можно ли используя метод Arc(), построить окружность или круговую диаграмму.

— Ну, конечно, можно.

С ней вы можете ознакомиться из официальной документации от Unity.

Глава 4: Остальное?

Здесь хочу рассмотреть, что не вошло в другие главы, и другие комментарии по отрисовке.

Первое, о чем хочется поговорить, это про дыры в заливке.

Когда вы вызываете Fill() для закраски области, содержащейся внутри пути, вы также можете создать «дыры» в этой закрашенной области, используя дополнительные подпути.

Чтобы создать дыру, вы должны создать дополнительный подпуть с помощью MoveTo(), а затем использовать правило заливки (fill rule), чтобы определить, какие области будут закрашены, а какие нет.

Вот два основных правила заливки:

  1. OddEven (Нечетное/Четное): Отрисовывается луч из данной точки в бесконечность в любом направлении и подсчитываются количество пересечений сегментов пути. Если количество пересечений нечетное, то точка считается внутри пути, если четное — снаружи.

  2. NonZero (Не нуль): Отрисовывается луч из данной точки в бесконечность в любом направлении, и подсчитываются пересечения сегментов пути. При этом, когда сегменты пересекают луч справа налево, счетчик уменьшается, а когда слева направо — увеличивается. Если счетчик равен нулю, то точка считается снаружи пути, иначе — внутри.

Таким образом, вы можете использовать эти правила, чтобы создать дыру в заполненной области, описав подпуть, который определяет контур этой дыры, и используя правило заливки для указания, как области должны быть закрашены.

В приведенном коде создается прямоугольник с дополнительным подпутем, который определяет форму ромба (бриллианта) внутри прямоугольника. Этот ромб будет являться «дырой» в заполненной области прямоугольника.

painter2D.BeginPath();
painter2D.MoveTo(new Vector2(10, 10));
painter2D.LineTo(new Vector2(300, 10));
painter2D.LineTo(new Vector2(300, 150));
painter2D.LineTo(new Vector2(10, 150));
painter2D.ClosePath();

painter2D.MoveTo(new Vector2(150, 50));
painter2D.LineTo(new Vector2(175, 75));
painter2D.LineTo(new Vector2(150, 100));
painter2D.LineTo(new Vector2(125, 75));
painter2D.ClosePath();

painter2D.Fill(FillRule.OddEven);

Прямоугольник с отверстием внутри

Прямоугольник с отверстием внутри

Второе, это возможность настраивать стили в каждом подпути.

Для этого нужно использовать методы BeginPath() и ClosePath() и между ними менять значения у painter2D.

private void GenerateVisualContent(MeshGenerationContext mgc)
{
    var painter2D = mgc.painter2D;
    painter2D.lineWidth = 10.0f;

    // Начало первого подпути
    painter2D.BeginPath();
    painter2D.strokeColor = Color.red;
  
    painter2D.MoveTo(new Vector2(50, 50));
    painter2D.LineTo(new Vector2(100, 100));
  
    painter2D.Stroke();
    painter2D.ClosePath();
    // Конец первого подпутя

    // Начало второго подпути
    painter2D.BeginPath();
    painter2D.strokeColor = Color.blue;
  
    painter2D.MoveTo(new Vector2(20, 20));
    painter2D.LineTo(new Vector2(60, 60));
    
    painter2D.Stroke();
    painter2D.ClosePath();
    // Конец второго подпутя

    // Начало третьего подпути
    painter2D.BeginPath();
    painter2D.strokeGradient = new Gradient()
    {
        colorKeys = new GradientColorKey[]
        {
            new() { color = Color.red, time = 0.0f },
            new() { color = Color.blue, time = 1.0f }
        }
    };
    painter2D.fillColor = Color.green;
    
    painter2D.MoveTo(new Vector2(50, 150));
    painter2D.LineTo(new Vector2(100, 200));
    painter2D.LineTo(new Vector2(150, 150));
    
    painter2D.Fill();
    painter2D.Stroke();
            
    painter2D.ClosePath();
    // Конец третьего подпутя
}

Три разных стиля

Три разных стиля

Внимательный зритель уже заметил следующую фишку — поддержка градиента для обводки.

painter2D.strokeGradient = new Gradient()
{
    colorKeys = new GradientColorKey[]
    {
        new() { color = Color.red, time = 0.0f },
        new() { color = Color.blue, time = 1.0f }
    }
};

Используя свойство strokeGradient можно рисовать обводку через градиент.

Глава 5: Финал!

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

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

Для более глубокого погружения и изучения создания элементов интерфейса, рекомендую официальную документацию.

Если у вас остались вопросы или не поняли какую-то часть, напишите в комментариях, постараюсь объяснить, что да как:)

Спасибо за внимание и до скорых встреч!

© Habrahabr.ru