///|
/// A 32-bit RGBA color representation (8 bits per channel).
pub(all) struct Rgba {
r : Byte
g : Byte
b : Byte
a : Byte
} derive(Eq, Debug, Default)
///|
pub fn Rgba::new(r : Byte, g : Byte, b : Byte, a~ : Byte = b'\xFF') -> Rgba {
{ r, g, b, a, }
}
///|
pub fn Rgba::transparent() -> Rgba {
{ r: b'\x00', g: b'\x00', b: b'\x00', a: b'\x00', }
}
///|
pub fn Rgba::from_hex(hex : Int32) -> Rgba {
let r = ((hex >> 16) & 0xFF).to_byte()
let g = ((hex >> 8) & 0xFF).to_byte()
let b = (hex & 0xFF).to_byte()
let a = if hex >> 24 != 0 { ((hex >> 24) & 0xFF).to_byte() } else { b'\xFF' }
{ r, g, b, a, }
}
///|
/// A 2D RGBA bitmap image buffer stored in row-major order.
pub struct PixelBuffer {
width : Int32
height : Int32
pixels : FixedArray[Byte]
} derive(Debug)
///|
pub fn PixelBuffer::new(
width : Int32,
height : Int32,
color~ : Rgba = Rgba::transparent(),
) -> PixelBuffer {
let total_bytes = width * height * 4
let pixels = FixedArray::make(total_bytes, b'\x00')
let buf = { width, height, pixels, }
if color.r != b'\x00' ||
color.g != b'\x00' ||
color.b != b'\x00' ||
color.a != b'\x00' {
buf.fill(color)
}
buf
}
///|
pub fn PixelBuffer::width(self : PixelBuffer) -> Int32 {
self.width
}
///|
pub fn PixelBuffer::height(self : PixelBuffer) -> Int32 {
self.height
}
///|
pub fn PixelBuffer::fill(self : PixelBuffer, color : Rgba) -> Unit {
let total_pixels = self.width * self.height
for i = 0; i < total_pixels; i = i + 1 {
let offset = i * 4
self.pixels[offset] = color.r
self.pixels[offset + 1] = color.g
self.pixels[offset + 2] = color.b
self.pixels[offset + 3] = color.a
}
}
///|
pub fn PixelBuffer::get_pixel(self : PixelBuffer, x : Int32, y : Int32) -> Rgba {
if x < 0 || x >= self.width || y < 0 || y >= self.height {
return Rgba::transparent()
}
let offset = (y * self.width + x) * 4
{
r: self.pixels[offset],
g: self.pixels[offset + 1],
b: self.pixels[offset + 2],
a: self.pixels[offset + 3],
}
}
///|
pub fn PixelBuffer::set_pixel(
self : PixelBuffer,
x : Int32,
y : Int32,
color : Rgba,
) -> Unit {
if x < 0 || x >= self.width || y < 0 || y >= self.height {
return
}
let offset = (y * self.width + x) * 4
self.pixels[offset] = color.r
self.pixels[offset + 1] = color.g
self.pixels[offset + 2] = color.b
self.pixels[offset + 3] = color.a
}
///|
/// Copies (blits) pixels from `src` into `self` at destination position `(dst_x, dst_y)`.
/// Supports optional 90-degree clockwise rotation.
pub fn PixelBuffer::blit(
self : PixelBuffer,
src : PixelBuffer,
dst_x : Int32,
dst_y : Int32,
rotated~ : Bool = false,
) -> Unit {
if !rotated {
for sy = 0; sy < src.height; sy = sy + 1 {
let dy = dst_y + sy
if dy < 0 || dy >= self.height {
continue
}
for sx = 0; sx < src.width; sx = sx + 1 {
let dx = dst_x + sx
if dx < 0 || dx >= self.width {
continue
}
let src_off = (sy * src.width + sx) * 4
let dst_off = (dy * self.width + dx) * 4
self.pixels[dst_off] = src.pixels[src_off]
self.pixels[dst_off + 1] = src.pixels[src_off + 1]
self.pixels[dst_off + 2] = src.pixels[src_off + 2]
self.pixels[dst_off + 3] = src.pixels[src_off + 3]
}
}
} else {
for sy = 0; sy < src.height; sy = sy + 1 {
for sx = 0; sx < src.width; sx = sx + 1 {
let dx = dst_x + (src.height - 1 - sy)
let dy = dst_y + sx
if dx >= 0 && dx < self.width && dy >= 0 && dy < self.height {
let src_off = (sy * src.width + sx) * 4
let dst_off = (dy * self.width + dx) * 4
self.pixels[dst_off] = src.pixels[src_off]
self.pixels[dst_off + 1] = src.pixels[src_off + 1]
self.pixels[dst_off + 2] = src.pixels[src_off + 2]
self.pixels[dst_off + 3] = src.pixels[src_off + 3]
}
}
}
}
}
///|
/// Eliminates GPU bilinear texture sampling bleeding by duplicating
/// the outer 1-pixel border of `rect` into the surrounding padding margin.
pub fn PixelBuffer::extrude_rect(
self : PixelBuffer,
rect : Rect,
margin~ : Int32 = 1,
) -> Unit {
if margin <= 0 || rect.width <= 0 || rect.height <= 0 {
return
}
// Extrude top and bottom edges
for m = 1; m <= margin; m = m + 1 {
for x = rect.x; x < rect.right(); x = x + 1 {
// Top row duplication
if rect.y - m >= 0 {
let top_color = self.get_pixel(x, rect.y)
self.set_pixel(x, rect.y - m, top_color)
}
// Bottom row duplication
if rect.bottom() - 1 + m < self.height {
let bot_color = self.get_pixel(x, rect.bottom() - 1)
self.set_pixel(x, rect.bottom() - 1 + m, bot_color)
}
}
}
// Extrude left and right edges
for m = 1; m <= margin; m = m + 1 {
for y = rect.y; y < rect.bottom(); y = y + 1 {
// Left col duplication
if rect.x - m >= 0 {
let left_color = self.get_pixel(rect.x, y)
self.set_pixel(rect.x - m, y, left_color)
}
// Right col duplication
if rect.right() - 1 + m < self.width {
let right_color = self.get_pixel(rect.right() - 1, y)
self.set_pixel(rect.right() - 1 + m, y, right_color)
}
}
}
// Extrude 4 corners
for my = 1; my <= margin; my = my + 1 {
for mx = 1; mx <= margin; mx = mx + 1 {
// Top-left
if rect.x - mx >= 0 && rect.y - my >= 0 {
self.set_pixel(rect.x - mx, rect.y - my, self.get_pixel(rect.x, rect.y))
}
// Top-right
if rect.right() - 1 + mx < self.width && rect.y - my >= 0 {
self.set_pixel(
rect.right() - 1 + mx,
rect.y - my,
self.get_pixel(rect.right() - 1, rect.y),
)
}
// Bottom-left
if rect.x - mx >= 0 && rect.bottom() - 1 + my < self.height {
self.set_pixel(
rect.x - mx,
rect.bottom() - 1 + my,
self.get_pixel(rect.x, rect.bottom() - 1),
)
}
// Bottom-right
if rect.right() - 1 + mx < self.width &&
rect.bottom() - 1 + my < self.height {
self.set_pixel(
rect.right() - 1 + mx,
rect.bottom() - 1 + my,
self.get_pixel(rect.right() - 1, rect.bottom() - 1),
)
}
}
}
}
///|
/// Encodes the pixel buffer into a standard uncompressed 32-bit BMP (Windows Bitmap) byte array.
/// Uses a top-down row orientation (-height) so pixel rows can be written directly without vertical flipping.
pub fn PixelBuffer::to_bmp(self : PixelBuffer) -> FixedArray[Byte] {
let header_size = 54
let image_size = self.width * self.height * 4
let file_size = header_size + image_size
let out = FixedArray::make(file_size, b'\x00')
// BITMAPFILEHEADER (14 bytes)
out[0] = b'B'
out[1] = b'M'
// bfSize (4 bytes little-endian)
out[2] = (file_size & 0xFF).to_byte()
out[3] = ((file_size >> 8) & 0xFF).to_byte()
out[4] = ((file_size >> 16) & 0xFF).to_byte()
out[5] = ((file_size >> 24) & 0xFF).to_byte()
// bfOffBits = 54
out[10] = b'\x36'
// BITMAPINFOHEADER (40 bytes)
// biSize = 40
out[14] = b'\x28'
// biWidth (4 bytes)
out[18] = (self.width & 0xFF).to_byte()
out[19] = ((self.width >> 8) & 0xFF).to_byte()
out[20] = ((self.width >> 16) & 0xFF).to_byte()
out[21] = ((self.width >> 24) & 0xFF).to_byte()
// biHeight (top-down DIB: -self.height)
let neg_h = -self.height
out[22] = (neg_h & 0xFF).to_byte()
out[23] = ((neg_h >> 8) & 0xFF).to_byte()
out[24] = ((neg_h >> 16) & 0xFF).to_byte()
out[25] = ((neg_h >> 24) & 0xFF).to_byte()
// biPlanes = 1
out[26] = b'\x01'
// biBitCount = 32 (BGRA)
out[28] = b'\x20'
// biCompression = 0 (BI_RGB)
// biSizeImage
out[34] = (image_size & 0xFF).to_byte()
out[35] = ((image_size >> 8) & 0xFF).to_byte()
out[36] = ((image_size >> 16) & 0xFF).to_byte()
out[37] = ((image_size >> 24) & 0xFF).to_byte()
// biXPelsPerMeter = 2835 (0x0B13)
out[38] = b'\x13'
out[39] = b'\x0B'
// biYPelsPerMeter = 2835
out[42] = b'\x13'
out[43] = b'\x0B'
// Pixel Data: BMP 32-bit stores BGRA (Blue, Green, Red, Alpha)
let total_pixels = self.width * self.height
for i = 0; i < total_pixels; i = i + 1 {
let src_off = i * 4
let dst_off = header_size + i * 4
let r = self.pixels[src_off]
let g = self.pixels[src_off + 1]
let b = self.pixels[src_off + 2]
let a = self.pixels[src_off + 3]
out[dst_off] = b
out[dst_off + 1] = g
out[dst_off + 2] = r
out[dst_off + 3] = a
}
out
}
///|
/// Converts the pixel buffer into an immutable `Bytes` object containing standard BMP file data.
pub fn PixelBuffer::to_bmp_bytes(self : PixelBuffer) -> Bytes {
let arr = self.to_bmp()
let list = []
for b in arr {
list.push(b)
}
Bytes::from_array(list)
}
///|
/// Encodes the pixel buffer into a standard uncompressed 32-bit Truevision TGA file byte array.
/// Widely used in 2D/3D game engines (OpenGL, Vulkan, DirectX) as an uncompressed raw asset format.
pub fn PixelBuffer::to_tga(self : PixelBuffer) -> FixedArray[Byte] {
let header_size = 18
let image_size = self.width * self.height * 4
let file_size = header_size + image_size
let out = FixedArray::make(file_size, b'\x00')
// TGA Header (18 bytes)
out[2] = b'\x02' // Uncompressed true-color image
// Width (little endian)
out[12] = (self.width & 0xFF).to_byte()
out[13] = ((self.width >> 8) & 0xFF).to_byte()
// Height (little endian)
out[14] = (self.height & 0xFF).to_byte()
out[15] = ((self.height >> 8) & 0xFF).to_byte()
// Pixel depth: 32 bits (8 bits per BGRA channel)
out[16] = b'\x20'
// Image descriptor: 0x28 (bit 5: top-to-bottom row order, bit 3: 8-bit alpha)
out[17] = b'\x28'
// Pixel Data: BGRA (Blue, Green, Red, Alpha)
let total_pixels = self.width * self.height
for i = 0; i < total_pixels; i = i + 1 {
let src_off = i * 4
let dst_off = header_size + i * 4
let r = self.pixels[src_off]
let g = self.pixels[src_off + 1]
let b = self.pixels[src_off + 2]
let a = self.pixels[src_off + 3]
out[dst_off] = b
out[dst_off + 1] = g
out[dst_off + 2] = r
out[dst_off + 3] = a
}
out
}
///|
/// Converts the pixel buffer into an immutable `Bytes` object containing standard TGA file data.
pub fn PixelBuffer::to_tga_bytes(self : PixelBuffer) -> Bytes {
let arr = self.to_tga()
let list = []
for b in arr {
list.push(b)
}
Bytes::from_array(list)
}
///|
/// Resets the pixel buffer by filling all pixels with `color`.
pub fn PixelBuffer::clear(
self : PixelBuffer,
color~ : Rgba = Rgba::transparent(),
) -> Unit {
self.fill(color)
}
///|
/// Draws a rectangle outline or filled rectangle on the canvas.
pub fn PixelBuffer::draw_rect(
self : PixelBuffer,
x : Int32,
y : Int32,
width : Int32,
height : Int32,
color : Rgba,
fill~ : Bool = false,
) -> Unit {
if width <= 0 || height <= 0 {
return
}
if fill {
for cy = y; cy < y + height; cy = cy + 1 {
for cx = x; cx < x + width; cx = cx + 1 {
self.set_pixel(cx, cy, color)
}
}
} else {
// Top and bottom borders
for cx = x; cx < x + width; cx = cx + 1 {
self.set_pixel(cx, y, color)
self.set_pixel(cx, y + height - 1, color)
}
// Left and right borders
for cy = y; cy < y + height; cy = cy + 1 {
self.set_pixel(x, cy, color)
self.set_pixel(x + width - 1, cy, color)
}
}
}
///|
/// Performs standard alpha blending (Porter-Duff Over) of `color` onto pixel at `(x, y)`.
pub fn PixelBuffer::blend_pixel(
self : PixelBuffer,
x : Int32,
y : Int32,
color : Rgba,
) -> Unit {
if x < 0 || x >= self.width || y < 0 || y >= self.height {
return
}
let src_a = color.a.to_int()
if src_a == 0 {
return
}
if src_a == 255 {
self.set_pixel(x, y, color)
return
}
let dst = self.get_pixel(x, y)
let dst_a = dst.a.to_int()
// out_a = src_a + dst_a * (255 - src_a) / 255
let inv_src_a = 255 - src_a
let out_a = src_a + dst_a * inv_src_a / 255
if out_a == 0 {
self.set_pixel(x, y, Rgba::transparent())
return
}
let out_r = (
color.r.to_int() * src_a + dst.r.to_int() * dst_a * inv_src_a / 255
) /
out_a
let out_g = (
color.g.to_int() * src_a + dst.g.to_int() * dst_a * inv_src_a / 255
) /
out_a
let out_b = (
color.b.to_int() * src_a + dst.b.to_int() * dst_a * inv_src_a / 255
) /
out_a
self.set_pixel(
x,
y,
Rgba::new(
out_r.to_byte(),
out_g.to_byte(),
out_b.to_byte(),
a=out_a.to_byte(),
),
)
}
///|
/// Sub-image item to be packed and baked into a master atlas.
pub(all) struct ImageItem {
id : Int32
image : PixelBuffer
} derive(Debug)
///|
pub fn ImageItem::new(id : Int32, image : PixelBuffer) -> ImageItem {
{ id, image, }
}
///|
/// Result of baking multiple sub-images into an atlas page.
pub(all) struct BakedAtlas {
page_index : Int32
image : PixelBuffer
frames : Array[Rect]
uv_frames : Array[UvRect]
} derive(Debug)
///|
/// High-level 2D Texture Atlas Baker that packs sub-images into pixel buffers
/// and applies physical 1-pixel edge extrusion to completely eliminate GPU sampling bleeding.
pub struct AtlasBaker {
options : AtlasOptions
extrude_margin : Int32
} derive(Debug)
///|
pub fn AtlasBaker::new(
options~ : AtlasOptions = AtlasOptions::default(),
extrude_margin~ : Int32 = 1,
) -> AtlasBaker {
{ options, extrude_margin, }
}
///|
/// Internal helper to render an `AtlasResult` onto baked pixel buffer pages.
fn AtlasBaker::render_atlas_result(
self : AtlasBaker,
items : Array[ImageItem],
result : AtlasResult,
) -> Array[BakedAtlas] {
let baked_pages = []
for p_idx = 0; p_idx < result.pages.length(); p_idx = p_idx + 1 {
let page_bin = result.pages[p_idx]
let canvas = PixelBuffer::new(
page_bin.width,
page_bin.height,
color=Rgba::transparent(),
)
let page_rects = []
let page_uvs = []
for r in result.rects {
if r.bin_index == p_idx {
page_rects.push(r)
let uv = r.to_uv(page_bin.width, page_bin.height, half_texel_inset=true)
page_uvs.push(uv)
// Find corresponding source image
let mut found_src : PixelBuffer? = None
for item in items {
if item.id == r.id {
found_src = Some(item.image)
break
}
}
match found_src {
Some(src) => {
canvas.blit(src, r.x, r.y, rotated=r.rotated)
if self.extrude_margin > 0 {
canvas.extrude_rect(r, margin=self.extrude_margin)
}
}
None => ()
}
}
}
baked_pages.push({
page_index: p_idx,
image: canvas,
frames: page_rects,
uv_frames: page_uvs,
})
}
baked_pages
}
///|
/// Bakes an array of image items into one or more pixel buffer atlas pages,
/// automatically performing:
/// 1. Geometric packing via AtlasPacker
/// 2. Pixel copying (blitting) with rotation support
/// 3. Physical 1-pixel edge color extrusion to eliminate GPU sampling bleeding
/// 4. Half-texel UV coordinate calculation for GPU shaders
pub fn AtlasBaker::bake(
self : AtlasBaker,
items : Array[ImageItem],
) -> Array[BakedAtlas] {
if items.length() == 0 {
return []
}
// Extract sizes
let sizes = []
for item in items {
sizes.push(Size::new(item.image.width, item.image.height))
}
let packer = AtlasPacker::new(options=self.options)
let result = packer.pack(sizes)
self.render_atlas_result(items, result)
}
///|
/// Automatically benchmarks all 18 heuristics via `best_pack` and bakes image items
/// into the most space-efficient atlas page layout.
/// Returns the array of baked pages and the winning algorithm name.
pub fn AtlasBaker::bake_best(
self : AtlasBaker,
items : Array[ImageItem],
) -> (Array[BakedAtlas], String) {
if items.length() == 0 {
return ([], "None")
}
let sizes = []
for item in items {
sizes.push(Size::new(item.image.width, item.image.height))
}
let (best_res, winning_name) = best_pack(
sizes,
max_width=self.options.max_width,
max_height=self.options.max_height,
padding=self.options.padding,
border_padding=self.options.border_padding,
allow_rotation=self.options.allow_rotation,
auto_grow_pot=self.options.auto_grow_pot,
)
let baked_pages = self.render_atlas_result(items, best_res)
(baked_pages, winning_name)
}