Skip to main content

Rsheet

Rsheet is a generic Python toolkit for 2D animation (background removal, sprite sheet splitting, size normalization, animation playback). It knows nothing about your specific game: it works just as well for a fighting game, a platformer, a physics simulation... any 2D project that needs to animate sprites.

Why "Rsheet"? It all starts with the sprite sheet: Rsheet analyzes it to automatically find where the frames are, in a few seconds, so you don't have to do it by hand.

This README follows the full pipeline, in the order you'll use it on a real project: 1) clean the background, 2) split the frames, 3) normalize the sizes, 4) play the animation. All screenshots use the same example image, penguin_walk.png:

sprite sheet of a walking penguin

Installation

pip install rsheet

No local compilation is needed (the only heavy computation, background detection, is delegated to scipy, distributed as precompiled wheels).

Try it now

Clone the repo and run the example — it's ready to go, no setup needed:

git clone https://github.com/ELiijah-dev/rsheet.git
cd rsheet/examples
python demo.py

This opens a real pygame window playing a normalized, background-free animation, built end to end from a raw sprite sheet.


Demo — what you'll see in your console

This is exactly the script from step 4 below, run from VS Code: detection + splitting + normalization happen in a fraction of a second in the terminal, then the pygame window opens with the already-normalized animation ready to play.

console demo + pygame window

(Compressed GIF for the preview — full video with sound and detailed logs)


1. rsheet.cached_removed_bg — isolating the character

A developer who just wants to strip a background often makes the mistake of targeting one specific color (the corner pixel, say). That breaks the moment the background has a slight gradient, anti-aliasing noise, or changes from one asset to another. Rsheet therefore assumes nothing about the color: it looks at the pixels along the edge of the image, infers the two dominant colors (K-means), then floods outward from those edges through everything connected that resembles the background — following the actual outline rather than guessing its shape ahead of time.

Concretely, cached_removed_bg returns a PNG with an alpha channel, keeping only the character. The result is cached next to the source file: the computation is only redone if the original image changes.

The tolerance parameter exists because "resembles" has no universal answer: every sprite has its own level of noise around its edges, so it's a dial to tune per project rather than a value baked into Rsheet's code. The default is 40, but 6 is a good starting point (raise it if background residue is still visible, lower it if chunks of the character disappear).

import rsheet

png_transparent = rsheet.cached_removed_bg("penguin_walk.png", tolerance=6)
# -> penguin_walk._rsheet_bg_cache.png (background removed, ready to load in pygame)
Before After (tolerance=6)
before after

2. rsheet.sprite_editor — splitting and naming sprite sheets

The real problem this module solves isn't "cutting up an image" — it's the time lost manually saying "this row has 6 frames, that one has 4, that other one has 8". Rsheet treats this as a pure geometry problem rather than a layout one: a row of non-background pixels is an animation, a column of non-background pixels inside that row is a frame. No grid is assumed, so it works the same on a neatly arranged sheet or one full of gaps.

The random naming (rsheet.vocab) comes from a similar observation: giving each detected animation a meaningful name is still a manual task, while the code only needs a stable, unique key. Rsheet picks a random name and guarantees it never collides with another already used by that character — finding a free slot quickly rather than choosing one yourself.

Concretely, process_project automatically detects the number of rows (animations) and frames per row — you never specify a frame count up front — then draws a unique name for each detected animation. The result is saved to a text file (frame_coords.txt) which is then used to build the in-game animation.

import rsheet

entries = rsheet.process_project(
    "frame_coords.txt",
    sheets=[
        ("penguin_walk.png", "penguin", "player"),  # (file, character, role)
    ],
)

for e in entries:
    print(e.character, e.sheet_num, list(e.animations.keys()))
# penguin 1 ['glide_a']   <- animation name drawn automatically

role ("player", "enemy", anything else, or None) is free-form — Rsheet never enforces it, it just stores it.


3. rsheet.normalizer — consistent on-screen sizes

An artist never draws two frames at the exact same size — a raised wing takes up a bit more space than a lowered one, there's a few extra or missing pixels of empty space depending on the pose. If each frame were displayed as-is, the character would seem to slightly "float" or "jump" on every frame change, even while standing still.

The normalizer fixes this by computing, for each frame, its offset from a common reference size — then always anchoring to the ground rather than the center, so that only a character's head moves on a small variation, never its feet. The result is cached and invalidated by a hash of the coordinates file, because this computation only ever needs redoing if the splitting changed — not on every game launch.

Concretely, load_or_compute_norm_cache computes this offset for every frame and saves it to frame_norm_cache.txt:

import pygame
import rsheet

norm = rsheet.load_or_compute_norm_cache(
    "frame_coords.txt",
    cache_path="frame_norm_cache.txt",
)

# frame_surface = the sub-image of one specific frame, cut from the
# sheet at the coordinates found by sprite_editor in step 2 (this is
# what `build_frame_cache`, in step 4, does for you automatically):
sheet = pygame.image.load("penguin_walk.png").convert_alpha()
frame_rect = entries[0].animations["glide_a"][0]  # `entries` comes from step 2
frame_surface = sheet.subsurface(frame_rect.to_tuple()).copy()

dw, dh = norm["penguin"]["glide_a"][0]
surf = rsheet.apply_norm_to_surface(frame_surface, dw, dh)  # ground-anchored

In practice, you'll almost never write this cutting logic by hand: build_frame_cache (step 4) does exactly this, for every frame at once.


4. rsheet.animation — building and playing the animation

Steps 1 to 3 are deliberately "offline": they never touch pygame, know nothing about a game loop, and write their result to plain text files. rsheet.animation is the only module that bridges to the runtime — it's the one that turns pixel rectangles into actual pygame.Surface objects ready to be displayed. This separation exists so the expensive computation (detection, normalization) is never redone while the game is running.

AnimationController stays deliberately "dumb": it only knows how to do one thing, advance a frame on a timer and loop — no fighting-game, platformer, or other gameplay logic gets mixed in, so it fits any project. Entity goes one step further by adding minimal physics (gravity, jumping, movement) because that's such a common need it was worth providing ready-made — but it stays optional: a project that already has its own physics can use only AnimationController and ignore Entity.

Concretely, build_frame_cache loads the sprite sheet as a pygame.Surface, cuts each frame at the right spot and automatically applies the normalization computed in step 3 — you get back {animation_name: [surfaces...]} directly, ready to play with AnimationController:

import pygame
import rsheet

pygame.init()
screen = pygame.display.set_mode((640, 360))
pygame.display.set_caption("Rsheet demo")
clock = pygame.time.Clock()

# Full pipeline (steps 1 to 3) — run once to prepare the files
png_transparent = rsheet.cached_removed_bg("penguin_walk.png", tolerance=6)
entries = rsheet.process_project(
    "frame_coords.txt",
    sheets=[(png_transparent, "penguin", "player")],
)
anim_name = list(entries[-1].animations.keys())[0]  # actual name generated in step 2
sheet_num = entries[-1].sheet_num
rsheet.load_or_compute_norm_cache("frame_coords.txt", cache_path="frame_norm_cache.txt")

# Step 4 — build and play the animation
frames = rsheet.build_frame_cache("penguin", {sheet_num: png_transparent}, "frame_coords.txt")
anim = rsheet.AnimationController(frames=frames)
anim.play(anim_name)

x, y = 270, 115
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    dt = clock.tick(60) / 1000
    anim.update(dt)

    screen.fill((40, 40, 40))
    screen.blit(anim.current_surface(), (x, y))
    pygame.display.flip()

pygame.quit()

This script opens a real window and plays the animation on loop until closed — copy-pasteable as-is (just put penguin_walk.png next to the script, or swap in your own sprite sheet). This exact script is also available ready to run in examples/demo.py — see Try it now above.

For an entity with simple physics (movement, gravity, jumping), Entity embeds an AnimationController directly — replace the last two lines of the loop above with:

hero = rsheet.Entity(anim=anim)
hero.move(1)                        # moves to the right
hero.update(dt, ground_y=300)       # physics + animation updated together
screen.blit(anim.current_surface(), (hero.x, hero.y))

What else is in there?

Rsheet also includes rsheet.vfx (generic light glow) and rsheet.baking (animation pre-computation, facing-direction handling, surface cropping/resizing) — useful once the 4 building blocks above are in place, but not essential to get started. See each module's docstrings for details.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rsheet-0.1.1.tar.gz (34.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rsheet-0.1.1-py3-none-any.whl (34.9 kB view details)

Uploaded Python 3

File details

Details for the file rsheet-0.1.1.tar.gz.

File metadata

  • Download URL: rsheet-0.1.1.tar.gz
  • Upload date:
  • Size: 34.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.2

File hashes

Hashes for rsheet-0.1.1.tar.gz
Algorithm Hash digest
SHA256 ea48654fed9abde81b5199b27dbcec36be9f6fa16e101c56b92060a595b21cd7
MD5 6e75fa29513bff8b766bb2703037ab4d
BLAKE2b-256 f8606eac2624e82bc98e04d2b4f8e618d0b73c3d049b6607e2e19a37ab2249a7

See more details on using hashes here.

File details

Details for the file rsheet-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: rsheet-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 34.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.2

File hashes

Hashes for rsheet-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f6b5538321f63f653c855420e5344e3445e8f18a7a9c417177bcde64c6e9163c
MD5 a9283829c06c0c25d9169237f4aa35e5
BLAKE2b-256 e77d5871fba8421e8681501a69c176104f92ea9d803a74eb603f0971566f31e6

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page