Sprites and textures
A Texture is an image on the GPU. A TextureRegion is a rectangle inside a texture. A Sprite is a node that draws a texture or a region.
Sprites
Sprite has three constructors:
| Constructor | Use it when |
|---|---|
new Sprite("assets/hero.png") | One sprite uses the image. The sprite loads it. |
new Sprite(texture) | Several sprites share one loaded texture. |
new Sprite(region) | The sprite shows one frame or tile of a larger image. |
ts
import { Sprite, Texture } from "willow";
class Coins {
static main(): void {
const coinTex: Texture = Texture.load("assets/coin.png");
for (let i: int = 0; i < 5; i++) {
const coin: Sprite = new Sprite(coinTex);
coin.setAnchor(0.5, 0.5);
coin.x = 100 + i * 80;
coin.y = 200;
engine.root2d.add(coin);
}
const ghost: Sprite = new Sprite(coinTex);
ghost.x = 100;
ghost.y = 300;
ghost.tint = Color.hex("#80C0FF");
ghost.alpha = 0.5;
engine.root2d.add(ghost);
}
}tint multiplies the image's colours. Color.WHITE (the default) leaves it unchanged. width and height read the size of the region being drawn, before scale.
To change what a sprite shows, call setTexture(texture) or setRegion(region).
Loading needs the fs permission in thicket.json. Texture.load caches by path, so loading the same file twice returns the same image; pass { cache: false } to load a fresh copy.
Textures from other sources
| Call | Makes a texture from |
|---|---|
Texture.load(path) | A PNG or JPEG file. |
Texture.fromBuffer(buffer) | PNG or JPEG bytes already in memory. |
Texture.fromPixels(w, h, rgba, updatable) | Raw RGBA bytes, w * h * 4 of them. Pass updatable = true to replace them later with update(rgba). |
Call dispose() on a texture you no longer need to free the GPU memory.
Regions and tilesets
TextureRegion.fromRect(texture, x, y, w, h) cuts out one rectangle. TextureRegion.grid(texture, tileW, tileH) slices a whole tileset into an array, row by row:
ts
import { Sprite, Texture, TextureRegion } from "willow";
class Tiles {
static main(): void {
const sheet: Texture = Texture.load("assets/tileset.png");
const tiles: Array<TextureRegion> = TextureRegion.grid(sheet, 32, 32);
const grass: Sprite = new Sprite(tiles[0]);
grass.x = 0;
engine.root2d.add(grass);
const water: Sprite = new Sprite(tiles[5]);
water.x = 32;
engine.root2d.add(water);
const door: TextureRegion = TextureRegion.fromRect(sheet, 64, 96, 32, 64);
const doorSprite: Sprite = new Sprite(door);
doorSprite.x = 64;
engine.root2d.add(doorSprite);
}
}grid takes an optional fourth argument { columns, count, margin, spacing } for tilesets with gaps between cells.
Atlases
An Atlas is a set of named regions on one texture page. Sprites that share a page can draw in one batch (see Performance).
| Factory | How the atlas is built |
|---|---|
Atlas.load(json, texture) | From a TexturePacker-style JSON manifest and its page texture. |
Atlas.create(config?) | Empty; pack images in one at a time with add(name, w, h, rgba). |
Atlas.createBatch(config?) | Empty; stage every image, then pack() once for a tighter fit. |
PackerConfig sets the page size (2048x2048 by default) and the padding between images (1 pixel).
ts
import { Sprite, Texture, Atlas } from "willow";
import { Files } from "fs";
class Packed {
static main(): void {
const page: Texture = Texture.load("assets/sprites.png");
const json: string = Files.readText("assets/sprites.json");
const atlas: Atlas = Atlas.load(json, page);
const hero: Sprite = new Sprite(atlas.region("hero_idle"));
hero.x = 200;
hero.y = 200;
engine.root2d.add(hero);
if (!atlas.has("hero_jump")) {
console.log("hero_jump is missing from the atlas");
}
}
}region(name) returns a small magenta placeholder for a missing name, so a typo shows up on screen instead of crashing. Check with has(name) when the name comes from data.
The atlaspack tool (thicket tool atlaspack) packs a folder of images into an atlas ahead of time.
Thousands of sprites: SpriteBatch
A Sprite is a full node: it can have children, filters and input handlers. When you have thousands of things that only move, scale, rotate or tint (bullets, stars, debris), a SpriteBatch is much cheaper. Its elements are a few numbers each, not nodes, and they all share the batch's one texture.
ts
import { SpriteBatch, Texture } from "willow";
class Starfield {
static main(): void {
const stars: SpriteBatch = new SpriteBatch(Texture.load("assets/star.png"));
engine.root2d.add(stars);
const handles: Array<float> = [];
const speeds: Array<float> = [];
for (let i: int = 0; i < 2000; i++) {
handles.push(stars.addSprite(Math.random() * 800, Math.random() * 600));
speeds.push(20 + Math.random() * 80);
}
App.onFrame((dt: float): void => {
for (let i: int = 0; i < handles.length; i++) {
const h: float = handles[i];
const angle: float = App.time * 0.1 + i;
stars.setSpritePosition(h, (i * 37) % 800, (App.time * speeds[i] + i * 13) % 600);
stars.setSpriteRotation(h, angle);
}
});
}
}addSprite returns a handle. Pass it to setSpritePosition, setSpriteScale, setSpriteRotation, setSpriteAlpha, setSpriteTint and setSpriteRegion, or remove the element with removeSprite(handle). A removed handle stays dead: it is rejected rather than pointing at another element. setSpritePositions(flatArray) moves every element in one call.
Elements rotate and scale around their top-left corner.
Frame animation
An AnimationPlayer swaps a sprite's region over time. Give it named **AnimationSequence**s and play one by name:
ts
import { Sprite, Texture, TextureRegion, AnimationPlayer, AnimationSequence } from "willow";
class Walker {
static main(): void {
const frames: Array<TextureRegion> = TextureRegion.grid(Texture.load("assets/hero.png"), 32, 32);
const hero: Sprite = new Sprite(frames[0]);
hero.setAnchor(0.5, 1.0);
hero.x = 400;
hero.y = 400;
engine.root2d.add(hero);
const anim: AnimationPlayer = new AnimationPlayer(hero);
anim.addSequence(new AnimationSequence("idle", [frames[0], frames[1]], 2, true));
anim.addSequence(new AnimationSequence("walk", [frames[2], frames[3], frames[4], frames[5]], 10, true));
anim.play("idle");
App.onFrame((dt: float): void => {
if (Keyboard.isDown(Key.RIGHT)) {
hero.x += 120 * dt;
hero.scaleX = 1;
anim.play("walk");
} else if (Keyboard.isDown(Key.LEFT)) {
hero.x -= 120 * dt;
hero.scaleX = -1;
anim.play("walk");
} else {
anim.play("idle");
}
});
}
}play(name)does nothing if that sequence is already playing, so calling it every frame is fine.- A player whose node is in the scene is advanced by the scene each frame. If you call
update(dt)yourself, you take over and the scene stops advancing that player. onComplete(fn)runs when a non-looping sequence reaches its last frame.scaleX = -1mirrors the sprite. With the anchor at the center, it flips in place.
Next
- Text and fonts
- Reference: Sprite, Texture, Atlas, AnimationPlayer