Guide

Guide Rendering Graphics

Graphics

Draw procedural shapes and mesh-based geometry.

Intro ~3 min read

What you'll learn

  • draw procedural shapes with Graphics
  • fill, stroke, and position drawn geometry

Before you start

Graphics

Graphics is a Container that builds vector shapes and filled meshes procedurally at runtime. Each call to drawCircle, drawRectangle, drawLine, or any of the path commands appends a new colored Mesh child. Graphics inherits full filter, blend, tint, and mask support from Container, so every shape you draw participates in the scene graph the same way a Sprite would.

Use Graphics when you need shapes that are defined in code rather than loaded from an image — UI frames, debug overlays, particle decals, health bars, level-editor annotations, or any geometry you assemble from primitives.

Fill, stroke, and the active color

Graphics holds a fillColor and a lineColor, both Color instances. When you call a fill shape method (drawRectangle, drawCircle, drawPolygon, drawStar, drawEllipse), the shape is filled with the current fillColor. When lineWidth is greater than zero, the shape is also outlined with the current lineColor.

import { Color, Graphics } from '@codexo/exojs';

const g = new Graphics();
g.fillColor = Color.tomato;
g.lineColor = Color.darkRed;
g.lineWidth = 3;

g.drawRectangle(0, 0, 120, 80);
g.drawCircle(200, 40, 40);

Each draw call creates a separate Mesh. Changing fillColor between calls produces multi-colored output without creating a new Graphics instance:

g.fillColor = Color.tomato;
g.drawCircle(0, 0, 30);

g.fillColor = Color.steelBlue;
g.drawRectangle(40, -20, 60, 40);

Built-in shapes

Seven shape methods cover the common cases. All coordinates are in the Graphics object’s local space and participate in its transform.

Rectangles:

g.drawRectangle(x, y, width, height);

Circles:

g.drawCircle(centerX, centerY, radius);

Ellipses:

g.drawEllipse(centerX, centerY, radiusX, radiusY);

Polygons from a flat [x0, y0, x1, y1, ...] array:

g.drawPolygon([0, -30, 30, 30, -30, 30]); // triangle

Stars with configurable points, inner radius, and rotation in radians (default inner radius is half the outer radius):

g.drawStar(centerX, centerY, points, radius, innerRadius, rotationRadians);

Lines between two explicit points (does not use or affect the pen position):

g.drawLine(startX, startY, endX, endY);

Paths from a flat point array, drawn as a stroked polyline:

g.drawPath([x0, y0, x1, y1, x2, y2, ...]);

Path and pen commands

Graphics tracks a cursor position. Path commands move the cursor and emit stroked line segments:

g.moveTo(50, 50);
g.lineTo(150, 100);
g.lineTo(100, 200);

// Quadratic Bézier curve: one control point
g.moveTo(20, 20);
g.quadraticCurveTo(cpX, cpY, toX, toY);

// Cubic Bézier curve: two control points
g.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, toX, toY);

// Arc tangent to two lines through (x1,y1)
g.arcTo(x1, y1, x2, y2, radius);

// Explicit arc
g.drawArc(centerX, centerY, radius, startAngle, endAngle, anticlockwise);

Each of these emits a drawPath call behind the scenes. The currentPoint property reflects where the pen currently sits.

Clearing and reusing

clear() removes all child meshes and resets the pen state (position, colors, line width). The Graphics object itself stays alive. This is the right pattern for per-frame redrawing:

update(delta) {
    this.g.clear();

    this.g.fillColor = Color.tomato;
    for (let i = 0; i < this.values.length; i++) {
        const h = this.values[i] * 200;
        this.g.drawRectangle(i * 30, -h, 26, h);
    }
}

For shapes that do not change, create the Graphics once in init and never call clear. The meshes live as children and render identically each frame.

Graphics in the scene graph

Because Graphics extends Container, it inherits position, rotation, scale, and origin. A single Graphics object with several drawn shapes behaves as a group — rotating the Graphics rotates all its shape children together:

init(loader) {
    this.group = new Graphics();
    this.group.fillColor = Color.goldenrod;
    this.group.drawCircle(-40, 0, 20);
    this.group.drawCircle(40, 0, 20);
    this.group.drawRectangle(-10, -15, 20, 30);

    this.group.setPosition(this.app.canvas.width / 2, this.app.canvas.height / 2);
    this.addChild(this.group);
}

update(delta) {
    this.group.rotate(60 * delta.seconds);
}

Graphics vs. Mesh

Graphics is a convenience builder on top of Mesh. If you need direct control over vertex indices, UVs, per-vertex colors, textures, or custom shaders, construct a Mesh directly. Graphics is the right choice when the geometry is simple enough to describe with shape primitives and you want the immediate-mode API.

Performance

Each draw method on Graphics creates a new Mesh child. A tight loop that calls drawCircle 500 times per frame creates 500 meshes per frame. For static shapes this is fine — create once, never clear. For per-frame redrawing of many shapes, consider whether a single Mesh with a custom vertex buffer, a ParticleSystem, or a pre-built Graphics with transform-only animation is a better fit.

Examples

Graphics Primitives WebGPU Open in Playground View source
import { Application, Color, Container, Graphics, Scene } from '@codexo/exojs';

const app = new Application({
    canvas: {
        width: 1280,
        height: 720,
        mount: document.body,
        sizingMode: 'fit',
    },
    clearColor: Color.midnightBlue,
    backend: { type: 'webgpu' },
});

class GraphicsPrimitivesScene extends Scene {
    private sceneRoot!: Container;
    private panel!: Graphics;
    private circle!: Graphics;
    private diamond!: Graphics;
    private star!: Graphics;

    override init(): void {
        const { width, height } = this.app.canvas;

        this.sceneRoot = new Container();
        this.sceneRoot.setPosition(width / 2, height / 2);

        this.panel = new Graphics();
        this.panel.fillColor = Color.darkSlateBlue;
        this.panel.drawRectangle(-190, -130, 380, 260);

        this.circle = new Graphics();
        this.circle.fillColor = Color.tomato;
        this.circle.drawCircle(-92, -6, 48);

        this.diamond = new Graphics();
        this.diamond.fillColor = Color.goldenrod;
        this.diamond.drawPolygon([0, -70, 70, 0, 0, 70, -70, 0]);

        this.star = new Graphics();
        this.star.fillColor = Color.mediumSeaGreen;
        this.star.drawStar(108, 12, 5, 58, 26, -18);

        this.sceneRoot.addChild(this.panel, this.circle, this.diamond, this.star);
    }

    override update(delta): void {
        this.sceneRoot.rotate(delta.seconds * 9);
        this.star.rotate(delta.seconds * 60);
        this.circle.y = Math.sin(this.app.activeTime.seconds * 2) * 18;
    }

    override draw(context): void {
        context.backend.clear();
        context.render(this.sceneRoot);
    }

    override unload(): void {
        this.sceneRoot?.destroy();
    }

    override destroy(): void {
        this.sceneRoot?.destroy();
    }
}

app.start(new GraphicsPrimitivesScene()).catch(() => {
    app.canvas.remove();
    app.destroy();
});

All seven shape primitives on one canvas, each animated differently.

Mesh Deformed Grid Open in Playground View source
import { Application, Color, Mesh, Scene, Texture } from '@codexo/exojs';

const app = new Application({
    canvas: {
        width: 1280,
        height: 720,
        mount: document.body,
        sizingMode: 'fit',
    },
    clearColor: Color.black,
});

const COLS = 16;
const ROWS = 16;
const SIZE = 460;

function buildGrid(): { vertices: Float32Array; uvs: Float32Array; indices: Uint16Array } {
    const half = SIZE / 2;
    const stepX = SIZE / COLS;
    const stepY = SIZE / ROWS;
    const vertices = new Float32Array((COLS + 1) * (ROWS + 1) * 2);
    const uvs = new Float32Array(vertices.length);
    const indices = new Uint16Array(COLS * ROWS * 6);

    let v = 0;
    let u = 0;
    for (let r = 0; r <= ROWS; r++) {
        for (let c = 0; c <= COLS; c++) {
            vertices[v++] = -half + c * stepX;
            vertices[v++] = -half + r * stepY;
            uvs[u++] = c / COLS;
            uvs[u++] = r / ROWS;
        }
    }

    let i = 0;
    for (let r = 0; r < ROWS; r++) {
        for (let c = 0; c < COLS; c++) {
            const tl = r * (COLS + 1) + c;
            const tr = tl + 1;
            const bl = tl + (COLS + 1);
            const br = bl + 1;
            indices[i++] = tl;
            indices[i++] = tr;
            indices[i++] = br;
            indices[i++] = tl;
            indices[i++] = br;
            indices[i++] = bl;
        }
    }

    return { vertices, uvs, indices };
}

const UV_GRID = assets.technical.filtering.uvGrid256;

class MeshDeformedGridScene extends Scene {
    private restVertices!: Float32Array;
    private mesh!: Mesh;
    private time = 0;

    override async load(loader): Promise<void> {
        await loader.load(Texture, { uvGrid: UV_GRID });
    }

    override init(loader): void {
        const { width, height } = this.app.canvas;
        const grid = buildGrid();

        this.restVertices = grid.vertices.slice();
        this.mesh = new Mesh({
            vertices: grid.vertices,
            uvs: grid.uvs,
            indices: grid.indices,
            texture: loader.get(Texture, 'uvGrid'),
        });
        this.mesh.setPosition((width / 2) | 0, (height / 2) | 0);
    }

    override update(delta): void {
        this.time += delta.seconds;
        const verts = this.mesh.vertices;
        const rest = this.restVertices;
        const t = this.time;

        for (let i = 0; i < verts.length; i += 2) {
            const rx = rest[i];
            const ry = rest[i + 1];
            verts[i] = rx + Math.sin(t * 2 + ry * 0.04) * 14;
            verts[i + 1] = ry + Math.cos(t * 1.6 + rx * 0.03) * 10;
        }

        this.mesh.recomputeLocalBounds();
    }

    override draw(context): void {
        context.backend.clear();
        context.render(this.mesh);
    }
}

app.start(new MeshDeformedGridScene());

Animated vertex deformation on a Mesh — the lower-level primitive Graphics builds on.

Where to go next

The next chapter, Sprites, covers the primary textured drawable — loading images, positioning, sizing, and the relationship between textures and sprites.