///|
/// Layout helpers for contact sheets, sticker boards, and sprite previews.
pub(all) enum LayoutDirection {
Horizontal
Vertical
Grid(columns~ : Int)
} derive(Eq, Debug)
///|
pub(all) struct LayoutItem {
canvas : Canvas
left : Int
top : Int
width : Int
height : Int
} derive(Debug)
///|
pub fn LayoutItem::canvas(self : LayoutItem) -> Canvas {
self.canvas
}
///|
pub fn LayoutItem::position(self : LayoutItem) -> (Int, Int) {
(self.left, self.top)
}
///|
pub fn LayoutItem::size(self : LayoutItem) -> (Int, Int) {
(self.width, self.height)
}
///|
pub(all) struct LayoutResult {
width : Int
height : Int
items : Array[LayoutItem]
} derive(Debug)
///|
pub fn LayoutResult::width(self : LayoutResult) -> Int {
self.width
}
///|
pub fn LayoutResult::height(self : LayoutResult) -> Int {
self.height
}
///|
pub fn LayoutResult::items(self : LayoutResult) -> ArrayView[LayoutItem] {
self.items[:]
}
///|
/// Lay out canvases in a row, column, or fixed-column grid.
pub fn layout_canvases(
canvases : ArrayView[Canvas],
direction : LayoutDirection,
gap : Int,
background : Rgba,
) -> LayoutResult {
let safe_gap = if gap < 0 { 0 } else { gap }
let items : Array[LayoutItem] = []
let mut total_width = 0
let mut total_height = 0
match direction {
Horizontal => {
let mut left = 0
for canvas in canvases {
items.push(LayoutItem::{
canvas,
left,
top: 0,
width: canvas.width,
height: canvas.height,
})
left += canvas.width + safe_gap
if canvas.height > total_height {
total_height = canvas.height
}
}
total_width = if left > 0 { left - safe_gap } else { 0 }
}
Vertical => {
let mut top = 0
for canvas in canvases {
items.push(LayoutItem::{
canvas,
left: 0,
top,
width: canvas.width,
height: canvas.height,
})
top += canvas.height + safe_gap
if canvas.width > total_width {
total_width = canvas.width
}
}
total_height = if top > 0 { top - safe_gap } else { 0 }
}
Grid(columns~) => {
let columns = if columns < 1 { 1 } else { columns }
let mut row_height = 0
let mut left = 0
let mut top = 0
for index, canvas in canvases {
if index > 0 && index % columns == 0 {
top += row_height + safe_gap
left = 0
row_height = 0
}
items.push(LayoutItem::{
canvas,
left,
top,
width: canvas.width,
height: canvas.height,
})
left += canvas.width + safe_gap
if canvas.height > row_height {
row_height = canvas.height
}
if left > total_width {
total_width = left - safe_gap
}
}
total_height = if canvases.length() == 0 { 0 } else { top + row_height }
}
}
ignore(background)
LayoutResult::{ width: total_width, height: total_height, items }
}
///|
/// Render a layout with clipped placement and alpha compositing.
pub fn LayoutResult::render(self : LayoutResult, background : Rgba) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=background,
)
for item in self.items {
ignore(
result.blit(item.canvas, destination_x=item.left, destination_y=item.top),
)
}
result
}
///|
/// Place a canvas inside a safe rectangle with letterboxing.
pub fn contain_layout(
canvas : Canvas,
width : Int,
height : Int,
background : Rgba,
) -> Canvas {
let result = Canvas::new(width~, height~, fill=background)
let contained = canvas.contain(width~, height~, fill=background)
let left = (width - contained.width) / 2
let top = (height - contained.height) / 2
ignore(result.blit(contained, destination_x=left, destination_y=top))
result
}
///|
/// Crop a nine-patch panel and fill its center without scaling borders.
pub fn nine_patch(
source : Canvas,
width : Int,
height : Int,
border : Int,
) -> Canvas {
let safe_border = if border < 0 { 0 } else { border }
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
if source.width == 0 || source.height == 0 {
result
} else {
for y in 0..= width - safe_border {
source.width - (width - x)
} else {
safe_border +
(x - safe_border) *
(source.width - 2 * safe_border) /
(width - 2 * safe_border + 1)
}
let sy = if y < safe_border {
y
} else if y >= height - safe_border {
source.height - (height - y)
} else {
safe_border +
(y - safe_border) *
(source.height - 2 * safe_border) /
(height - 2 * safe_border + 1)
}
let cx = if sx < 0 {
0
} else if sx >= source.width {
source.width - 1
} else {
sx
}
let cy = if sy < 0 {
0
} else if sy >= source.height {
source.height - 1
} else {
sy
}
result.pixels[x + y * width] = source.pixels[cx + cy * source.width]
}
}
result
}
}
///|
/// Render a frame sequence as a centered board with a fixed cell size.
pub fn board_animation(
animation : Animation,
columns : Int,
cell_width : Int,
cell_height : Int,
gap : Int,
background : Rgba,
) -> Canvas {
let selected = animation.sample_with_policy(EveryFrame)
let canvases : Array[Canvas] = []
for frame in selected.frames {
canvases.push(
contain_layout(frame.canvas(), cell_width, cell_height, background),
)
}
layout_canvases(canvases[:], Grid(columns~), gap, background).render(
background,
)
}