LÖVE (Love2D) Sprite Atlas and Quads — No Hand-Written Rects
LÖVE has no atlas format, so frame rectangles usually end up typed by hand. This export writes them for you as a Lua quads table, plus nerulio_atlas.lua to draw and step animations — run in LÖVE 11.5 frame by frame.
LÖVE has no atlas file format: you load the PNG with love.graphics.newImage, describe each frame as a Quad with love.graphics.newQuad(x, y, w, h, pageW, pageH), and draw it with love.graphics.draw(image, quad, x, y, r, sx, sy, ox, oy). Nerulio packs the frames and writes those numbers as a Lua table (rectangle, trim offset, pivot, animations with durations in seconds), plus nerulio_atlas.lua, a small helper that builds the quads and steps animations. LÖVE 11.5 drew every frame and animation through that helper.

- LÖVEVerified· LÖVE 11.5
- 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
- DefoldBuilt· Defold bob.jar 1.13.1
- GameMakerUNVERIFIED
- CSS spritesVerified· Chromium
- GIF / APNGDecoded· Pillow
Quads, origins and trimmed frames
A Quad is only a rectangle of an image. Drawing it puts the rectangle's top-left corner at x, y, unless you pass an origin offset ox, oy as the last arguments of love.graphics.draw; that offset is how a frame is drawn from its feet or its centre, and it is also the point scaling and rotation happen around.
A trimmed frame stores only its visible pixels, so the origin has to be corrected by the trim: origin = pivot − trim offset. In the exported table px, py is the pivot on the full frame canvas and ox, oy is where the stored pixels start on that canvas; the helper draws with the origin (px − ox, py − oy).
The LÖVE preset packs with 1 px extrude and 2 px padding and never rotates, so every quad is upright and has a copy of its edge pixels around it. The helper calls love.graphics.setDefaultFilter("nearest", "nearest") before it loads the image, which keeps pixel art sharp at 4×.
- Quad
- A rectangle of an image, made with
love.graphics.newQuadfrom x, y, width, height and the image's size. - ox, oy in draw
- The origin offset of
love.graphics.draw: the point that lands on x, y. - ox, oy in the table
- Where a trimmed frame's stored pixels start on its original canvas.
- durations
- Seconds per frame of an animation, converted from the tag's milliseconds.
Example: one frame from table to screen
run.lua (page run.png 64 × 37: extrude 1, padding 2, no rotation)
["run_0"] = { page = 1, x = 1, y = 20, w = 18, h = 15, ox = 10, oy = 10, sw = 40, sh = 29, px = 20, py = 29 }
["run"] = { loop = true, frames = { "run_0", …, "run_5" }, durations = { 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 } }
quad = love.graphics.newQuad(1, 20, 18, 15, 64, 37)
origin = (px − ox, py − oy) = (20 − 10, 29 − 10) = (10, 19)
love.graphics.draw(image, quad, 200, 200, 0, 4, 4, 10, 19)
→ the frame's feet at (200, 200), drawn 4× larger
spacing on the page: run_5 covers x = 1–21, its extrude column is x = 22,
padding x = 23–24, run_3's extrude column x = 25, run_3 starts at x = 26Each stored piece sits 1 px in from its own extrude belt and at least 4 px (1 + 2 + 1) from the next piece, so a quad drawn at a fractional position or scale still samples its own colours at the edge.
What it does
- Atlas PNG with 1 px extrude by default, which avoids bleeding at non-integer positions.
- Lua table of quads with pivots, animations and per-frame durations.
- nerulio_atlas.lua helper and a main.lua that draws every animation with nearest filtering.
- Rotation refused for this target, so every quad is upright.
How to do it
- Drop frames, a GIF, an .aseprite file or a sheet (then Apply).
- Set animations and pivots.
- In Pack & Export choose LÖVE.
- Put the files next to your main.lua and require nerulio_atlas.
Files you get
| File | What it is for |
|---|---|
run.lua | images, pages (w, h), frames (x, y, w, h, trim ox/oy, canvas sw/sh, pivot px/py), order and animations (loop, frames, durations in seconds). |
nerulio_atlas.lua | Atlas.load(path) builds images and quads; atlas:draw(key, x, y, sx, sy) draws from the pivot; atlas:play(name) returns a small player with update(dt) and draw. |
main.lua | An example that plays the first animation at 4× (love . in the folder). |
run.png | The page, with a 1 px extrude around every frame. |
Use it in LÖVE 11
- Put
run.png,run.luaandnerulio_atlas.luain your game folder, next tomain.luaor in a subfolder whose path you pass toAtlas.load. - In
love.load:Atlas = require("nerulio_atlas"),atlas = Atlas.load("run.lua"),anim = atlas:play("run"). - In
love.update(dt)callanim:update(dt); inlove.draw()callanim:draw(x, y, 4), which draws with the pivot at x, y at 4× scale. - Without the helper:
local data = love.filesystem.load("run.lua")(), create onelove.graphics.newQuad(f.x, f.y, f.w, f.h, page.w, page.h)per frame, and draw with the originf.px − f.ox, f.py − f.oy.
Check the result
- Run
love .inside the exported folder:main.luaplays the first animation at 4×, with square pixel edges. - Draw run_0 and run_4 at the same point: the feet stay on one line although their pieces start at different trim offsets, (10, 10) and (10, 8).
- One cycle of
runlasts 0.6 s: six durations of 0.1 s.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| Frames jump up and down while the animation plays | Each quad is drawn from its own top-left corner, without the trim and pivot origin | Draw two frames at one point and compare with the Studio preview | Pass the origin px − ox, py − oy, or use the helper's draw |
| Pixels are blurry when scaled | The image was loaded before setDefaultFilter("nearest", "nearest"), or your own image has a linear filter | Where the filter is set relative to newImage | Set the default filter first (the helper does), or call image:setFilter("nearest", "nearest") |
| Thin lines from neighbouring frames at fractional positions or scales | A hand-made sheet without extrude or padding around the quads | Watch a frame edge at 4× while moving it by half a pixel | Repack with the LÖVE preset: extrude 1, padding 2 |
module 'nerulio_atlas' not found | The helper is not on the require path | love.filesystem.getInfo("nerulio_atlas.lua") | Put it next to main.lua, or require it with its folder, for example require("lib.nerulio_atlas") |
| An animation stops on its last frame | The tag does not loop, so the player holds the last frame | data.animations.run.loop | Set the tag to repeat forever before exporting, or start the player again with atlas:play("run") |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| A uniform grid and quads computed in a loop | All frames have the same size and no trim: love.graphics.newQuad(col * w, row * h, w, h, sheetW, sheetH) for each cell, no data file needed. |
| A TexturePacker JSON read with a Lua JSON library | You already have a JSON atlas: create quads from each frame rectangle the same way; trimmed frames still need the spriteSourceSize offset in the origin. |
| The same frames for another engine | Defold atlas or Phaser texture atlas. |
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
LÖVE 11.5 (portable build) ran a probe that requires the exported nerulio_atlas.lua, drew every frame into a Canvas, stepped every animation through the helper's player and drew one frame at 4×; the pixels were read back and compared with the source frames. Function signatures are those of the LÖVE 11.5 source.
Official documentation used
Questions
What was checked in LÖVE?
LÖVE 11.5 drew every frame and stepped every animation through the shipped helper, and the result was sharp at 4×.
Do I have to use the helper?
No. The quads table is plain Lua data; the helper only shows one way to draw and time it.
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).