Phaser Atlas JSON from AI Sprites: TexturePacker Format, Anims and Pivots
tutorial9 min

Phaser Atlas JSON from AI Sprites: TexturePacker Format, Anims and Pivots

How Phaser's texture atlas JSON format works, how to build animations from it with generateFrameNames, and how to import an AI-generated Character Package into a Phaser scene.

Phaser doesn't care where a texture atlas came from, hand-packed in TexturePacker, exported from an editor, or generated by an AI pipeline, as long as the JSON follows a format it understands. This guide covers that format in enough detail to debug it yourself, plus the two ways Phaser builds animations from it: manual frame naming, and automatic tag-based creation from an Aseprite-style export.

The TexturePacker "Hash" JSON shape

Phaser's atlas loader reads what's commonly called the TexturePacker JSON Hash format (as opposed to the "Array" format, which is a different but related shape). The top level has two keys:

{
  "frames": {
    "walk/0": {
      "frame": { "x": 0, "y": 0, "w": 32, "h": 32 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
      "sourceSize": { "w": 32, "h": 32 }
    },
    "walk/1": { "...": "..." }
  },
  "meta": {
    "app": "sprixen",
    "image": "atlas.png",
    "size": { "w": 256, "h": 256 },
    "scale": "1",
    "frameTags": [
      { "name": "walk", "from": 0, "to": 7, "direction": "forward" }
    ]
  }
}

frames is an object, not an array, keyed by frame name. Each entry describes where that frame sits inside the packed image (frame), whether it was rotated or trimmed during packing, and its original, untrimmed size (sourceSize). meta carries information about the atlas as a whole: which image file it belongs to, its total size, and (in Sprixen's export, among others) a frameTags array describing named animation ranges.

Frame names can contain slashes; there's nothing special about the character. Sprixen names its frames <animation>/<index>, for example walk/0, walk/1, purely as a naming convention to keep animations visually grouped in the JSON, not because Phaser treats the slash as meaningful.

What rotated, trimmed, and sourceSize actually mean

rotated and trimmed describe how a packer chose to fit each frame into the smallest possible atlas image. A packer will sometimes rotate a frame 90 degrees, or trim away fully transparent padding around a sprite's edges, purely to save space, then rely on the JSON to tell the renderer how to undo that when drawing. frame gives the frame's actual rectangle inside the packed image (post-trim, if trimmed). spriteSourceSize and sourceSize record where that trimmed content sat within, and how big, the original untrimmed frame was, so a sprite that had transparent padding stripped for packing efficiency still renders at the correct offset relative to frames that weren't trimmed. Sprixen's Character Package export sets rotated: false and trimmed: false on every frame, since normalization already pads every frame in an animation to one uniform cell size before packing, which means there's no space-saving trim to reverse and no rotation to account for. You'll still see these fields in a hand-packed TexturePacker atlas, and Phaser reads them correctly either way; Sprixen's export just doesn't need them to do anything.

Loading it

this.load.atlas(
  'character',
  'assets/character/atlas.png',
  'assets/character/atlas.json'
);

this.load.atlas(key, textureURL, atlasURL) loads both files and registers them together under one key in Phaser's Texture Manager. Once loaded, you reference frames from that atlas by their string keys, exactly as they appear in the JSON's frames object.

Building animations with generateFrameNames

Rather than listing every frame name by hand, Phaser's this.anims.generateFrameNames(key, config) builds the list for you from a naming pattern:

this.anims.create({
  key: 'walk',
  frames: this.anims.generateFrameNames('character', {
    prefix: 'walk/',
    start: 0,
    end: 7,
  }),
  frameRate: 12,
  repeat: -1,
});

prefix, suffix, start, end, and zeroPad are the relevant config fields. start and end give the numeric range to generate; zeroPad pads that number with leading zeros if your frame names use them (for example attack_003 instead of attack_3). Since Sprixen's frame names have no zero-padding (walk/0, not walk/00), you'd omit zeroPad or leave it at its default. This one call replaces having to type out eight frame names by hand, and it's the same pattern regardless of how many frames the animation has, so adding frames later doesn't require touching the anims.create call at all, just the end value.

The automatic alternative: Aseprite tags

If you'd rather not write an anims.create call per animation, Phaser has a dedicated loader for Aseprite's JSON export format, which embeds named animation tags directly in the data:

this.load.aseprite(
  'character',
  'assets/character/atlas.png',
  'assets/character/atlas-aseprite.json'
);
create() {
  this.anims.createFromAseprite('character', ['idle', 'walk']);
}

createFromAseprite reads the tag definitions from the loaded Aseprite JSON and creates one Phaser animation per tag automatically; passing an array of tag names limits it to just those, useful if the file contains tags you don't want turned into playable animations. Sprixen's Character Package includes both an atlas.json (plain TexturePacker Hash, for the manual anims.create approach) and an atlas-aseprite.json (true Aseprite export shape, for this automatic approach) in the same export, so which one you use is a matter of preference for how you want animation definitions to live in your project, in code or in data, not a limitation of the export itself.

Setting the pivot with setOrigin

sprite.setOrigin(x, y) sets a sprite's anchor point as a fraction of its size, where both axes default to 0.5 (dead center). For a character whose animations vary in height (a crouch versus a standing idle, for instance), centering the origin means the character appears to float up or sink down as animations switch, since the vertical center of a taller frame sits higher than the vertical center of a shorter one. Setting setOrigin(0.5, 1) anchors the sprite at the horizontal center and the very bottom, so its feet stay on the same line regardless of which animation is playing, matching the bottom-center pivot Sprixen records in its Character Package manifest.

Multi-directional characters and atlas size

A character with four or eight directions of walk, plus idle, attack, and hurt in each, adds up to a lot of frames in one atlas quickly. Phaser has no hard limit on how many frames a single atlas can hold beyond the underlying texture size limits of the device's GPU (commonly 4096x4096 or 8192x8192 on modern hardware, smaller on older mobile GPUs), but packing every direction and animation for a large cast into one enormous shared atlas can start to hurt load time and memory on lower-end devices. Sprixen's Character Package packs one atlas per character, not one atlas for your whole game, which keeps each texture reasonably sized; if you're combining many characters, consider whether your target platform's texture budget calls for a build step that repacks several characters' atlases together, versus loading each character's atlas independently and accepting the extra texture bind switches.

Keeping pixel art crisp

If your sprites are pixel art, set pixelArt: true in your Phaser game config. This switches Phaser's default texture filtering to nearest-neighbor across the game, so scaling a small sprite up doesn't blur its edges. It's a single config flag, not something you need to set per texture or per sprite.

const config = {
  type: Phaser.AUTO,
  pixelArt: true,
  width: 800,
  height: 600,
  scene: [MainScene],
};

Sprixen's Phaser package end to end

Calling GET /v1/sprites/:id/package?engine=phaser, or the export_character_package MCP tool with engine: "phaser", returns a ZIP containing the normalized atlas.png, both JSON variants described above, a manifest.json with the pivot and animation list, and a ready-made phaser/example.js with working load.atlas and anims.create calls for every animation in the export, already using the correct frame prefixes and setOrigin value. Copying from that file directly into your scene is faster than re-deriving the frame names from the atlas JSON by hand, and it's guaranteed to match whatever frame naming that particular export actually used.

For a full walkthrough of generating the character in the first place and wiring it into a working scene, see building a Phaser game in Cursor with consistent AI art. For the MCP tool-calling side of this, see generating sprites from Claude Code with MCP. Full API reference: /docs.

FAQ

Does Phaser support the TexturePacker "Array" format too?

Phaser has separate loading paths for different TexturePacker export shapes; the Hash format described here, where frames is an object keyed by name, is the one Sprixen and most modern tooling produce, and it's the most common shape you'll encounter for hand-authored atlases as well.

What's the difference between atlas.json and atlas-aseprite.json in a Character Package?

Both describe the same packed image and the same frames. atlas.json is the plain TexturePacker Hash format, meant for manually written anims.create calls. atlas-aseprite.json is the same data in true Aseprite export shape, meant for Phaser's load.aseprite and createFromAseprite, which build animations automatically from embedded tags.

Do frameTags in atlas.json's meta block do anything if I use the plain loader?

No. this.load.atlas ignores meta.frameTags entirely; it's informational metadata in that file. Tag-based automatic animation creation only works through the dedicated load.aseprite loader and the separate atlas-aseprite.json file.

Why do my animation frames play in the wrong order?

Check that your start and end values in generateFrameNames match the actual numeric range of frame names in the atlas, and that the prefix matches exactly, including any trailing slash. A mismatched prefix silently generates an empty frame list rather than throwing an error.

Can I mix a Sprixen-generated atlas with hand-drawn frames in the same animation?

Technically yes, since Phaser just reads whatever frame names you give anims.create regardless of their source atlas, but keeping cell sizes and pivots consistent across mixed sources is on you; Sprixen's normalization only applies within a single Character Package export.

Phaseratlas JSONTexturePackersprite animationtutorial

Ready to try Sprixen?

Generate consistent, style-locked sprites for your game. 6 free credits on signup, no credit card required.

Get Started Free