Sprite Jitter After Trimming — Keep Offsets and Pivots
A character that stood still before packing and wobbles after it has lost its trim offsets: each frame was cropped to its visible pixels, and the engine now places or pivots every crop on its own. The fix is to keep each frame's original size and offset, or to move the pivot along with the crop.
Trimming crops each frame to its visible pixels, so every frame gets its own size and its own top-left corner. If the engine then places frames by that cropped rectangle, by its corner or by a "centre" pivot of the crop, each frame is drawn from a different point of the body and the character wobbles by the difference. The fix is to keep each frame's original size and offset (sourceSize and spriteSourceSize, Godot AtlasTexture margins, Unity pivots computed on the full canvas) or to put the frames back on one canvas, and then to measure what is left. Nerulio's Measure reports the wobble in pixels, and its fixes remove it while keeping motion you drew on purpose.

- 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
What trimming changes, and how that becomes wobble
Before trimming, every frame of a 48 × 64 animation shares one canvas, so a pivot at pixel (24, 64) is the same point of the body on every frame. Trimming keeps only the opaque rectangle: a standing frame becomes 20 × 30 at (14, 34), a frame with the sword out becomes 34 × 30 at the same corner. Size and origin now differ per frame, and the only link back to the original position is the offset the packer writes: sourceSize and spriteSourceSize in JSON atlases, the AtlasTexture margin in Godot, offsets in a Spine atlas, frameX and frameY in Sparrow XML.
The wobble equals how far the reference point moves on the body. With a pivot at the centre of each crop, the standing frame's centre is at x = 14 + 10 = 24 but the sword frame's at 14 + 17 = 31: the engine puts both centres on the object's position, so the body jumps 7 px left whenever the sword is out. With a top-left origin and the offset ignored, a crop that starts 3 px lower is drawn 3 px higher. Three things cause it: the offset was never written, it was written but the loader ignored it, or the pivot was defined per crop instead of on the full canvas.
Not every wobble comes from the packer; frames drawn or cut unevenly wobble too. Nerulio measures it on the frames themselves: Measure (Align panel) takes one reference point per frame (bottom-centre of the art, centre of the art, alpha centroid or pivot), fits a smooth path through them with a local quadratic five frames wide, and reports how far each frame sits off that path, as the maximum and RMS in pixels, marked on the timeline. Above 1.5 px it warns. Movement from frame to frame is shown separately, because a walk or a bob is motion you want to keep.
That is also the rule for fixing. Fix (keep motion) moves each frame only by the rounded distance to the smooth path, so a drawn bob or a travelling walk survives. Pin to first frame puts every frame's reference on frame 1's point: right for an idle that must stand still, wrong for anything that travels, and wrong when the game moves the character itself and the frames carry no motion (then the motion was never in the frames to begin with). Both move art by whole pixels only.
- sourceSize / spriteSourceSize
- The frame's original size, and where its trimmed rectangle sat on it.
- Pivot on the full canvas
- A pivot measured on the untrimmed frame, so it stays on the same body point after trimming.
- Wobble (residual)
- Distance of a frame's reference point from the smoothed path, in pixels.
Example: measuring and fixing, with and without intended motion
Numbers from Nerulio's jitter measure (bottom-centre of the art, default settings).
idle, 8 frames, x of the art's bottom-centre: 24, 24, 27, 24, 24, 21, 24, 24
Measure wobble max 1.54 px, RMS 0.86 px; movement up to 3.0 px; warning: over 1.5 px shows
Fix (keep motion) shifts 0, +1, −2, +1, −1, +2, −1, 0 px → wobble 0.51 px
Pin to first frame shifts 0, 0, −3, 0, 0, +3, 0, 0 px → wobble 0.00 px
walk that travels 2 px per frame with a 1 px bob: x 18, 20, 22 … 32
Measure wobble max 0.34 px, no warning; movement up to 2 px
Pin to first frame would shift the frames by 0, −2, −4 … −14 px and erase the walkThe idle is a case for pinning (it must stand still), the walk for keeping motion, or for no fix at all. Frames must share one canvas before a fix: see aligning frames on one canvas.
What it does
- Trim (keep size and position) stores every frame's original canvas size and where its cropped pixels sat on it.
- Each export writes that offset the way its engine reads it (table below), and the pivot is taken from the full frame canvas, never from the crop.
- Crop, keep position (pivot moves) makes frames really smaller and shifts the pivot by the same amount, so the art stays where it was.
- In the engine runs the trimmed frames were drawn by Godot 4.7.2, Phaser 3.90 and 4.2 and PixiJS 8.21 and compared with the original frames; Unity 6000.5.3f1 read back the pivots on one common anchor.
- If the art itself wobbles, because frames were drawn at slightly different spots, Align frames measures the drift in pixels and fixes it with whole-pixel moves.
How to do it
- Drop the frames, a sheet (then Apply), a GIF or an .aseprite file.
- Set the pivot (P) on one frame and apply it to the whole tag, for example bottom-centre under the feet.
- Play the loop with onion skin (F3) to make sure the art itself does not jump.
- In Pack & Export keep Trim (keep size and position), or pick Crop, keep position for smaller frames, and export.
Check the result
- Before export, play the tag with onion skin (F3): the feet of neighbouring frames overlap.
- Press Measure in the Align panel: no warning, and wobble under 1.5 px.
- In the engine, pause on two frames with very different crop sizes and compare where the feet are, in pixels: they must be at the same position.
- In Unity, a trimmed frame's Custom Pivot may lie outside 0–1; that is expected and keeps the frames aligned.
How each target keeps a trimmed frame in place
| Target | How the offset is kept |
|---|---|
| Godot 4 | AtlasTexture margin = the offset plus the size that was cut away, so every frame keeps its full size |
| Unity 6 | Pivot normalised inside the trimmed sprite rect from the full-canvas pivot (it may lie outside 0–1) |
| Phaser, PixiJS, Aseprite JSON, Generic JSON | spriteSourceSize (the offset) and sourceSize (the full size), TexturePacker style |
| Spine / libGDX .atlas | offsets: x from the left, y from the bottom of the original frame, plus the original size |
| Starling / Sparrow XML | frameX / frameY (negative offsets) with frameWidth / frameHeight |
| Defold | Full-canvas frame images, so there is no offset to restore |
| CSS sprites | Never trimmed: the element's box is the stored rectangle |
| GameMaker strips | Every frame drawn on one cell with its pivot on the origin (UNVERIFIED in GameMaker) |
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| The body shifts sideways whenever a weapon or limb sticks out | The pivot is the centre of each cropped rectangle | Compare crop widths: the shift equals the difference between crop centres | Measure pivots on the full canvas; Nerulio writes every target's pivot that way. In another packer set the pivot before trimming, or turn trim off |
| Frames bob after packing but not in the editor | The loader ignores the trim offset (spriteSourceSize, frameX / frameY) | Draw one trimmed frame and the untrimmed original at the same position | Use a loader that reads the offset. Phaser 3.90 draws trimmed Sparrow XML wrongly: use the Sparrow XML (Phaser 3) preset, which turns trim off |
| Godot: steady inside one animation, a jump when switching | AnimatedSprite2D has one offset per node, taken from the first frame's pivot, and other animations use other pivots | The Godot export notes warn about differing pivots | Give the character one pivot for all tags, or set the offset from metadata/nerulio in a script |
| Some trimmed frames sit 1 px off in Phaser 3.90 | With pixelArt or roundPixels, Phaser 3.90 draws a trimmed frame 1 px right (down) when its source width (height) is odd; Phaser 4 and PixiJS 8 draw the same files exactly | The shifted frames have an odd canvas width or height | Use even frame canvases (align the tag to an even size), or Phaser 4 |
| The wobble is already in the source frames, before any trim | The art was drawn or cut unevenly: a drawing problem, not a packing one | Measure the untrimmed frames in Nerulio | Fix (keep motion) or Pin to first frame; no tool can tell a deliberate bob from a mistake, so choose per tag |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| Pack with trim off | Always stable, at the cost of atlas space; CSS sprites never trim for this reason. |
| TexturePacker's Trim mode (the sprite keeps its original size) | You pack with TexturePacker: its Trim mode stores the offsets, while Crop modes make frames smaller. See free alternatives to TexturePacker. |
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
- Nerulio cannot change how an engine or loader reads offsets; it can only write them in each target's own form.
- A pivot a game sets in code overrides the exported one.
- 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
Nerulio's trimmed frames were drawn by Godot 4.7.2, Phaser 3.90 and 4.2 and PixiJS 8.21 and compared with the original frames (placement check), and Unity 6000.5.3f1 read back pivots that keep every frame on one anchor. The Phaser 3.90 odd-size shift and the trimmed-XML bug were measured in the same runs. TexturePacker's modes and Phaser's parser are described from their own documentation and source.
Official documentation used
Questions
Why did my frames wobble after another packer trimmed them?
Either the offset was never written, or it was written but ignored, or the pivot was set per cropped rectangle, so a "centre" pivot moved with each crop. Pivots measured on the full canvas avoid all three.
Should I just turn trim off?
That works and costs atlas space. The Studio does it only where it has to: CSS sprites never trim, because an element's box is the stored rectangle and a crop would lose its offset.
Why are some Unity pivots outside 0–1?
Unity pivots are relative to the sprite rect. For a trimmed frame the rect is the crop, so a pivot measured on the full canvas can land outside it, and that is exactly what keeps the frames aligned.
Godot uses only one offset per node. Does that break it?
AnimatedSprite2D has one offset per node, so the scene uses the first frame's pivot. The trim itself is kept per frame by AtlasTexture margins, and every pivot is kept in the resource metadata.
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).