Load and draw sprat-cli sprite sheets with raylib.
A single-file C99 library in the style of raylib's own extras (raygui.h,
rres-raylib.h): plain structs, Load/Unload/Is…Valid/Draw functions, and
TraceLog() for problems. It reads the metadata written by
spratconvert --transform raw through sprat-lib,
loads the atlas textures next to it, and draws sprites the way they were
authored: placed by their pivot, with trimming and packing rotation undone.
spratlayout ./frames --trim-transparent --rotate > layout.txt
spratpack -a atlas.png < layout.txt > /dev/null
spratconvert --transform raw -a atlas.png \
--markers markers.txt --animations animations.txt < layout.txt > hero.sprat#include "raylib.h"
#define SPRAT_RAYLIB_IMPLEMENTATION
#include "sprat-raylib.h"
int main(void)
{
InitWindow(800, 450, "sprat-raylib");
SpratSheet sheet = LoadSpratSheet("resources/hero.sprat");
SpratAnimationPlayer run = MakeSpratAnimationPlayer(GetSpratAnimation(sheet, "run"));
while (!WindowShouldClose())
{
UpdateSpratAnimation(&run, GetFrameTime());
BeginDrawing();
ClearBackground(RAYWHITE);
DrawSpratAnimation(sheet, run, (Vector2){ 400, 300 }, WHITE);
EndDrawing();
}
UnloadSpratSheet(sheet);
CloseWindow();
return 0;
}src/sprat-raylib.h is the whole library. Define SPRAT_RAYLIB_IMPLEMENTATION in
exactly one C file before including it, and link raylib (5.5 or newer) and
sprat-lib (0.5 or newer):
cc game.c -Isrc -I../sprat-lib/include ../sprat-lib/src/*.c -lraylib -lmWith CMake, sprat-raylib::sprat-raylib (the sprat-raylib target) compiles the
implementation for you and brings raylib along:
add_subdirectory(sprat-raylib) # or find_package(sprat-raylib) after an install
target_link_libraries(game PRIVATE sprat-raylib::sprat-raylib)#include "sprat-raylib.h" // no SPRAT_RAYLIB_IMPLEMENTATION: the library has itThe CMake build uses installed raylib and sprat packages when it finds them and
downloads them otherwise. To build against a local checkout, pass
-DFETCHCONTENT_SOURCE_DIR_SPRAT=../sprat-lib (or FETCHCONTENT_SOURCE_DIR_RAYLIB).
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build --parallel
ctest --test-dir build --output-on-failure
./build/examples/sprite_sheet| Option | Default | Effect |
|---|---|---|
SPRAT_RAYLIB_BUILD_EXAMPLES |
ON when top level |
Build examples/sprite_sheet |
SPRAT_RAYLIB_SANITIZE |
OFF |
Build with AddressSanitizer and UBSan (GCC/Clang) |
// Sheet loading/unloading functions
SpratSheet LoadSpratSheet(const char *fileName);
SpratSheet LoadSpratSheetFromMemory(const unsigned char *fileData, int dataSize, const char *atlasDir);
bool IsSpratSheetValid(SpratSheet sheet);
void UnloadSpratSheet(SpratSheet sheet);
// Sheet query functions
int GetSpratFrameIndex(SpratSheet sheet, const char *pathOrName);
const SpratMarker *GetSpratFrameMarker(SpratSheet sheet, int frame, const char *name);
Vector2 GetSpratFramePoint(SpratSheet sheet, int frame, Vector2 point, Vector2 position, Vector2 scale, float rotation, int flip);
// Sheet drawing functions
void DrawSpratFrame(SpratSheet sheet, int frame, Vector2 position, Color tint);
void DrawSpratFrameEx(SpratSheet sheet, int frame, Vector2 position, float rotation, float scale, Color tint);
void DrawSpratFramePro(SpratSheet sheet, int frame, Vector2 position, Vector2 scale, float rotation, int flip, Color tint);
void DrawSpratFrameNPatch(SpratSheet sheet, int frame, Rectangle dest, Color tint);
// Animation functions
SpratAnimation GetSpratAnimation(SpratSheet sheet, const char *name);
bool IsSpratAnimationValid(SpratAnimation animation);
SpratAnimationPlayer MakeSpratAnimationPlayer(SpratAnimation animation);
void UpdateSpratAnimation(SpratAnimationPlayer *player, float deltaTime);
void PlaySpratAnimation(SpratAnimationPlayer *player);
void StopSpratAnimation(SpratAnimationPlayer *player);
void PauseSpratAnimation(SpratAnimationPlayer *player);
void ResumeSpratAnimation(SpratAnimationPlayer *player);
void SetSpratAnimationSpeed(SpratAnimationPlayer *player, float speed);
bool IsSpratAnimationPlaying(SpratAnimationPlayer player);
bool IsSpratAnimationFinished(SpratAnimationPlayer player);
int GetSpratAnimationFrame(SpratAnimationPlayer player);
void DrawSpratAnimation(SpratSheet sheet, SpratAnimationPlayer player, Vector2 position, Color tint);SpratSheet is a plain struct, like raylib's Model or Font: pass it by
value, read its arrays directly, and unload it once.
typedef struct SpratSheet {
int atlasCount; // Number of atlas textures
Texture2D *atlases; // Atlas textures
int frameCount; // Number of frames
SpratFrame *frames; // Frames, in sheet order
int animationCount; // Number of animations
SpratAnimation *animations; // Animations, in sheet order
float scale; // Scale the art was packed at: draw at 1.0f/scale for its original size
} SpratSheet;- Frames are the sheet's sprites, in its order. A
SpratFramekeeps the atlas region as stored (source,rotation), the original image'ssize, the trimmedcontentregion inside it, thepivot, the nine-slice insets (slice) and the frame'smarkers. Frame geometry is in original-image pixels, scaled to atlas pixels, with Y pointing down. - Animations list frame indexes in play order, with an
fpsand theflipthey are played with. Aliases are already resolved:run-left, an alias ofrunwithflip h, hasrun's frames (the same array, not a copy) andflipset toSPRAT_FLIP_HORIZONTAL. An animation may have no frames. - Names: a frame's
nameis its file's basename and can repeat across folders; itspathis unique.GetSpratFrameIndex()takes either: it matches the path first, then a name that only one frame has, and logs a warning and returns -1 otherwise. Look names up once, after loading, and keep the indexes.
Frames and animations keep the order of the sprat file, so the indexes that
sprat-lib's sprat_sheet_bind() resolves (and its animation frame indexes)
index sheet.frames and sheet.animations as they are.
Every draw function places a frame by its pivot, the point
spratconvert was given with a pivot marker (0, 0, the top-left of the
original image, when there was none). Trimmed margins are left out, and
frames that were packed turned are drawn upright.
DrawSpratFramePro() scales the frame, mirrors it about its pivot with
SPRAT_FLIP_HORIZONTAL and/or SPRAT_FLIP_VERTICAL, then turns it clockwise
around the pivot by rotation degrees, the same convention as
DrawTexturePro(). DrawSpratFrame() and DrawSpratFrameEx() are the
shorthands raylib users expect.
scale is yours to choose, and is not sheet.scale: frame geometry is in atlas
pixels, so 1.0f draws atlas pixels one to one. For a sheet packed at
--scale 0.5, draw at 1.0f/sheet.scale to show the art at its original
size.
An animation from the sheet is a template: GetSpratAnimation() returns it by
name (zeroed, with a warning, when there is none; IsSpratAnimationValid()
tells). Playing it takes a SpratAnimationPlayer, a plain struct you keep one
of per animated thing, the way raylib's Music is played and updated:
typedef struct SpratAnimationPlayer {
SpratAnimation animation; // Animation played (shares the sheet's frame array)
float time; // Seconds into the animation, from 0 to its duration (frameCount/fps)
float speed; // Playback rate: 1.0f plays at the animation's fps, negative plays in reverse
bool playing; // Advanced by UpdateSpratAnimation()
bool loop; // Wrap around at the end, otherwise stop on the last frame
} SpratAnimationPlayer;MakeSpratAnimationPlayer() returns one already playing, looping, at speed
1.0f; it allocates nothing, so copy it or drop it freely. Call
UpdateSpratAnimation(&player, GetFrameTime()) once per frame: time only moves
while you update it, so pausing the game, slow motion or a fixed time step
apply to animations too. PauseSpratAnimation() and ResumeSpratAnimation()
keep the time, PlaySpratAnimation() restarts and StopSpratAnimation()
rewinds without playing.
A negative speed plays in reverse, and PlaySpratAnimation() then starts from
the last frame. Set loop to false to play once: the player stops on the
last frame it reaches (the first when reversed) and IsSpratAnimationFinished()
becomes true. The fields are yours to read and set, time included.
GetSpratAnimationFrame() gives the frame shown now, an index into
sheet.frames, for its markers or a hitbox. DrawSpratAnimation() draws it with
the animation's flip. To draw it with your own transform, combine that flip
with yours:
int frame = GetSpratAnimationFrame(enemy.walk);
DrawSpratFramePro(sheet, frame, enemy.position, (Vector2){ 2, 2 }, 0.0f,
enemy.walk.animation.flip ^ enemy.flip, WHITE);Markers (pivot, hitboxes, hands, spawn points...) are in original-image
pixels. GetSpratFramePoint() takes the same transform as DrawSpratFramePro()
and tells where a point of the original image lands on screen, for attaching
items or placing collision shapes:
const SpratMarker *hand = GetSpratFrameMarker(sheet, frame, "hand");
if (hand != NULL)
{
Vector2 grip = GetSpratFramePoint(sheet, frame, hand->position, hero, scale, 0.0f, flip);
DrawSpratFramePro(sheet, sword, grip, scale, swordAngle, 0, WHITE);
}A rectangle marker's position is its top-left corner; once mirrored, take
both corners through GetSpratFramePoint() and order them, as the example's
DrawHitbox() does.
DrawSpratFrameNPatch() draws a frame stretched to fill dest, keeping its
slice borders unscaled, like DrawTextureNPatch(). dest covers the whole
original image, trimmed margins included, and the pivot plays no part. It
cuts the slices in original-image space, so it handles trimmed frames and
frames packed turned, which DrawTextureNPatch() cannot. Borders shrink
proportionally when dest is smaller than them, and a frame without slice is
simply stretched.
Every fill mode is drawn as SPRAT_SLICE_MODE_STRETCH. slice.horizontalMode
and slice.verticalMode say which mode the artist chose, for code that tiles
the middle itself.
LoadSpratSheet() reads the file with LoadFileData(), so it honors
SetLoadFileDataCallback(), and loads each atlas with LoadTexture(), joining
its path with the sheet's folder (absolute paths are used as written).
LoadSpratSheetFromMemory() takes the folder to resolve atlas paths against,
or NULL to use them as written.
Like raylib's loaders, these never return an error: problems go to
TraceLog(), with sprat-lib's message naming the value at fault and where it
is:
WARNING: SPRAT: [resources/hero.sprat] Failed to load sheet: 4:14: sprites[0].rect.w: expected an integer, found "4x"
A sheet that failed to parse is zeroed, and drawing with it draws nothing. A
missing atlas leaves its texture at id 0, so the frames on it are skipped.
IsSpratSheetValid() is true only when the metadata loaded and every atlas
texture is in the GPU. You can replace an atlas: unload sheet.atlases[i] and
assign another texture, and UnloadSpratSheet() will unload that one.
The metadata is copied into one allocation (with RL_CALLOC, released with
RL_FREE), and sprat-lib's parsed sheet is freed before the loader returns.
Every array and string of a SpratSheet points into that allocation and stays
valid until UnloadSpratSheet(). sheet.frames is its start, so it is not
NULL after a successful load even when the sheet has no frames.
examples/sprite_sheet.c draws every frame of the
example sheet (most of them stored turned in the atlas), plays the run,
run-left and spin animations, resizes a nine-patch panel, and lets you run
a hero holding a sword at its hand marker. D shows pivots and hitboxes.
The sheet in examples/resources/ was packed with sprat-cli by
make_resources.sh, from frames drawn by make_sources.py.
- Normal and collision maps are not loaded. sprat-lib gives their paths if you need them.
- Atlas paths are used as written, so a sheet from an untrusted source could
name any file. Parse such a sheet with sprat-lib first and check each
atlases[i].pathwithsprat_path_is_safe()before loading it here. - The sheet format has no loop setting yet: every player loops until you set
looptofalse.
MIT. See LICENSE.