Sprite Sheet Maker — Pack Frames into an Atlas
Drop frame images, a GIF or an .aseprite file and get a tightly packed sprite sheet with the data file your engine reads — packed live in a worker as you change settings.
A sprite sheet puts many frames on one image so a game loads one texture instead of dozens; a packed atlas also writes a data file that names every frame and gives its rectangle. Drop numbered PNGs, a GIF or an .aseprite file, pick your engine's preset, and Nerulio packs the frames in your browser (MaxRects, trim on, 2 px padding by default) and exports the PNG with that engine's data file. For a plain grid of equal cells, the classic maker is linked further down.

- Godot 4Verified· Godot 4.7.2
- Unity 6Verified· Unity 6000.5.3f1
- Phaser 3 / 4Verified· Phaser 3.90 + 4.2
- PixiJS 8Verified· PixiJS 8.21
- Aseprite JSONVerified· Phaser load.aseprite · PixiJS 8.21
- .asepriteVerified· Aseprite 1.3.18
- Spine / libGDXVerified· spine-canvas 4.2
- Starling / Sparrow XMLVerified· Phaser 4.2
- LÖVEVerified· LÖVE 11.5
- DefoldBuilt· Defold bob.jar 1.13.1
- GameMakerUNVERIFIED
- CSS spritesVerified· Chromium
- GIF / APNGDecoded· Pillow
Grid sheet or packed atlas
A grid sheet gives every frame the same cell, so an engine finds frame n by arithmetic: column = n mod columns, row = n ÷ columns, rounded down. It needs no data file, but every cell is as large as the largest frame, transparent border included.
A packed atlas trims each frame to its visible pixels and fits the pieces wherever they fit. Positions are no longer predictable, so the packer writes a data file: for each frame its name, where the stored pixels are on the page (frame), where they sat on the original canvas (spriteSourceSize), the original canvas size (sourceSize) and whether the piece lies turned by 90° (rotated). The engine uses those numbers to draw the frame at full size, so a trimmed frame does not move.
Two settings protect the edges. Shape padding leaves transparent pixels between frames, and extrude repeats each frame's outermost pixels around it, so a texture filter that samples just past an edge reads the same colour instead of a neighbour. Rotation saves a little area, but only some engines turn a rotated frame back; Nerulio's presets allow it only for PixiJS and Spine, where the engine runs drew rotated frames correctly.
- Page
- One atlas image. Frames that do not fit the maximum size go on further pages (multipack, up to 64).
- Trim
- Cutting the transparent border off a frame and recording the offset, so it is drawn at its old place.
- Shape padding
- Transparent pixels between two frames: 2 px by default.
- Extrude
- Copies of a frame's edge pixels placed around it: 0 by default, 1 in the LÖVE preset.
- Alias
- Frames with identical visible pixels share one stored region.
Example: six 40 × 29 running frames
The six frames of a CC0 ninja run cycle (40 × 29 px each, mostly transparent), packed by Nerulio with the Phaser preset (trim on, padding 2, no rotation) and compared with other layouts:
layout page area vs strip
horizontal strip, 6 × 40 px 240 × 29 6,960 px² 100 %
packed, trim off, padding 2 82 × 91 7,462 px² 107 %
packed, trim on, padding 2 38 × 50 1,900 px² 27 %
packed, trim on, rotation 33 × 57 1,881 px² 27 %
visible pixels (6 bounding boxes) 1,591 px²
share of the trimmed page used 1,591 / 1,900 = 0.84
"run_0": { "frame": {"x":0,"y":34,"w":18,"h":15},
"spriteSourceSize": {"x":10,"y":10,"w":18,"h":15},
"sourceSize": {"w":40,"h":29}, "rotated": false, "trimmed": true }Only 18 × 15 pixels of run_0 are stored; the engine draws them 10 px right and 10 px down inside a 40 × 29 box, exactly where they were. Trim does the saving here: without it the packed page is slightly larger than a plain strip, because padding is added between frames.
What it does
- MaxRects (five rules), Skyline and Guillotine; "try all" keeps the smallest page.
- Trim that keeps offsets, alias for identical frames, extrude, border and shape padding, POT / square / fixed sizes, up to 64 pages.
- Scale variants @0.5x–@4x with nearest-neighbour, optional premultiplied alpha.
- Colour-exact PNGs: frames are decoded in JavaScript, not through a canvas, so colours and alpha do not drift (interlaced PNGs fall back to the browser decoder, and the stage says so).
How to do it
- Drop your frames (a folder of numbered PNGs works) or a GIF / .aseprite file.
- Pick your engine's preset; the atlas repacks live.
- Check the used %, pages and the frame list; change trim, padding or size if needed.
- Press Export for your engine.
What each engine receives, and how it loads the sheet
Keep every PNG next to its data file: the data names its image (meta.image in JSON, imagePath in XML, the page line in a Spine .atlas).
| File | What it is for |
|---|---|
run.png + run.json (Phaser 3 / 4) | this.load.atlas("run", "run.png", "run.json"), then this.add.sprite(x, y, "run", "run_0"); details on the Phaser atlas page. |
run.png + run.json (PixiJS 8) | const sheet = await Assets.load("run.json"), then sheet.textures["run_0"] or sheet.animations["run"]; see PixiJS spritesheet JSON. |
run.tres + run.tscn (Godot 4) | A ready SpriteFrames resource and scene instead of a bare atlas; see sprite sheets in Godot. |
run.unity.json + importer script (Unity 6) | An editor script slices the texture and builds clips; see sprite sheets in Unity. |
run.lua, run.atlas, run.xml, run.css | LÖVE quads, a Defold or Spine atlas, Sparrow XML or CSS classes: LÖVE, Defold, Spine, CSS sprites. |
Check the result
- Read the used share in Pack & Export: 0.84 means 84 % of the page is visible frame area. A low figure usually means trim is off or one frame is much larger than the rest.
- Hover the rows of the Packed frames list: each shows canvas → stored size, its page and flags such as trimmed, rotated or "same as" (an alias).
- In the engine, draw one trimmed frame at the same point as its untrimmed neighbour; if it is shifted, the loader ignores
spriteSourceSize(see troubleshooting).
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| Frames jump or wobble while the animation plays | Trimmed frames drawn without their offset: the loader or your code uses frame but ignores spriteSourceSize and sourceSize | Draw two frames with different trims at one point; they should line up | Use a loader that reads trim data, or pack with trim off (larger page); more in jitter after trimming |
| Thin lines or colour fringes at frame edges when the sprite is scaled or moves by fractions of a pixel | The texture filter reads pixels just outside the frame: a neighbour, or transparent padding | Zoom in on the edge: the line has a neighbour's colour, or is see-through | Keep shape padding at 2 or more, add extrude 1, use nearest filtering for pixel art; see atlas padding |
| Some frames appear sideways, mirrored or clipped | Rotation was allowed for an engine or loader that does not turn frames back | Those entries have "rotated": true | Pack with rotation off; Nerulio's Godot, Unity, Phaser, LÖVE, XML and CSS exports refuse a rotated pack |
| An error says sprites do not fit the page | One frame plus its padding and extrude is larger than the maximum page size | The message names the frame and its size | Raise the maximum size (up to 16384 px), allow rotation where the engine reads it, or scale that frame down |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| A plain grid sheet (the classic maker below) | Every frame already has the same size and your engine slices by cell, for example GameMaker strips or Godot's Add Frames from Sprite Sheet. There is no data file to keep in sync. |
Aseprite's own sheet export (--sheet-pack, --trim, --merge-duplicates on its command line) | The art lives in Aseprite and its JSON with tags and durations is all you need. It writes no engine bundles such as Godot SpriteFrames. |
| TexturePacker Pro | You need polygon packing, GPU texture formats or a command line in a build pipeline; measured against Nerulio on the TexturePacker alternative page. |
| The engine's own packer (Defold atlas, Spine's texture packer) | The engine packs separate images at build or export time anyway; then you only need the frame images, for example from a sheet split into PNG frames. |
Engine exports and how each was checked
"Verified" means the exported files were loaded by the real engine or tool on real CC0 assets and the result was compared with the source. Anything that was not loaded is labelled as such.
| Target | Files | Check |
|---|---|---|
| Godot 4 | .tres SpriteFrames · .tscn AnimatedSprite2D · .png.import | Verified Godot 4.7.2Loaded by Godot; every animation frame drawn and compared with the source file. The scene uses Nearest filtering; rotation is never used. |
| Unity 6 | .unity.json · Editor/NerulioSpriteImporter.cs · AnimationClip | Verified Unity 6000.5.3f1Batch mode: sprite rects, pivots, pixels, clip keys and durations checked. |
| Phaser 3 / 4 | atlas JSON (multiatlas) · .anims.json | Verified Phaser 3.90 + 4.2Frames and animations drawn by both versions. |
| PixiJS 8 | spritesheet JSON · animations · anchor · meta.scale | Verified PixiJS 8.21Rotated and multi-page atlases included. |
| Aseprite JSON | hash / array · frameTags · slices | Verified Phaser load.aseprite · PixiJS 8.21Tags keep their direction and repeat count. |
| .aseprite | layers · tags · durations · slices | Verified Aseprite 1.3.18Opened and rendered by the Aseprite command line. |
| Spine / libGDX | .atlas | Verified spine-canvas 4.2Every region drawn by the Spine runtime, rotated and trimmed; libGDX itself was not run. |
| Starling / Sparrow XML | XML per page | Verified Phaser 4.2Phaser 3.90 draws trimmed XML frames wrongly (its own bug); the "Sparrow XML (Phaser 3)" preset turns trim off and passes in 3.90 and 4.2. |
| LÖVE | Lua quads · nerulio_atlas.lua · main.lua | Verified LÖVE 11.5Every frame drawn and every animation stepped through the shipped helper. |
| Defold | .atlas · .tilesource | Built Defold bob.jar 1.13.1Built by Defold's command-line builder; the built texture's UVs were read back. |
| GameMaker | spr_<name>_stripN.png · gamemaker.json | UNVERIFIEDNot loaded in GameMaker (no headless GameMaker). The strips are cut the way its strip importer cuts them. |
| CSS sprites | .css · preview .html | Verified ChromiumEvery class drawn from the shipped stylesheet. |
| GIF / APNG | one file per animation | Decoded PillowEvery frame and delay decoded again after export; APNG is exact. |
Evidence
Exports: 49 runs in the real engines on real CC0 assets, 47 pass — the 2 failures are Phaser 3.90's trimmed-XML bug, which the Phaser 3 preset avoids. Packer head-to-head on 5 real frame sets (CodeAndWeb free web packer, GAPTools, free-tex-packer): smallest sheet on all 5 with rotation allowed, 4 of 5 without; every frame restored pixel-exact.
Limits
- Polygon (mesh) packing is not offered: none of the verified engines draws a mesh atlas from exported data.
- GameMaker strips are UNVERIFIED in GameMaker itself.
- Scale variants (@0.5x–@4x) use nearest-neighbour only.
- Trimmed Starling XML is drawn wrongly by Phaser 3.90; use the "Sparrow XML (Phaser 3)" preset there.
Versions and sources
The sizes above are the output of Nerulio's packer on the six ninja frames of the repository's test corpus, the same numbers as the 2026-09-23 head-to-head. Exports were loaded in Godot 4.7.2, Unity 6000.5.3f1, Phaser 3.90 and 4.2, PixiJS 8.21, spine-canvas 4.2 and LÖVE 11.5; Defold bob.jar 1.13.1 built the Defold files. The other tools' option names follow their documentation.
Official documentation used
Classic tool
A plain uniform grid sheet (fixed cells, rows × columns) is still made by the classic sprite sheet maker.
Open the classic tool →Questions
Can it make a plain grid sheet (rows × columns)?
The Studio packs tightly. For a uniform grid with fixed cells, the classic sprite sheet maker linked below still does that.
Are the frames pixel-exact?
Yes. On five real frame sets every frame restored from the atlas equals its source file pixel for pixel.
Is it free, and do I need an account?
Everything on these pages is free and works without an account. There is no watermark and no export that is held back.
Are my sprites uploaded?
No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.
Does it need Aseprite or an engine plugin installed?
No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).