Guide

Sprites

Render image-based content from textures, sheets, SVG, and video.

Intro~4 min read

What you'll learn

  • render textures, sheets, SVG, and video as sprites
  • control anchor, blend mode, and frames

Before you start

Sprites

A Sprite is the primary drawable for textured quads — rectangles that display an image, a sub-region of an image, or a rendered-to-texture surface. Most visible content in a 2D scene passes through sprites.

From texture to screen

A sprite needs a texture. The shortest path from file to visible quad:

examples/guides/sprites/hero-scene.ts
class HeroScene extends Scene {
  private hero!: Sprite;

  override async load(): Promise<void> {
    await this.loader.load('image/hero.png');
  }

  override init(): void {
    this.hero = new Sprite(this.loader.get('image/hero.png'));
    this.addChild(this.hero);
  }

  override draw(context: RenderingContext): void {
    context.render(this.root);
  }

  private centerHero(): void {
    // #region guide:anchor-center
    const { width, height } = this.app;

    this.hero = new Sprite(this.loader.get('image/hero.png'));
    this.hero.setAnchor(0.5);
    this.hero.setPosition(width / 2, height / 2);
    this.addChild(this.hero);
    // #endregion guide:anchor-center
  }

  private resizeHero(): void {
    // #region guide:size-from-scale
    this.hero.width = 64; // scale.x becomes 64 / textureFrame.width
    this.hero.height = 64; // scale.y becomes 64 / textureFrame.height
    // #endregion guide:size-from-scale
  }
}

The sprite’s rendered size defaults to the texture’s pixel dimensions. The sprite draws a quad from (0, 0) to (texture.width, texture.height) in local space.

Positioning and anchor

A sprite’s position places its anchor point in the parent’s coordinate space. By default the anchor is (0, 0) — the top-left corner — so setPosition(400, 300) places the top-left of the sprite at world position (400, 300).

setAnchor(0.5) centers the anchor at the sprite’s middle, making setPosition(400, 300) place the sprite’s center at (400, 300):

examples/guides/sprites/hero-scene.ts
const { width, height } = this.app;

this.hero = new Sprite(this.loader.get('image/hero.png'));
this.hero.setAnchor(0.5);
this.hero.setPosition(width / 2, height / 2);
this.addChild(this.hero);

setAnchor(x, y) accepts two arguments for separate horizontal and vertical anchors. A value of 0 is left/top, 0.5 is center, 1 is right/bottom.

Size and scale

sprite.width and sprite.height report the rendered pixel dimensions (texture frame size multiplied by absolute scale). Setting them adjusts the scale to match:

examples/guides/sprites/hero-scene.ts
this.hero.width = 64; // scale.x becomes 64 / textureFrame.width
this.hero.height = 64; // scale.y becomes 64 / textureFrame.height

setScale(x, y) sets raw scale factors. A scale of (2, 2) doubles the sprite’s visual size; (-1, 1) mirrors it horizontally.

Tinting

sprite.tint is a Color that multiplies the sprite’s pixel colors at draw time. White tint (Color.white) leaves the texture unchanged. A red tint mutes green and blue channels. Tinting does not allocate or create new textures:

import { Color, Sprite } from '@codexo/exojs';

declare const hero: Sprite;

hero.tint = new Color(255, 128, 128, 1); // reddish tint
hero.tint = new Color(128, 128, 255, 0.8); // blue tint, 80% opacity

Texture frames

A sprite can display a sub-region of its texture through textureFrame. This is the mechanism behind spritesheet frame selection:

import { Rectangle, Sprite } from '@codexo/exojs';

declare const sprite: Sprite;

// Show only the top-left 32x32 pixels of a 128x128 texture
sprite.setTextureFrame(new Rectangle(0, 0, 32, 32));

By default setTextureFrame resets the sprite’s width and height to the frame’s dimensions. Pass false as the second argument to keep the current display size — useful for animation where frame dimensions vary but the on-screen size should stay constant:

import { Rectangle, Sprite } from '@codexo/exojs';

declare const sprite: Sprite;

sprite.setTextureFrame(new Rectangle(32, 0, 32, 32), false);

Call resetTextureFrame() to restore the full texture.

Blend modes

sprite.blendMode controls how the sprite composites with pixels already in the framebuffer. The default is BlendModes.Normal (standard alpha blending):

import { BlendModes, Sprite } from '@codexo/exojs';

declare const glow: Sprite;

glow.blendMode = BlendModes.Additive;

Blend modes are per-sprite. Combined with tinting they cover common glow, shadow, and knock-out compositing without needing custom shaders.

BlendModes implements the full W3C compositing/blending set. Normal, Additive, Subtract, Multiply, and Screen are fixed-function GPU blends with no extra draw cost. The remaining “advanced” modes go through a backdrop-aware compositor that copies the current render target before drawing each sprite, so they carry a small per-draw backdrop-copy cost — reach for them deliberately in draw-call-heavy scenes. isAdvancedBlendMode(mode) reports which category a given mode falls into.

Mode Visual effect
Normal Standard alpha compositing — source drawn over the backdrop.
Additive Adds source and backdrop colors; brightens. Good for glows, light, fire, particles.
Subtract Subtracts source from backdrop; darkens toward black.
Multiply Multiplies channel values; darkens, like stacking transparencies.
Screen Inverse of multiply; lightens, like projecting two images onto the same surface.
Darken * Keeps the darker of source and backdrop, per channel.
Lighten * Keeps the lighter of source and backdrop, per channel.
Overlay * Multiplies dark backdrop areas and screens light ones; boosts contrast.
ColorDodge * Brightens the backdrop to reflect the source color; intense highlights.
ColorBurn * Darkens the backdrop to reflect the source color; intense shadows.
HardLight * Overlay with source and backdrop roles swapped; harsh, directional lighting.
SoftLight * Softer version of Overlay; subtle, diffused lighting.
Difference * Absolute value of the channel difference; inversion / comparison effects.
Exclusion * Lower-contrast variant of Difference.
Hue * Source hue with backdrop saturation and luminosity.
Saturation * Source saturation with backdrop hue and luminosity.
Color * Source hue and saturation with backdrop luminosity; recolors while preserving shading.
Luminosity * Source luminosity with backdrop hue and saturation; inverse of Color.

* Advanced mode — routed through the backdrop-aware compositor (isAdvancedBlendMode returns true).

Spritesheets

A Spritesheet slices a single texture atlas into named frames and optional animation sequences. It accepts a JSON descriptor in the Aseprite / TexturePacker format:

import { Container, Spritesheet, Texture } from '@codexo/exojs';

declare const atlasTexture: Texture;
declare const sceneRoot: Container;

const sheet = new Spritesheet(atlasTexture, {
    frames: {
        'walk_01': { frame: { x: 0, y: 0, w: 32, h: 32 } },
        'walk_02': { frame: { x: 32, y: 0, w: 32, h: 32 } },
        'walk_03': { frame: { x: 64, y: 0, w: 32, h: 32 } },
    },
    animations: {
        walk: ['walk_01', 'walk_02', 'walk_03'],
    },
});

// Pre-built Sprite for a single frame
const sprite = sheet.sprites.get('walk_01');
if (sprite) sceneRoot.addChild(sprite);

// Rectangle for a single frame (useful for textureFrame)
const frame = sheet.frames.get('walk_01');

The animations map feeds into AnimatedSprite.fromSpritesheet(), covered in the Animation chapter.

Non-image textures

Sprite accepts any Texture or RenderTexture. This means sprites can display video frames, SVG rasterizations, off-screen renders, or DataTexture pixel buffers — any source the engine can wrap as a Texture:

// Video as a sprite texture
import { RenderTexture, Sprite, Video } from '@codexo/exojs';

declare const video: Video;
const { texture: videoTexture } = video;
const sprite = new Sprite(videoTexture);

// A RenderTexture (offscreen render) as a sprite texture
const rt = new RenderTexture(256, 256);
// ... render something into rt ...
const offscreenSprite = new Sprite(rt);

Sprite transforms in the graph

A Sprite inherits from Drawable, which extends RenderNode. This means every sprite carries a full transform (position, rotation, scale, origin) and participates in the scene graph as a node. When a sprite is a child of a rotated Container, it rotates with the container. When it has its own filters array, those filters apply to the sprite’s rendered quad before it composites into the parent.

Collision helpers — sprite.contains(x, y) for point tests and sprite.getBounds() for AABB queries — work on the sprite’s world-space quad and account for rotation.

Examples

Sprite BasicsOpen in PlaygroundView source

Preview is paused until you click Play.

import { Application, Color, FixedResolutionCanvasSizing, type RenderingContext, Scene, type Seconds, Sprite } from '@codexo/exojs';
import { mountControls } from '@examples/runtime';

class SpriteBasicsScene extends Scene {
  private ship!: Sprite;
  // A single reusable tint colour whose alpha channel we animate each frame.
  private tint = new Color(120, 200, 255, 1);
  private elapsed = 0;
  private hud!: ReturnType<typeof mountControls>;

  override init(): void {
    const app = this.app;
    const { width, height } = app;

    this.ship = new Sprite(this.loader.get('image/ship-a.png'));
    this.ship.setPosition((width / 2) | 0, (height / 2) | 0);
    this.ship.setAnchor(0.5);
    this.ship.setScale(3);
    this.ship.setTint(this.tint);

    this.hud = mountControls({
      title: 'Sprite Basics',
      hint: 'One Sprite, four transforms: position drifts, rotation spins, scale pulses, alpha fades.',
      status: 'alpha 1.00',
    });
  }

  override update(delta: Seconds): void {
    const app = this.app;
    this.elapsed += delta;

    const { width, height } = app;

    // Position: a gentle figure-eight drift around the canvas centre.
    const driftX = Math.sin(this.elapsed * 0.8) * 90;
    const driftY = Math.sin(this.elapsed * 1.6) * 50;
    this.ship.setPosition(width / 2 + driftX, height / 2 + driftY);

    // Rotation: a steady spin (degrees per second).
    this.ship.rotate(delta * 90);

    // Scale: a slow breathing pulse between 2.4x and 3.6x.
    this.ship.setScale(3 + Math.sin(this.elapsed * 1.2) * 0.6);

    // Alpha: fade the tint's alpha channel between 0.2 and 1.0 and re-apply.
    const alpha = 0.6 + Math.sin(this.elapsed * 2) * 0.4;
    this.tint.a = alpha;
    this.ship.setTint(this.tint);

    this.hud.setStatus(`alpha ${alpha.toFixed(2)}`);
  }

  override draw(context: RenderingContext): void {
    context.render(this.ship);
  }
}

const app = new Application({
  scenes: { SpriteBasicsScene },
  canvas: {
    width: 1280,
    height: 720,
    mount: document.body,
    sizing: new FixedResolutionCanvasSizing(),
  },
  clearColor: new Color(18, 20, 28),
  loader: {
    basePath: 'assets/',
  },
});

await app.start(SpriteBasicsScene);

A single sprite in the center of the canvas, rotating and cycling through tints.

Spritesheet FramesOpen in PlaygroundView source

Preview is paused until you click Play.

import { Application, Asset, Color, FixedResolutionCanvasSizing, type RenderingContext, Scene, type Seconds, Spritesheet, type SpritesheetData } from '@codexo/exojs';
import { mountControlPanel, mountControls } from '@examples/runtime';

const CHARACTERS = ['beige', 'green', 'pink', 'purple', 'yellow'];

class SpritesheetFramesScene extends Scene {
  private spritesheet!: Spritesheet;
  private character = CHARACTERS[0];
  private frameIndex = 0;
  private fps = 8;
  private elapsed = 0;
  private playing = true;
  private hud!: ReturnType<typeof mountControls>;

  override async load(): Promise<void> {
    const app = this.app;
    const { width, height } = app;
    const texture = this.loader.get('image/platformer-characters.png');
    const data = (await this.loader.load(Asset.type('json', 'json/platformer-characters.json'))) as SpritesheetData;

    this.spritesheet = new Spritesheet(texture, data);

    // The spritesheet caches one Sprite per named frame; configure them all
    // once so any frame we draw is centred and scaled up for visibility.
    for (const sprite of this.spritesheet.sprites.values()) {
      sprite.setAnchor(0.5);
      sprite.setPosition(width / 2, height / 2);
      sprite.setScale(3);
    }

    this.hud = mountControls({
      title: 'Spritesheet Frames',
      controls: [{ keys: 'Right-click', action: 'next character' }],
      hint: 'A two-frame walk cycle stepped on a timer from named spritesheet frames.',
    });

    const panel = mountControlPanel({ title: 'Animation' });
    panel.addSlider({ label: 'Speed (fps)', min: 1, max: 16, step: 1, value: this.fps, onChange: value => (this.fps = value) });
    panel.addToggle({ label: 'Playing', value: true, onChange: on => (this.playing = on) });

    app.input.onContextMenu.add(() => {
      const index = (CHARACTERS.indexOf(this.character) + 1) % CHARACTERS.length;

      this.character = CHARACTERS[index]!;
      this.frameIndex = 0;
      this.updateHud();
    });

    this.updateHud();
  }

  private walkFrames(): string[] {
    return [`character_${this.character}_walk_a`, `character_${this.character}_walk_b`];
  }

  private updateHud(): void {
    const frames = this.walkFrames();

    this.hud.setStatus(`Frame: ${frames[this.frameIndex]}  (${this.frameIndex + 1}/${frames.length})`);
  }

  override update(delta: Seconds): void {
    if (!this.playing) {
      return;
    }

    this.elapsed += delta;

    const frameDuration = 1 / this.fps;

    while (this.elapsed >= frameDuration) {
      this.elapsed -= frameDuration;
      this.frameIndex = (this.frameIndex + 1) % this.walkFrames().length;
      this.updateHud();
    }
  }

  override draw(context: RenderingContext): void {
    context.render(this.spritesheet.getFrameSprite(this.walkFrames()[this.frameIndex]));
  }
}

const app = new Application({
  scenes: { SpritesheetFramesScene },
  canvas: {
    width: 1280,
    height: 720,
    mount: document.body,
    sizing: new FixedResolutionCanvasSizing(),
  },
  clearColor: new Color(24, 28, 38),
  loader: {
    basePath: 'assets/',
  },
});

await app.start(SpritesheetFramesScene);

Navigating a spritesheet’s frames by name — the texture-frame mechanism in practice.

Where to go next

The next chapter, Text, covers GPU-accelerated text rendering — font loading, styling, layout, and using text nodes in the scene graph.