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::{pack_rects, atlas_size, create_uv_map, …} 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
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 {#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"]}")