Skip to content

Sprite

import { sprite_c, type SpriteData } from "@f0rbit/forge/pixi";
type SpriteData = {
texture: string; // alias of an image OR atlas
frame?: string; // frame name within an atlas
anchor?: { x: number; y: number }; // 0–1, sprite-relative pivot
tint?: number; // 0xRRGGBB
visible?: boolean;
z?: number; // zIndex within world container
scale?: { x: number; y: number }; // non-uniform scale, defaults (1, 1)
};
w.spawn(
[pos_c, { x: 80, y: 40 }],
[sprite_c, { texture: "hero", anchor: { x: 0.5, y: 1 } }],
);

SpriteData is config only. The PIXI runtime Sprite node lives in a private WeakMap<World, Map<Id, Sprite>> owned by sprite_sync_system — it isn’t exposed on the public type. Consumers never touch it.

The sprite_sync_system (added by boot to the post stage) watches (pos_c, sprite_c) pairs each tick:

  • First time it sees an entity: creates a PIXI Sprite, adds to the world container, stores it in the system’s private node map.
  • Resolves the texture: assets.texture(alias) first, then atlas frame lookup, then first frame of the atlas as a last resort. If still nothing, sprite stays without a texture (often visible as a tiny white square or invisible).
  • Updates: position from pos_c, anchor / tint / visibility / zIndex from sprite_c.
  • Despawn cleanup: any sprite whose entity vanished from the query gets removeFromParent() + destroy().

The lazy-mount means you can w.set(id, sprite_c, {...}) without ever touching PIXI — the system handles the pixi-side construction.

For the common “update one sprite field” pattern, sprite.set does the read/merge/write round-trip:

import { sprite } from "@f0rbit/forge/pixi";
sprite.set(w, id, { visible: false }); // hide
sprite.set(w, id, { tint: 0xff0000 }); // recolour
sprite.set(w, id, { scale: { x: -1, y: 1 } }); // mirror horizontally
// shortcuts
sprite.show(w, id); // visible: true
sprite.hide(w, id); // visible: false

Each helper returns Result<void, EngineError>component_missing if the entity has no sprite_c, entity_not_found if it’s been despawned.

type sprite = {
set: (w: World, id: Id, patch: Partial<SpriteData>) => Result<void, EngineError>;
show: (w: World, id: Id) => Result<void, EngineError>;
hide: (w: World, id: Id) => Result<void, EngineError>;
};

SpriteData.scale is the optional { x: number; y: number } non-uniform scale applied via PIXI’s Sprite.scale.set(x, y) after texture assignment. Defaults to (1, 1) when absent.

// 2× upscale for HUD overlays vs world sprites
sprite.set(w, hud_icon, { scale: { x: 2, y: 2 } });
// horizontal flip
sprite.set(w, player, { scale: { x: -1, y: 1 } });

This is the only valid way to do non-uniform scale on a PIXI sprite. Common uses:

  • HUD vs world sprite ratios on a single design surface.
  • Pixel-art atlas frames at non-1× factors (e.g. 16×16 atlas → 8 px tiles via scale: { x: 0.5, y: 0.5 }).
  • Mirroring/flipping for left/right-facing sprites without a separate atlas frame.

The system applies scale every tick the entity is in the (pos_c, sprite_c) query, so reactive changes (sprite.set(w, id, { scale: { x: -1, y: 1 } })) take effect on the next post-tick.

SpriteData.node is removed from the public type. The spread-and-set pattern collapses into sprite.set:

// v0.2.0
const sd = w.get(id, sprite_c);
if (!sd.ok) return;
w.set(id, sprite_c, { ...sd.value, visible: false });
// v0.3.0
sprite.set(w, id, { visible: false });
// or
sprite.hide(w, id);