spritepack
stablePack sprite rectangles into a texture atlas using binary tree bin-packing, and generate UV coordinates, sorting helpers, and packing statistics.
use plugin spritepack Plugin snippets can be edited in the Playground, but native capabilities are executed locally with the Zolo CLI.
Functions (11)
- pack_rects Pack rectangles into an atlas using bin-packing
- atlas_size Compute the bounding size of packed rectangles
- create_uv_map Generate normalised UV coordinates from packed rects
- can_fit Test whether all rects fit in a given atlas size
- sort_by_area Sort rects by area descending
- sort_by_perimeter Sort rects by perimeter descending
- sort_by_max_side Sort rects by longest side descending
- total_area Sum the area of all rectangles
- packing_efficiency Compute used area / atlas area ratio
- next_power_of_two Round an integer up to the next power of two
- metadata Get statistics about a set of rectangles
From import to result
- 01Import only what you use
- 02Copy the smallest example
- 03Run it with the Zolo CLI
Overview¶
spritepack solves the texture-atlas packing problem: given a set of sprite
rectangles, it arranges them inside a single atlas so a GPU can draw them all
from one texture. Packing uses a binary-tree bin-packing algorithm, and every
function works with plain tables rather than opaque handles — a rectangle is just
#{"id": n, "w": w, "h": h} and a packed result is #{"id", "x", "y", "w", "h", "packed"},
so you can store, inspect, and pass them around freely.
The plugin is stateless: each call takes your data in and hands new tables back,
nothing is retained between calls. A typical pipeline is to optionally sort the
rectangles, pack them with pack_rects, measure the result with atlas_size,
round up to a power-of-two texture with next_power_of_two, then derive UV
coordinates with create_uv_map. The remaining helpers (can_fit, total_area,
packing_efficiency, metadata) answer feasibility and quality questions about a
set of rectangles.
Common patterns¶
Sort, pack, size the atlas, and emit UV coordinates ready for the GPU:
use plugin spritepack::{sort_by_max_side, pack_rects, atlas_size, next_power_of_two, create_uv_map}
let rects = [
#{"id": 1, "w": 64, "h": 64},
#{"id": 2, "w": 128, "h": 32},
#{"id": 3, "w": 32, "h": 96}
]
let ordered = sort_by_max_side(rects)
let packed = pack_rects(ordered, 256, 256)
let size = atlas_size(packed)
let aw = next_power_of_two(size["width"])
let ah = next_power_of_two(size["height"])
let uvs = create_uv_map(packed, aw, ah)
print("atlas {aw}x{ah} with {len(uvs)} sprites")Check feasibility before committing to an atlas size, growing it until everything fits:
use plugin spritepack::{can_fit, total_area, next_power_of_two}
let rects = [
#{"id": 1, "w": 200, "h": 200},
#{"id": 2, "w": 128, "h": 128}
]
let side = next_power_of_two(total_area(rects))
while !can_fit(rects, side, side) {
side = side * 2
}
print("smallest square atlas: {side}x{side}")Inspect a set of rectangles, then measure how efficiently they packed:
use plugin spritepack::{metadata, pack_rects, packing_efficiency}
let rects = [
#{"id": 1, "w": 64, "h": 64},
#{"id": 2, "w": 64, "h": 64},
#{"id": 3, "w": 32, "h": 32}
]
let info = metadata(rects)
print("packing {info["count"]} rects, {info["total_area"]} px total")
let packed = pack_rects(rects, 128, 128)
print("efficiency: {packing_efficiency(packed, 128, 128)}")Pack rectangles into an atlas using bin-packing
Packs a table of {id, w, h} rectangles into an atlas of up to max_width x max_height pixels using binary tree bin-packing. Returns a table of {id, x, y, w, h, packed} entries. Rectangles that could not fit have packed = false.
use plugin spritepack::{pack_rects}
let rects = [
#{"id": 1, "w": 64, "h": 64},
#{"id": 2, "w": 32, "h": 32},
#{"id": 3, "w": 128, "h": 64}
]
let packed = pack_rects(rects, 256, 256)
for _, r in packed {
print("id={r["id"]} x={r["x"]} y={r["y"]} packed={r["packed"]}")
}When the atlas is too small, some rectangles come back with packed = false and
zeroed positions — filter them to find sprites that need a larger atlas:
use plugin spritepack::{pack_rects}
let rects = [
#{"id": 1, "w": 200, "h": 200},
#{"id": 2, "w": 200, "h": 200}
]
let packed = pack_rects(rects, 256, 256)
for _, r in packed {
if !r["packed"] {
print("did not fit: id={r["id"]}")
}
}Compute the bounding size of packed rectangles
Computes the minimum bounding box {width, height} that contains all successfully packed rectangles. Use this to determine the actual atlas texture size needed.
use plugin spritepack::{pack_rects, atlas_size}
let rects = [
#{"id": 1, "w": 64, "h": 64},
#{"id": 2, "w": 64, "h": 64}
]
let packed = pack_rects(rects, 256, 256)
let size = atlas_size(packed)
print("atlas: {size["width"]}x{size["height"]}")Generate normalised UV coordinates from packed rects
Converts packed pixel positions into normalised UV coordinates for GPU rendering. Returns a table of {id, u0, v0, u1, v1} entries where all values are in the 0.0–1.0 range.
use plugin spritepack::{pack_rects, atlas_size, create_uv_map, next_power_of_two}
let rects = [
#{"id": 1, "w": 64, "h": 64},
#{"id": 2, "w": 32, "h": 32}
]
let packed = pack_rects(rects, 256, 256)
let sz = atlas_size(packed)
let aw = next_power_of_two(sz["width"])
let ah = next_power_of_two(sz["height"])
let uvs = create_uv_map(packed, aw, ah)
for _, uv in uvs {
print("id={uv["id"]} u0={uv["u0"]} v0={uv["v0"]}")
}Test whether all rects fit in a given atlas size
Returns true if all rectangles in rects can be packed into an atlas of size max_w x max_h. Use this to check atlas size feasibility before packing.
use plugin spritepack::{can_fit}
let rects = [
#{"id": 1, "w": 128, "h": 128},
#{"id": 2, "w": 128, "h": 128}
]
if can_fit(rects, 256, 256) {
print("all sprites fit")
} else {
print("atlas too small")
}Sort rects by area descending
Returns the rectangles sorted by area (width * height) in descending order. Sorting before packing often improves packing efficiency.
use plugin spritepack::{sort_by_area}
let rects = [
#{"id": 1, "w": 32, "h": 32},
#{"id": 2, "w": 128, "h": 64},
#{"id": 3, "w": 64, "h": 64}
]
let sorted = sort_by_area(rects)
for _, r in sorted {
print("id={r["id"]} area={r["w"] * r["h"]}")
}Sort rects by perimeter descending
Returns the rectangles sorted by perimeter (2 * (w + h)) in descending order.
use plugin spritepack::{sort_by_perimeter}
let rects = [
#{"id": 1, "w": 16, "h": 128},
#{"id": 2, "w": 64, "h": 64}
]
let sorted = sort_by_perimeter(rects)Sort rects by longest side descending
Returns the rectangles sorted by their longest side in descending order, with area as a tiebreaker.
use plugin spritepack::{sort_by_max_side}
let rects = [
#{"id": 1, "w": 256, "h": 32},
#{"id": 2, "w": 64, "h": 64}
]
let sorted = sort_by_max_side(rects)Sum the area of all rectangles
Returns the sum of all rectangle areas (w * h). Useful for estimating the minimum atlas size required.
use plugin spritepack::{total_area}
let rects = [
#{"id": 1, "w": 64, "h": 64},
#{"id": 2, "w": 32, "h": 32}
]
print("total pixels: {total_area(rects)}")Compute used area / atlas area ratio
Returns the ratio of used area to total atlas area as a value between 0.0 and 1.0. Higher values indicate less wasted space.
use plugin spritepack::{pack_rects, packing_efficiency}
let rects = [
#{"id": 1, "w": 64, "h": 64},
#{"id": 2, "w": 64, "h": 64}
]
let packed = pack_rects(rects, 256, 256)
let eff = packing_efficiency(packed, 256, 256)
print("efficiency: {eff}")Round an integer up to the next power of two
Returns the smallest power of two that is greater than or equal to value. Most GPUs require power-of-two texture dimensions.
use plugin spritepack::{next_power_of_two}
print(next_power_of_two(100))
print(next_power_of_two(128))
print(next_power_of_two(129))Get statistics about a set of rectangles
Returns statistics about a set of rectangles: count, total_area, avg_area, min_width, max_width, min_height, max_height.
use plugin spritepack::{metadata}
let rects = [
#{"id": 1, "w": 32, "h": 32},
#{"id": 2, "w": 128, "h": 64},
#{"id": 3, "w": 64, "h": 64}
]
let info = metadata(rects)
print("count={info["count"]} max_w={info["max_width"]}")