Creating interface elements using VectorApi Unity UI Toolkit

In the recent past I talked about how you can create your own custom elements through mesh generationit will be useful to read to understand many aspects of this article.

And today, we will create custom elements for interfaces, but using VectorApi V UI Toolkit'e engine Unity.

After reading this article you will learn:

  1. What's happened painter2D

  2. How to create elements using it

  3. How to surprise your colleagues with your knowledge in the field of creating interfaces

Chapter 1: Lines!

Just like in the previous part, we will write our own classes, inheriting from the class VisualElement (is the base class for creating a custom interface element).

Let's move from simple to complex, using code as an example:

using UnityEngine;
using UnityEngine.UIElements;

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

        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();

       }
    }
}

In this case, we will consider the class EmojiIconElement.

Yes, this is an interpretation of this smiley – |_|

Yes, this is an interpretation of this smiley – |_|

Within the framework of this chapter, the method will be considered GenerateVisualContent(...) and its internals, but about the constructor, base class and UxmlFactory I already talked about it in the previous article, in the chapter Mesh and triangle!

I won’t delay any longer with the introduction, let’s look at our code.

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();
}

As I said earlier, we are interested in the GenerateVisualContent method.

In short, it is activated when our VisualElement'y will need to render our element or regenerate itself (this usually happens if there have been changes to the UI element).

Four variables top, left, right, bottom are needed to simplify working with positions within our UI element.

It is also important to note that these variable values ​​will change depending on the size of the UI element (in UI Builder / in the build), and due to this our UI element will be scaled relative to the size of the screen and the element itself.

It is on this very contentRect that our generation will take place.

It is on this very contentRect that our generation will take place.

Next we have a description of our painter2D and here we will go into more detail.

In official terms, this is a class that allows you to draw vector graphics.

– How exactly do you draw vector graphics?

– Good question!

It provides various API calls using which you can draw lines, arcs, curves.

Also, it has various properties that affect the result of the sketch:

  • lineWidth – responsible for the line width

  • strokeColor – stroke color

  • fillColor – fill color

  • lineJoin – what the lines will look like when connected

  • lineCap – what the ends of the lines will look like

More clearly what it looks like

More clearly what it looks like

With the preparation of our painter2D We’ve decided, then let’s look at how to work with paths in graphical programming.

In the context of graphics programming, a “path” is a sequence of geometric shapes, such as lines, curves, rectangles, and circles, that define the shape or outline of an object.

The path can be open or closed.

  1. The beginning of a new journey: This step defines the start of a new vector path. When calling BeginPath() you start writing down drawing commands for the new path.

  2. Adding drawing commands: After starting a new path you add drawing commands like ArcTo(), LineTo()and others, to create shapes and geometric objects your way.

  3. Completing the journey: After you have drawn all the necessary shapes and geometric objects for the current path, you call ClosePath() to complete this path.
    This tells the graphics engine that you have finished drawing that path and that it should render it.

Let's now go back to our example and see what we're doing:

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();

We start with the team BeginPath() after which we call the method MoveTo(Vector2 pos) – which moves the drawing point to a new position from which the following commands will be executed.

Next comes the method LineTo(Vector2 pos) as the name suggests, it draws a straight line from the current position painter2D to the position specified in the method argument.

Next are two packets of commands that move the drawing cursor and draw a line.

At the end, we can notice the method Stroke() – which directly draws the outline of the current path that we defined earlier.

After calling the method, the current path will be drawn on the canvas using the current stroke style, such as color and line weight.

Congratulations, you now have a custom UI element!

Chapter 2: Curves!

We have two different options for being able to draw curved lines:

  1. Method BezierCurveTo() generates a cubic Bezier curve from two control points and the end position of a cubic Bezier curve.

  2. Method QuadraticCurveTo() generates a quadratic Bezier curve from a control point and the end position of a quadratic Bezier curve.

Let's consider their use:

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

Bezier curve

And also give the code for the second example:

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

Quadratic Bezier curve

For a deeper understanding of Bezier curves, read Wikipedia.

Chapter 3: Arcs!

The following methods can be used to draw arcs:

  1. Method Arc() Creates an arc based on the provided arc center, radius, and start and end angles.

  2. Method ArcTo()creates an arc between two straight segments.

As part of drawing arcs, it’s also worth talking about filling the path.

When at the end of the road we get closed figurethen we can paint it some color by calling the method painter2D.Fill().

Let's look at an example of constructing an arc using the method 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();

And just like that, the parameter painter2D.FillColor answers what color the filled area will be.

Red stroke and blue fill

Red stroke and blue fill

And using the method painter2D.ArcTo()you can draw a curve:

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();
Curve through arc

Curve through arc

You might have probably wondered whether it is possible using the method Arc() construct a circle or pie chart.

– Well, of course you can.

You can view it from official documentation from Unity.

Chapter 4: The rest?

Here I want to consider what was not included in other chapters, and other comments on drawing.

The first thing I want to talk about is holes in the fill.

When you call Fill() To fill the area contained within a path, you can also create “holes” in that painted area using additional subpaths.

To create a hole you must create an additional subpath with MoveTo()and then use a fill rule to determine which areas will be filled in and which areas will not.

Here are two basic rules for filling:

  1. OddEven (Odd/Even): A ray is drawn from a given point to infinity in any direction and the number of intersections of path segments is counted. If the number of intersections is odd, then the point is considered inside the path; if it is even, the point is considered outside.

  2. NonZero (Not zero): A ray is drawn from a given point to infinity in any direction, and the intersections of path segments are counted. In this case, when segments cross the beam from right to left, the counter decreases, and when from left to right, it increases. If the counter is zero, then the point is considered outside the path, otherwise – inside.

So you can use these rules to create a hole in a filled area by describing a subpath that defines the outline of that hole and using a fill rule to specify how the areas should be filled.

The code below creates a rectangle with an additional subpath that defines the diamond shape inside the rectangle. This diamond will be a “hole” in the filled area of ​​the rectangle.

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);
Rectangle with a hole inside

Rectangle with a hole inside

The second is the ability to customize styles in each subpath.

To do this you need to use methods BeginPath() And ClosePath() and change the values ​​between them 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();
    // Конец третьего подпутя
}
Three different styles

Three different styles

An attentive viewer has already noticed the next feature – gradient support for strokes.

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

Using the property strokeGradient you can draw a stroke through a gradient.

Chapter 5: Finale!

Congratulations on reading this article to the end.

Today we figured out how you can draw custom elements in interfaces, what tools are available for this in the Unity engine.

For a deeper dive and learning how to create interface elements, I recommend the official documentation.

If you have any questions or don’t understand some part, write in the comments, I’ll try to explain what and how 🙂

Thank you for your attention and see you soon!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *