Skip to content

Nodes and transforms ​

Every node has a local transform: where it sits, how it is scaled and rotated, relative to its parent. willow multiplies the transforms down the tree to get each node's world transform. Only nodes whose transform (or an ancestor's) changed are recomputed, so a still subtree costs nothing per frame.

Transform fields ​

FieldMeaningDefault
x, yPosition in the parent's space, in pixels.0
scaleX, scaleYScale factor. 1 is the authored size; negative flips.1
rotationRotation in radians, clockwise on screen.0
skewX, skewYSkew in radians.0
pivotX, pivotYThe point, in the node's own pixels, that x/y place and that rotation and scale turn around.0 (top-left)
alphaOpacity 0 to 1. Multiplies with the parent's alpha.1
visiblefalse hides the node and its whole subtree.true

Shortcuts set two fields at once: setPosition(x, y), setScale(s) or setScale(sx, sy), setSkew(sx, sy), setRotation(r), setPivot(px, py).

Pivots and anchors ​

By default a node's origin is its top-left corner, so it rotates around that corner. Move the pivot to make it spin in place:

CallPivot
setAnchor(0.5, 0.5)The center, as a fraction of the node's size.
setAnchor(0.5, 1.0)Bottom middle: the feet of a character.
setPivot(16, 32)An exact pixel inside the node.
alignPivot()The center of the current content.
alignPivot(Align.CENTER, Align.BOTTOM)Bottom middle of the current content.

alignPivot reads the size once. If the content later changes size, call it again.

setSize(w, h) resizes a node by changing its scale: a 64x64 sprite given setSize(128, 32) gets scaleX = 2 and scaleY = 0.5.

Containers carry their children ​

ts
import { Node, Quad } from "willow";

class Carry {
    static main(): void {
        const ship: Node = new Node("ship");
        ship.setPosition(100, 50);
        ship.setScale(2);

        const turret: Quad = new Quad(20, 20, Color.WHITE);
        turret.setPosition(10, 5);
        ship.add(turret);

        const p: Vec2 = turret.worldPosition();
        console.log("turret in the world: " + p.x + ", " + p.y);

        ship.x = 300;
        const q: Vec2 = turret.worldPosition();
        console.log("after moving the ship: " + q.x + ", " + q.y);
    }
}

Output:

text
turret in the world: 120, 60
after moving the ship: 320, 60

The turret's own x and y never changed. Moving, scaling or rotating ship moves everything inside it.

Converting between spaces ​

MethodFromTo
node.localToWorld(p)The node's local spaceWorld space
node.worldToLocal(p)World spaceThe node's local space
node.toScreen(x, y)The node's local spaceScreen pixels (through the camera for world nodes)
node.fromScreen(x, y)Screen pixelsThe node's local space
node.worldPosition()The node's origin in world space
node.worldBounds()The node's bounding Rect in world space
a.distanceTo(b)World distance between two nodes, in pixels
a.directionTo(b)Unit Vec2 from a toward b

localToWorld and worldToLocal accept any { x, y } value. Each call returns a new Vec2.

ts
import { Node } from "willow";

class Spaces {
    static main(): void {
        const map: Node = new Node("map");
        map.setPosition(200, 100);
        map.setScale(0.5);

        const local: Vec2 = map.worldToLocal(Vec2{ x: 300.0, y: 150.0 });
        console.log("a click at (300, 150) is (" + local.x + ", " + local.y + ") on the map");

        const hero: Node = new Node("hero");
        const goal: Node = new Node("goal");
        hero.setPosition(0, 0);
        goal.setPosition(30, 40);
        console.log("distance: " + hero.distanceTo(goal));
    }
}

Output:

text
a click at (300, 150) is (200, 100) on the map
distance: 50

Names and user data ​

name is a free-form label, useful in logs and for lookups with a NodeIndex. userData holds any value you want to attach to a node; willow never reads it.

Next ​