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)

  • Switching animations and playback rate at runtime with play()

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

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  - Switching animations and playback rate at runtime with play()
 13  - Looping vs one-shot playback (the one-shot anim freezes on its last frame)
 14"""
 15
 16import numpy as np
 17
 18from simvx.core import AnimatedSprite2D, Input, InputMap, Key, Node2D, Text2D, Vec2
 19from simvx.graphics import App
 20
 21WIDTH, HEIGHT = 800, 600
 22FRAME = 64        # pixels per frame
 23FRAMES = 8        # frames in the horizontal strip
 24
 25
 26def _make_spritesheet() -> np.ndarray:
 27    """Build an 8-frame horizontal strip: a dot sweeping around a ring.
 28
 29    Each frame places a bright marker at a different angle so the running
 30    animation reads as a clear rotation, with the frame index drawn as a
 31    brightening bar so playback order is obvious.
 32    """
 33    sheet = np.zeros((FRAME, FRAME * FRAMES, 4), dtype=np.uint8)
 34    cx = cy = FRAME / 2
 35    for i in range(FRAMES):
 36        x0 = i * FRAME
 37        # Dark frame background with a thin border so frames are distinct.
 38        sheet[:, x0:x0 + FRAME] = (24, 24, 36, 255)
 39        sheet[0:2, x0:x0 + FRAME] = (60, 60, 90, 255)
 40        sheet[-2:, x0:x0 + FRAME] = (60, 60, 90, 255)
 41        # Marker dot rotating around the ring, one step per frame.
 42        angle = (i / FRAMES) * 2 * np.pi
 43        mx = cx + np.cos(angle) * FRAME * 0.32
 44        my = cy + np.sin(angle) * FRAME * 0.32
 45        yy, xx = np.ogrid[0:FRAME, 0:FRAME]
 46        dot = (xx - mx) ** 2 + (yy - my) ** 2 <= 7 ** 2
 47        sheet[:, x0:x0 + FRAME][dot] = (255, 210, 70, 255)
 48    return sheet
 49
 50
 51class AnimatedSpriteScene(Node2D):
 52    """One looping sprite, one rate-varied sprite, and a one-shot sprite."""
 53
 54    def on_ready(self):
 55        InputMap.add_action("play", [Key.SPACE])
 56        InputMap.add_action("quit", [Key.ESCAPE])
 57        sheet = _make_spritesheet()
 58
 59        # Steady loop at 10 fps.
 60        self.loop_sprite = self.add_child(AnimatedSprite2D(
 61            texture=sheet, frames_horizontal=FRAMES, frames_vertical=1,
 62            width=128, height=128, position=Vec2(WIDTH * 0.25, HEIGHT * 0.5),
 63            name="Loop",
 64        ))
 65        self.loop_sprite.add_animation("spin", frames=list(range(FRAMES)), fps=10, loop=True)
 66        self.loop_sprite.play("spin")
 67
 68        # Faster loop reusing the same sheet at a higher fps.
 69        self.fast_sprite = self.add_child(AnimatedSprite2D(
 70            texture=sheet, frames_horizontal=FRAMES, frames_vertical=1,
 71            width=128, height=128, position=Vec2(WIDTH * 0.5, HEIGHT * 0.5),
 72            name="Fast",
 73        ))
 74        self.fast_sprite.add_animation("spin_fast", frames=list(range(FRAMES)), fps=24, loop=True)
 75        self.fast_sprite.play("spin_fast")
 76
 77        # One-shot: plays once then freezes on the final frame.
 78        self.once_sprite = self.add_child(AnimatedSprite2D(
 79            texture=sheet, frames_horizontal=FRAMES, frames_vertical=1,
 80            width=128, height=128, position=Vec2(WIDTH * 0.75, HEIGHT * 0.5),
 81            name="Once",
 82        ))
 83        self.once_sprite.add_animation("burst", frames=list(range(FRAMES)), fps=12, loop=False)
 84        self.once_sprite.play("burst")
 85
 86        self.add_child(Text2D(text="AnimatedSprite2D", position=(10, 10), font_scale=1.5, name="Title"))
 87        self.add_child(Text2D(text="10 fps loop", position=(WIDTH * 0.25 - 50, HEIGHT * 0.5 + 80), name="L1"))
 88        self.add_child(Text2D(text="24 fps loop", position=(WIDTH * 0.5 - 50, HEIGHT * 0.5 + 80), name="L2"))
 89        self.add_child(Text2D(text="one-shot", position=(WIDTH * 0.75 - 40, HEIGHT * 0.5 + 80), name="L3"))
 90        self.add_child(Text2D(
 91            text="Space = replay one-shot | Esc = quit", position=(10, HEIGHT - 30), name="Hud",
 92        ))
 93
 94    def on_update(self, dt: float):
 95        if Input.is_action_just_pressed("play"):
 96            self.once_sprite.play("burst")   # re-arm the one-shot from frame 0
 97        if Input.is_action_just_pressed("quit"):
 98            self.app.quit()
 99
100
101if __name__ == "__main__":
102    App(width=WIDTH, height=HEIGHT, title="AnimatedSprite2D Demo").run(AnimatedSpriteScene())