September 1, 2024

08 Hello Canvas


Cross-platform vector graphic rendering is always a challenge. Tellusim SDK solves this problem by providing a low-level Canvas system for vector graphics rendering. Canvas system provides a variety of usefull interfaces for different tasks, and more importantly, each interface can be fully customized with external shaders with control over the graphics API. It works on all supported platforms or can be embedded into your applications.

While the Canvas system focuses solely on rendering, higher-level abstraction layers are required to handle user input or logic. The Tellusim Controls system, for example, is built entirely on Canvas interfaces for rendering.

The base Canvas interface is Canvas class. It is responsible for managing and rendering of all CanvasElement classes. The built-in shaders support antialiased rendering, stroke and gradient styles, and different texture filtering modes. Multiple Canvas instances can be combined together to create multiple interface layers or abstractions. Another important feature is compatibility with 3D rendering mode for virtual reality applications.

To create Canvas instance a Device instance is required alongside with color and depth formats of the render target. Formats can be provided manually or via Target instance. All required resources are created within the Canvas::create() method, making the Canvas::draw() method very efficient, so it can be called multiple times, for example, for stereo rendering.

A user-defined PipelineCallback can be used for additional Pipeline customization, such as different shaders with additional resources. Canvas rendering is even simpler than creation and requires only Command and Target instances.

// create canvas resources for target with dynamic font resolution
if(!canvas.create(device, target, 100)) return false;

// window target
target.begin();
{
    // create command list
    Command command = device.createCommand(target);

    // draw canvas
    canvas.draw(command, target);
}
target.end();

CanvasElement is the base interface element of the Canvas system. It provides basic control over transformation, graphics Pipeline, Sampler, and Texture parameters. Any additional per-element resources can be provided in the used-defined DrawCallback. Each CanvasElement has its own rendering order index that is used during rendering. Multiple elements can be grouped together with stack operations for color, transformation, or scissor parameters. Stack operations include Push, Pop, Set, Mul, and Get commands. For example, the first CanvasElement in the group can define transformation and scissor rectangle, while the following elements will inherit these parameters. The last CanvasElement in the group can perform a Pop operation to restore values. Each element can have a custom alignment for automatic positioning within the parent Canvas.

CanvasText is responsible for a simple texture-based text rendering. FontStyle structure provides control over color, spacing, shadow, and style parameters. Multiple text elements can be combined into a single element by using the FontBatch structure, where each batch has unique text, position, and optional style. It’s not the best solution when the element must be large due to high texture memory consumption, but it is ideal for 2D text rendering performance on low-level or embedded devices. Tellusim ControlText interface is based on CanvasText.

// create text
CanvasText text(canvas);
text.setFontName("font.ttf");
text.getFontStyle().offset = Vector3f(6.0f, -6.0f, 0.0f);
text.setFontSize(48);
text.setOrder(order++);

// create font batches
const FontBatch font_batches[] = {
    FontBatch(Vector3f(256.0f, 64.0f, 0.0f), "Rect"),
    FontBatch(Vector3f(768.0f, 64.0f, 0.0f), "Triangle"),
    FontBatch(Vector3f(1280.0f, 64.0f, 0.0f), "Ellipse"),
    FontBatch(Vector3f(256.0f, 448.0f, 0.0f), "Quadratic"),
    FontBatch(Vector3f(768.0f, 448.0f, 0.0f), "Cubic"),
    FontBatch(Vector3f(1280.0f, 448.0f, 0.0f), "Strip"),
};
text.setBatches(font_batches, TS_COUNTOF(font_batches));

CanvasMesh can render user-defined meshes formed from vertex and index buffers. 3D position, 2D texture coordinates, and color attributes allow the drawing of any UI geometry. Texture coordinates can control texture sampling in texture mode or gradient color in gradient mode.

CanvasRect is responsive for rendering rectangle-like elements. It supports rounded corners and StrokeStyle for frame-like styling. The background can be filled with an input texture with custom texture coordinates or by a gradient from GradentStyle. CanvasRect is always antialiased. Tellusim ControlPanel|Dialog uses CanvasRect as a background element.

// create rectangle
CanvasRect rect(canvas, 32.0f);
rect.setMode(CanvasElement::ModeGradient);
rect.setStrokeStyle(StrokeStyle(8.0f, -8.0f, Color::black));
rect.setGradientStyle(GradientStyle(1.5f, Vector2f(0.0f, 1.0f), Color::blue, Color::magenta));
rect.setOrder(order++);

CanvasTriangle has all features of CanvasRect with additional control over the shape by providing three independent positions.

CanvasEllipse can draw circles or custom ellipse shapes, with support for stroke and gradient styles and two ellipse focus positions.

CanvasShape is an advanced element that supports quadratic (3 control points) and cubic (4 control points) spline rendering. It is defined by a sequence of control points that form the shape. Multiple shapes can be combined into a single CanvasShape by using the sequence of the same control points. Lines must have only two different points in the sequence. CanvasShape supports shapes with holes, but they must have different winding orders and be represented as a new shape in sequence. The rendering is always antialiased, and stroke and gradient styles are supported. CanvasShape can be created manually from control points or from a string with an SVG path declaration.

// create quadratic shape
CanvasShape shape(canvas);
shape.setMode(CanvasElement::ModeGradient);
shape.setStrokeStyle(StrokeStyle(8.0f, 8.0f, Color::black));
shape.setGradientStyle(GradientStyle(1.0f, Vector2f(0.0f, 0.0f), Color::blue, Color::cyan));
shape.setTexCoord(-128.0f, 128.0f, -128.0f, 128.0f);
{
    Vector3f old_position;
    float32_t radius = 96.0f;
    for(uint32_t i = 0; i < 7; i++) {
        float32_t angle = Pi2 * i / 6.0f;
        Vector3f position = Vector3f(sin(angle) * radius, cos(angle) * radius, 0.0f);
        if(i) {
            shape.addPosition(old_position);
            shape.addPosition(old_position + position);
            shape.addPosition(position);
        }
        old_position = position;
    }
}
shape.setOrder(order++);

CanvasStrip is the best choice for antialiased line and curve rendering. It supports variable line width and stroke style.

Antialiased rendering does not require MSAA render targets.

Example of a WebGL/GPU application with different Canvas elements (click on the image to open the demo):