Lightweight, secure sprite atlas generator for Phaser games with zero vulnerabilities.
Most sprite atlas generators are either:
- Heavy desktop applications (TexturePacker)
- Abandoned or unmaintained (free-tex-packer)
- Have complex dependencies with security vulnerabilities
- Overkill for simple use cases
Simple Sprite Atlas provides:
- ✅ Zero vulnerabilities - Only secure, well-maintained dependencies
- ✅ Lightweight - Minimal footprint with just
sharp,glob, andcommander - ✅ TypeScript - Full type safety and excellent DX
- ✅ Dual API - Use as CLI tool or programmatically
- ✅ Phaser 3 compatible - Direct integration with Phaser games
- ✅ Open source - MIT licensed, free forever
Install directly from GitHub:
npm install --save-dev github:prossm/simple-sprite-atlasOr install globally to use as a CLI tool:
npm install -g github:prossm/simple-sprite-atlasNote: This package is not yet published to npm. Once published, you'll be able to use
npm install simple-sprite-atlas.
After installing, add to your package.json scripts:
{
"scripts": {
"build:atlas": "simple-sprite-atlas --input assets/sprites --output dist/atlas"
}
}Then run:
npm run build:atlasOr if installed globally:
# Basic usage
simple-sprite-atlas --input assets/sprites --output dist/atlas
# With options
simple-sprite-atlas \
--input "assets/sprites/**/*.png" \
--output dist/characters \
--format phaser3-hash \
--max-size 2048 \
--padding 2 \
--trimimport { generateAtlas } from 'simple-sprite-atlas';
const result = await generateAtlas({
input: 'assets/sprites',
output: 'dist/atlas',
format: 'phaser3-hash',
maxSize: 2048,
padding: 2,
trim: true,
});
console.log(`Generated atlas with ${result.spriteCount} sprites`);
console.log(`Size: ${result.size.width}x${result.size.height}`);| Option | Alias | Description | Default |
|---|---|---|---|
--input <path> |
-i |
Input directory or glob pattern | required |
--output <path> |
-o |
Output path (without extension) | required |
--format <format> |
-f |
Output format: phaser3-hash, phaser3-array, or tiled |
phaser3-hash |
--max-size <size> |
-m |
Maximum atlas size (power of 2) | 2048 |
--padding <pixels> |
-p |
Padding between sprites | 2 |
--spacing <pixels> |
-s |
Spacing around each sprite | 0 |
--trim |
-t |
Trim transparent pixels | false |
--scale <scale> |
Scale factor for atlas | 1 |
|
--resize-to <size> |
Resize all sprites to fit within size (maintains aspect ratio) | none | |
--resize-mode <mode> |
Resize mode: contain, cover, or stretch |
contain |
|
--resize-filter <filter> |
Resize filter: lanczos, nearest, or linear |
lanczos |
|
--grid-size <size> |
Layout sprites on fixed grid of size pixels | none | |
--grid-metadata |
Include grid position data in JSON output | false |
|
--stable-order |
Sort sprites alphabetically for stable tile IDs (recommended for Tiled) | false |
Generate a sprite atlas from input images.
interface AtlasOptions {
input: string; // Directory path or glob pattern
output: string; // Output path without extension
format?: 'phaser3-hash' | 'phaser3-array' | 'tiled';
maxSize?: number; // Default: 2048
padding?: number; // Default: 2
spacing?: number; // Default: 0
trim?: boolean; // Default: false
scale?: number; // Default: 1
resizeTo?: number; // Resize sprites to fit within this size
resizeMode?: 'contain' | 'cover' | 'stretch'; // Default: 'contain'
resizeFilter?: 'lanczos' | 'nearest' | 'linear'; // Default: 'lanczos'
gridSize?: number; // Layout sprites on fixed grid
gridMetadata?: boolean; // Include grid position data (default: false)
stableOrder?: boolean; // Enable persistent manifest for stable tile IDs (default: false)
preserveIds?: boolean; // Preserve IDs from existing manifest (default: true when stableOrder=true)
}interface AtlasResult {
imagePath: string; // Path to generated PNG
jsonPath: string; // Path to generated JSON
spriteCount: number; // Number of sprites packed
size: {
width: number;
height: number;
};
}{
"frames": {
"hero/walk_01.png": {
"frame": { "x": 0, "y": 0, "w": 64, "h": 64 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
"sourceSize": { "w": 64, "h": 64 }
}
},
"meta": {
"image": "atlas.png",
"format": "RGBA8888",
"size": { "w": 2048, "h": 2048 },
"scale": 1
}
}// In your Phaser game
class GameScene extends Phaser.Scene {
preload() {
// Load the generated atlas
this.load.atlas(
'characters',
'assets/characters.png',
'assets/characters.json'
);
}
create() {
// Use sprites from the atlas
this.add.sprite(100, 100, 'characters', 'hero/walk_01.png');
// Or create animations
this.anims.create({
key: 'walk',
frames: this.anims.generateFrameNames('characters', {
prefix: 'hero/walk_',
suffix: '.png',
start: 1,
end: 8,
zeroPad: 2
}),
frameRate: 10,
repeat: -1
});
}
}Simple Sprite Atlas can generate Tiled-compatible tilesets with stable tile IDs that won't change when you add new sprites.
Traditional atlas generators optimize packing by sorting sprites by size, which means:
- Adding a new sprite can cause ALL existing sprites to shift positions
- Tile IDs in Tiled maps change unpredictably
- Your maps break every time you add a sprite
Even alphabetical sorting isn't enough! If you have "cat" and "dog" sprites, adding "dingo" will shift "dog" and break your maps.
Simple Sprite Atlas solves this with a manifest file that tracks which sprites have which IDs:
Use --stable-order to enable persistent ID tracking:
simple-sprite-atlas \
--input "assets/tiles/**/*.png" \
--output dist/tileset \
--format tiled \
--stable-order \
--grid-size 32This will:
- Generate a
.manifest.jsonfile tracking sprite→ID mappings - On subsequent builds, read the manifest and preserve existing sprite positions
- Append new sprites to the end with new IDs
- Sort new sprites alphabetically before appending
- Generate a
.tsj(Tiled tileset JSON) file with stable IDs
How it works:
- First build: Sprites are sorted alphabetically and a manifest is created
- Subsequent builds: Existing sprites keep their IDs (even if renamed files would sort differently), new sprites are added at the end
-
Generate your tileset:
simple-sprite-atlas \ --input "world/tiles" \ --output "world/tileset" \ --format tiled \ --stable-order \ --grid-size 32
-
In Tiled, add the generated tileset:
- Go to Map → Add External Tileset
- Select
world/tileset.tsj - The tileset will reference
world/tileset.png
-
Each tile includes custom properties:
filename: Original sprite filename (e.g., "ground/grass_02.png")originalPath: Full path to source file
Without --stable-order (size-based sorting):
Initial build:
grass_01.png → Tile ID 10
grass_02.png → Tile ID 126
After adding tree_01.png:
grass_01.png → Tile ID 57 ← ID CHANGED!
grass_02.png → Tile ID 203 ← ID CHANGED!
tree_01.png → Tile ID 10
❌ Your Tiled maps now show the wrong tiles!
With alphabetical sorting only:
Initial: cat.png → ID 0, dog.png → ID 1
Add dingo.png: cat.png → ID 0, dingo.png → ID 1, dog.png → ID 2
❌ "dog" shifted from ID 1 to ID 2, maps still break!
With --stable-order (persistent manifest):
Initial build (creates .manifest.json):
cat.png → Tile ID 0
dog.png → Tile ID 1
After adding dingo.png:
cat.png → Tile ID 0 ← STABLE (from manifest)
dog.png → Tile ID 1 ← STABLE (from manifest)
dingo.png → Tile ID 2 ← NEW (appended)
✅ Your existing maps still work perfectly!
The manifest file ensures that once a sprite has an ID, it keeps that ID forever (until you explicitly delete the manifest).
# Development: Generate tileset with stable IDs
simple-sprite-atlas \
--input "assets/world/tiles" \
--output "assets/world/tileset" \
--format tiled \
--stable-order \
--grid-size 32 \
--padding 0
# Use in Tiled, then export your maps
# Maps reference tiles by stable IDs
# In your game, load the same atlas for rendering
# Phaser can also use the Tiled format or you can generate both:
# Generate Phaser format alongside Tiled format
simple-sprite-atlas \
--input "assets/world/tiles" \
--output "assets/world/atlas" \
--format phaser3-hash \
--stable-order \
--grid-size 32 \
--padding 0The .manifest.json file tracks sprite order:
{
"version": "1.0",
"spriteOrder": [
"cat.png",
"dog.png",
"dingo.png"
],
"metadata": {
"created": "2025-01-15T10:30:00.000Z",
"modified": "2025-01-15T11:45:00.000Z"
}
}Important:
- ✅ Commit the manifest to version control alongside your atlas files
- ✅ New sprites are appended to the end automatically
- ✅ Deleted sprites are removed from the manifest (gaps in IDs are okay)
- ✅ You can rename sprite files - the manifest preserves their original keys
⚠️ Deleting the manifest resets all tile IDs (re-sorts alphabetically)
- Consistent naming helps: Use patterns like
tile_001.png,tile_002.pngfor the initial alphabetical sort - Renaming is safe: With the manifest, you can rename files and IDs stay stable
- Use subdirectories: Organize sprites in folders (e.g.,
ground/,walls/,items/) - The Tiled format (.tsj) includes properties to help you identify tiles by name in your game code
- Version control: Always commit
.manifest.jsonwith your atlas to keep IDs stable across team members
Automatically resize sprites to create uniform texture atlases:
# Resize all sprites to fit within 32x32 pixels (maintains aspect ratio)
simple-sprite-atlas --input sprites --output atlas --resize-to 32
# Use different resize modes
simple-sprite-atlas --input sprites --output atlas --resize-to 32 --resize-mode cover
# Use nearest-neighbor filtering for pixel art
simple-sprite-atlas --input sprites --output atlas --resize-to 32 --resize-filter nearestResize Modes:
contain(default): Fit sprite within bounds, maintain aspect ratio, add paddingcover: Fill bounds completely, maintain aspect ratio, may cropstretch: Distort sprite to exactly fill bounds
Resize Filters:
lanczos(default): High-quality smooth scalinglinear: Fast smooth scalingnearest: Pixel-perfect scaling for pixel art
Layout sprites on a fixed grid for GPU-friendly texture atlases:
# Layout sprites on a 32x32 pixel grid
simple-sprite-atlas --input sprites --output atlas --grid-size 32
# Include grid metadata in JSON output
simple-sprite-atlas --input sprites --output atlas --grid-size 32 --grid-metadataWith grid layout:
- All sprite positions are aligned to grid boundaries (multiples of grid size)
- Atlas dimensions are multiples of grid size
- Improves GPU cache utilization
- Perfect for tile-based games
Grid Metadata Output:
When --grid-metadata is enabled, each frame includes grid position data:
{
"frame": { "x": 64, "y": 128, "w": 32, "h": 32 },
"grid": {
"x": 2, // Grid column (64 / 32)
"y": 4, // Grid row (128 / 32)
"cellWidth": 1, // Number of grid cells wide
"cellHeight": 1 // Number of grid cells high
}
}Combine resizing and grid layout for optimal results:
# Resize all sprites to 32x32 and pack on 32x32 grid
simple-sprite-atlas \
--input "assets/sprites/**/*.png" \
--output dist/atlas \
--resize-to 32 \
--grid-size 32 \
--grid-metadataThis creates uniform, grid-aligned atlases perfect for:
- Tile-based games
- Pixel art games
- Performance-critical applications
- Consistent visual scaling
Frame keys preserve the directory structure of input files:
assets/sprites/
├── hero/
│ ├── walk_01.png
│ └── walk_02.png
└── enemy/
└── idle.png
→ Frame keys: "hero/walk_01.png", "hero/walk_02.png", "enemy/idle.png"
Automatically trim transparent pixels to save atlas space:
npx simple-sprite-atlas --input sprites --output atlas --trimAtlas dimensions are automatically rounded to the nearest power of 2 for optimal GPU performance:
- Input sprites: 800x600
- Output atlas: 1024x1024 (next power of 2)
Use glob patterns to select specific images:
# All PNGs in any subdirectory
npx simple-sprite-atlas --input "assets/**/*.png" --output atlas
# Only character sprites
npx simple-sprite-atlas --input "assets/characters/**/*.png" --output characters
# Multiple file types
npx simple-sprite-atlas --input "assets/**/*.{png,jpg}" --output atlas| Feature | Simple Sprite Atlas | TexturePacker | free-tex-packer |
|---|---|---|---|
| Free & Open Source | ✅ MIT | ❌ Paid | ✅ |
| Zero Vulnerabilities | ✅ | N/A | ❌ Unmaintained |
| Lightweight | ✅ 3 deps | ❌ Desktop app | |
| TypeScript | ✅ | N/A | ❌ |
| CLI + API | ✅ | ❌ GUI only | |
| Phaser 3 Support | ✅ | ✅ | ✅ |
| Maintained | ✅ Active | ✅ | ❌ Abandoned |
| Complex Packing | ❌ Grid only | ✅ | ✅ |
Add to your package.json:
{
"scripts": {
"build:atlas": "simple-sprite-atlas -i assets/sprites -o dist/atlas",
"dev": "npm run build:atlas && vite",
"build": "npm run build:atlas && vite build"
}
}{
"scripts": {
"watch:atlas": "nodemon --watch assets/sprites --ext png,jpg --exec 'npm run build:atlas'"
}
}// build-atlases.js
import { generateAtlas } from 'simple-sprite-atlas';
async function buildAtlases() {
await generateAtlas({
input: 'assets/characters',
output: 'dist/characters'
});
await generateAtlas({
input: 'assets/ui',
output: 'dist/ui'
});
await generateAtlas({
input: 'assets/enemies',
output: 'dist/enemies'
});
}
buildAtlases();- Node.js >= 18.0.0
- sharp - High-performance image processing
- glob - File pattern matching
- commander - CLI framework
All dependencies are actively maintained and security-audited.
# Install dependencies
npm install
# Run tests
npm test
# Build
npm run build
# Run locally
npm run devContributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.
Created as a lightweight, secure alternative for game developers who need a simple, reliable sprite atlas generator.
Perfect for:
- Phaser 3 games
- Indie game development
- Rapid prototyping
- CI/CD pipelines
- Educational projects
Made with ❤️ for the game dev community