///|
/// A named sprite input for atlas packing.
pub(all) struct SpriteSource {
name : String
canvas : Canvas
padding : Int
extrude : Bool
} derive(Debug)
///|
pub fn SpriteSource::new(name : String, canvas : Canvas) -> SpriteSource {
{ name, canvas, padding: 0, extrude: false }
}
///|
pub fn SpriteSource::with_padding(
self : SpriteSource,
padding : Int,
) -> SpriteSource {
{ ..self, padding: if padding < 0 { 0 } else { padding } }
}
///|
pub fn SpriteSource::with_extrude(
self : SpriteSource,
enabled : Bool,
) -> SpriteSource {
{ ..self, extrude: enabled }
}
///|
pub fn SpriteSource::name(self : SpriteSource) -> String {
self.name
}
///|
pub fn SpriteSource::canvas(self : SpriteSource) -> Canvas {
self.canvas
}
///|
pub fn SpriteSource::padding(self : SpriteSource) -> Int {
self.padding
}
///|
pub fn SpriteSource::extrude(self : SpriteSource) -> Bool {
self.extrude
}
///|
/// A placement in a packed atlas page.
pub(all) struct SpritePlacement {
name : String
page : Int
x : Int
y : Int
width : Int
height : Int
source_width : Int
source_height : Int
rotated : Bool
} derive(Debug)
///|
pub fn SpritePlacement::name(self : SpritePlacement) -> String {
self.name
}
///|
pub fn SpritePlacement::page(self : SpritePlacement) -> Int {
self.page
}
///|
pub fn SpritePlacement::rect(self : SpritePlacement) -> GifRect {
GifRect::new(left=self.x, top=self.y, width=self.width, height=self.height)
}
///|
pub fn SpritePlacement::source_size(self : SpritePlacement) -> (Int, Int) {
(self.source_width, self.source_height)
}
///|
pub fn SpritePlacement::rotated(self : SpritePlacement) -> Bool {
self.rotated
}
///|
pub(all) struct AtlasPackOptions {
page_width : Int
page_height : Int
padding : Int
allow_rotation : Bool
extrude : Bool
background : Rgba
} derive(Debug)
///|
pub fn AtlasPackOptions::default() -> AtlasPackOptions {
{
page_width: 512,
page_height: 512,
padding: 1,
allow_rotation: false,
extrude: false,
background: rgba(0, 0, 0, 0),
}
}
///|
pub fn AtlasPackOptions::page_size(
self : AtlasPackOptions,
width : Int,
height : Int,
) -> AtlasPackOptions {
{
..self,
page_width: if width < 1 {
1
} else {
width
},
page_height: if height < 1 {
1
} else {
height
},
}
}
///|
pub fn AtlasPackOptions::padding(
self : AtlasPackOptions,
value : Int,
) -> AtlasPackOptions {
{ ..self, padding: if value < 0 { 0 } else { value } }
}
///|
pub fn AtlasPackOptions::rotation(
self : AtlasPackOptions,
enabled : Bool,
) -> AtlasPackOptions {
{ ..self, allow_rotation: enabled }
}
///|
pub fn AtlasPackOptions::extrusion(
self : AtlasPackOptions,
enabled : Bool,
) -> AtlasPackOptions {
{ ..self, extrude: enabled }
}
///|
pub fn AtlasPackOptions::background(
self : AtlasPackOptions,
color : Rgba,
) -> AtlasPackOptions {
{ ..self, background: color }
}
///|
pub(all) struct AtlasPage {
index : Int
canvas : Canvas
placements : Array[SpritePlacement]
} derive(Debug)
///|
pub fn AtlasPage::index(self : AtlasPage) -> Int {
self.index
}
///|
pub fn AtlasPage::canvas(self : AtlasPage) -> Canvas {
self.canvas
}
///|
pub fn AtlasPage::placements(self : AtlasPage) -> ArrayView[SpritePlacement] {
self.placements[:]
}
///|
pub(all) struct SpriteAtlas {
pages : Array[AtlasPage]
placements : Array[SpritePlacement]
page_width : Int
page_height : Int
} derive(Debug)
///|
pub fn SpriteAtlas::pages(self : SpriteAtlas) -> ArrayView[AtlasPage] {
self.pages[:]
}
///|
pub fn SpriteAtlas::placements(
self : SpriteAtlas,
) -> ArrayView[SpritePlacement] {
self.placements[:]
}
///|
pub fn SpriteAtlas::page_count(self : SpriteAtlas) -> Int {
self.pages.length()
}
///|
pub fn SpriteAtlas::find(self : SpriteAtlas, name : String) -> SpritePlacement? {
for placement in self.placements {
if placement.name() == name {
return Some(placement)
}
}
None
}
///|
/// Extract a named sprite from its atlas page.
pub fn SpriteAtlas::extract(self : SpriteAtlas, name : String) -> Canvas? {
match self.find(name) {
None => None
Some(placement) => {
let page = self.pages[placement.page()]
let rect = placement.rect()
Some(
page
.canvas()
.crop(
x=rect.left(),
y=rect.top(),
width=rect.width(),
height=rect.height(),
),
)
}
}
}
///|
fn sprite_copy(
source : Canvas,
target : Canvas,
x : Int,
y : Int,
extrude : Bool,
) -> Unit {
ignore(target.blit(source, destination_x=x, destination_y=y))
if extrude && source.width > 0 && source.height > 0 {
for column in 0.. Array[SpritePlacement] {
let result : Array[SpritePlacement] = []
for value in values {
result.push(value)
}
result
}
///|
pub fn pack_sprites(
sources : ArrayView[SpriteSource],
options : AtlasPackOptions,
) -> SpriteAtlas {
let pages : Array[AtlasPage] = []
let all_placements : Array[SpritePlacement] = []
let page_width = if options.page_width < 1 { 1 } else { options.page_width }
let page_height = if options.page_height < 1 {
1
} else {
options.page_height
}
let mut page_index = 0
let mut canvas = Canvas::new(
width=page_width,
height=page_height,
fill=options.background,
)
let page_placements : Array[SpritePlacement] = []
let mut cursor_x = 0
let mut cursor_y = 0
let mut row_height = 0
let mut has_content = false
for source in sources {
let padding = options.padding + source.padding()
let source_canvas = source.canvas()
let normal_width = source_canvas.width + padding * 2
let normal_height = source_canvas.height + padding * 2
let rotated_width = source_canvas.height + padding * 2
let rotated_height = source_canvas.width + padding * 2
let use_rotation = options.allow_rotation &&
normal_width > page_width - cursor_x &&
rotated_width <= page_width - cursor_x &&
rotated_height <= page_height - cursor_y
let item_width = if use_rotation { rotated_width } else { normal_width }
let item_height = if use_rotation { rotated_height } else { normal_height }
if cursor_x + item_width > page_width {
cursor_x = 0
cursor_y += row_height
row_height = 0
}
if cursor_y + item_height > page_height {
if has_content {
pages.push(AtlasPage::{
index: page_index,
canvas,
placements: copy_sprite_placements(page_placements),
})
}
page_index += 1
canvas = Canvas::new(
width=page_width,
height=page_height,
fill=options.background,
)
page_placements.clear()
cursor_x = 0
cursor_y = 0
row_height = 0
}
if item_width > page_width || item_height > page_height {
continue
}
let placed_canvas = if use_rotation {
source_canvas.rotate90()
} else {
source_canvas
}
let placement = SpritePlacement::{
name: source.name(),
page: page_index,
x: cursor_x + padding,
y: cursor_y + padding,
width: placed_canvas.width,
height: placed_canvas.height,
source_width: source_canvas.width,
source_height: source_canvas.height,
rotated: use_rotation,
}
sprite_copy(
placed_canvas,
canvas,
placement.x,
placement.y,
options.extrude || source.extrude(),
)
page_placements.push(placement)
all_placements.push(placement)
has_content = true
cursor_x += item_width
if item_height > row_height {
row_height = item_height
}
}
if has_content {
pages.push(AtlasPage::{
index: page_index,
canvas,
placements: copy_sprite_placements(page_placements),
})
}
SpriteAtlas::{ pages, placements: all_placements, page_width, page_height }
}
///|
/// Pack each animation frame as a named sprite.
pub fn Animation::to_sprite_atlas(
self : Animation,
options : AtlasPackOptions,
) -> SpriteAtlas {
let sources : Array[SpriteSource] = []
for i, frame in self.frames {
sources.push(SpriteSource::new("frame-\{i}", frame.canvas()))
}
pack_sprites(sources[:], options)
}
///|
/// Convert an atlas page into a new animation in placement order.
pub fn SpriteAtlas::to_animation(
self : SpriteAtlas,
page_index : Int,
delay_cs~ : Int,
) -> Animation {
let safe_page = if page_index < 0 {
0
} else if page_index >= self.pages.length() {
self.pages.length() - 1
} else {
page_index
}
if self.pages.length() == 0 {
Animation::new(width=0, height=0)
} else {
let page = self.pages[safe_page]
let frames : Array[Frame] = []
for placement in page.placements {
let frame_canvas = page.canvas.crop(
x=placement.x,
y=placement.y,
width=placement.width,
height=placement.height,
)
frames.push(
Frame::new(canvas=frame_canvas, delay_cs~).with_offset(left=0, top=0),
)
}
Animation::{
width: page.canvas.width,
height: page.canvas.height,
frames,
loop_count: Some(0),
background: rgba(0, 0, 0, 0),
}
}
}
///|
/// Build a regular grid atlas without searching for free rectangles.
pub fn grid_atlas(
frames : ArrayView[Canvas],
columns : Int,
cell_width : Int,
cell_height : Int,
padding : Int,
) -> SpriteAtlas {
let safe_columns = if columns < 1 { 1 } else { columns }
let safe_width = if cell_width < 1 { 1 } else { cell_width }
let safe_height = if cell_height < 1 { 1 } else { cell_height }
let safe_padding = if padding < 0 { 0 } else { padding }
let rows = if frames.length() == 0 {
0
} else {
(frames.length() + safe_columns - 1) / safe_columns
}
let page = Canvas::new(
width=safe_columns * (safe_width + safe_padding * 2),
height=rows * (safe_height + safe_padding * 2),
fill=rgba(0, 0, 0, 0),
)
let placements : Array[SpritePlacement] = []
for i, frame in frames {
let column = i % safe_columns
let row = i / safe_columns
let x = column * (safe_width + safe_padding * 2) + safe_padding
let y = row * (safe_height + safe_padding * 2) + safe_padding
let clipped = frame.crop(
x=0,
y=0,
width=if frame.width < safe_width { frame.width } else { safe_width },
height=if frame.height < safe_height { frame.height } else { safe_height },
)
sprite_copy(clipped, page, x, y, false)
placements.push(SpritePlacement::{
name: "frame-\{i}",
page: 0,
x,
y,
width: clipped.width,
height: clipped.height,
source_width: frame.width,
source_height: frame.height,
rotated: false,
})
}
let atlas_page = AtlasPage::{
index: 0,
canvas: page,
placements: copy_sprite_placements(placements),
}
SpriteAtlas::{
pages: [atlas_page],
placements,
page_width: page.width,
page_height: page.height,
}
}