How to create sprite sheets & animations for PixiJS 8

Andreas Löw
Last updated:
GitHub
How to create sprite sheets & animations for PixiJS 8

Who is this tutorial for?

This tutorial is for anyone interested in using PixiJS with sprite sheets. It covers the basics of adding sprites and animations to a scene, as well as advanced techniques like setting pivot points, packing sprite sheets, and optimizing loading times.

This tutorial uses PixiJS 8. If you are still using an older version, see: How to create sprite sheets and animations for PixiJS 7.x

This is the scene you are going to create in this tutorial:

Creating a simple PixiJS game scene

Downloading the assets

Download the sprites and background images from here: assets.zip. You can use these assets for learning purposes. Please don't publish them in your own blog, tutorial or use them in a game or app without my written consent.

Extract the contents into a folder that you now use for development of this demo. The archive contains the graphics you see in the demo above:

  • scene
    • background.png
    • middleground.png
  • sprites
    • character
      • 01.png ... 08.png

We also offer you the full source code for this tutorial on GitHub.

Downloading PixiJS

Download the current version of PixiJS. You only need the pixi.js file for this tutorial. Save the files in the same folder as the assets.

Getting started with a simple scene

Start by creating an index.html file with this content:

index.html
<!doctype html>
<html lang="en">
    <head>
        <title>PixiJS Sprite Sheet Demo</title>
    </head>
    <body>
        <script src="pixi.js"></script>
        <script src="pixijs-spritesheet-example.js"></script>
    </body>
</html>

Create your game file: pixijs-spritesheet-example.js in the same folder:

pixijs-spritesheet-example.js
(async () => {
    // Create a PixiJS application
    const app = new PIXI.Application();
    await app.init({width: 960, height: 540});

    // add the canvas that Pixi created for you to the DOM
    document.body.appendChild(app.canvas);

    // load the assets
    await PIXI.Assets.load([ "scene/background.png" ]);

    // initialize background image
    const background = PIXI.Sprite.from("scene/background.png");
    app.stage.addChild(background);

    // scale stage container to match the background size
    app.stage.scale.x = app.canvas.width / background.width;
    app.stage.scale.y = app.canvas.height / background.height;
})();

Let's take a look at this code:

The first lines initialize the PixiJS application with a width of 960 and a height of 540. It then adds the application's view to the DOM.

The next lines load the background.png image, PIXI.Sprite.from("scene/background.png") creates a sprite from that resource, the next line adds it to the stage.

The stage is resized to match the background images size. We have to do this because I included high-resolution versions of all images. I'll show you how you can resize them to match different device resolutions later.

Testing the scene in your browser

You can't run the application by simply opening the HTML file in your browser. This is because of security restrictions. PixiJS loads the resources dynamically which is not permitted in HTML files loaded from the file system.

The easiest way to display the application is to use a local web server. You don't need to install Apache — a simple server like the npm module http-server is sufficient.

If you already have node installed, open a command prompt in the folder where your index.html is located and type:

npx http-server -c-1 .

The -c-1 option disables caching, which is important during development. Otherwise, you might not see changes after editing files.

Now open http://localhost:8080 in your browser. You should see the background of the game scene:

Background scene for the PixiJS sprite sheet demo

Creating a sprite sheet

To create the sprite sheets, we use TexturePacker, a robust desktop application, for creating sprite sheets and optimizing images for game development. Since its initial release in 2010, TexturePacker has been continually updated with new features and bug fixes, making it a reliable and efficient tool for game developers.

Please download TexturePacker from here:

After installation, activate the trial by clicking on Try TexturePacker Pro. You can now use all features of TexturePacker for the next 7 days. After that, you can still use TexturePacker for free, but only with a reduced set of features.

In the main screen:

  1. Choose PixiJS from the center screen
  2. Drop the sprites folder and the scene/middleground.png image onto the left panel
Select PixiJS as Framework and drop the sprites folder onto TexturePacker
Select PixiJS and drop the sprites folder onto TexturePacker

In case you dropped the sprites first, or you already used TexturePacker for a project, click on Framework and select PixiJS from the dialog that opens here.

If a folder is added to TexturePacker, it collects all sprites in that folder recursively, the sub-folder names are prepended to the sprite names. In our example the sprites will be accessible using names like character/walk_01.png, character/walk_02.png, and so on.

The main folder name (sprites) is omitted by default - if you want to include it as part of the sprite name use Prepend folder name from the Advanced settings. You can also remove the .png extension from the sprite names by enabling Trim sprite names, also in the Advanced Settings.

Create a new folder called spritesheets in the demo directory. In TexturePacker, click on the folder icon next to Data file and navigate to the spritesheets folder. Name the data file character.json.

Finally, press Publish sprite sheet to write the sprite sheet to your project folder.

Create a sprite sheet for PixiJS
Set the file name and press publish sprite sheet

This automatically creates 2 files:

  • character.png - the sprite sheet image
  • character.json - the sprite sheet data file that contains the positions and names of your sprites

Optimizing your sprite sheets for faster loading

The resulting character.png is now about 700kb in size. But we can do better. TexturePacker can dramatically reduce the file size of your sprite sheets, making game loading faster.

All you need to do is change the Texture format from PNG-32 to PNG-8 (indexed) and press Publish sprite sheet again. The result is a sprite sheet that looks almost identical but is now only 194kb.

That's less than 1/3 of the original sprite sheet size — and it works right out of the box by simply changing a setting in TexturePacker. If you want to learn more, read A Beginner's Guide To PNG Optimization.

Loading the sprite sheet in your game

Back to your pixijs-spritesheet-example.js. Add the spritesheets/character.json to the list of assets to load:

pixijs-spritesheet-example.js
  await PIXI.Assets.load([
      "spritesheets/character.json",
      "scene/background.png"
  ]);

Adding a sprite to your scene

Adding the sprite from the sprite sheet looks identical to adding the background image:

pixijs-spritesheet-example.js
    // add the middle ground from the sprite sheet
    const middleground = PIXI.Sprite.from("middleground.png");
    app.stage.addChild(middleground);

The background and middle ground images are perfectly aligned because the middle ground contains transparency at the top.

But is adding transparency a bad thing? Doesn't it waste RAM and hurt performance? Usually it would — that's right. But thanks to TexturePacker, that's not the case. When you add an image with transparency to a sprite sheet, the transparency is removed — this feature is called trim. PixiJS is smart enough to compensate for that. When working with the sprite, it's like working with the original image.

Adding and playing animations

TexturePacker detects animations in your sprites and creates lists of all frames. This makes creating animations in PixiJS very simple.

This works with files named 01.png, 02.png, ... but also for files ending with a _ or - and a number. For example, character/walk-01.png, character/walk-02.png will also be detected.

The animations are named the same as the sprites, but without the numbers and the .png. In our case: character/walk.

First, you have to get the animations from the texture. You can do this by accessing the Asset cache.

pixijs-spritesheet-example.js
    // get the sheet json data, required for resolving animations
    const animations = PIXI.Assets.cache.get('spritesheets/character.json').data.animations;

Now create a new AnimatedSprite from the animation data. Configure the main parameters for it: The position and the speed. Finally, start playing the animation by calling play() and add it to the stage:

pixijs-spritesheet-example.js
    // create an animated sprite
    const character = PIXI.AnimatedSprite.fromFrames(animations["character/walk"]);

    // configure + start animation:
    character.animationSpeed = 1 / 6;                     // 6 fps
    character.position.set(150, background.height - 780); // almost bottom-left corner of the canvas
    character.play();

    // add it to the stage and render!
    app.stage.addChild(character);

Go back to your browser and refresh the scene. You should see the character doing a moon-walk in place.

Add these lines to make him walk from left to right:

pixijs-spritesheet-example.js
    // move the character to the right, restart on the left
    app.ticker.add(ticker => {
        const speed = 6;
        character.x = (character.x + speed * ticker.deltaTime + 400) % (background.width + 800) - 400;
    });

This function is called at regular intervals. We use it to update the x position of the sprite. The function gets a Ticker object passed. It has a property called deltaTime, which is the time passed since the last call of the function. We use this to calculate the distance to move the character by multiplying the value with the speed.

This makes the character move at the same speed on all devices — regardless of the frame rate. If you do not use deltaTime for that calculation, the character may walk faster or slower on different devices.

The final trick is the % which is a modulo division that makes the character appear on the left side after it disappears on the right (x greater or equal to background.width + 800). The +400 and -400 center the walk animation on the screen, so that the guy disappears on the right and enters the scene on the left.

Setting anchor points for your PixiJS sprites

The anchor point is the point around which the sprite rotates and scales, and it also determines the position of the sprite when it is placed on the x and y coordinates. Essentially, the anchor point acts as the center of the sprite and is used to control its orientation and movement in the game.

To show you what I mean, please update the ticker function to the following and refresh your browser:

    app.ticker.add(ticker => {
        const speed = 6;
        character.x = 500;
        character.rotation += ticker.deltaTime/100;
});

At first glance, the sprite may appear to rotate around a point that seems disconnected from the sprite itself. This is because the default anchor point is set to the coordinate (0,0), and there are large areas of transparency within the sprite's frame.

The default anchor point in PixiJS is at 0/0
The default anchor pont is at 0/0

To overcome this issue, adjust the anchor point to a suitable location within the sprite. This can be done in TexturePacker and will ensure that the sprite rotates and scales correctly in PixiJS.

To do so, click on Sprite settings in TexturePacker's toolbar and select all character sprites in the left panel. Click Fit to see all sprites at once in the center view.

Select all sprites in the center view by pressing the mouse button at the top left of the first sprite and dragging to the bottom right of the last sprite.

The circle in the sprite frames is the anchor point - which are called pivot points in TexturePacker. Move these to the bottom center of the sprite:

Pivot point editor in TexturePacker
The pivot point / anchor point editor in TexturePacker

Press Publish sprite sheet to export the updated pivot points and reload the scene in your browser. You should now see the character rotating around this point.

Change the code to this to make him walk again:

pixijs-spritesheet-example.js
    // configure + start animation:
    character.animationSpeed = 1 / 6;                     // 6 fps
    character.position.set(150, background.height - 180); // almost bottom-left corner of the canvas
    character.play();

    // Enable this to update the anchor points with each animation frame
    character.updateAnchor = true;

    // add it to the stage and render!
    app.stage.addChild(character);

    app.ticker.add(ticker => {
        const speed = 6;
        character.x = (character.x + speed * ticker.deltaTime) % (background.width + 200);
    });

TexturePacker allows you to set individual anchor points for each frame of the animation. In PixiJS, this isn't enabled by default. To update the anchor point for each sprite frame, set the updateAnchor property on the AnimatedSprite to true.

Using 9-slice / 9-scale sprites in PixiJS

PixiJS provides support for 9-slice scaling, a technique designed to enhance image scalability. If you're unfamiliar with the concept: It divides an image into nine segments — four corners, four edges, and the center. When scaling, only the center and edges adjust in size, leaving the corners unchanged. This ensures that elements like buttons or panels retain their original visual appeal even when scaled across various aspect ratios.

9-slice scaling

In TexturePacker, click Sprite Settings in the toolbar and select the button sprite. In the right panel, activate the checkbox Enable 9-patch scaling.

Using TexturePacker's 9 slice editor with PixiJS
Using TexturePacker's 9 slice editor with PixiJS

You can drag the green lines to match the scaling and non-scaling parts of your sprite. Finally, publish the sprite sheet.

Adding the sprite is straightforward:

pixijs-spritesheet-example.js
    const sprite9a = new PIXI.NineSliceSprite(PIXI.Texture.from("button.png"));
    sprite9a.position.set(10,10);
    sprite9a.width = 200;
    sprite9a.height = 200;
    app.stage.addChild(sprite9a);

Change the width and height to whatever you need — the border of the button image stays crisp and is not blurred when the sprite is resized.

Using MultiPack with PixiJS

MultiPack is a feature of TexturePacker that allows you to pack many sprites at once. It automatically adds new sprite sheets if there isn't enough space. This makes handling many sprites much easier.

Enabling MultiPack in TexturePacker

To enable the feature, you have to use TexturePacker 7.0.2 or newer because older versions are not fully compatible.

To enable MultiPack, you have 2 choices:

  • Auto - if you use this variant, TexturePacker does the whole work for you.
  • Manual - you have to add sprite-sheets manually and can then assign folders or sprites to a sheet. This is great — e.g. if you have multiple levels that don't share sprites. To assign the sprites, use the tree view on the left. The limitation is that you can't move sprites inside a smart folder. You can only move the smart folder itself.

TexturePacker requires a name extension for the base name of the sprite sheet. {n} now has to be part of the file name and is either replaced with the sheet number (auto multipack) or the sheet name (manual multipack). For example, you can name your files sheet-{n}.png and sheet-{n}.json, which creates sheet-0.png, sheet-1.png, or sheet-level1.png, sheet-level2.png, etc.

Make sure that all your sprite names are unique - using the same name is not possible.

Using MultiPack in PixiJS

In PixiJS it's now sufficient to only load the first sprite sheet:

PIXI.Assets.load([
    "spritesheets/sheet-0.json",
]).then(() => {

Make sure to only load the first sprite sheet of your multipack set. If you load more than one sheet from the same set, PixiJS deadlocks and does not complete the loading process.

TexturePacker stores all animation information in the first JSON file of the multipack set — no matter on which sprite sheet the images are located.

To receive the animation array, use

pixijs-spritesheet-example.js
// get all animations  of the multipack set (stored in the first data file)
const animations = PIXI.Assets.cache.get('spritesheets/sheet-0.json').data.animations;

To create the animation, use

pixijs-spritesheet-example.js
// create an animated sprite
const character = PIXI.AnimatedSprite.fromFrames(animations["character/walk"]);

Restrictions in PixiJS

PixiJS works with multipack — but there are some calls that won't work.

When you create a Sprite, always use the static method PIXI.Sprite.from():

pixijs-spritesheet-example.js
// this works 👍
const sprite = PIXI.Sprite.from("image.png");

// this does not work 👎
const sheet = PIXI.Assets.cache.get('spritesheets/sheet-0.json');
const sprite = new PIXI.Sprite(sheet.textures["image.png"]);

There are also restrictions for the NineSliceSprite. You have to use PIXI.Texture.from() to get the texture to pass into the constructor. Accessing the textures on the sheet directly might lead to null in case the sprite is not located on the first sheet of the multipack set.

pixijs-spritesheet-example.js
// this works 👍
const sprite9 = new PIXI.NineSliceSprite(PIXI.Texture.from("button.png"));

// this does not work 👎
const sprite9 = new PIXI.NineSliceSprite(sheet.textures["button.png"]);

And for the AnimatedSprite, you have to go through sheet.data.animations[] to get the frame names. sheet.animations[] is null for all frames that are not located on that sheet.

pixijs-spritesheet-example.js
// this works 👍
const anim = PIXI.AnimatedSprite.fromFrames(sheet.data.animations["character/walk"]);

// this does not work 👎
const anim = new PIXI.AnimatedSprite(sheet.animations["character/walk"]);