///|
/// Integer-only geometric effects for deterministic sprite and GIF output.
pub(all) enum TileMode {
Repeat
Mirror
Clamp
} derive(Eq, Debug)
///|
pub(all) struct VignetteOptions {
radius_per_mille : Int
strength : Int
color : Rgba
} derive(Debug)
///|
pub fn VignetteOptions::default() -> VignetteOptions {
{ radius_per_mille: 1000, strength: 600, color: rgba(0, 0, 0, 255) }
}
///|
pub fn VignetteOptions::radius(
self : VignetteOptions,
value : Int,
) -> VignetteOptions {
{ ..self, radius_per_mille: if value < 1 { 1 } else { value } }
}
///|
pub fn VignetteOptions::strength(
self : VignetteOptions,
value : Int,
) -> VignetteOptions {
{
..self,
strength: if value < 0 {
0
} else if value > 1000 {
1000
} else {
value
},
}
}
///|
pub fn VignetteOptions::color(
self : VignetteOptions,
value : Rgba,
) -> VignetteOptions {
{ ..self, color: value }
}
///|
fn wrapped_coordinate(value : Int, size : Int, mode : TileMode) -> Int {
if size <= 0 {
0
} else {
match mode {
Repeat => {
let result = value % size
if result < 0 {
result + size
} else {
result
}
}
Mirror => {
let period = size * 2
let offset = value % period
let normalized = if offset < 0 { offset + period } else { offset }
if normalized < size {
normalized
} else {
period - normalized - 1
}
}
Clamp =>
if value < 0 {
0
} else if value >= size {
size - 1
} else {
value
}
}
}
}
///|
/// Translate a canvas with explicit wrapping semantics.
pub fn Canvas::translate(
self : Canvas,
offset_x : Int,
offset_y : Int,
mode? : TileMode = Clamp,
) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for y in 0.. Canvas {
if width < 0 || height < 0 {
panic()
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
for y in 0.. 0 && self.height > 0 {
result.pixels[x + y * width] = self.pixels[sx + sy * self.width]
}
}
}
result
}
///|
/// Mirror an image around its vertical and horizontal axes.
pub fn Canvas::mirror(
self : Canvas,
horizontal : Bool,
vertical : Bool,
) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for y in 0.. Canvas {
let safe_sectors = if sectors < 2 {
2
} else if sectors > 8 {
8
} else {
sectors
}
let result = self.copy()
let center_x = self.width / 2
let center_y = self.height / 2
for y in 0..= dy.abs() { 0 } else { 1 }
let sx = if quadrant == 0 {
center_x + dx.abs()
} else {
center_x + dy.abs()
}
let sy = if quadrant == 0 {
center_y + dy.abs()
} else {
center_y + dx.abs()
}
let clamped_x = if sx < 0 {
0
} else if sx >= self.width {
self.width - 1
} else {
sx
}
let clamped_y = if sy < 0 {
0
} else if sy >= self.height {
self.height - 1
} else {
sy
}
let base = self.pixels[clamped_x + clamped_y * self.width]
let angle_shift = (x + y) % safe_sectors
result.pixels[x + y * self.width] = if angle_shift == 0 {
base
} else {
rgba(base.blue(), base.red(), base.green(), base.alpha())
}
}
}
result
}
///|
/// Apply a radial falloff over a canvas.
pub fn Canvas::vignette(
self : Canvas,
options? : VignetteOptions = VignetteOptions::default(),
) -> Canvas {
let result = self.copy()
let center_x = self.width / 2
let center_y = self.height / 2
let max_distance = center_x * center_x + center_y * center_y
let radius = if options.radius_per_mille < 1 {
1
} else {
options.radius_per_mille
}
for y in 0.. 1000 {
1000
} else {
value
}
}
let source = result.pixels[x + y * self.width]
let keep = 1000 - falloff
let red = (
source.red().to_int() * keep + options.color.red().to_int() * falloff
) /
1000
let green = (
source.green().to_int() * keep +
options.color.green().to_int() * falloff
) /
1000
let blue = (
source.blue().to_int() * keep +
options.color.blue().to_int() * falloff
) /
1000
result.pixels[x + y * self.width] = rgba(
red.to_byte(),
green.to_byte(),
blue.to_byte(),
source.alpha(),
)
}
}
result
}
///|
/// Draw a soft rectangular border around the visible image.
pub fn Canvas::border(self : Canvas, thickness : Int, color : Rgba) -> Canvas {
let result = self.copy()
let safe = if thickness < 0 { 0 } else { thickness }
for y in 0..= self.width - safe ||
y >= self.height - safe {
result.pixels[x + y * self.width] = blend_rgba_mode(
color,
result.pixels[x + y * self.width],
Normal,
)
}
}
}
result
}
///|
/// Add a regular grid useful for pixel-art previews.
pub fn Canvas::grid(self : Canvas, spacing : Int, color : Rgba) -> Canvas {
let result = self.copy()
let safe = if spacing < 1 { 1 } else { spacing }
for y in 0.. Canvas {
let rotated = self.rotate(rotation)
let result = Canvas::new(width=rotated.width, height=rotated.height, fill~)
ignore(result.blit(rotated, destination_x=0, destination_y=0))
result
}
///|
/// Extract evenly spaced stripes for a glitch-style animated effect.
pub fn Canvas::stripe_shift(
self : Canvas,
stripe_height : Int,
offset : Int,
) -> Canvas {
let result = self.copy()
let safe_height = if stripe_height < 1 { 1 } else { stripe_height }
for y in 0..