rsheet 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rsheet-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ELiijah-dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rsheet-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: rsheet
3
+ Version: 0.1.0
4
+ Summary: Boîte à outils générique pour sprite sheets et animations 2D.
5
+ Requires-Python: >=3.9
6
+ License-File: LICENSE
7
+ Requires-Dist: numpy
8
+ Requires-Dist: Pillow
9
+ Requires-Dist: scipy
10
+ Requires-Dist: pygame
11
+ Dynamic: license-file
12
+ Dynamic: requires-python
rsheet-0.1.0/README.md ADDED
@@ -0,0 +1,273 @@
1
+ ![Rsheet](assets/logo.jpg)
2
+
3
+ **Rsheet** is a generic Python toolkit for 2D animation (background
4
+ removal, sprite sheet splitting, size normalization, animation
5
+ playback). It knows nothing about your specific game: it works just
6
+ as well for a fighting game, a platformer, a physics simulation... any
7
+ 2D project that needs to animate sprites.
8
+
9
+ **Why "Rsheet"?** It all starts with the sprite **sheet**: Rsheet
10
+ analyzes it to automatically find where the frames are, in a few
11
+ seconds, so you don't have to do it by hand.
12
+
13
+ This README follows the full pipeline, in the order you'll use it on
14
+ a real project: **1) clean the background, 2) split the frames,
15
+ 3) normalize the sizes, 4) play the animation**. All screenshots use
16
+ the same example image, `penguin_walk.png`:
17
+
18
+ ![sprite sheet of a walking penguin](assets/penguin_walk.png)
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install rsheet
24
+ ```
25
+
26
+ No local compilation is needed (the only heavy computation,
27
+ background detection, is delegated to `scipy`, distributed as
28
+ precompiled wheels).
29
+
30
+ ## Try it now
31
+
32
+ Clone the repo and run the example — it's ready to go, no setup
33
+ needed:
34
+
35
+ ```bash
36
+ git clone https://github.com/ELiijah-dev/rsheet.git
37
+ cd rsheet/examples
38
+ python demo.py
39
+ ```
40
+
41
+ This opens a real pygame window playing a normalized, background-free
42
+ animation, built end to end from a raw sprite sheet.
43
+
44
+ ---
45
+
46
+ ## Demo — what you'll see in your console
47
+
48
+ This is exactly the script from step 4 below, run from VS Code:
49
+ detection + splitting + normalization happen in a fraction of a
50
+ second in the terminal, then the pygame window opens with the
51
+ already-normalized animation ready to play.
52
+
53
+ ![console demo + pygame window](assets/console_demo_preview.gif)
54
+
55
+ *(Compressed GIF for the preview — [full video with sound and detailed logs](assets/console_demo.mp4))*
56
+
57
+ ---
58
+
59
+ ## 1. `rsheet.cached_removed_bg` — isolating the character
60
+
61
+ A developer who just wants to strip a background often makes the
62
+ mistake of targeting *one* specific color (the corner pixel, say).
63
+ That breaks the moment the background has a slight gradient,
64
+ anti-aliasing noise, or changes from one asset to another. Rsheet
65
+ therefore assumes nothing about the color: it looks at the pixels
66
+ along the **edge** of the image, infers the two dominant colors
67
+ (K-means), then floods outward from those edges through everything
68
+ connected that resembles the background — following the actual
69
+ outline rather than guessing its shape ahead of time.
70
+
71
+ Concretely, `cached_removed_bg` returns a PNG with an alpha channel,
72
+ keeping only the character. The result is cached next to the source
73
+ file: the computation is only redone if the original image changes.
74
+
75
+ The `tolerance` parameter exists because "resembles" has no universal
76
+ answer: every sprite has its own level of noise around its edges, so
77
+ it's a dial to tune per project rather than a value baked into
78
+ Rsheet's code. The default is `40`, but **`6` is a good starting
79
+ point** (raise it if background residue is still visible, lower it if
80
+ chunks of the character disappear).
81
+
82
+ ```python
83
+ import rsheet
84
+
85
+ png_transparent = rsheet.cached_removed_bg("penguin_walk.png", tolerance=6)
86
+ # -> penguin_walk._rsheet_bg_cache.png (background removed, ready to load in pygame)
87
+ ```
88
+
89
+ | Before | After (`tolerance=6`) |
90
+ |---|---|
91
+ | ![before](assets/penguin_walk.png) | ![after](assets/penguin_walk_no_bg_t6.png) |
92
+
93
+ ---
94
+
95
+ ## 2. `rsheet.sprite_editor` — splitting and naming sprite sheets
96
+
97
+ The real problem this module solves isn't "cutting up an image" — it's
98
+ the time lost manually saying "this row has 6 frames, that one has 4,
99
+ that other one has 8". Rsheet treats this as a pure geometry problem
100
+ rather than a layout one: a row of non-background pixels is an
101
+ animation, a column of non-background pixels inside that row is a
102
+ frame. No grid is assumed, so it works the same on a neatly arranged
103
+ sheet or one full of gaps.
104
+
105
+ The random naming (`rsheet.vocab`) comes from a similar observation:
106
+ giving each detected animation a meaningful name is still a manual
107
+ task, while the code only needs a stable, unique key. Rsheet picks a
108
+ random name and guarantees it never collides with another already
109
+ used by that character — finding a free slot quickly rather than
110
+ choosing one yourself.
111
+
112
+ Concretely, `process_project` **automatically** detects the number of
113
+ rows (animations) and frames per row — you never specify a frame
114
+ count up front — then draws a unique name for each detected animation.
115
+ The result is saved to a text file (`frame_coords.txt`) which is then
116
+ used to build the in-game animation.
117
+
118
+ ```python
119
+ import rsheet
120
+
121
+ entries = rsheet.process_project(
122
+ "frame_coords.txt",
123
+ sheets=[
124
+ ("penguin_walk.png", "penguin", "player"), # (file, character, role)
125
+ ],
126
+ )
127
+
128
+ for e in entries:
129
+ print(e.character, e.sheet_num, list(e.animations.keys()))
130
+ # penguin 1 ['glide_a'] <- animation name drawn automatically
131
+ ```
132
+
133
+ `role` (`"player"`, `"enemy"`, anything else, or `None`) is free-form —
134
+ Rsheet never enforces it, it just stores it.
135
+
136
+ ---
137
+
138
+ ## 3. `rsheet.normalizer` — consistent on-screen sizes
139
+
140
+ An artist never draws two frames at the exact same size — a raised
141
+ wing takes up a bit more space than a lowered one, there's a few
142
+ extra or missing pixels of empty space depending on the pose. If each
143
+ frame were displayed as-is, the character would seem to slightly
144
+ "float" or "jump" on every frame change, even while standing still.
145
+
146
+ The normalizer fixes this by computing, for each frame, its offset
147
+ from a common reference size — then always anchoring to the ground
148
+ rather than the center, so that only a character's head moves on a
149
+ small variation, never its feet. The result is cached and invalidated
150
+ by a hash of the coordinates file, because this computation only ever
151
+ needs redoing if the splitting changed — not on every game launch.
152
+
153
+ Concretely, `load_or_compute_norm_cache` computes this offset for
154
+ every frame and saves it to `frame_norm_cache.txt`:
155
+
156
+ ```python
157
+ import pygame
158
+ import rsheet
159
+
160
+ norm = rsheet.load_or_compute_norm_cache(
161
+ "frame_coords.txt",
162
+ cache_path="frame_norm_cache.txt",
163
+ )
164
+
165
+ # frame_surface = the sub-image of one specific frame, cut from the
166
+ # sheet at the coordinates found by sprite_editor in step 2 (this is
167
+ # what `build_frame_cache`, in step 4, does for you automatically):
168
+ sheet = pygame.image.load("penguin_walk.png").convert_alpha()
169
+ frame_rect = entries[0].animations["glide_a"][0] # `entries` comes from step 2
170
+ frame_surface = sheet.subsurface(frame_rect.to_tuple()).copy()
171
+
172
+ dw, dh = norm["penguin"]["glide_a"][0]
173
+ surf = rsheet.apply_norm_to_surface(frame_surface, dw, dh) # ground-anchored
174
+ ```
175
+
176
+ > In practice, you'll almost never write this cutting logic by hand:
177
+ > `build_frame_cache` (step 4) does exactly this, for every frame at
178
+ > once.
179
+
180
+ ---
181
+
182
+ ## 4. `rsheet.animation` — building and playing the animation
183
+
184
+ Steps 1 to 3 are deliberately "offline": they never touch `pygame`,
185
+ know nothing about a game loop, and write their result to plain text
186
+ files. `rsheet.animation` is the only module that bridges to the
187
+ runtime — it's the one that turns pixel rectangles into actual
188
+ `pygame.Surface` objects ready to be displayed. This separation exists
189
+ so the expensive computation (detection, normalization) is never
190
+ redone while the game is running.
191
+
192
+ `AnimationController` stays deliberately "dumb": it only knows how to
193
+ do one thing, advance a frame on a timer and loop — no fighting-game,
194
+ platformer, or other gameplay logic gets mixed in, so it fits any
195
+ project. `Entity` goes one step further by adding minimal physics
196
+ (gravity, jumping, movement) because that's such a common need it was
197
+ worth providing ready-made — but it stays optional: a project that
198
+ already has its own physics can use only `AnimationController` and
199
+ ignore `Entity`.
200
+
201
+ Concretely, `build_frame_cache` loads the sprite sheet as a
202
+ `pygame.Surface`, cuts each frame at the right spot and automatically
203
+ applies the normalization computed in step 3 — you get back
204
+ `{animation_name: [surfaces...]}` directly, ready to play with
205
+ `AnimationController`:
206
+
207
+ ```python
208
+ import pygame
209
+ import rsheet
210
+
211
+ pygame.init()
212
+ screen = pygame.display.set_mode((640, 360))
213
+ pygame.display.set_caption("Rsheet demo")
214
+ clock = pygame.time.Clock()
215
+
216
+ # Full pipeline (steps 1 to 3) — run once to prepare the files
217
+ png_transparent = rsheet.cached_removed_bg("penguin_walk.png", tolerance=6)
218
+ entries = rsheet.process_project(
219
+ "frame_coords.txt",
220
+ sheets=[(png_transparent, "penguin", "player")],
221
+ )
222
+ anim_name = list(entries[-1].animations.keys())[0] # actual name generated in step 2
223
+ sheet_num = entries[-1].sheet_num
224
+ rsheet.load_or_compute_norm_cache("frame_coords.txt", cache_path="frame_norm_cache.txt")
225
+
226
+ # Step 4 — build and play the animation
227
+ frames = rsheet.build_frame_cache("penguin", {sheet_num: png_transparent}, "frame_coords.txt")
228
+ anim = rsheet.AnimationController(frames=frames)
229
+ anim.play(anim_name)
230
+
231
+ x, y = 270, 115
232
+ running = True
233
+ while running:
234
+ for event in pygame.event.get():
235
+ if event.type == pygame.QUIT:
236
+ running = False
237
+
238
+ dt = clock.tick(60) / 1000
239
+ anim.update(dt)
240
+
241
+ screen.fill((40, 40, 40))
242
+ screen.blit(anim.current_surface(), (x, y))
243
+ pygame.display.flip()
244
+
245
+ pygame.quit()
246
+ ```
247
+
248
+ This script opens a real window and plays the animation on loop until
249
+ closed — copy-pasteable as-is (just put `penguin_walk.png` next to the
250
+ script, or swap in your own sprite sheet). This exact script is also
251
+ available ready to run in `examples/demo.py` — see [Try it now](#try-it-now)
252
+ above.
253
+
254
+ For an entity with simple physics (movement, gravity, jumping),
255
+ `Entity` embeds an `AnimationController` directly — replace the last
256
+ two lines of the loop above with:
257
+
258
+ ```python
259
+ hero = rsheet.Entity(anim=anim)
260
+ hero.move(1) # moves to the right
261
+ hero.update(dt, ground_y=300) # physics + animation updated together
262
+ screen.blit(anim.current_surface(), (hero.x, hero.y))
263
+ ```
264
+
265
+ ---
266
+
267
+ ## What else is in there?
268
+
269
+ Rsheet also includes `rsheet.vfx` (generic light glow) and
270
+ `rsheet.baking` (animation pre-computation, facing-direction handling,
271
+ surface cropping/resizing) — useful once the 4 building blocks above
272
+ are in place, but not essential to get started. See each module's
273
+ docstrings for details.
@@ -0,0 +1,10 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rsheet"
7
+ version = "0.1.0"
8
+ description = "Boîte à outils générique pour sprite sheets et animations 2D."
9
+ requires-python = ">=3.9"
10
+ dependencies = ["numpy", "Pillow", "scipy", "pygame"]
@@ -0,0 +1,90 @@
1
+ """
2
+ Rsheet — boîte à outils générique pour sprite sheets et animations 2D.
3
+
4
+ Usage typique :
5
+
6
+ from rsheet.vocab import AnimVocabulary
7
+ from rsheet.sprite_editor import process_project
8
+
9
+ process_project(
10
+ "frame_coords.txt",
11
+ sheets=[
12
+ ("hero_1.png", "hero", "player"),
13
+ ("hero_2.png", "hero", "player"),
14
+ ("golem_1.png", "golem", "enemy"),
15
+ ],
16
+ )
17
+
18
+ from rsheet.animation import build_frame_cache, AnimationController, Entity
19
+ from rsheet.config import RsheetConfig
20
+
21
+ cfg = RsheetConfig.load("game_config.txt")
22
+ frames = build_frame_cache("hero", {1: "hero_1.png", 2: "hero_2.png"},
23
+ "frame_coords.txt", "game_config.txt")
24
+
25
+ hero = Entity(config=cfg)
26
+ hero.anim.frames = frames
27
+ hero.anim.config = cfg
28
+ hero.anim.play("idle")
29
+ """
30
+
31
+ from .vocab import AnimVocabulary, VocabExhaustedError
32
+ from .config import RsheetConfig
33
+ from .coords_io import SheetEntry, load_coords, save_coords
34
+ from .detection import analyze_sheet, FrameRect, DetectedRow
35
+ from .sprite_editor import add_sheet, process_project
36
+ from .normalizer import (
37
+ load_or_compute_norm_cache,
38
+ apply_norm_to_surface,
39
+ load_norm_cache_file,
40
+ save_norm_cache_file,
41
+ )
42
+ from . import vfx
43
+ from . import baking
44
+ from .animation import AnimationController, Entity, build_frame_cache
45
+ from .cache_utils import cached_removed_bg, cache_path_for
46
+ from .parallel import precompute_backgrounds_parallel
47
+ from .vfx import add_glow
48
+ from .baking import (
49
+ bake_frames, precompute_mirrored, BakedCycle, mirror_angle_for_facing,
50
+ BurstPauseCycle, resolve_facing_sprite, DepletingScale,
51
+ slice_single_row, trim_and_scale,
52
+ )
53
+
54
+ __version__ = "0.1.0"
55
+
56
+ __all__ = [
57
+ "vfx",
58
+ "baking",
59
+ "AnimVocabulary",
60
+ "VocabExhaustedError",
61
+ "RsheetConfig",
62
+ "SheetEntry",
63
+ "load_coords",
64
+ "save_coords",
65
+ "analyze_sheet",
66
+ "FrameRect",
67
+ "DetectedRow",
68
+ "add_sheet",
69
+ "process_project",
70
+ "load_or_compute_norm_cache",
71
+ "apply_norm_to_surface",
72
+ "load_norm_cache_file",
73
+ "save_norm_cache_file",
74
+ "AnimationController",
75
+ "Entity",
76
+ "build_frame_cache",
77
+ "cached_removed_bg",
78
+ "cache_path_for",
79
+ "precompute_backgrounds_parallel",
80
+ "add_glow",
81
+ "bake_frames",
82
+ "precompute_mirrored",
83
+ "BakedCycle",
84
+ "mirror_angle_for_facing",
85
+ "BurstPauseCycle",
86
+ "resolve_facing_sprite",
87
+ "DepletingScale",
88
+ "slice_single_row",
89
+ "trim_and_scale",
90
+ ]
@@ -0,0 +1,52 @@
1
+ """
2
+ Fallback pur Python pour les opérations pixel-par-pixel de Rsheet.
3
+ Utilisé automatiquement quand scipy n'est pas disponible. Fonctionnellement
4
+ identique, juste plus lent.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import deque
10
+
11
+ import numpy as np
12
+
13
+
14
+ def flood_fill_bfs(arr_rgb: np.ndarray, bg1: np.ndarray, bg2: np.ndarray, tolerance: int) -> np.ndarray:
15
+ """BFS depuis les 4 bords de l'image pour marquer les pixels de fond.
16
+
17
+ Retourne un masque booléen (True = pixel de fond) de même taille
18
+ que l'image.
19
+ """
20
+ h, w = arr_rgb.shape[:2]
21
+ threshold = tolerance * 3
22
+ rgb = arr_rgb[:, :, :3].astype(np.int32)
23
+ d1 = np.abs(rgb - bg1).sum(axis=2)
24
+ d2 = np.abs(rgb - bg2).sum(axis=2)
25
+ maybe = (d1 < threshold) | (d2 < threshold)
26
+
27
+ visited = np.zeros((h, w), dtype=bool)
28
+ is_bg = np.zeros((h, w), dtype=bool)
29
+ q = deque()
30
+
31
+ for x in range(w):
32
+ for y in (0, h - 1):
33
+ if maybe[y, x] and not visited[y, x]:
34
+ visited[y, x] = is_bg[y, x] = True
35
+ q.append((y, x))
36
+ for y in range(h):
37
+ for x in (0, w - 1):
38
+ if maybe[y, x] and not visited[y, x]:
39
+ visited[y, x] = is_bg[y, x] = True
40
+ q.append((y, x))
41
+
42
+ dirs = ((-1, 0), (1, 0), (0, -1), (0, 1))
43
+ while q:
44
+ cy, cx = q.popleft()
45
+ for dy, dx in dirs:
46
+ ny, nx = cy + dy, cx + dx
47
+ if 0 <= ny < h and 0 <= nx < w and not visited[ny, nx]:
48
+ visited[ny, nx] = True
49
+ if maybe[ny, nx]:
50
+ is_bg[ny, nx] = True
51
+ q.append((ny, nx))
52
+ return is_bg
@@ -0,0 +1,46 @@
1
+ """
2
+ Flood-fill de détection de fond, version scipy.
3
+
4
+ Reformule le problème comme une recherche de composantes connexes,
5
+ déjà résolue par scipy.ndimage.label (écrit en C, distribué en wheels
6
+ précompilées pour toutes les plateformes — `pip install scipy` ne
7
+ compile jamais rien chez l'utilisateur).
8
+
9
+ Équivalence : un pixel est "fond" si (a) sa couleur est proche de l'une
10
+ des deux couleurs de fond détectées, ET (b) il appartient à une région
11
+ connexe (4-connectivité, comme le BFS) qui touche au moins un bord de
12
+ l'image.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import numpy as np
18
+ from scipy.ndimage import label, generate_binary_structure
19
+
20
+ # 4-connectivité (haut/bas/gauche/droite), identique aux directions du BFS d'origine
21
+ _STRUCT = generate_binary_structure(2, 1)
22
+
23
+
24
+ def flood_fill_bfs(arr_rgb: np.ndarray, bg1: np.ndarray, bg2: np.ndarray, tolerance: int) -> np.ndarray:
25
+ """Retourne un masque booléen (True = pixel de fond), résultat
26
+ identique à un BFS depuis les 4 bords, calculé via composantes
27
+ connexes (scipy), sans aucune compilation requise."""
28
+ h, w = arr_rgb.shape[:2]
29
+ threshold = tolerance * 3
30
+ rgb = arr_rgb[:, :, :3].astype(np.int32)
31
+ d1 = np.abs(rgb - bg1).sum(axis=2)
32
+ d2 = np.abs(rgb - bg2).sum(axis=2)
33
+ maybe = (d1 < threshold) | (d2 < threshold)
34
+
35
+ labeled, n_labels = label(maybe, structure=_STRUCT)
36
+ if n_labels == 0:
37
+ return np.zeros((h, w), dtype=bool)
38
+
39
+ border_labels = set(labeled[0, :].tolist()) | set(labeled[-1, :].tolist())
40
+ border_labels |= set(labeled[:, 0].tolist()) | set(labeled[:, -1].tolist())
41
+ border_labels.discard(0) # 0 = pixels hors du masque `maybe`, pas une vraie composante
42
+
43
+ if not border_labels:
44
+ return np.zeros((h, w), dtype=bool)
45
+
46
+ return np.isin(labeled, list(border_labels))