Skip to content

Shapes and meshes ​

willow draws shapes without any image: rectangles, lines, polygons and raw triangle meshes. They are ordinary nodes, so they move, rotate, fade and take input like sprites.

Rectangles and lines ​

ts
import { Quad } from "willow";

class Bars {
    static main(): void {
        const back: Quad = new Quad(200, 20, Color.hex("#3A1010"));
        back.setPosition(20, 20);
        engine.root2d.add(back);

        const health: Quad = new Quad(200, 20, Color.hex("#E04040"));
        health.setPosition(20, 20);
        health.scaleX = 0.6;
        engine.root2d.add(health);

        const laser: Quad = Quad.line(100, 300, 500, 220, 3, Color.hex("#7CF2FF"));
        laser.blendMode = "add";
        engine.root2d.add(laser);
    }
}
  • new Quad(w, h, color) is a solid rectangle. Change it with color, width and height.
  • Scaling a bar with scaleX from its left edge is the easy way to draw a meter.
  • Quad.line(x1, y1, x2, y2, thickness, color) makes a line as a thin rotated quad, with the position and rotation set for you.

Polygons ​

Polygon fills a shape given as a flat list of points [x0, y0, x1, y1, ...]. Factories cover the common shapes:

FactoryShape
new Polygon(points, color)Any simple polygon, at least three points.
Polygon.regular(sides, radius, color)A regular polygon centered on the origin.
Polygon.circle(radius, color)A circle (a 32-sided regular polygon).
Polygon.star(points, outer, inner, color)A star.
Polygon.triangle(x1, y1, x2, y2, x3, y3, color)A triangle.
Polygon.textured(texture, points)A polygon filled with an image, mapped across its bounding box.
ts
import { Polygon } from "willow";

class Shapes {
    static main(): void {
        const sun: Polygon = Polygon.circle(40, Color.hex("#FFD24A"));
        sun.setPosition(120, 120);
        engine.root2d.add(sun);

        const star: Polygon = Polygon.star(5, 30, 12, Color.WHITE);
        star.setPosition(300, 120);
        engine.root2d.add(star);

        const hill: Polygon = new Polygon([0, 600, 0, 450, 200, 380, 450, 430, 800, 360, 800, 600], Color.hex("#2F6B3A"));
        engine.root2d.add(hill);

        App.onFrame((dt: float): void => {
            star.rotation += dt;
        });
    }
}

Polygon colours are full Color values with alpha. If you write a raw number, use all eight hex digits (0xRRGGBBAA): a six-digit number has an alpha of 0 and is invisible.

Replace a polygon's points with setPoints(points, color?).

Meshes ​

Mesh draws your own triangles. Vertices are flat, eight numbers each: x, y, u, v, r, g, b, a (colours 0 to 1). Indices list three vertices per triangle.

ts
import { Mesh } from "willow";

class Gradient {
    static main(): void {
        const vertices: Array<float> = [
            0, 0, 0, 0, 1, 0, 0, 1,
            300, 0, 1, 0, 0, 1, 0, 1,
            300, 200, 1, 1, 0, 0, 1, 1,
            0, 200, 0, 1, 1, 1, 0, 1,
        ];
        const indices: Array<int> = [0, 1, 2, 0, 2, 3];
        const quad: Mesh = new Mesh(vertices, indices);
        quad.setPosition(250, 200);
        engine.root2d.add(quad);
    }
}

tint multiplies every vertex colour at draw time, so recolouring a mesh does not rebuild it. After editing vertices with setVertices, willow updates the bounds; call invalidateBounds() only if you changed them some other way.

Custom paint ​

For full control, give a node a paint function with setCustomPaint. It runs each frame instead of the node's normal drawing and receives a Painter.

ts
import { Node, Painter, TrianglesPaint } from "willow";

class Painted {
    static main(): void {
        const node: Node = new Node("painted");
        node.setPosition(400, 300);
        engine.root2d.add(node);

        node.setCustomPaint((painter: Painter): void => {
            const m: Array<float> = painter.worldTransform();
            const px: Array<float> = [0, -40, 40, 30, -40, 30];
            const verts: Array<float> = [];
            for (let i: int = 0; i < 3; i++) {
                const x: float = px[i * 2];
                const y: float = px[i * 2 + 1];
                verts.push(m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5], 0, 0, 1, 0.5, 0, 1);
            }
            painter.appendTriangles(new TrianglesPaint(verts, [0, 1, 2]));
        });
    }
}
  • Painted vertices are in world space. worldTransform() returns [a, b, c, d, tx, ty]; a local point (x, y) maps to (a*x + c*y + tx, b*x + d*y + ty). The camera is applied afterwards.
  • painter.paintDefault() draws the node's normal content too, so you can add to a sprite rather than replace it.
  • painter.isBuildingCache() is true while a cached subtree is being recorded; skip animated extras then.
  • The Painter is only valid during the call. Don't keep it.
  • Pass null to setCustomPaint to remove the function.

Next ​