///|
/// Draw a rectangle. When `fill` is false, `width` sets the stroke thickness.
///
/// The rectangle spans rows `[y, y + h)` and columns `[x, x + w)`. When
/// `fill` is true the interior is filled; otherwise a stroked outline of
/// thickness `width` is drawn. Uses source-over alpha blending; the input
/// image is not modified.
pub fn draw_rect(
img : Image,
y : Int,
x : Int,
h : Int,
w : Int,
r : Byte,
g : Byte,
b : Byte,
a : Byte,
fill : Bool,
width : Int,
) -> Image {
let out = img.clone()
if fill {
for yy = y; yy < y + h; yy = yy + 1 {
for xx = x; xx < x + w; xx = xx + 1 {
blend_at(out, yy, xx, r, g, b, a)
}
}
} else {
for xx = x; xx < x + w; xx = xx + 1 {
stamp(out, y, xx, r, g, b, a, width)
stamp(out, y + h - 1, xx, r, g, b, a, width)
}
for yy = y; yy < y + h; yy = yy + 1 {
stamp(out, yy, x, r, g, b, a, width)
stamp(out, yy, x + w - 1, r, g, b, a, width)
}
}
out
}
///|
/// Stamp the eight symmetric points of a circle octant.
fn circle_points(
img : Image,
cy : Int,
cx : Int,
yy : Int,
xx : Int,
r : Byte,
g : Byte,
b : Byte,
a : Byte,
width : Int,
) -> Unit {
stamp(img, cy + yy, cx + xx, r, g, b, a, width)
stamp(img, cy + yy, cx - xx, r, g, b, a, width)
stamp(img, cy - yy, cx + xx, r, g, b, a, width)
stamp(img, cy - yy, cx - xx, r, g, b, a, width)
stamp(img, cy + xx, cx + yy, r, g, b, a, width)
stamp(img, cy + xx, cx - yy, r, g, b, a, width)
stamp(img, cy - xx, cx + yy, r, g, b, a, width)
stamp(img, cy - xx, cx - yy, r, g, b, a, width)
}
///|
/// Draw a circle. When `fill` is false, `width` sets the stroke thickness.
///
/// Centred at `(cy, cx)` with the given `radius`. Filled circles scan each
/// row over the implicit `x² + y² <= r²` disc; outlines use the midpoint
/// circle algorithm. Uses source-over alpha blending; the input image is not
/// modified.
pub fn draw_circle(
img : Image,
cy : Int,
cx : Int,
radius : Int,
r : Byte,
g : Byte,
b : Byte,
a : Byte,
fill : Bool,
width : Int,
) -> Image {
let out = img.clone()
if fill {
let r2 = (radius * radius).to_double()
for dy = -radius; dy <= radius; dy = dy + 1 {
let span = (r2 - (dy * dy).to_double()).sqrt().to_int()
for dx = -span; dx <= span; dx = dx + 1 {
blend_at(out, cy + dy, cx + dx, r, g, b, a)
}
}
} else {
let mut xx = radius
let mut yy = 0
let mut err = 0
while xx >= yy {
circle_points(out, cy, cx, yy, xx, r, g, b, a, width)
yy = yy + 1
err = err + 1 + 2 * yy
if 2 * (err - xx) + 1 > 0 {
xx = xx - 1
err = err + 1 - 2 * xx
}
}
}
out
}
///|
/// Draw an ellipse. When `fill` is false, `width` sets the stroke thickness.
///
/// Centred at `(cy, cx)` with semi-axes `ry` (rows) and `rx` (columns).
/// Outlines use the midpoint ellipse algorithm; filled ellipses scan each
/// row over the implicit disc. Uses source-over alpha blending; the input
/// image is not modified.
pub fn draw_ellipse(
img : Image,
cy : Int,
cx : Int,
ry : Int,
rx : Int,
r : Byte,
g : Byte,
b : Byte,
a : Byte,
fill : Bool,
width : Int,
) -> Image {
let out = img.clone()
if fill {
for dy = -ry; dy <= ry; dy = dy + 1 {
let rx2 = (rx * rx).to_double()
let ry2 = (ry * ry).to_double()
let span = (rx2 * (1.0 - (dy * dy).to_double() / ry2)).sqrt().to_int()
for dx = -span; dx <= span; dx = dx + 1 {
blend_at(out, cy + dy, cx + dx, r, g, b, a)
}
}
} else {
// Midpoint ellipse algorithm
let mut x = 0
let mut y = ry
let rx2 = (rx * rx).to_double()
let ry2 = (ry * ry).to_double()
let mut d1 = ry2 - rx2 * ry.to_double() + 0.25 * rx2
let mut dx = 2.0 * ry2 * x.to_double()
let mut dy = 2.0 * rx2 * y.to_double()
// Region 1
while dx < dy {
stamp(out, cy + y, cx + x, r, g, b, a, width)
stamp(out, cy + y, cx - x, r, g, b, a, width)
stamp(out, cy - y, cx + x, r, g, b, a, width)
stamp(out, cy - y, cx - x, r, g, b, a, width)
if d1 < 0.0 {
x = x + 1
dx = dx + 2.0 * ry2
d1 = d1 + dx + ry2
} else {
x = x + 1
y = y - 1
dx = dx + 2.0 * ry2
dy = dy - 2.0 * rx2
d1 = d1 + dx - dy + ry2
}
}
// Region 2
let mut d2 = ry2 * (x.to_double() + 0.5) * (x.to_double() + 0.5) +
rx2 * (y.to_double() - 1.0) * (y.to_double() - 1.0) -
rx2 * ry2
while y >= 0 {
stamp(out, cy + y, cx + x, r, g, b, a, width)
stamp(out, cy + y, cx - x, r, g, b, a, width)
stamp(out, cy - y, cx + x, r, g, b, a, width)
stamp(out, cy - y, cx - x, r, g, b, a, width)
if d2 > 0.0 {
y = y - 1
dy = dy - 2.0 * rx2
d2 = d2 + rx2 - dy
} else {
y = y - 1
x = x + 1
dx = dx + 2.0 * ry2
dy = dy - 2.0 * rx2
d2 = d2 + dx - dy + rx2
}
}
}
out
}
///|
/// Draw a polygon. When `fill` is false, `width` sets the stroke thickness.
///
/// `points` is an array of `(y, x)` vertices connected in order, closing the
/// path from the last back to the first. Filled polygons use a scanline fill;
/// outlines call `draw_line` for each edge. Returns the input image unchanged
/// when fewer than two vertices are supplied. The input image is not modified.
pub fn draw_polygon(
img : Image,
points : Array[(Int, Int)],
r : Byte,
g : Byte,
b : Byte,
a : Byte,
fill : Bool,
width : Int,
) -> Image {
let mut out = img.clone()
if points.length() < 2 {
return out
}
if fill {
// Scanline fill
let mut min_y = points[0].0
let mut max_y = points[0].0
for i = 1; i < points.length(); i = i + 1 {
min_y = min_y.min(points[i].0)
max_y = max_y.max(points[i].0)
}
for y = min_y; y <= max_y; y = y + 1 {
let intersections : Array[Int] = []
let n = points.length()
for i = 0; i < n; i = i + 1 {
let (y0, x0) = points[i]
let (y1, x1) = points[(i + 1) % n]
if (y0 <= y && y1 > y) || (y1 <= y && y0 > y) {
let x_int = x0.to_double() +
(y - y0).to_double() * (x1 - x0).to_double() / (y1 - y0).to_double()
intersections.push(x_int.to_int())
}
}
// Sort intersections
let arr = intersections
for i = 0; i < arr.length(); i = i + 1 {
for j = i + 1; j < arr.length(); j = j + 1 {
if arr[j] < arr[i] {
let tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
}
}
}
// Fill between pairs
for i = 0; i + 1 < arr.length(); i = i + 2 {
for x = arr[i]; x <= arr[i + 1]; x = x + 1 {
blend_at(out, y, x, r, g, b, a)
}
}
}
} else {
// Draw outline
for i = 0; i < points.length(); i = i + 1 {
let (y0, x0) = points[i]
let (y1, x1) = points[(i + 1) % points.length()]
out = draw_line(out, y0, x0, y1, x1, r, g, b, a, width)
}
}
out
}
///|
/// Flood fill: replace the colour at `(y, x)` and all connected pixels of
/// similar colour (within `tolerance`) with the new colour.
///
/// 4-connected BFS from the seed pixel; a pixel is replaced when each of its
/// RGBA channels is within `tolerance` of the seed colour. Out-of-bounds seeds
/// and no-op seeds (fill colour equals seed colour) return a clone of the
/// input. The input image is not modified.
pub fn flood_fill(
img : Image,
y : Int,
x : Int,
r : Byte,
g : Byte,
b : Byte,
a : Byte,
tolerance : Byte,
) -> Image {
let out = img.clone()
if y < 0 || x < 0 || y >= out.h || x >= out.w {
return out
}
let o0 = out.offset(y, x)
let sr = out.data[o0]
let sg = out.data[o0 + 1]
let sb = out.data[o0 + 2]
let sa = out.data[o0 + 3]
// If target colour is the same as fill colour, nothing to do
if sr == r && sg == g && sb == b && sa == a {
return out
}
let tol = tolerance.to_int()
let is_match = fn(img_ : Image, yy : Int, xx : Int) -> Bool {
if yy < 0 || xx < 0 || yy >= img_.h || xx >= img_.w {
return false
}
let o = img_.offset(yy, xx)
(img_.data[o].to_int() - sr.to_int()).abs() <= tol &&
(img_.data[o + 1].to_int() - sg.to_int()).abs() <= tol &&
(img_.data[o + 2].to_int() - sb.to_int()).abs() <= tol &&
(img_.data[o + 3].to_int() - sa.to_int()).abs() <= tol
}
// BFS flood fill
let stack : Array[(Int, Int)] = [(y, x)]
while stack.length() > 0 {
let (cy, cx) = stack.unsafe_pop()
if !is_match(out, cy, cx) {
continue
}
let o = out.offset(cy, cx)
out.data[o] = r
out.data[o + 1] = g
out.data[o + 2] = b
out.data[o + 3] = a
stack.push((cy - 1, cx))
stack.push((cy + 1, cx))
stack.push((cy, cx - 1))
stack.push((cy, cx + 1))
}
out
}