///|
/// A positioned image layer used to compose stickers, captions, and overlays.
pub(all) struct ImageLayer {
name : String
canvas : Canvas
left : Int
top : Int
opacity : Byte
blend_mode : BlendMode
visible : Bool
} derive(Debug)
///|
pub fn ImageLayer::new(name : String, canvas : Canvas) -> ImageLayer {
{
name,
canvas,
left: 0,
top: 0,
opacity: 255,
blend_mode: Normal,
visible: true,
}
}
///|
pub fn ImageLayer::name(self : ImageLayer) -> String {
self.name
}
///|
pub fn ImageLayer::canvas(self : ImageLayer) -> Canvas {
self.canvas
}
///|
pub fn ImageLayer::offset(self : ImageLayer) -> (Int, Int) {
(self.left, self.top)
}
///|
pub fn ImageLayer::opacity(self : ImageLayer) -> Byte {
self.opacity
}
///|
pub fn ImageLayer::blend_mode(self : ImageLayer) -> BlendMode {
self.blend_mode
}
///|
pub fn ImageLayer::move_to(
self : ImageLayer,
left : Int,
top : Int,
) -> ImageLayer {
{ ..self, left, top }
}
///|
pub fn ImageLayer::with_opacity(
self : ImageLayer,
opacity : Byte,
) -> ImageLayer {
{ ..self, opacity, }
}
///|
pub fn ImageLayer::with_blend(
self : ImageLayer,
mode : BlendMode,
) -> ImageLayer {
{ ..self, blend_mode: mode }
}
///|
pub fn ImageLayer::hidden(self : ImageLayer) -> ImageLayer {
{ ..self, visible: false }
}
///|
pub fn ImageLayer::shown(self : ImageLayer) -> ImageLayer {
{ ..self, visible: true }
}
///|
pub(all) struct LayerStack {
width : Int
height : Int
background : Rgba
layers : Array[ImageLayer]
} derive(Debug)
///|
pub fn LayerStack::new(width~ : Int, height~ : Int) -> LayerStack {
{ width, height, background: rgba(0, 0, 0, 0), layers: [] }
}
///|
pub fn LayerStack::background(self : LayerStack, color : Rgba) -> LayerStack {
{ ..self, background: color }
}
///|
pub fn LayerStack::add(self : LayerStack, layer : ImageLayer) -> LayerStack {
self.layers.push(layer)
self
}
///|
pub fn LayerStack::insert(
self : LayerStack,
index : Int,
layer : ImageLayer,
) -> LayerStack {
let safe_index = if index < 0 {
0
} else if index > self.layers.length() {
self.layers.length()
} else {
index
}
let result : Array[ImageLayer] = []
for i in 0..<(self.layers.length() + 1) {
if i == safe_index {
result.push(layer)
} else if i < safe_index {
result.push(self.layers[i])
} else {
result.push(self.layers[i - 1])
}
}
{ ..self, layers: result }
}
///|
pub fn LayerStack::remove(self : LayerStack, index : Int) -> LayerStack {
if index < 0 || index >= self.layers.length() {
self
} else {
let result : Array[ImageLayer] = []
for i, layer in self.layers {
if i != index {
result.push(layer)
}
}
{ ..self, layers: result }
}
}
///|
pub fn LayerStack::layers(self : LayerStack) -> ArrayView[ImageLayer] {
self.layers[:]
}
///|
fn layer_canvas(stack : LayerStack, layer : ImageLayer) -> Canvas {
let prepared = layer.canvas().scale_alpha(layer.opacity())
let positioned = Canvas::new(
width=stack.width,
height=stack.height,
fill=rgba(0, 0, 0, 0),
)
ignore(
positioned.blit(prepared, destination_x=layer.left, destination_y=layer.top),
)
positioned
}
///|
/// Composite the visible layers from back to front.
pub fn LayerStack::render(self : LayerStack) -> Canvas {
let mut result = Canvas::new(
width=self.width,
height=self.height,
fill=self.background,
)
for layer in self.layers {
if layer.visible {
let positioned = layer_canvas(self, layer)
result = result.blend(positioned, layer.blend_mode())
}
}
result
}
///|
/// Render each layer stack into an animation frame.
pub fn render_layers(
width : Int,
height : Int,
stacks : ArrayView[LayerStack],
delay_cs? : Int = 10,
) -> Animation {
let result = Animation::new(width~, height~)
let safe_delay = if delay_cs < 0 {
0
} else if delay_cs > 65535 {
65535
} else {
delay_cs
}
for stack in stacks {
ignore(
result.add_frame(Frame::new(canvas=stack.render(), delay_cs=safe_delay)),
)
}
result
}
///|
/// Construct a stack from one base canvas and ordered overlays.
pub fn compose_layers(
base : Canvas,
overlays : ArrayView[ImageLayer],
) -> Canvas {
let stack = LayerStack::new(width=base.width, height=base.height).add(
ImageLayer::new("base", base),
)
let mut result = stack
for overlay in overlays {
result = result.add(overlay)
}
result.render()
}
///|
/// Apply a single layer at all frame times while keeping the base timeline.
pub fn Animation::overlay_layer(
self : Animation,
layer : ImageLayer,
) -> Animation {
let result = Animation::new(width=self.width, height=self.height)
.with_loop_count(loop_count=self.loop_count)
.with_background(background=self.background)
for frame in self.frames {
let stack = LayerStack::new(width=self.width, height=self.height)
.add(ImageLayer::new("frame", frame.canvas()))
.add(layer)
ignore(result.add_frame(frame.with_canvas(stack.render())))
}
result
}
///|
/// Render a moving overlay across a timeline using integer interpolation.
pub fn Animation::moving_layer(
self : Animation,
layer : ImageLayer,
start_left : Int,
start_top : Int,
end_left : Int,
end_top : Int,
) -> Animation {
let result = Animation::new(width=self.width, height=self.height)
.with_loop_count(loop_count=self.loop_count)
.with_background(background=self.background)
let count = self.frames.length()
for index, frame in self.frames {
let denominator = if count <= 1 { 1 } else { count - 1 }
let left = start_left + (end_left - start_left) * index / denominator
let top = start_top + (end_top - start_top) * index / denominator
let positioned = layer.move_to(left, top)
let stack = LayerStack::new(width=self.width, height=self.height)
.add(ImageLayer::new("frame", frame.canvas()))
.add(positioned)
ignore(result.add_frame(frame.with_canvas(stack.render())))
}
result
}
///|
/// Return the bounding rectangle of all visible layer pixels.
pub fn LayerStack::visible_bounds(self : LayerStack) -> GifRect? {
let mut found = false
let mut left = self.width
let mut top = self.height
let mut right = -1
let mut bottom = -1
for layer in self.layers {
if layer.visible && layer.opacity > 0 {
match layer.canvas().opaque_bounds() {
None => ()
Some(bounds) => {
let layer_left = layer.left + bounds.left()
let layer_top = layer.top + bounds.top()
let layer_right = layer_left + bounds.width() - 1
let layer_bottom = layer_top + bounds.height() - 1
if layer_left < left {
left = layer_left
}
if layer_top < top {
top = layer_top
}
if layer_right > right {
right = layer_right
}
if layer_bottom > bottom {
bottom = layer_bottom
}
found = true
}
}
}
}
if found {
Some(
GifRect::new(left~, top~, width=right - left + 1, height=bottom - top + 1),
)
} else {
None
}
}
///|
/// Scale every layer around the stack center for a poster-style effect.
pub fn LayerStack::scale_layers(
self : LayerStack,
scale_per_mille : Int,
) -> LayerStack {
let safe_scale = if scale_per_mille < 0 { 0 } else { scale_per_mille }
let result : Array[ImageLayer] = []
for layer in self.layers {
let scaled = layer
.canvas()
.resize_nearest(
width=layer.canvas().width * safe_scale / 1000,
height=layer.canvas().height * safe_scale / 1000,
)
let left = self.width / 2 - scaled.width / 2
let top = self.height / 2 - scaled.height / 2
result.push({ ..layer, canvas: scaled, left, top })
}
{ ..self, layers: result }
}