Features:
- Add/remove atlas frames on the fly
- Image transparency trimming for smaller memory footprints.
- Factories for creating new Phaser objects that are tied into the atlas
- All factories return native Phaser
GameObject
s (i.e.Image
orSprite
) atlas.make.image()
creates aImage
with the correct atlas referenceatlas.make.sprite()
creates aSprite
- All factories return native Phaser
- Spritesheet support
- Import other spritesheets into your atlas
- Each sprite frame is trimmed and packed just like any other image, saving space
- Animation support
- An
atlas.anims.play(...)
function is available to facilitate working with animationsplay
is async, so you can easilyawait
animations to complete on a given sprite
- Animation data is stored on the scene itself to save memory
- This also means any animation can be played on any
Sprite
- This also means any animation can be played on any
- Sprites and one-off animations are easily created via
make.sprite
andmake.animation
- An
- Serialization support
- localStorage + sessionStorage support for smaller atlases (< 5mb)
- IndexedDB support for atlases larger than
- Storage utils to determine current usage, max size limit, and prompt user for persistence.
- "Pixel art mode" available for a crisp texture appearance
- A new
LiveAtlas
is created, and "frames" are registered with the atlas accordingly - When a new frame is registered with the atlas, it is immediately created and made available for use - regardless of the state of the actual asset being loaded
- This allows devs to immediately reference a frame within the atlas without worrying about missing frame errors
- A network request is made (via Phaser) for the texture asset.
- Upon load, the frame's image data is quickly trimmed of its transparency
- The new, trimmed frame is put into the "packer" and a spot for it is found in the LA texture
- Finally, the frame is drawn into the LA texture, effectively making it available for use
- If the texture under the hood is not large enough, the size increases accordingly before drawing
- New assets are not guaranteed to be placed into the atlas in the most optimal configuration. When necessary, the atlas can be
repack
ed to save space and find a more optimal packing. - Finally, we can serialize the atlas via the
save
methods available (and later imported viaload
):toLocalStorage
toSessionStorage
toIndexedDB
toBrowserStorage
- Selects local storage or IDB depending on the atlas size.toDiskFile
- Saves the image, frame, and packer data to an.ATLAS
filetoImage
- Returns the spritesheet as anHTMLImageElement
, useful for debugging.
const liveAtlas = new LiveAtlas(scene);
// Crisp appearance when texture is scaled
liveAtlas.setPixelArt(true);
// Default - Smooth/antialiased appearance when texture is scaled
liveAtlas.setPixelArt(false);
// This setting can be changed at runtime!
// Use the `add` functions to add new images to your atlas.
liveAtlas.add.image('your-texture-key', '/path/to/image.png');
// `imageList` allows you to import many images at once.
// These will be keyed on their URL.
liveAtlas.add.imageList(['/path/to/image1.png', '/path/to/image2.jpg']);
// Spritesheets can also be loaded via `add`. See below for more information
// on how those are configured when importing.
liveatlas.add.spritesheet(/*...*/);
You can await
any of the add
methods, too:
await liveAtlas.add.image('your-texture-key', '/path/to/image.png');
// your-texture-key has loaded!
// Use the `make` functions to create new assets tied into the LiveAtlas
const img = liveAtlas.make.image(x, y, 'your-texture-key');
// `img` is a `Phaser.GameObjects.Image` but uses `liveAtlas` as its texture.
// Use `applyFrame` on existing Images when changing their frame
const img = liveAtlas.make.image(x, y, 'your-texture-key');
liveAtlas.applyFrame('my-other-texture-key', img);
// `img` now displays `my-other-texture-key`.
There are a couple extra parts required when importing spritesheets: defining the frames (or cells), and optionally defining any animations inside that spritesheet.
// Load the spritesheet into the atlas before use
liveAtlas.add.spritesheet('inventory-items', '/items.png', {
// We can pass `frames` to denote exactly what each frame is
frames: {
"sword": {x: 0, y: 0, width: 32, height: 64},
"potion": {x: 32, y: 0, width: 32, height: 64},
"key": {x: 32, y: 64, width: 32, height: 64},
// ... etc ...
}
});
// Frames are namespaced under their spritesheet's key
const img = liveAtlas.make.image(x, y, 'sword', 'inventory-items');
const img2 = liveAtlas.make.image(x, y, 'potion', 'inventory-items');
const img3 = liveAtlas.make.image(x, y, 'key', 'inventory-items');
// Load the spritesheet into the atlas before use
liveAtlas.add.spritesheet('inventory-items', '/items.png', {
// This tells the LiveAtlas that each frame of this spritesheet is 96px wide by 64px tall
// Each frame is labeled `0..n-1` where `n` is the number of frames found in the spritesheet.
// Frames are numbered moving from left to right, top to bottom.
dimensions: {
width: 96,
height: 64,
},
});
// Frames are namespaced under their spritesheet's key
const img = liveAtlas.make.image(x, y, 0, 'inventory-items');
const img2 = liveAtlas.make.image(x, y, 1, 'inventory-items');
const img3 = liveAtlas.make.image(x, y, 2, 'inventory-items');
Spritesheets can take an optional anims
configuration object, which takes the following shape:
{
[animationName: string]: {
// `start`/`end` is used to denote the sequence of frames to use for this animation.
start?: number;
end?: number;
// `frames` denotes individual frames for the animation, useful if your frames are named
// or are not in sequential order.
frames?: number[];
// Duration of the animation in milliseconds. If not specified, `frameRate` must be present.
duration?: number;
// Framerate of the animation. If not specified, `duration` must be present.
frameRate?: number;
// Repeat/delays
repeat?: number;
repeatDelay?: number;
delay?: number;
// `yoyo` will have the animation play forward and then in reverse
yoyo?: boolean;
}
}
// Load the spritesheet into the atlas before use
liveAtlas.add.spritesheet('fishing', '/fishing-spritesheet.png', {
dimensions: {
width: 96,
height: 64,
},
anims: {
cast: { frameRate: 6, start: 0, end: 13 },
idle: { frameRate: 3, start: 11, end: 13, yoyo: true, repeat: Phaser.FOREVER },
nibble: { frameRate: 6, start: 14, end: 17, repeat: Phaser.FOREVER },
reel: { frameRate: 8, start: 22, end: 33 },
}
});
// Use `make.sprite` to create a sprite which uses the atlas for rendering + animation frames.
// (Here, 'idle' is the default animation played once the sprite is added to the scene.)
fishingRod = liveAtlas.make.sprite(x, y, 'fishing', 'idle');
// To play animations on an existing sprite, use `liveAtlas.anims.play`:
liveAtlas.anims.play('fishing', 'cast', fishingRod);
// Animations can be chained via `await`:
await liveAtlas.anims.play('fishing', 'cast', fishingRod)
await liveAtlas.anims.play('fishing', 'reel', fishingRod)
await liveAtlas.anims.play('fishing', 'idle', fishingRod)
// If you want to display the first frame of an animation, you can use `goto`:
liveAtlas.anims.goto('fishing', 'idle', fishingRod); // Paused on first frame of 'idle'
// Similar to `Animated Sprites`, you must define the spritesheet animation beforehand:
await liveAtlas.add.spritesheet('confetti', '/confetti-spritesheet.png', {
dimensions: {
width: 160,
height: 160,
},
// Note the only animation here is 'default'
anims: {
'default': {
frameRate: 60,
start: 0,
end: 71,
},
}
});
// Playing one-shot animations is simple through `make.animation`.
// Note this function returns a `Sprite`, but by default will automatically destroy
// the sprite once the animation is complete.
liveAtlas.make.animation(x, y, 'confetti');
// We can also one-shot any other animation stored in the atlas:
liveAtlas.make.animation(x, y, 'fishing', 'cast');
Creating a particle emitter is fairly simple, as we only need to reference the atlas texture key upon creation:
const manager = scene.add.particles(liveAtlas.textureKey);
const emitter = manager.createEmitter({
// Just reference keys/URLs already registered with the atlas for the `frame` property
frame: ['frame-key-1', 'other-frame-key', 'etc'],
/* ..other emitter options go here.. */
});
Each LiveAtlas instance has a save
field with several options on how to get the compiled atlas data:
// LocalStorage and SessionStorage have a size limit of ~5mb and are unlikely to persist for long.
await liveAtlas.save.toLocalStorage();
await liveAtlas.save.toSessionStorage();
// You can also save to IndexedDB which has a considerably higher storage limit
await liveAtlas.save.toIndexedDB();
// Using `toBrowserStorage` will automatically choose LocalStorage or IndexedDB based on the atlas size.
await liveAtlas.save.toBrowserStorage();
// This will prompt a download of a file with extension `.atlas`. This file contains everything required
// to be imported into another LiveAtlas.
await liveAtlas.save.toDiskFile();
// Just want the data?
const atlasData = await liveAtlas.save.toJSON();
// You can also get an `HTMLImageElement`, particularly nice for debugging:
const img = await liveAtlas.save.toImage();
There are a few options for loading a serialized atlas.
First, you can load from the browser storage:
await liveAtlas.load.fromLocalStorage();
await liveAtlas.load.fromSessionStorage();
await liveAtlas.load.fromBrowserStorage();
There are a number of way to load files that have been save to disk and have been loaded in via drag-n-drop, fs
, etc:
await liveAtlas.load.fromBlob(yourBlob);
await liveAtlas.load.fromDiskFile(yourBlobOrFileOrJson);
await liveAtlas.load.fromNetworkRequest('/path/to/file.atlas', { /* fetch options */ });
There are also a number of utilities available for managing browser storage.
// Determine how much space can be used - useful for displaying to end users of your game
await liveAtlas.storage.getQuotaEstimate();
// Determines how much space is being used by the given atlas in storage
// Note this only measures the _stored_ size - if the atlas is not stored then this wil be 0.
await liveAtlas.storage.getStoredSize();
TODO:
- Split texture into multiple sources (multiple RTs) so we can effectively have a multiatlas backed by render textures
IN PROGRESS:
- Identify WebGL vs Canvas issues and maybe report to the Phaser repo
BUGS:
- Multiple things calling
addFrame
at the same time produces weird results- race condition with loading/processing - we constantly create/destroy/etc even though once is enough
NON-GOALS:
- Off-thread texture save?
- This can be handled by the application embedding the LA