///|
fn int_abs(value : Int) -> Int {
if value < 0 {
-value
} else {
value
}
}
///|
fn clamp_byte_int(value : Int) -> Byte {
if value < 0 {
0
} else if value > 255 {
255
} else {
value.to_byte()
}
}
///|
fn blend_rgba(source : Rgba, destination : Rgba) -> Rgba {
let source_alpha = source.alpha().to_int()
if source_alpha >= 255 {
source
} else if source_alpha == 0 {
destination
} else {
let destination_alpha = destination.alpha().to_int()
let inverse = 255 - source_alpha
let output_alpha = source_alpha + destination_alpha * inverse / 255
if output_alpha == 0 {
rgba(0, 0, 0, 0)
} else {
rgba(
clamp_byte_int(
(
source.red().to_int() * source_alpha +
destination.red().to_int() * destination_alpha * inverse / 255
) /
output_alpha,
),
clamp_byte_int(
(
source.green().to_int() * source_alpha +
destination.green().to_int() * destination_alpha * inverse / 255
) /
output_alpha,
),
clamp_byte_int(
(
source.blue().to_int() * source_alpha +
destination.blue().to_int() * destination_alpha * inverse / 255
) /
output_alpha,
),
output_alpha.to_byte(),
)
}
}
}
///|
/// Fill the entire canvas with a color.
pub fn Canvas::fill(self : Canvas, color : Rgba) -> Unit {
for i in 0.. Int {
if width <= 0 || height <= 0 {
0
} else {
let start_x = if x < 0 { 0 } else { x }
let start_y = if y < 0 { 0 } else { y }
let end_x = if x + width > self.width { self.width } else { x + width }
let end_y = if y + height > self.height { self.height } else { y + height }
let mut count = 0
if start_x < end_x && start_y < end_y {
for row in start_y.. Bool {
match self.get(x~, y~) {
None => false
Some(previous) => {
self.pixels[x + y * self.width] = blend_rgba(color, previous)
true
}
}
}
///|
/// Draw a clipped Bresenham line.
pub fn Canvas::draw_line(
self : Canvas,
x0~ : Int,
y0~ : Int,
x1~ : Int,
y1~ : Int,
color~ : Rgba,
) -> Int {
let mut x = x0
let mut y = y0
let dx = int_abs(x1 - x0)
let sx = if x0 < x1 { 1 } else { -1 }
let dy = -int_abs(y1 - y0)
let sy = if y0 < y1 { 1 } else { -1 }
let mut error = dx + dy
let mut written = 0
let mut done = false
while !done {
if self.set(x~, y~, color~) {
written += 1
}
if x == x1 && y == y1 {
done = true
} else {
let twice = 2 * error
if twice >= dy {
error += dy
x += sx
}
if twice <= dx {
error += dx
y += sy
}
}
}
written
}
///|
/// Draw a rectangle outline. Coordinates are inclusive.
pub fn Canvas::draw_rect(
self : Canvas,
x~ : Int,
y~ : Int,
width~ : Int,
height~ : Int,
color~ : Rgba,
) -> Int {
if width <= 0 || height <= 0 {
0
} else {
let right = x + width - 1
let bottom = y + height - 1
let mut count = self.draw_line(x0=x, y0=y, x1=right, y1=y, color~)
count += self.draw_line(x0=right, y0=y, x1=right, y1=bottom, color~)
count += self.draw_line(x0=right, y0=bottom, x1=x, y1=bottom, color~)
count += self.draw_line(x0=x, y0=bottom, x1=x, y1=y, color~)
count
}
}
///|
/// Fill a circle using an integer distance test.
pub fn Canvas::fill_circle(
self : Canvas,
center_x~ : Int,
center_y~ : Int,
radius~ : Int,
color~ : Rgba,
) -> Int {
if radius < 0 {
0
} else {
let mut count = 0
let radius_squared = radius * radius
for y in (center_y - radius)..<(center_y + radius + 1) {
for x in (center_x - radius)..<(center_x + radius + 1) {
let dx = x - center_x
let dy = y - center_y
if dx * dx + dy * dy <= radius_squared && self.set(x~, y~, color~) {
count += 1
}
}
}
count
}
}
///|
/// Draw a circle outline using a midpoint-style eight-way rasterization.
pub fn Canvas::draw_circle(
self : Canvas,
center_x~ : Int,
center_y~ : Int,
radius~ : Int,
color~ : Rgba,
) -> Int {
if radius < 0 {
0
} else if radius == 0 {
if self.set(x=center_x, y=center_y, color~) {
1
} else {
0
}
} else {
let mut x = radius
let mut y = 0
let mut decision = 1 - radius
let mut count = 0
while x >= y {
count += draw_circle_points(self, center_x, center_y, x, y, color)
y += 1
if decision <= 0 {
decision += 2 * y + 1
} else {
x -= 1
decision += 2 * (y - x) + 1
}
}
count
}
}
///|
fn draw_circle_points(
canvas : Canvas,
cx : Int,
cy : Int,
x : Int,
y : Int,
color : Rgba,
) -> Int {
let points = [
(cx + x, cy + y),
(cx + y, cy + x),
(cx - y, cy + x),
(cx - x, cy + y),
(cx - x, cy - y),
(cx - y, cy - x),
(cx + y, cy - x),
(cx + x, cy - y),
]
let mut count = 0
for point in points {
let (px, py) = point
if canvas.set(x=px, y=py, color~) {
count += 1
}
}
count
}
///|
/// Copy a source canvas with alpha compositing and clipping.
pub fn Canvas::blit(
self : Canvas,
source : Canvas,
destination_x~ : Int,
destination_y~ : Int,
) -> Int {
let mut count = 0
for y in 0.. ()
Some(color) =>
if self.blend_pixel(x=destination_x + x, y=destination_y + y, color~) {
count += 1
}
}
}
}
count
}
///|
/// Crop a rectangle. Pixels outside the source are filled with transparent black.
pub fn Canvas::crop(
self : Canvas,
x~ : Int,
y~ : Int,
width~ : Int,
height~ : Int,
) -> Canvas {
if width < 0 || height < 0 {
panic()
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
for row in 0.. ()
Some(color) => ignore(result.set(x=column, y=row, color~))
}
}
}
result
}
///|
/// Resize with nearest-neighbor sampling, suitable for pixel art and thumbnails.
pub fn Canvas::resize_nearest(
self : Canvas,
width~ : Int,
height~ : Int,
) -> Canvas {
if width < 0 || height < 0 {
panic()
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
if width > 0 && height > 0 && self.width > 0 && self.height > 0 {
for y in 0.. Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for y in 0.. Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for y in 0.. Int {
let mut count = 0
for i in 0.. Int {
let mut count = 0
for value in self.pixels {
if value == color {
count += 1
}
}
count
}
///|
/// Return true when every pixel has an opaque alpha channel.
pub fn Canvas::is_opaque(self : Canvas) -> Bool {
let mut result = true
for color in self.pixels {
if color.alpha() != 255 {
result = false
}
}
result
}
///|
/// Serialize pixels as tightly packed RGBA bytes.
pub fn Canvas::to_rgba_bytes(self : Canvas) -> Bytes {
let bytes : Array[Byte] = []
for color in self.pixels {
bytes.push(color.red())
bytes.push(color.green())
bytes.push(color.blue())
bytes.push(color.alpha())
}
Bytes::from_array(bytes[:])
}
///|
/// Construct a canvas from tightly packed RGBA bytes.
pub fn canvas_from_rgba_bytes(
width~ : Int,
height~ : Int,
bytes : Bytes,
) -> Canvas raise GifError {
if width < 0 || height < 0 || bytes.length() != width * height * 4 {
raise InvalidBlock(
context="RGBA byte length does not match canvas dimensions",
)
}
let pixels : Array[Rgba] = []
for i in 0..<(width * height) {
let offset = i * 4
pixels.push(
rgba(
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
),
)
}
Canvas::{ width, height, pixels }
}
///|
/// Count pixels that differ between two canvases.
pub fn pixel_difference(left : Canvas, right : Canvas) -> Int {
if left.width != right.width || left.height != right.height {
left.width * left.height + right.width * right.height
} else {
let mut count = 0
for i in 0..