Metadata-Version: 2.4
Name: rsheet
Version: 0.1.1
Summary: Generic toolkit for sprite sheets and 2D animations.
Author-email: Elijah <dondajohson@gmail.com>
License: MIT License
        
        Copyright (c) 2026 ELiijah-dev
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/ELiijah-dev/rsheet
Project-URL: Repository, https://github.com/ELiijah-dev/rsheet
Project-URL: Issues, https://github.com/ELiijah-dev/rsheet/issues
Keywords: pygame,sprite,sprite-sheet,animation,2d,game-development
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Games/Entertainment
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: Pillow
Requires-Dist: scipy
Requires-Dist: pygame
Dynamic: license-file

![Rsheet](assets/logo.jpg)

**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](assets/penguin_walk.png)

## Installation

```bash
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:

```bash
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](assets/console_demo_preview.gif)

*(Compressed GIF for the preview — [full video with sound and detailed logs](assets/console_demo.mp4))*

---

## 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).

```python
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](assets/penguin_walk.png) | ![after](assets/penguin_walk_no_bg_t6.png) |

---

## 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.

```python
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`:

```python
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`:

```python
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](#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:

```python
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.
