Skip to content

PixiJS Spritesheet JSON Generator — Animations, Anchors, Scale

A spritesheet JSON with frames only gives PixiJS no animations, and an @2x copy whose meta.scale still says "1" draws at double size. This export writes both, with each frame's anchor taken from its pivot — loaded and drawn by PixiJS 8.21, rotated and multi-page atlases included.

PixiJS 8 loads a spritesheet from one JSON file: const sheet = await Assets.load("run.json") also loads the PNG named in meta.image, then sheet.textures["run_0"] is one frame and sheet.animations["run"] the list of textures of one animation. Nerulio writes that JSON with the animations map in playback order, an anchor per frame taken from your pivot, meta.scale for each @2x variant and related_multi_packs for extra pages; per-frame milliseconds go in meta.nerulio. Checked in PixiJS 8.21, rotated and multi-page sheets included.

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
  • PixiJS 8Verified· PixiJS 8.21
  • Godot 4Verified· Godot 4.7.2
  • Unity 6Verified· Unity 6000.5.3f1
  • Phaser 3 / 4Verified· Phaser 3.90 + 4.2
  • 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 PixiJS reads from the JSON

Each entry under frames becomes a Texture. frame is the rectangle on the page, rotated says the piece lies turned, spriteSourceSize and sourceSize restore a trimmed frame to its full size, and anchor becomes the texture's default anchor, so a Sprite made from it is placed by that point (0.5, 1 = bottom centre).

The animations map lists frame names per animation and carries no timing. An AnimatedSprite built from plain textures plays them at one rate, scaled by animationSpeed. For per-frame timing, pass objects { texture, time } with time in milliseconds; Nerulio stores those milliseconds in meta.nerulio.animations, because the standard format has no field for them.

meta.scale is the resolution of the page: at "2" the textures of an @2x page have the same size in PixiJS units as those of the @1x page, with twice the detail. Extra pages are named in meta.related_multi_packs and load together with the first JSON.

Texture
One frame: a rectangle of the page plus trim, rotation and default anchor.
anchor
The point of the texture placed at the sprite's position, 0–1 on each axis.
animations
Name → ordered list of frame names; PixiJS turns each list into an array of textures.
meta.scale
The page's resolution: "1" for @1x, "2" for @2x.

Example: one frame of the run cycle, file and PixiJS side by side

run.json (page run.png 38 × 50, "scale": "1")
"run_0": { "frame": {"x":0,"y":34,"w":18,"h":15}, "rotated": false, "trimmed": true,
           "spriteSourceSize": {"x":10,"y":10,"w":18,"h":15}, "sourceSize": {"w":40,"h":29},
           "anchor": {"x":0.5,"y":1} }
"animations": { "run": ["run_0","run_1","run_2","run_3","run_4","run_5"] }
"meta": { …, "nerulio": { "animations": { "run": { "fps":10, "durationsMs":[100,100,100,100,100,100] } } } }

PixiJS 8.21 read back for run_0:
  frame 18 × 15 at (0, 34)   trim at (10, 10)   orig 40 × 29   rotate 0
  new Sprite(texture): width 40, height 29, anchor (0.5, 1)

With rotation allowed, the same six frames pack into 33 × 57 and five of them are stored turned; PixiJS 8.21 drew rotated frames upright in the verification runs, so the PixiJS preset allows rotation.

What it does

  • Spritesheet JSON (TexturePacker hash style) with an animations map in playback order.
  • anchor per frame from the pivot you set in the Sprite workspace.
  • Scale variants @0.5x–@4x (nearest), each with its own meta.scale; several pages linked by related_multi_packs.
  • Per-step milliseconds kept in meta.nerulio, since PixiJS's animations map has no timing.

How to do it

  1. Drop frames, a GIF, an .aseprite file or a sheet (then Apply).
  2. Set pivots and animations in the Sprite workspace.
  3. In Pack & Export choose PixiJS 8; add scale variants if you need @2x.
  4. Load it with Assets.load and play the animations.

Files you get

FileWhat it is for
run.jsonFrames with anchor, the animations map (on the first page), meta.scale, meta.related_multi_packs when there are several pages, and meta.nerulio with fps, loop, direction and durationsMs per animation.
run.pngThe page; with scale variants also run@2x.png with its own run@2x.json, and so on.
README-PIXI.mdLoading code for this bundle, including an AnimatedSprite that uses the exported frame times.

Use it in PixiJS 8

  1. Put run.json and run.png in the same folder of your app's public assets; the JSON names its PNG in meta.image.
  2. For pixel art, before loading anything: TextureSource.defaultOptions.scaleMode = "nearest".
  3. const sheet = await Assets.load("assets/run.json"); a still frame is new Sprite(sheet.textures["run_0"]).
  4. One rate for the whole animation: const hero = new AnimatedSprite(sheet.animations["run"]), hero.animationSpeed = …, hero.play().
  5. With the exported timing: const ms = sheet.data.meta.nerulio.animations.run.durationsMs, then new AnimatedSprite(sheet.animations.run.map((texture, i) => ({ texture, time: ms[i] }))), hero.play() and app.stage.addChild(hero).

Check the result

  • Object.keys(sheet.textures) lists run_0 … run_5.
  • new Sprite(sheet.textures.run_0) is 40 wide and 29 high: the full frame, not the 18 × 15 stored piece.
  • Zoom to 4×: with nearest scaling the edges stay square; soft edges mean the scale mode was not set before loading.

Troubleshooting

SymptomLikely causeHow to checkFix
sheet.animations is undefined or emptyThe JSON has no animations map (many exports write frames only), or you look at a later page of a multi-page sheetOpen the JSON: is there a top-level animations? With several pages it is on the firstExport with tags, or build the list yourself: ["run_0", …].map(n => sheet.textures[n])
Sprites hang from their top-left corner or sit at the wrong spotNo anchor in the JSON, or your code overrides itsprite.anchor.x, sprite.anchor.yKeep the exported anchor, or set pivots in the pivot editor before exporting
An @2x sheet draws twice as largeIts JSON says "scale": "1"sheet.data.meta.scaleSet it to "2", or export scale variants; see the @2x scale fix
The animation plays faster or slower than in the editorPlain textures play at one rate set by animationSpeed (default 1), not at the tag's millisecondsCompare hero.animationSpeed with the timing you wantUse { texture, time } objects with the exported milliseconds, or tune animationSpeed
Pixel art looks softLinear scaling, PixiJS's defaultThe texture source's scaleModeSet TextureSource.defaultOptions.scaleMode = "nearest" before Assets.load

Other ways to do it

OptionWhen it is the better choice
Aseprite's JSON loaded by PixiJSThe art is already exported from Aseprite; PixiJS reads its frames but builds no animations from frameTags. See Aseprite JSON to PixiJS.
TexturePacker's PixiJS exportYou already use TexturePacker; its pixijs4 output, which rotates by default, passed in PixiJS 8.21 on the four sets we tested.
Loose PNGs, one Assets.load eachA prototype with a handful of frames; one request and one texture per frame, no 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.

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

  • 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

Loaded in PixiJS 8.21.0 (WebGL in Chromium) with a plain Assets.load: rectangles, trim, anchors, animations, rotated frames and multi-page sheets were read back and every frame compared with its source; the numbers in the example are that run's read-back. API names follow the PixiJS 8 API reference.

Official documentation used

Questions

Where does the frame timing go?

PixiJS's animations map is a list of frame names; the per-step milliseconds are written in meta.nerulio so you can set animationSpeed or durations yourself.

Is rotation allowed?

Yes for PixiJS: rotated frames were drawn correctly by PixiJS 8.21.

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).