Skip to content

spritepack

stable

Pack 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, …}
11 functions Graphics
/ filter jk navigate Esc clear
Functions (11)
  1. pack_rects Pack rectangles into an atlas using bin-packing
  2. atlas_size Compute the bounding size of packed rectangles
  3. create_uv_map Generate normalised UV coordinates from packed rects
  4. can_fit Test whether all rects fit in a given atlas size
  5. sort_by_area Sort rects by area descending
  6. sort_by_perimeter Sort rects by perimeter descending
  7. sort_by_max_side Sort rects by longest side descending
  8. total_area Sum the area of all rectangles
  9. packing_efficiency Compute used area / atlas area ratio
  10. next_power_of_two Round an integer up to the next power of two
  11. 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"]}")
enespt-br