ignifx0.x · unpublished
GitHub

Recipes·skills/ignifx/references/recipes/animate-a-sprite-from-an-atlas.md

Animate a sprite from an atlas

<!-- Generated by `pnpm docs:recipes`. Do not edit by hand. -->

Two documents and two components. The .atlas.json names the frames inside one image, in image pixels with a top-left origin; the .spriteanim.json names clips over those frames. A SpriteRenderer draws the atlas and a SpriteAnimator picks the frame, advancing in PostUpdate on ignifx's own clock — so time.timeScale slows the animation and a headless test steps it.

Direction is flipX, not a mirrored clip: one set of frames, flipped when the character walks left. A frame's pivot is where the entity's origin sits inside it, in [0, 1] of the frame with [0, 0] at the top-left, so [0.5, 1] puts the origin at the sprite's feet.

pixelsPerUnit defaults to 100, which makes a 16-px sprite 0.16 m wide; set it to the art's pixel size. Sampling belongs to the atlas ("sampling": "nearest"), because a texture's sampler is fixed at upload — pixelPerfect on the camera does not change it.

The two documents beside this file are what 2d/hero.atlas.json and 2d/hero.spriteanim.json resolve to through the asset manifest.

typescript
import { Camera2D, SpriteAnimator, SpriteRenderer, twoD } from "@ignifx/2d";import { Script, createApp, f32 } from "@ignifx/core";import type { SpriteAnimationAsset, SpriteAtlasAsset } from "@ignifx/2d";import type { ScriptCallbacks } from "@ignifx/core";/** Chooses the clip and the facing from the entity's own movement. */class HeroAnimation extends Script.define({ speed: f32(3) }) implements ScriptCallbacks {  static typeId = "recipes/HeroAnimation";  #animator: SpriteAnimator | null = null;  #renderer: SpriteRenderer | null = null;  #clip = "";  awake(): void {    this.#animator = this.entity.requireComponent(SpriteAnimator);    this.#renderer = this.entity.requireComponent(SpriteRenderer);    this.#animator.onEvent.connect(      (name: string): void => {        this.app.log.info("animation event:", name);      },      { owner: this },    );  }  // `direction` is -1, 0 or 1 — whatever the game's input or AI decided for this frame.  drive(direction: number, dt: number): void {    this.transform.translate({ x: direction * this.speed * dt, y: 0, z: 0 });    if (direction !== 0 && this.#renderer !== null) {      this.#renderer.flipX = direction < 0;    }    const wanted = direction === 0 ? "idle" : "run";    if (wanted !== this.#clip) {      this.#clip = wanted;      // Guarded, because playing the clip that is already running would restart it.      this.#animator?.play(wanted);    }  }}const canvas = document.querySelector("canvas");if (!(canvas instanceof HTMLCanvasElement)) {  throw new Error("ignifx renders into a <canvas> element.");}const app = await createApp({  canvas,  settings: { assets: { root: "assets" }, sortingLayers: { sortingLayers: ["Background", "Default"] } },  extensions: [twoD({ pixelsPerUnit: 16 })],});app.registerComponents([HeroAnimation]);// Awaited before `app.start()`, where a completed load settles at once instead of waiting a frame.const atlas = await app.assets.loadAsync<SpriteAtlasAsset>("2d/hero.atlas.json");const clips = await app.assets.loadAsync<SpriteAnimationAsset>("2d/hero.spriteanim.json");app.world.createEntity("Camera").addComponent(Camera2D, { orthographicSize: 5 });const hero = app.world.createEntity("Hero");const renderer = hero.addComponent(SpriteRenderer, { sortingLayer: "Default", orderInLayer: 10 });renderer.sprite = atlas;const animator = hero.addComponent(SpriteAnimator, { defaultClip: "idle", playOnAwake: true, speed: 1 });animator.animations = clips;const animation = hero.addComponent(HeroAnimation, { speed: 3 });await app.start();// Runs left: the same frames, mirrored.animation.drive(-1, app.time.deltaTime);window.addEventListener("pagehide", () => {  atlas.release();  clips.release();  app.dispose();});

Source: examples/recipes/animate-a-sprite-from-an-atlas/main.ts