atlasforge

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 4, 2026 License: MIT Imports: 7 Imported by: 0

README

atlasforge

atlasforge is a Go module for 2D texture atlas packing (sprite sheet generation). It packs many small images into one larger atlas image and returns layout metadata with exact rectangle coordinates.

The package is built around deterministic MaxRects packing, so repeated runs with the same input produce the same layout. It supports multiple placement heuristics, optional 90-degree rotation, standalone planning (Plan), atlas rendering from existing layout (Render), and a one-shot end-to-end flow (Pack).

It is useful for game asset pipelines, UI icon atlases, build-time resource packing, and any workflow where you need predictable atlas output plus JSON friendly placement data (id, x, y, width, height, rotated).

Install

go install github.com/woozymasta/atlasforge

Packing Examples

Below are examples of how different heuristics pack the same kind of sprite set.

Best Short Side Fit (BSSF)
BSSF

HeuristicBestShortSideFit chooses a position where the short leftover edge is as small as possible. In practice this gives a stable and predictable packing style, where rectangles usually sit tightly without creating too many thin unusable gaps near each other.

This heuristic is often used as a safe baseline when you need reasonable quality without tuning. It is usually not the top performer in pure speed, but it behaves consistently across different sprite sets and keeps resource usage around the middle of the pack.


Best Long Side Fit (BLSF)
BLSF

HeuristicBestLongSideFit minimizes the larger leftover edge, so it tries to avoid creating very large open strips after every placement. This often keeps free space in shapes that remain useful for upcoming rectangles.

In everyday workloads this mode is one of the fastest among the quality-oriented heuristics. It is a strong choice when you still care about packing quality, but you also want quick planning and a practical balance between speed and memory cost.


Best Area Fit (BAF)
BAF

HeuristicBestAreaFit focuses on minimizing wasted area in candidate free rectangles. Instead of looking only at one edge, it evaluates how much surface would be lost after placement and picks the option with lower area waste.

Because of this behavior it often produces visually compact results close to BLSF. Performance is usually in the same range, sometimes a bit slower, but still in the efficient group for production packing pipelines.


Bottom Left (BL)
BL

HeuristicBottomLeft follows a simple geometric rule: lowest possible Y, then lowest possible X. This makes the behavior easy to reason about when debugging layouts, because placements look naturally layered from bottom to top and from left to right.

The tradeoff is speed. In most benchmark scenarios this mode is the slowest for planning and packing, while memory usage stays around average. It can still be useful when deterministic bottom-left style placement is preferred over raw throughput.


Contact Point (CP)
CP

HeuristicContactPoint tries to maximize border contact with already placed rectangles and atlas edges. The intuition is to grow packed clusters by touching existing geometry as much as possible, which can reduce scattered islands of free space.

This mode often yields compact, visually dense packing, but it usually runs slower than BLSF and BAF. At the same time it tends to be relatively light on allocations among quality-focused heuristics, so it can be attractive when packing compactness is more important than peak speed.


First Fit (FF)
FF

HeuristicFirstFit takes the first free rectangle that can accept the item and moves on immediately. It avoids expensive candidate scoring and minimizes decision overhead during planning.

This is the fastest option by a wide margin and works very well for preview generation, rapid iteration, and high-throughput batch jobs. The main tradeoff is higher memory footprint compared to most quality-oriented modes, so it is best when throughput is the top priority.


Practical Example

The example below shows a practical flow: read source files from disk, pack them into one atlas image, save atlas.png, and save placement metadata to atlas-layout.json.

package main

import (
    "encoding/json"
    "image/png"
    "os"
    "path/filepath"

    "github.com/woozymasta/atlasforge"
)

func main() {
    // Collect source image files.
    files := []string{
        "assets/icons/ok.png",
        "assets/icons/warn.png",
        "assets/icons/error.png",
    }

    // Decode files and convert them into atlasforge sprites.
    sprites := make([]atlasforge.Sprite, 0, len(files))
    for _, path := range files {
        file, err := os.Open(path)
        if err != nil {
            panic(err)
        }

        img, err := png.Decode(file)
        file.Close()
        if err != nil {
            panic(err)
        }

        sprites = append(sprites, atlasforge.Sprite{
            ID:    filepath.ToSlash(path),
            Image: img,
        })
    }

    // Run packing and get atlas image + layout metadata.
    opts := atlasforge.DefaultOptions()
    opts.Padding = 2

    atlas, err := atlasforge.Pack(sprites, opts)
    if err != nil {
        panic(err)
    }

    // Save atlas image as PNG.
    atlasFile, err := os.Create("atlas.png")
    if err != nil {
        panic(err)
    }

    if err := png.Encode(atlasFile, atlas.Image); err != nil {
        atlasFile.Close()
        panic(err)
    }
    if err := atlasFile.Close(); err != nil {
        panic(err)
    }

    // Save atlas layout as formatted JSON.
    layoutFile, err := os.Create("atlas-layout.json")
    if err != nil {
        panic(err)
    }
    enc := json.NewEncoder(layoutFile)
    enc.SetIndent("", "  ")
    if err := enc.Encode(atlas.Layout); err != nil {
        layoutFile.Close()
        panic(err)
    }
    if err := layoutFile.Close(); err != nil {
        panic(err)
    }
}

atlas-layout.json will look like this:

{
  "placements": [
    {
      "id": "assets/icons/ok.png",
      "x": 2,
      "y": 2,
      "width": 32,
      "height": 32,
      "rotated": false
    },
    {
      "id": "assets/icons/warn.png",
      "x": 40,
      "y": 2,
      "width": 48,
      "height": 24,
      "rotated": true
    }
  ],
  "width": 256,
  "height": 256
}

In placement entries, rotated: true means the source sprite was placed with a +90 degree clockwise rotation in the atlas. width and height store original sprite dimensions before rotation.

Documentation

Overview

Package atlasforge provides 2D atlas packing utilities.

The package is centered around three APIs:

* Plan: calculate deterministic sprite placement using MaxRects. * Render: render a precomputed layout into an atlas image. * Pack: one-shot helper that plans and renders in one call.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidOptions reports invalid packing options.
	ErrInvalidOptions = errors.New("invalid options")

	// ErrInvalidItem reports invalid item dimensions or identifiers.
	ErrInvalidItem = errors.New("invalid item")

	// ErrPlacementFailed reports that an item cannot be placed into an atlas.
	ErrPlacementFailed = errors.New("placement failed")

	// ErrLayoutRequired reports a missing layout for rendering.
	ErrLayoutRequired = errors.New("layout is required")

	// ErrInvalidLayout reports malformed layout dimensions.
	ErrInvalidLayout = errors.New("invalid layout")

	// ErrSourceNotFound reports missing image source for a placement ID.
	ErrSourceNotFound = errors.New("source not found")

	// ErrSourceImageMissing reports a nil image in provided sources.
	ErrSourceImageMissing = errors.New("source image is missing")

	// ErrSourceSizeMismatch reports mismatch between placement size and source image size.
	ErrSourceSizeMismatch = errors.New("source size mismatch")
)

Functions

func Render

func Render(layout *Layout, sources []Source) (image.Image, error)

Render renders a precomputed layout using provided image sources.

Types

type Atlas

type Atlas struct {
	// Image is the rendered atlas pixels.
	Image image.Image `json:"-" yaml:"-"`

	// Layout describes where each source item was placed in Image.
	Layout Layout `json:"layout" yaml:"layout"`
}

Atlas combines rendered image and the layout used to produce it.

func Pack

func Pack(sprites []Sprite, opts Options) (*Atlas, error)

Pack plans and renders atlas image in a single call.

Example
sprites := []Sprite{
	{ID: "a", Image: image.NewRGBA(image.Rect(0, 0, 2, 2))},
	{ID: "b", Image: image.NewRGBA(image.Rect(0, 0, 3, 2))},
}

opts := Options{
	MinSize:     8,
	MaxSize:     8,
	Padding:     0,
	Heuristic:   HeuristicBestShortSideFit,
	AllowRotate: false,
}

atlas, err := Pack(sprites, opts)
if err != nil {
	fmt.Println("pack error")
	return
}

fmt.Printf(
	"%dx%d %d\n",
	atlas.Layout.Width,
	atlas.Layout.Height,
	len(atlas.Layout.Placements),
)
Output:
8x8 2

type Heuristic

type Heuristic int

Heuristic is the MaxRects scoring heuristic used for placement.

const (
	// HeuristicBestShortSideFit minimizes the smaller leftover edge first.
	HeuristicBestShortSideFit Heuristic = iota
	// HeuristicBestLongSideFit minimizes the larger leftover edge first.
	HeuristicBestLongSideFit
	// HeuristicBestAreaFit minimizes wasted free rectangle area first.
	HeuristicBestAreaFit
	// HeuristicBottomLeft prefers lower Y, then smaller X placements.
	HeuristicBottomLeft
	// HeuristicContactPoint maximizes contact with borders/used rectangles.
	HeuristicContactPoint
	// HeuristicFirstFit places into the first matching free rectangle.
	// It favors planning speed over packing density.
	HeuristicFirstFit
)

type Item

type Item struct {
	// ID is a unique stable identifier used in resulting Placement.ID.
	ID string `json:"id" yaml:"id"`

	// Width is the payload width in pixels and must be > 0.
	Width int `json:"width" yaml:"width"`

	// Height is the payload height in pixels and must be > 0.
	Height int `json:"height" yaml:"height"`
}

Item describes a rectangular payload for layout planning.

type Layout

type Layout struct {
	// Placements contains one entry per input item.
	// Order follows internal packing workflow, not input order guarantees.
	Placements []Placement `json:"placements" yaml:"placements"`

	// Width is the final atlas width in pixels.
	Width int `json:"width" yaml:"width"`

	// Height is the final atlas height in pixels.
	Height int `json:"height" yaml:"height"`
}

Layout contains atlas dimensions and all placements.

func Plan

func Plan(items []Item, opts Options) (*Layout, error)

Plan computes deterministic atlas placements without rendering image data.

type Options

type Options struct {
	// MinSize is the lower bound for each atlas side candidate.
	// Plan will not produce Width/Height below this value.
	MinSize int `json:"min_size" yaml:"min_size"`

	// MaxSize is the upper bound for each atlas side candidate.
	// Plan/Pack fail when items require a larger side.
	MaxSize int `json:"max_size" yaml:"max_size"`

	// Padding reserves empty pixels around each placed item.
	// Higher values reduce packing density and increase atlas size.
	Padding int `json:"padding" yaml:"padding"`

	// AspectPenalty adds a penalty for non-square atlas shapes.
	// Use 0 to disable shape bias and prioritize fit only.
	AspectPenalty float64 `json:"aspect_penalty" yaml:"aspect_penalty"`

	// Heuristic selects MaxRects placement scoring policy.
	// It affects placement order quality and final atlas utilization.
	Heuristic Heuristic `json:"heuristic" yaml:"heuristic"`

	// PreferHeight controls tie-breaking for equally scored candidates.
	// When true, ties prefer taller atlases over wider ones.
	PreferHeight bool `json:"prefer_height" yaml:"prefer_height"`

	// ForceSquare limits size search to square candidates only (w == h).
	// Rectangular candidates are skipped even when they have lower area.
	ForceSquare bool `json:"force_square" yaml:"force_square"`

	// AllowRotate enables 90-degree clockwise item rotation.
	// Rotated placements are reported via Placement.Rotated.
	AllowRotate bool `json:"allow_rotate" yaml:"allow_rotate"`
}

Options controls atlas planning behavior.

func DefaultOptions

func DefaultOptions() Options

DefaultOptions returns practical defaults for UI/sprite atlas packing.

type Placement

type Placement struct {
	// ID matches Item.ID / Sprite.ID used for this placement.
	ID string `json:"id" yaml:"id"`

	// X is the top-left X coordinate in atlas pixels.
	X int `json:"x" yaml:"x"`

	// Y is the top-left Y coordinate in atlas pixels.
	Y int `json:"y" yaml:"y"`

	// Width is the original source width before rotation.
	Width int `json:"width" yaml:"width"`

	// Height is the original source height before rotation.
	Height int `json:"height" yaml:"height"`

	// Rotated reports clockwise 90-degree placement during rendering.
	Rotated bool `json:"rotated" yaml:"rotated"`
}

Placement describes final coordinates of one packed item.

type Source

type Source struct {
	// Image provides pixels for rendering.
	// Bounds must match Placement width/height for the same ID.
	Image image.Image `json:"-" yaml:"-"`

	// ID must match Placement.ID from a planned layout.
	ID string `json:"id" yaml:"id"`
}

Source binds an image payload to an item ID for rendering.

type Sprite

type Sprite struct {
	// Image is the source pixels used by Pack.
	// It is excluded from json/yaml serialization.
	Image image.Image `json:"-" yaml:"-"`

	// ID is a unique stable identifier propagated into Placement.ID.
	ID string `json:"id" yaml:"id"`

	// Width overrides Image bounds when > 0.
	// Use with care: mismatch with Image bounds causes render errors.
	Width int `json:"width,omitempty" yaml:"width,omitempty"`

	// Height overrides Image bounds when > 0.
	// Use with care: mismatch with Image bounds causes render errors.
	Height int `json:"height,omitempty" yaml:"height,omitempty"`
}

Sprite is a high-level input used by Pack.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL