///|
pub(all) struct Canvas {
width : Int
height : Int
pixels : Array[Byte]
} derive(Debug)
///|
pub fn Canvas::new(
width~ : Int,
height~ : Int,
background? : Color = Color::rgb(r=15, g=23, b=42),
) -> Canvas {
let pixels : Array[Byte] = []
for _ in 0..<(width * height) {
pixels.push(background.r.to_byte())
pixels.push(background.g.to_byte())
pixels.push(background.b.to_byte())
pixels.push((background.a * 255.0).round().to_int().to_byte())
}
{ width, height, pixels }
}
///|
pub fn Canvas::from_image_spec(
image : ImageSpec,
background? : Color = Color::rgb(r=15, g=23, b=42),
) -> Canvas {
Canvas::new(width=image.width, height=image.height, background~)
}
///|
pub fn Canvas::set_pixel(
self : Canvas,
x : Int,
y : Int,
color : Color,
) -> Unit {
if x >= 0 && y >= 0 && x < self.width && y < self.height {
let index = (y * self.width + x) * 4
self.pixels[index] = color.r.to_byte()
self.pixels[index + 1] = color.g.to_byte()
self.pixels[index + 2] = color.b.to_byte()
self.pixels[index + 3] = (color.a * 255.0).round().to_int().to_byte()
}
}
///|
pub fn Canvas::fill_rect(self : Canvas, rect : Rect, color : Color) -> Unit {
let x0 = floor_to_int(rect.x)
let y0 = floor_to_int(rect.y)
let x1 = ceil_to_int(rect.right())
let y1 = ceil_to_int(rect.bottom())
for y in y0.. Unit {
self.fill_polygons([polygon], color)
}
///|
/// Fills polygons with the even-odd rule, allowing nested polygons to form holes.
///
/// Pixels are evaluated at `(x + 0.5, y + 0.5)`. The same half-open edge rule as
/// `fill_polygon` makes boundary coverage reproducible regardless of edge order.
pub fn Canvas::fill_polygons(
self : Canvas,
polygons : Array[Array[Point]],
color : Color,
) -> Unit {
let valid_polygons = polygons.filter(polygon => polygon.length() >= 3)
if valid_polygons.is_empty() {
()
} else {
match bounds_of_polygons(valid_polygons) {
None => ()
Some(bounds) => {
let raw_x0 = floor_to_int(bounds.x)
let raw_y0 = floor_to_int(bounds.y)
let raw_x1 = ceil_to_int(bounds.right())
let raw_y1 = ceil_to_int(bounds.bottom())
let x0 = if raw_x0 < 0 { 0 } else { raw_x0 }
let y0 = if raw_y0 < 0 { 0 } else { raw_y0 }
let x1 = if raw_x1 > self.width { self.width } else { raw_x1 }
let y1 = if raw_y1 > self.height { self.height } else { raw_y1 }
for y in y0.. Unit {
let x0 = floor_to_int(rect.x)
let y0 = floor_to_int(rect.y)
let x1 = ceil_to_int(rect.right())
let y1 = ceil_to_int(rect.bottom())
for offset in 0.. Unit {
let cx = point.x.round().to_int()
let cy = point.y.round().to_int()
let rr = radius * radius
for y in (cy - radius)..<=(cy + radius) {
for x in (cx - radius)..<=(cx + radius) {
let dx = x - cx
let dy = y - cy
if dx * dx + dy * dy <= rr {
self.set_pixel(x, y, color)
}
}
}
}
///|
pub fn DebugDocument::to_canvas(
self : DebugDocument,
background? : Color = Color::rgb(r=15, g=23, b=42),
) -> Canvas {
let canvas = Canvas::from_image_spec(self.image, background~)
for layer_index, layer in self.visible_layers() {
for overlay_index, overlay in layer.overlays {
draw_overlay(canvas, overlay, layer_index + overlay_index)
}
}
canvas
}
///|
pub fn DebugDocument::to_png(self : DebugDocument) -> Bytes {
self.to_canvas().to_png()
}
///|
fn draw_overlay(canvas : Canvas, overlay : Overlay, index : Int) -> Unit {
match overlay {
BBox(rect~, color~, ..) =>
canvas.stroke_rect(rect, resolve_raster_color(color, index))
Mask(polygons~, color~, ..) => {
let color = resolve_raster_color(color, index).with_alpha(0.45)
canvas.fill_polygons(polygons, color)
}
Keypoints(points~, color~) => {
let color = resolve_raster_color(color, index)
for point in points {
if point.visible {
canvas.draw_point(point.point, color)
}
}
}
Trajectory(path~, color~) => {
let color = resolve_raster_color(color, index)
for point in path.points {
canvas.draw_point(point, color, radius=2)
}
}
Heatmap(cells~, low~, high~) => {
let low_color = resolve_raster_color(low, 2)
let high_color = resolve_raster_color(high, 1)
for cell in cells {
canvas.fill_rect(
cell.rect,
raster_mix(low_color, high_color, clamp_raster(cell.value)).with_alpha(
0.55,
),
)
}
}
ErrorRegion(rect~, severity~, ..) => {
let color = raster_mix(
Color::rgb(r=245, g=158, b=11),
Color::rgb(r=220, g=38, b=38),
clamp_raster(severity),
)
canvas.stroke_rect(rect, color, width=4)
}
}
}
///|
pub fn Canvas::to_png(self : Canvas) -> Bytes {
let png : Array[Byte] = [
byte(137),
byte(80),
byte(78),
byte(71),
byte(13),
byte(10),
byte(26),
byte(10),
]
append_chunk(png, "IHDR", ihdr_data(self.width, self.height))
let scanlines = png_scanlines(self)
append_chunk(png, "IDAT", zlib_store(scanlines))
append_chunk(png, "IEND", [])
Bytes::from_array(png)
}
///|
fn ihdr_data(width : Int, height : Int) -> Array[Byte] {
let data : Array[Byte] = []
append_u32_be(data, width.reinterpret_as_uint())
append_u32_be(data, height.reinterpret_as_uint())
data.push(byte(8)) // bit depth
data.push(byte(6)) // RGBA
data.push(byte(0)) // compression
data.push(byte(0)) // filter
data.push(byte(0)) // interlace
data
}
///|
fn png_scanlines(canvas : Canvas) -> Array[Byte] {
let raw : Array[Byte] = []
for y in 0.. Array[Byte] {
let out : Array[Byte] = [byte(0x78), byte(0x01)]
let mut offset = 0
while offset < raw.length() {
let remaining = raw.length() - offset
let len = if remaining > 65535 { 65535 } else { remaining }
let final_block = offset + len >= raw.length()
out.push(if final_block { byte(1) } else { byte(0) })
append_u16_le(out, len)
append_u16_le(out, 65535 - len)
for i in 0.. Unit {
append_u32_be(out, data.length().reinterpret_as_uint())
let kind_bytes = string_ascii(kind)
out.append(kind_bytes)
out.append(data)
let crc_input = kind_bytes + data
append_u32_be(out, crc32(crc_input))
}
///|
fn append_u16_le(out : Array[Byte], value : Int) -> Unit {
out.push(value.to_byte())
out.push((value >> 8).to_byte())
}
///|
fn byte(value : Int) -> Byte {
value.to_byte()
}
///|
fn append_u32_be(out : Array[Byte], value : UInt) -> Unit {
out.push((value >> 24).to_byte())
out.push((value >> 16).to_byte())
out.push((value >> 8).to_byte())
out.push(value.to_byte())
}
///|
fn string_ascii(value : String) -> Array[Byte] {
value.iter().map(ch => ch.to_int().to_byte()).collect()
}
///|
fn crc32(bytes : Array[Byte]) -> UInt {
let mut crc = 0xffffffffU
for byte in bytes {
crc = crc ^ byte.to_uint()
for _ in 0..<8 {
if (crc & 1U) == 1U {
crc = (crc >> 1) ^ 0xedb88320U
} else {
crc = crc >> 1
}
}
}
crc ^ 0xffffffffU
}
///|
fn adler32(bytes : Array[Byte]) -> UInt {
let mut a = 1
let mut b = 0
for byte in bytes {
a = (a + byte.to_int()) % 65521
b = (b + a) % 65521
}
((b << 16) | a).reinterpret_as_uint()
}
///|
fn resolve_raster_color(color : Color?, index : Int) -> Color {
match color {
Some(value) => value
None => palette(index)
}
}
///|
fn raster_mix(a : Color, b : Color, t : Double) -> Color {
let k = clamp_raster(t)
Color::rgba(
r=(a.r.to_double() + (b.r - a.r).to_double() * k).round().to_int(),
g=(a.g.to_double() + (b.g - a.g).to_double() * k).round().to_int(),
b=(a.b.to_double() + (b.b - a.b).to_double() * k).round().to_int(),
a=a.a + (b.a - a.a) * k,
)
}
///|
fn clamp_raster(value : Double) -> Double {
if value < 0.0 {
0.0
} else if value > 1.0 {
1.0
} else {
value
}
}
///|
fn floor_to_int(value : Double) -> Int {
value.floor().to_int()
}
///|
fn ceil_to_int(value : Double) -> Int {
value.ceil().to_int()
}
///|
fn point_is_inside_polygons(
x : Double,
y : Double,
polygons : Array[Array[Point]],
) -> Bool {
let mut inside = false
for polygon in polygons {
if point_is_inside_polygon(x, y, polygon) {
inside = !inside
}
}
inside
}
///|
fn point_is_inside_polygon(
x : Double,
y : Double,
polygon : Array[Point],
) -> Bool {
if polygon.length() < 3 {
false
} else {
let mut inside = false
let mut previous = polygon[polygon.length() - 1]
for current in polygon {
let crosses_scanline = (previous.y <= y && y < current.y) ||
(current.y <= y && y < previous.y)
if crosses_scanline {
let crossing_x = previous.x +
(y - previous.y) * (current.x - previous.x) / (current.y - previous.y)
if x < crossing_x {
inside = !inside
}
}
previous = current
}
inside
}
}