Animated Sprite

Frame animation from a procedural spritesheet via AnimatedSprite2D.

▶ Run in browser

Tags: 2d sprite animation spritesheet

What it demonstrates

  • AnimatedSprite2D driving frame playback from a single sheet texture

  • A spritesheet built procedurally as an RGBA uint8 ndarray (no external asset)

  • Named animations registered with add_animation(name, frames, fps, loop)

  • The same sheet driven at two playback rates (10 fps and 24 fps)

  • Looping vs one-shot playback (the one-shot anim freezes on its last frame)

  • Replaying a one-shot at runtime with play(), which re-arms it from frame 0

Controls: SPACE - Replay the one-shot sprite ESC - Quit

Source

  1"""Animated Sprite: Frame animation from a procedural spritesheet via AnimatedSprite2D.
  2
  3# /// simvx
  4# tags = ["sprite", "animation", "spritesheet"]
  5# web = { root = "AnimatedSpriteScene", width = 800, height = 600, responsive = true }
  6# ///
  7
  8## What it demonstrates
  9  - AnimatedSprite2D driving frame playback from a single sheet texture
 10  - A spritesheet built procedurally as an RGBA uint8 ndarray (no external asset)
 11  - Named animations registered with add_animation(name, frames, fps, loop)
 12  - The same sheet driven at two playback rates (10 fps and 24 fps)
 13  - Looping vs one-shot playback (the one-shot anim freezes on its last frame)
 14  - Replaying a one-shot at runtime with play(), which re-arms it from frame 0
 15
 16Controls:
 17  SPACE - Replay the one-shot sprite
 18  ESC   - Quit
 19"""
 20
 21import numpy as np
 22
 23from simvx.core import AnimatedSprite2D, Input, InputMap, Key, Node2D, Text2D, Vec2
 24from simvx.graphics import App
 25
 26WIDTH, HEIGHT = 800, 600
 27FRAME = 64  # pixels per frame
 28FRAMES = 8  # frames in the horizontal strip
 29
 30
 31def _make_spritesheet() -> np.ndarray:
 32    """Build an 8-frame horizontal strip: a dot sweeping around a ring.
 33
 34    Each frame places a bright marker at a different angle so the running
 35    animation reads as a clear rotation, with the frame index drawn as a
 36    brightening bar so playback order is obvious.
 37    """
 38    sheet = np.zeros((FRAME, FRAME * FRAMES, 4), dtype=np.uint8)
 39    cx = cy = FRAME / 2
 40    for i in range(FRAMES):
 41        x0 = i * FRAME
 42        # Dark frame background with a thin border so frames are distinct.
 43        sheet[:, x0 : x0 + FRAME] = (24, 24, 36, 255)
 44        sheet[0:2, x0 : x0 + FRAME] = (60, 60, 90, 255)
 45        sheet[-2:, x0 : x0 + FRAME] = (60, 60, 90, 255)
 46        # Marker dot rotating around the ring, one step per frame.
 47        angle = (i / FRAMES) * 2 * np.pi
 48        mx = cx + np.cos(angle) * FRAME * 0.32
 49        my = cy + np.sin(angle) * FRAME * 0.32
 50        yy, xx = np.ogrid[0:FRAME, 0:FRAME]
 51        dot = (xx - mx) ** 2 + (yy - my) ** 2 <= 7**2
 52        sheet[:, x0 : x0 + FRAME][dot] = (255, 210, 70, 255)
 53    return sheet
 54
 55
 56class AnimatedSpriteScene(Node2D):
 57    """One looping sprite, one rate-varied sprite, and a one-shot sprite."""
 58
 59    def on_ready(self):
 60        InputMap.add_action("play", [Key.SPACE])
 61        InputMap.add_action("quit", [Key.ESCAPE])
 62        sheet = _make_spritesheet()
 63
 64        # Steady loop at 10 fps.
 65        self.loop_sprite = self.add_child(
 66            AnimatedSprite2D(
 67                texture=sheet,
 68                frames_h=FRAMES,
 69                frames_v=1,
 70                width=128,
 71                height=128,
 72                position=Vec2(WIDTH * 0.25, HEIGHT * 0.5),
 73                name="Loop",
 74            )
 75        )
 76        self.loop_sprite.add_animation("spin", frames=list(range(FRAMES)), fps=10, loop=True)
 77        self.loop_sprite.play("spin")
 78
 79        # Faster loop reusing the same sheet at a higher fps.
 80        self.fast_sprite = self.add_child(
 81            AnimatedSprite2D(
 82                texture=sheet,
 83                frames_h=FRAMES,
 84                frames_v=1,
 85                width=128,
 86                height=128,
 87                position=Vec2(WIDTH * 0.5, HEIGHT * 0.5),
 88                name="Fast",
 89            )
 90        )
 91        self.fast_sprite.add_animation("spin_fast", frames=list(range(FRAMES)), fps=24, loop=True)
 92        self.fast_sprite.play("spin_fast")
 93
 94        # One-shot: plays once then freezes on the final frame.
 95        self.once_sprite = self.add_child(
 96            AnimatedSprite2D(
 97                texture=sheet,
 98                frames_h=FRAMES,
 99                frames_v=1,
100                width=128,
101                height=128,
102                position=Vec2(WIDTH * 0.75, HEIGHT * 0.5),
103                name="Once",
104            )
105        )
106        self.once_sprite.add_animation("burst", frames=list(range(FRAMES)), fps=12, loop=False)
107        self.once_sprite.play("burst")
108
109        self.add_child(Text2D(text="AnimatedSprite2D", position=(10, 10), font_scale=1.5, name="Title"))
110        self.add_child(Text2D(text="10 fps loop", position=(WIDTH * 0.25 - 50, HEIGHT * 0.5 + 80), name="L1"))
111        self.add_child(Text2D(text="24 fps loop", position=(WIDTH * 0.5 - 50, HEIGHT * 0.5 + 80), name="L2"))
112        self.add_child(Text2D(text="one-shot", position=(WIDTH * 0.75 - 40, HEIGHT * 0.5 + 80), name="L3"))
113        self.add_child(
114            Text2D(
115                text="Space = replay one-shot | Esc = quit",
116                position=(10, HEIGHT - 30),
117                name="Hud",
118            )
119        )
120
121    def on_update(self, dt: float):
122        if Input.is_action_just_pressed("play"):
123            self.once_sprite.play("burst")  # re-arm the one-shot from frame 0
124        if Input.is_action_just_pressed("quit"):
125            self.app.quit()
126
127
128if __name__ == "__main__":
129    App(width=WIDTH, height=HEIGHT, title="AnimatedSprite2D Demo").run(AnimatedSpriteScene())