Skip to content

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.

Drop frame images, a GIF or an .aseprite file to pack
Runs in your browser. Files are not uploaded.
Studio Pack & Export: the packed atlas page with every frame outlined, the used area and the export targets
The real Studio with a CC0 asset (OpenGameArt).
Exports to
  • 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 walk

The 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

  1. Drop the frames, a sheet (then Apply), a GIF or an .aseprite file.
  2. Set the pivot (P) on one frame and apply it to the whole tag, for example bottom-centre under the feet.
  3. Play the loop with onion skin (F3) to make sure the art itself does not jump.
  4. 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

TargetHow the offset is kept
Godot 4AtlasTexture margin = the offset plus the size that was cut away, so every frame keeps its full size
Unity 6Pivot normalised inside the trimmed sprite rect from the full-canvas pivot (it may lie outside 0–1)
Phaser, PixiJS, Aseprite JSON, Generic JSONspriteSourceSize (the offset) and sourceSize (the full size), TexturePacker style
Spine / libGDX .atlasoffsets: x from the left, y from the bottom of the original frame, plus the original size
Starling / Sparrow XMLframeX / frameY (negative offsets) with frameWidth / frameHeight
DefoldFull-canvas frame images, so there is no offset to restore
CSS spritesNever trimmed: the element's box is the stored rectangle
GameMaker stripsEvery frame drawn on one cell with its pivot on the origin (UNVERIFIED in GameMaker)

Troubleshooting

SymptomLikely causeHow to checkFix
The body shifts sideways whenever a weapon or limb sticks outThe pivot is the centre of each cropped rectangleCompare crop widths: the shift equals the difference between crop centresMeasure 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 editorThe loader ignores the trim offset (spriteSourceSize, frameX / frameY)Draw one trimmed frame and the untrimmed original at the same positionUse 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 switchingAnimatedSprite2D has one offset per node, taken from the first frame's pivot, and other animations use other pivotsThe Godot export notes warn about differing pivotsGive 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.90With 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 exactlyThe shifted frames have an odd canvas width or heightUse even frame canvases (align the tag to an even size), or Phaser 4
The wobble is already in the source frames, before any trimThe art was drawn or cut unevenly: a drawing problem, not a packing oneMeasure the untrimmed frames in NerulioFix (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

OptionWhen it is the better choice
Pack with trim offAlways 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.

TargetFilesCheck
Godot 4.tres SpriteFrames · .tscn AnimatedSprite2D · .png.importVerified 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 · AnimationClipVerified Unity 6000.5.3f1Batch mode: sprite rects, pivots, pixels, clip keys and durations checked.
Phaser 3 / 4atlas JSON (multiatlas) · .anims.jsonVerified Phaser 3.90 + 4.2Frames and animations drawn by both versions.
PixiJS 8spritesheet JSON · animations · anchor · meta.scaleVerified PixiJS 8.21Rotated and multi-page atlases included.
Aseprite JSONhash / array · frameTags · slicesVerified Phaser load.aseprite · PixiJS 8.21Tags keep their direction and repeat count.
.asepritelayers · tags · durations · slicesVerified Aseprite 1.3.18Opened and rendered by the Aseprite command line.
Spine / libGDX.atlasVerified spine-canvas 4.2Every region drawn by the Spine runtime, rotated and trimmed; libGDX itself was not run.
Starling / Sparrow XMLXML per pageVerified 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ÖVELua quads · nerulio_atlas.lua · main.luaVerified LÖVE 11.5Every frame drawn and every animation stepped through the shipped helper.
Defold.atlas · .tilesourceBuilt Defold bob.jar 1.13.1Built by Defold's command-line builder; the built texture's UVs were read back.
GameMakerspr_<name>_stripN.png · gamemaker.jsonUNVERIFIEDNot loaded in GameMaker (no headless GameMaker). The strips are cut the way its strip importer cuts them.
CSS sprites.css · preview .htmlVerified ChromiumEvery class drawn from the shipped stylesheet.
GIF / APNGone file per animationDecoded 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).