Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

binpack

Cuts PNGs into their separate sprites, then packs them into a single power of two spritesheet with a JSON map. No dependencies outside the standard library.

go install ella.to/binpack/cmd/binpack@v0.0.1

Two commands, meant to be run in this order:

binpack split -outFolder ./assets/fragments ./assets/*.png ./assets/char/*.png
binpack join -outPNG ./assets/merge.png -outJSON ./assets/merge.json ./assets/fragments/*.png

Always split, rename, then join

The JSON map is keyed by file name, and split can only name fragments by number. So the workflow has a manual step in the middle, and that step is the point of splitting separately from joining:

1. Split. Every non transparent region in every input becomes its own file, trimmed of the transparent space around it.

$ binpack split -outFolder ./assets/fragments ./assets/*.png ./assets/char/*.png
split 4 files into 12 fragments, 00000.png to 00011.png
./assets/fragments

2. Rename. Look through ./assets/fragments and give each fragment the name you want to reference it by in code.

$ cd ./assets/fragments
$ mv 00000.png hero-idle.png
$ mv 00001.png hero-walk-1.png
$ mv 00002.png hero-walk-2.png

3. Join. Pack the renamed fragments. Each file name without its extension becomes a key in the map.

$ binpack join -outPNG ./assets/merge.png -outJSON ./assets/merge.json \
    -padding 0 -allowRect -minSize 128 -maxSize 2048 \
    ./assets/fragments/*.png ./assets/character/*.png
joined 12 sprites into 256x256 (39.0% used)
{
  "hero-idle": { "x": 0, "y": 0, "w": 67, "h": 67 },
  "hero-walk-1": { "x": 68, "y": 0, "w": 40, "h": 36 }
}

Skip the rename and you get "00000": {...}, which tells you nothing at the call site. Rename first and the map reads for itself.

Joining is repeatable: rename another fragment, run join again, and only that key changes. Coordinates are top left origin, in pixels on the sheet.

split

binpack split -outFolder <dir> <png paths...>
flag meaning
-outFolder directory to write the fragments to, required

The trailing paths are files, globs, or directories, as many as you like; directories are walked recursively for .png. Cutting and trimming are always applied, there is nothing to configure.

A source PNG holding a grid, a strip, or a scattering of sprites yields one fragment per sprite. Fragments are numbered 00000.png upwards, in input file order and then reading order within each file, top to bottom then left to right.

Detection finds every group of touching non transparent pixels, using 8 way connectivity, then applies two fixed rules so a sprite does not come apart:

  • Regions separated by 2 transparent pixels or fewer are treated as one sprite, which keeps a dot over an i or a detached highlight attached to its body.
  • Regions smaller than 2 pixels on either axis are dropped as antialiasing noise rather than art.

Split into an empty directory. binpack refuses an -outFolder that holds one of the input files, and warns when the target already contains PNGs: numbering restarts at 00000 on every run, so a re-run recreates the original names next to the ones you renamed, and join would then pack both copies.

join

binpack join -outPNG <file> -outJSON <file> [flags] <png paths...>
flag default meaning
-outPNG output spritesheet PNG, required
-outJSON -outPNG with .json output JSON map
-padding 0 transparent pixels to keep between sprites
-allowRect off also try half height sheets, such as 512x256
-minSize 128 smallest power of two sheet size to try
-maxSize 8192 largest power of two sheet size to try

One input file is one sprite, keyed by its file name without the extension, and each is trimmed to its non transparent bounds so a hand edited fragment does not carry padding onto the sheet. Files that share a name across directories get a #2 suffix, reported on stderr.

Join does not re-cut its inputs. A fragment you edited into two disconnected pieces stays one sprite under one key, since resplitting it would throw away the name you just gave it.

The sheet is the smallest power of two square that fits everything, from -minSize up to -maxSize, doubling each step. -allowRect also tries half height sheets on the way up, which often wastes less space than the next square. Sprites are never rotated, so the map needs no rotation flag.

-padding is worth setting to 1 or 2 when the sheet is sampled with linear filtering or mipmaps, since neighbouring sprites otherwise bleed into each other. The reported x, y, w, h always describes the sprite itself, never its padding.

Library

files, err := binpack.FindPNGs([]string{"./assets"})

// split
fragments, err := binpack.Split(files, binpack.DefaultCut(), nil)
err = binpack.DumpFragments("./assets/fragments", fragments)

// join
files, err = binpack.FindPNGs([]string{"./assets/fragments"})
sprites, err := binpack.Load(files, 0, nil)
result, err := binpack.Pack(sprites, binpack.Options{MinSize: 128, MaxSize: 2048})

err = binpack.SavePNG("./assets/merge.png", result.Sheet)
err = binpack.WriteMap("./assets/merge.json", result.Frames)

The last argument to Split and Load is an optional Warnf for non fatal problems such as a skipped transparent file. Packing is MaxRects with the best short side fit heuristic, largest sprite first.

About

bin packing your png assets

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages