Sprite Pivot Editor — Origin Points per Frame
Click once to place a frame's pivot on the pixel grid, or set it for a whole tag at once. The pivot travels with the frame into each engine's own origin field.
A pivot (also called origin or anchor) is the point of a frame that sits on the game object's position: rotation turns around it, and frames of different sizes line up on it. In Nerulio you click it onto the pixel grid (P) for one frame, the selection, a tag or every frame; it is stored as 0–1 of the frame canvas and written into each engine's own field on export. Unity, Phaser and PixiJS can follow a pivot per frame; a Godot node has one offset, so the table below says what each target really gets.

- 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
One pivot value, many engine conventions
Engines count pivots differently. Unity measures a sprite's pivot from the bottom-left of that sprite's rect, as 0–1 (Normalized) or in pixels. Phaser and PixiJS use 0–1 of the frame with y pointing down. Godot's AnimatedSprite2D has no pivot at all, only a drawing offset in pixels for the whole node. Nerulio keeps one value, 0–1 of the frame canvas with y down (bottom-centre 0.5, 1.0 unless you set it), and converts it per target, so a pivot set once survives trimming, packing and a change of engine.
Per-frame pivots matter when frames differ in size or are trimmed. The Godot scene therefore uses the first frame's pivot as the node offset, keeps every frame's pivot in the resource metadata, and warns when one animation has differing pivots. The robust setup for any engine is frames on one canvas and one pivot per tag at the feet.
The pivot tool snaps to whole pixels, and pivots travel with the art when frames are aligned, mirrored or fixed for jitter.
- Pivot / origin / anchor
- The point of a frame placed on the object's position; the name depends on the engine.
- Normalized pivot
- 0–1 across the frame or sprite rect; Unity counts y from the bottom, Phaser and PixiJS from the top.
- Node offset (Godot)
- Pixels by which an AnimatedSprite2D draws its texture away from the node origin; one value for every frame.
Example: one pivot, written for five engines
A 48 × 64 frame canvas with the pivot at bottom-centre. The packer trims a standing frame to 20 × 30 at (14, 34) and a jumping frame to 20 × 30 at (14, 20).
Nerulio pivot (0.5, 1.0) of 48 × 64 → pixel (24, 64)
Godot .tscn centered = false, offset = (−24, −64)
Phaser / PixiJS pivot / anchor (0.5, 1.0) of the 48 × 64 source frame
LÖVE px = 24, py = 64
Unity, stand ((24 − 14) ÷ 20, (30 − (64 − 34)) ÷ 30) = (0.5, 0.0)
Unity, jump ((24 − 14) ÷ 20, (30 − (64 − 20)) ÷ 30) = (0.5, −0.467)The jump frame's Unity pivot lies below its own rect; that is exactly what keeps the feet on the same spot. Why trimming moves pivots is explained on sprite jitter after trimming.
What each target receives
| Target | What Nerulio writes | What the engine does with it |
|---|---|---|
| Godot 4 | Scene: centered = false, offset = −pivot of the first frame of the first animation; every frame's pivot in metadata/nerulio | AnimatedSprite2D draws every frame with that one offset; per-frame pivots need your own script |
| Unity 6 | A custom pivot per sprite, 0–1 inside the sprite rect with y up (outside 0–1 for some trimmed frames) | Each sprite keeps its own pivot, so a clip changes pivot with the frame (read back in Unity 6000.5.3f1) |
| Phaser 3 / 4 | pivot {x, y} per frame in the atlas JSON | Phaser 3.90 marks it as the frame's custom pivot and calls setOrigin with it on every frame change, animation frames included |
| PixiJS 8 | anchor {x, y} per frame | It becomes the texture's default anchor: a Sprite takes it when created; an AnimatedSprite follows it per frame only with updateAnchor = true |
| Aseprite JSON, .aseprite | A pivot slice keyed on the frames where the pivot changes | Aseprite shows a slice with a pivot point |
| LÖVE 11 | px, py in the Lua frame table | The shipped nerulio_atlas.lua draws each frame with its pivot on (x, y) |
| GameMaker | One strip per tag with every pivot on one point; that point is the origin in gamemaker.json and the README | Type it as the sprite's Origin in the Sprite Editor (UNVERIFIED in GameMaker) |
| Defold, Spine / libGDX, CSS | No pivot | Set it in the engine |
Starling / Sparrow XML also gets pivotX and pivotY in pixels; whether a reader uses them depends on that reader.
What it does
- Pivot tool (P): a click snaps the pivot to the pixel grid of the current frame.
- Scope: this frame, the selected frames, the tag or every frame.
- Pivots stay attached to the art through alignment and mirroring.
- Exports: Godot scene offset, Unity sprite pivot, PixiJS anchor, Aseprite slice pivot, GameMaker origin.
How to do it
- Drop and apply your sprite import.
- Choose a scope in the Frame panel and press P.
- Click where the feet (or the centre) should be; nudge with the arrow keys.
- Export for your engine; the pivot is written in its format.
Check the result
- Turn on onion skin (F3) and step through the tag: the pivot cross should stay on the same spot of the body.
- Godot: read the export notes (they warn about differing pivots) and
print($Hero.sprite_frames.get_meta("nerulio"))for every frame's pivot. - Unity: open the texture in the Sprite Editor; each sprite's Pivot is Custom with the normalized value from the example.
- Phaser: after
sprite.play('run'),sprite.originXandsprite.originYequal the current frame's pivot.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| The Godot character jumps between animations | The node has one offset, taken from the first frame of the first animation, and other animations use other pivots | The Godot export notes; compare pivots in metadata/nerulio | Give all tags of the character one pivot, or set offset from the metadata in a script when the animation changes |
| A PixiJS AnimatedSprite ignores per-frame pivots | updateAnchor is false by default, so the anchor of the first texture stays | Log sprite.updateAnchor | Set updateAnchor = true, or keep one pivot for the tag |
| Unity pivots below 0 or above 1 | The frame was trimmed and the full-canvas pivot lies outside the smaller rect | Sprite Editor: the Custom Pivot value | Nothing to fix: it keeps the art aligned. Pack with trim off if a tool demands 0–1 |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| Set the pivot in the engine (Unity Sprite Editor Pivot, GameMaker Origin, Godot node offset) | One sprite with one pivot that you rarely re-export. |
| Pivot slices in Aseprite | You keep pivots in the .aseprite source; Nerulio reads a pivot slice on import and writes it back to .aseprite and Aseprite JSON. |
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
GIF decoder: 408 of 408 frames identical to Pillow on 29 real GIFs; APNG 132 of 132 on 16 files. .aseprite: 231 corpus files imported, re-written and reopened in Aseprite 1.3.18 with the same tags, durations and pixels (231/231). 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.
Limits
- Godot gets one pivot per node, not per frame, unless you script it.
- Defold, Spine / libGDX and CSS exports carry no pivot.
- GameMaker origins are UNVERIFIED in GameMaker itself.
- A grid or island guess is a preview with a confidence; nothing is cut until you press Apply, and every choice has one-click alternatives.
- .aseprite files whose layers do not compose exactly (e.g. group opacity) are imported flattened, and the Import panel says why.
- Rotated frames in an imported atlas stay rotated; their export is UNVERIFIED.
- Images up to 32,768 px per side (268 million pixels), subject to your browser's memory.
Versions and sources
Unity 6000.5.3f1 read back every sprite's pivot and it put the art where it was relative to one common anchor. Godot 4.7.2 loaded the scene with its offset. What Phaser and PixiJS do with the pivot comes from the Phaser 3.90 source and the PixiJS 8 documentation, not from a Nerulio run.
Official documentation used
- Unity 6 manual: Sprite Editor tab reference
- Godot 4.7 docs: AnimatedSprite2D
- Phaser 3.90 source: JSONHash parser (pivot, trim)
- Phaser 3.90 source: AnimationState.setCurrentFrame
- PixiJS 8 docs: AnimatedSprite (updateAnchor)
- PixiJS 8 docs: Texture.defaultAnchor
- Aseprite docs: Slices
- GameMaker manual: The Sprite Editor
Questions
Is the pivot stored in pixels or 0–1?
It is stored as 0–1 of the frame canvas and the tool snaps it to whole pixels; each engine format is converted from that value.
Does Unity get one pivot per frame?
Yes. Unity 6 sprite rects are created with their pivots by the included importer script (checked in Unity 6000.5.3f1).
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).