///|
/// SVG Rasterizer
/// Efficient shape rasterization algorithms (Bresenham, Scanline, etc.)
///|
/// Pixel setter callback type
/// (x, y, color) -> Unit
pub(all) struct PixelSetter {
set : (Int, Int, Color) -> Unit
}
///|
/// Draw a single pixel
pub fn PixelSetter::pixel(
self : PixelSetter,
x : Int,
y : Int,
color : Color,
) -> Unit {
(self.set)(x, y, color)
}
///|
/// Create a clipped pixel setter that only draws within the clip rect
pub fn PixelSetter::with_clip(
self : PixelSetter,
clip : ClipRect,
) -> PixelSetter {
let inner_set = self.set
{ set: fn(x, y, color) { if clip.contains(x, y) { inner_set(x, y, color) } } }
}
///|
/// Create a clipped pixel setter with offset (for camera translation)
pub fn PixelSetter::with_clip_and_offset(
self : PixelSetter,
clip : ClipRect,
offset_x : Int,
offset_y : Int,
) -> PixelSetter {
let inner_set = self.set
{
set: fn(x, y, color) {
let tx = x + offset_x
let ty = y + offset_y
if clip.contains(tx, ty) {
inner_set(tx, ty, color)
}
},
}
}
///|
/// Bresenham's line algorithm - integer-only, efficient line drawing
pub fn raster_line(
x0 : Int,
y0 : Int,
x1 : Int,
y1 : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
let mut x0 = x0
let mut y0 = y0
let dx = if x1 > x0 { x1 - x0 } else { x0 - x1 }
let dy = if y1 > y0 { y1 - y0 } else { y0 - y1 }
let sx = if x0 < x1 { 1 } else { -1 }
let sy = if y0 < y1 { 1 } else { -1 }
let mut err = dx - dy
while true {
setter.pixel(x0, y0, color)
if x0 == x1 && y0 == y1 {
break
}
let e2 = 2 * err
if e2 > -dy {
err = err - dy
x0 = x0 + sx
}
if e2 < dx {
err = err + dx
y0 = y0 + sy
}
}
}
///|
/// Bresenham's line with dash pattern support
pub fn raster_line_dashed(
x0 : Int,
y0 : Int,
x1 : Int,
y1 : Int,
color : Color,
dasharray : Array[Double],
dashoffset : Double,
setter : PixelSetter,
) -> Unit {
if dasharray.length() == 0 {
// No dash pattern, draw solid line
raster_line(x0, y0, x1, y1, color, setter)
return
}
// Calculate total line length
let dx_f = (x1 - x0).to_double()
let dy_f = (y1 - y0).to_double()
let total_len = (dx_f * dx_f + dy_f * dy_f).sqrt()
if total_len < 0.5 {
return
}
// Dash pattern state
let mut dash_pos = -dashoffset
// Normalize dashoffset to be within pattern
let total_dash = dasharray.fold(init=0.0, fn(acc, v) { acc + v })
while dash_pos < 0.0 {
dash_pos = dash_pos + total_dash
}
while dash_pos >= total_dash {
dash_pos = dash_pos - total_dash
}
// Bresenham setup
let mut x = x0
let mut y = y0
let dx = if x1 > x0 { x1 - x0 } else { x0 - x1 }
let dy = if y1 > y0 { y1 - y0 } else { y0 - y1 }
let sx = if x0 < x1 { 1 } else { -1 }
let sy = if y0 < y1 { 1 } else { -1 }
let mut err = dx - dy
while true {
// Calculate distance from start
let cur_dx = (x - x0).to_double()
let cur_dy = (y - y0).to_double()
let cur_dist = (cur_dx * cur_dx + cur_dy * cur_dy).sqrt()
// Update dash state based on distance traveled
let travel = cur_dist
let mut pos_in_pattern = dash_pos + travel
while pos_in_pattern >= total_dash {
pos_in_pattern = pos_in_pattern - total_dash
}
// Find current dash index
let mut cum = 0.0
let mut cur_idx = 0
for i = 0; i < dasharray.length(); i = i + 1 {
if cum + dasharray[i] > pos_in_pattern {
cur_idx = i
break
}
cum = cum + dasharray[i]
}
// Even indices draw, odd indices gap
let is_drawing = cur_idx % 2 == 0
if is_drawing {
setter.pixel(x, y, color)
}
if x == x1 && y == y1 {
break
}
let e2 = 2 * err
if e2 > -dy {
err = err - dy
x = x + sx
}
if e2 < dx {
err = err + dx
y = y + sy
}
}
}
///|
/// Draw polyline with dash pattern
pub fn raster_polyline_dashed(
points : Array[(Int, Int)],
color : Color,
dasharray : Array[Double],
dashoffset : Double,
setter : PixelSetter,
) -> Unit {
if points.length() < 2 {
return
}
if dasharray.length() == 0 {
raster_polyline(points, color, setter)
return
}
let mut offset = dashoffset
for i = 0; i < points.length() - 1; i = i + 1 {
let (x0, y0) = points[i]
let (x1, y1) = points[i + 1]
raster_line_dashed(x0, y0, x1, y1, color, dasharray, offset, setter)
// Calculate line length and update offset
let dx = (x1 - x0).to_double()
let dy = (y1 - y0).to_double()
let len = (dx * dx + dy * dy).sqrt()
offset = offset + len
// Normalize offset within dash pattern total length
let total_dash = dasharray.fold(init=0.0, fn(acc, v) { acc + v })
while offset >= total_dash {
offset = offset - total_dash
}
}
}
///|
/// Draw rectangle outline (stroke only)
pub fn raster_rect_stroke(
x : Int,
y : Int,
w : Int,
h : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if w <= 0 || h <= 0 {
return
}
// Top edge
for px = x; px < x + w; px = px + 1 {
setter.pixel(px, y, color)
}
// Bottom edge
for px = x; px < x + w; px = px + 1 {
setter.pixel(px, y + h - 1, color)
}
// Left edge
for py = y + 1; py < y + h - 1; py = py + 1 {
setter.pixel(x, py, color)
}
// Right edge
for py = y + 1; py < y + h - 1; py = py + 1 {
setter.pixel(x + w - 1, py, color)
}
}
///|
/// Fill rectangle
pub fn raster_rect_fill(
x : Int,
y : Int,
w : Int,
h : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if w <= 0 || h <= 0 {
return
}
for py = y; py < y + h; py = py + 1 {
for px = x; px < x + w; px = px + 1 {
setter.pixel(px, py, color)
}
}
}
///|
/// Draw filled rectangle with linear gradient
pub fn raster_rect_gradient(
x : Int,
y : Int,
w : Int,
h : Int,
grad : LinearGradient,
opacity : Double,
setter : PixelSetter,
) -> Unit {
if w <= 0 || h <= 0 {
return
}
// Gradient direction vector (in normalized 0-1 space)
let gx1 = grad.x1
let gy1 = grad.y1
let gx2 = grad.x2
let gy2 = grad.y2
let gdx = gx2 - gx1
let gdy = gy2 - gy1
let glen_sq = gdx * gdx + gdy * gdy
for py = y; py < y + h; py = py + 1 {
for px = x; px < x + w; px = px + 1 {
// Normalize pixel position to 0-1 within rect
let nx = if w > 1 {
(px - x).to_double() / (w - 1).to_double()
} else {
0.5
}
let ny = if h > 1 {
(py - y).to_double() / (h - 1).to_double()
} else {
0.5
}
// Project onto gradient line
let t = if glen_sq > 0.0001 {
((nx - gx1) * gdx + (ny - gy1) * gdy) / glen_sq
} else {
0.0
}
// Get color at this position
let color = grad.color_at(t)
let final_color = if opacity < 1.0 {
Color::rgba(
color.r,
color.g,
color.b,
(color.a.to_double() * opacity).to_int(),
)
} else {
color
}
setter.pixel(px, py, final_color)
}
}
}
///|
/// Draw filled rectangle with radial gradient
pub fn raster_rect_radial_gradient(
x : Int,
y : Int,
w : Int,
h : Int,
grad : RadialGradient,
opacity : Double,
setter : PixelSetter,
) -> Unit {
if w <= 0 || h <= 0 {
return
}
for py = y; py < y + h; py = py + 1 {
for px = x; px < x + w; px = px + 1 {
// Normalize pixel position to 0-1 within rect
let nx = if w > 1 {
(px - x).to_double() / (w - 1).to_double()
} else {
0.5
}
let ny = if h > 1 {
(py - y).to_double() / (h - 1).to_double()
} else {
0.5
}
// Get color using radial gradient
let color = grad.color_at(nx, ny, 1.0, 1.0)
let final_color = if opacity < 1.0 {
Color::rgba(
color.r,
color.g,
color.b,
(color.a.to_double() * opacity).to_int(),
)
} else {
color
}
setter.pixel(px, py, final_color)
}
}
}
///|
/// Draw filled circle with radial gradient
pub fn raster_circle_radial_gradient(
cx : Int,
cy : Int,
r : Int,
grad : RadialGradient,
opacity : Double,
setter : PixelSetter,
) -> Unit {
if r <= 0 {
return
}
let r_sq = r * r
for py = cy - r; py <= cy + r; py = py + 1 {
for px = cx - r; px <= cx + r; px = px + 1 {
let dx = px - cx
let dy = py - cy
let dist_sq = dx * dx + dy * dy
if dist_sq <= r_sq {
// Normalize position: center is (0.5, 0.5), edges at distance r
let nx = 0.5 + dx.to_double() / (2.0 * r.to_double())
let ny = 0.5 + dy.to_double() / (2.0 * r.to_double())
let color = grad.color_at(nx, ny, 1.0, 1.0)
let final_color = if opacity < 1.0 {
Color::rgba(
color.r,
color.g,
color.b,
(color.a.to_double() * opacity).to_int(),
)
} else {
color
}
setter.pixel(px, py, final_color)
}
}
}
}
///|
/// Draw filled ellipse with radial gradient
pub fn raster_ellipse_radial_gradient(
cx : Int,
cy : Int,
rx : Int,
ry : Int,
grad : RadialGradient,
opacity : Double,
setter : PixelSetter,
) -> Unit {
if rx <= 0 || ry <= 0 {
return
}
for py = cy - ry; py <= cy + ry; py = py + 1 {
for px = cx - rx; px <= cx + rx; px = px + 1 {
let dx = (px - cx).to_double() / rx.to_double()
let dy = (py - cy).to_double() / ry.to_double()
let dist_sq = dx * dx + dy * dy
if dist_sq <= 1.0 {
// Normalize position to 0-1 range
let nx = 0.5 + (px - cx).to_double() / (2.0 * rx.to_double())
let ny = 0.5 + (py - cy).to_double() / (2.0 * ry.to_double())
let color = grad.color_at(nx, ny, 1.0, 1.0)
let final_color = if opacity < 1.0 {
Color::rgba(
color.r,
color.g,
color.b,
(color.a.to_double() * opacity).to_int(),
)
} else {
color
}
setter.pixel(px, py, final_color)
}
}
}
}
///|
/// Draw rounded rectangle outline
pub fn raster_rounded_rect_stroke(
x : Int,
y : Int,
w : Int,
h : Int,
rx : Int,
ry : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if w <= 0 || h <= 0 {
return
}
// Clamp radii to half dimensions
let rx = min_int(rx, w / 2)
let ry = min_int(ry, h / 2)
if rx <= 0 || ry <= 0 {
// No rounding, draw regular rect
raster_rect_stroke(x, y, w, h, color, setter)
return
}
// Top edge (middle)
for px = x + rx; px < x + w - rx; px = px + 1 {
setter.pixel(px, y, color)
}
// Bottom edge (middle)
for px = x + rx; px < x + w - rx; px = px + 1 {
setter.pixel(px, y + h - 1, color)
}
// Left edge (middle)
for py = y + ry; py < y + h - ry; py = py + 1 {
setter.pixel(x, py, color)
}
// Right edge (middle)
for py = y + ry; py < y + h - ry; py = py + 1 {
setter.pixel(x + w - 1, py, color)
}
// Draw corners using ellipse arc algorithm
draw_corner_arc(x + rx, y + ry, rx, ry, 2, color, setter) // top-left
draw_corner_arc(x + w - rx - 1, y + ry, rx, ry, 1, color, setter) // top-right
draw_corner_arc(x + rx, y + h - ry - 1, rx, ry, 3, color, setter) // bottom-left
draw_corner_arc(x + w - rx - 1, y + h - ry - 1, rx, ry, 4, color, setter) // bottom-right
}
///|
/// Draw corner arc (quadrant of ellipse)
/// quadrant: 1=top-right, 2=top-left, 3=bottom-left, 4=bottom-right
fn draw_corner_arc(
cx : Int,
cy : Int,
rx : Int,
ry : Int,
quadrant : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
// Simplified ellipse arc using midpoint algorithm
let rx2 = rx * rx
let ry2 = ry * ry
let two_rx2 = 2 * rx2
let two_ry2 = 2 * ry2
let mut x = 0
let mut y = ry
let mut px = 0
let mut py = two_rx2 * y
// Plot initial point
plot_corner_point(cx, cy, x, y, quadrant, color, setter)
// Region 1
let mut p = ry2 - rx2 * ry + rx2 / 4
while px < py {
x = x + 1
px = px + two_ry2
if p < 0 {
p = p + ry2 + px
} else {
y = y - 1
py = py - two_rx2
p = p + ry2 + px - py
}
plot_corner_point(cx, cy, x, y, quadrant, color, setter)
}
// Region 2
p = ry2 * (x * 2 + 1) * (x * 2 + 1) / 4 + rx2 * (y - 1) * (y - 1) - rx2 * ry2
while y > 0 {
y = y - 1
py = py - two_rx2
if p > 0 {
p = p + rx2 - py
} else {
x = x + 1
px = px + two_ry2
p = p + rx2 - py + px
}
plot_corner_point(cx, cy, x, y, quadrant, color, setter)
}
}
///|
/// Plot point in specific quadrant relative to center
fn plot_corner_point(
cx : Int,
cy : Int,
x : Int,
y : Int,
quadrant : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
match quadrant {
1 => setter.pixel(cx + x, cy - y, color) // top-right
2 => setter.pixel(cx - x, cy - y, color) // top-left
3 => setter.pixel(cx - x, cy + y, color) // bottom-left
4 => setter.pixel(cx + x, cy + y, color) // bottom-right
_ => ()
}
}
///|
/// Midpoint circle algorithm - draw circle outline
pub fn raster_circle_stroke(
cx : Int,
cy : Int,
r : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if r <= 0 {
setter.pixel(cx, cy, color)
return
}
let mut x = r
let mut y = 0
let mut err = 1 - r
while x >= y {
// Draw all 8 octants
setter.pixel(cx + x, cy + y, color)
setter.pixel(cx - x, cy + y, color)
setter.pixel(cx + x, cy - y, color)
setter.pixel(cx - x, cy - y, color)
setter.pixel(cx + y, cy + x, color)
setter.pixel(cx - y, cy + x, color)
setter.pixel(cx + y, cy - x, color)
setter.pixel(cx - y, cy - x, color)
y = y + 1
if err < 0 {
err = err + 2 * y + 1
} else {
x = x - 1
err = err + 2 * (y - x) + 1
}
}
}
///|
/// Fill circle using scanlines
pub fn raster_circle_fill(
cx : Int,
cy : Int,
r : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if r <= 0 {
setter.pixel(cx, cy, color)
return
}
let mut x = r
let mut y = 0
let mut err = 1 - r
while x >= y {
// Draw horizontal lines for each y level
for px = cx - x; px <= cx + x; px = px + 1 {
setter.pixel(px, cy + y, color)
setter.pixel(px, cy - y, color)
}
for px = cx - y; px <= cx + y; px = px + 1 {
setter.pixel(px, cy + x, color)
setter.pixel(px, cy - x, color)
}
y = y + 1
if err < 0 {
err = err + 2 * y + 1
} else {
x = x - 1
err = err + 2 * (y - x) + 1
}
}
}
///|
/// Midpoint ellipse algorithm - draw ellipse outline
pub fn raster_ellipse_stroke(
cx : Int,
cy : Int,
rx : Int,
ry : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if rx <= 0 && ry <= 0 {
setter.pixel(cx, cy, color)
return
}
if rx <= 0 {
// Vertical line
for y = cy - ry; y <= cy + ry; y = y + 1 {
setter.pixel(cx, y, color)
}
return
}
if ry <= 0 {
// Horizontal line
for x = cx - rx; x <= cx + rx; x = x + 1 {
setter.pixel(x, cy, color)
}
return
}
let rx2 = rx * rx
let ry2 = ry * ry
let two_rx2 = 2 * rx2
let two_ry2 = 2 * ry2
let mut x = 0
let mut y = ry
let mut px = 0
let mut py = two_rx2 * y
// Plot initial points
setter.pixel(cx + x, cy + y, color)
setter.pixel(cx - x, cy + y, color)
setter.pixel(cx + x, cy - y, color)
setter.pixel(cx - x, cy - y, color)
// Region 1
let mut p = ry2 - rx2 * ry + rx2 / 4
while px < py {
x = x + 1
px = px + two_ry2
if p < 0 {
p = p + ry2 + px
} else {
y = y - 1
py = py - two_rx2
p = p + ry2 + px - py
}
setter.pixel(cx + x, cy + y, color)
setter.pixel(cx - x, cy + y, color)
setter.pixel(cx + x, cy - y, color)
setter.pixel(cx - x, cy - y, color)
}
// Region 2
p = ry2 * (x * 2 + 1) * (x * 2 + 1) / 4 + rx2 * (y - 1) * (y - 1) - rx2 * ry2
while y > 0 {
y = y - 1
py = py - two_rx2
if p > 0 {
p = p + rx2 - py
} else {
x = x + 1
px = px + two_ry2
p = p + rx2 - py + px
}
setter.pixel(cx + x, cy + y, color)
setter.pixel(cx - x, cy + y, color)
setter.pixel(cx + x, cy - y, color)
setter.pixel(cx - x, cy - y, color)
}
}
///|
/// Fill ellipse using scanlines
pub fn raster_ellipse_fill(
cx : Int,
cy : Int,
rx : Int,
ry : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if rx <= 0 && ry <= 0 {
setter.pixel(cx, cy, color)
return
}
if rx <= 0 {
for y = cy - ry; y <= cy + ry; y = y + 1 {
setter.pixel(cx, y, color)
}
return
}
if ry <= 0 {
for x = cx - rx; x <= cx + rx; x = x + 1 {
setter.pixel(x, cy, color)
}
return
}
// Fill using scanline for each y from -ry to +ry
for dy = -ry; dy <= ry; dy = dy + 1 {
// Calculate x extent for this y using ellipse equation
// x^2/rx^2 + y^2/ry^2 = 1
// x = rx * sqrt(1 - y^2/ry^2)
let y_frac = dy.to_double() / ry.to_double()
let x_extent_sq = 1.0 - y_frac * y_frac
if x_extent_sq >= 0.0 {
let x_extent = (x_extent_sq.sqrt() * rx.to_double()).to_int()
for dx = -x_extent; dx <= x_extent; dx = dx + 1 {
setter.pixel(cx + dx, cy + dy, color)
}
}
}
}
///|
/// Polygon fill using scanline algorithm with edge table
pub fn raster_polygon_fill(
points : Array[(Int, Int)],
color : Color,
setter : PixelSetter,
) -> Unit {
let n = points.length()
if n < 3 {
return
}
// Find bounding box
let mut min_y = points[0].1
let mut max_y = points[0].1
for i = 1; i < n; i = i + 1 {
let y = points[i].1
if y < min_y {
min_y = y
}
if y > max_y {
max_y = y
}
}
// Scanline fill
for y = min_y; y <= max_y; y = y + 1 {
// Find all intersections with polygon edges
let intersections : Array[Int] = []
for i = 0; i < n; i = i + 1 {
let j = (i + 1) % n
let (x1, y1) = points[i]
let (x2, y2) = points[j]
// Check if edge crosses this scanline
if (y1 <= y && y < y2) || (y2 <= y && y < y1) {
// Calculate x intersection
let x = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
intersections.push(x)
}
}
// Sort intersections
sort_ints(intersections)
// Fill between pairs of intersections
let mut i = 0
while i + 1 < intersections.length() {
let x_start = intersections[i]
let x_end = intersections[i + 1]
for x = x_start; x <= x_end; x = x + 1 {
setter.pixel(x, y, color)
}
i = i + 2
}
}
}
///|
/// Draw polygon outline
pub fn raster_polygon_stroke(
points : Array[(Int, Int)],
color : Color,
setter : PixelSetter,
) -> Unit {
let n = points.length()
if n < 2 {
return
}
for i = 0; i < n; i = i + 1 {
let j = (i + 1) % n
let (x1, y1) = points[i]
let (x2, y2) = points[j]
raster_line(x1, y1, x2, y2, color, setter)
}
}
///|
/// Draw polyline (open polygon)
pub fn raster_polyline(
points : Array[(Int, Int)],
color : Color,
setter : PixelSetter,
) -> Unit {
let n = points.length()
if n < 2 {
return
}
for i = 0; i < n - 1; i = i + 1 {
let (x1, y1) = points[i]
let (x2, y2) = points[i + 1]
raster_line(x1, y1, x2, y2, color, setter)
}
}
///|
/// Helper: min of two ints
fn min_int(a : Int, b : Int) -> Int {
if a < b {
a
} else {
b
}
}
///|
/// Helper: simple insertion sort for small arrays
fn sort_ints(arr : Array[Int]) -> Unit {
let n = arr.length()
for i = 1; i < n; i = i + 1 {
let key = arr[i]
let mut j = i - 1
while j >= 0 && arr[j] > key {
arr[j + 1] = arr[j]
j = j - 1
}
arr[j + 1] = key
}
}
///|
/// Draw a thick line (stroke width > 1)
pub fn raster_thick_line(
x0 : Int,
y0 : Int,
x1 : Int,
y1 : Int,
width : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
if width <= 1 {
raster_line(x0, y0, x1, y1, color, setter)
return
}
// For thick lines, draw multiple parallel lines
let half_w = width / 2
let dx = x1 - x0
let dy = y1 - y0
let len = (dx * dx + dy * dy).to_double().sqrt()
if len < 0.001 {
// Point - draw filled circle
raster_circle_fill(x0, y0, half_w, color, setter)
return
}
// Perpendicular direction
let px = (-dy.to_double() / len * half_w.to_double()).to_int()
let py = (dx.to_double() / len * half_w.to_double()).to_int()
// Draw as polygon
let points : Array[(Int, Int)] = [
(x0 + px, y0 + py),
(x1 + px, y1 + py),
(x1 - px, y1 - py),
(x0 - px, y0 - py),
]
raster_polygon_fill(points, color, setter)
}
///|
/// Simple 5x7 bitmap font for ASCII characters
/// Each character is represented as 7 bytes (rows), each byte has 5 bits (columns)
/// Bit 0x10 = leftmost pixel, 0x01 = rightmost pixel
fn get_char_bitmap(c : Char) -> Array[Int] {
match c {
' ' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
'!' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04]
'"' => [0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00]
'#' => [0x0A, 0x1F, 0x0A, 0x0A, 0x1F, 0x0A, 0x00]
'$' => [0x04, 0x0F, 0x14, 0x0E, 0x05, 0x1E, 0x04]
'%' => [0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03]
'&' => [0x08, 0x14, 0x14, 0x08, 0x15, 0x12, 0x0D]
'\'' => [0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]
'(' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02]
')' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08]
'*' => [0x04, 0x15, 0x0E, 0x1F, 0x0E, 0x15, 0x04]
'+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00]
',' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x08]
'-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00]
'.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04]
'/' => [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10]
'0' => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E]
'1' => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E]
'2' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F]
'3' => [0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E]
'4' => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02]
'5' => [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E]
'6' => [0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E]
'7' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08]
'8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E]
'9' => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C]
':' => [0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00]
';' => [0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x08]
'<' => [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02]
'=' => [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00]
'>' => [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08]
'?' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04]
'@' => [0x0E, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0E]
'A' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11]
'B' => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E]
'C' => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E]
'D' => [0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E]
'E' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F]
'F' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10]
'G' => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E]
'H' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11]
'I' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E]
'J' => [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C]
'K' => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11]
'L' => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F]
'M' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11]
'N' => [0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11]
'O' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E]
'P' => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10]
'Q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D]
'R' => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11]
'S' => [0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E]
'T' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04]
'U' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E]
'V' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04]
'W' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11]
'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11]
'Y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04]
'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F]
'[' => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E]
'\\' => [0x10, 0x08, 0x08, 0x04, 0x02, 0x02, 0x01]
']' => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E]
'^' => [0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00]
'_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F]
'`' => [0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]
'a' => [0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F]
'b' => [0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E]
'c' => [0x00, 0x00, 0x0E, 0x10, 0x10, 0x10, 0x0E]
'd' => [0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F]
'e' => [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E]
'f' => [0x06, 0x08, 0x08, 0x1E, 0x08, 0x08, 0x08]
'g' => [0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x0E]
'h' => [0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x11]
'i' => [0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E]
'j' => [0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0C]
'k' => [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12]
'l' => [0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E]
'm' => [0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11]
'n' => [0x00, 0x00, 0x1E, 0x11, 0x11, 0x11, 0x11]
'o' => [0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E]
'p' => [0x00, 0x00, 0x1E, 0x11, 0x1E, 0x10, 0x10]
'q' => [0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x01]
'r' => [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10]
's' => [0x00, 0x00, 0x0E, 0x10, 0x0E, 0x01, 0x1E]
't' => [0x08, 0x08, 0x1E, 0x08, 0x08, 0x09, 0x06]
'u' => [0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0E]
'v' => [0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04]
'w' => [0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A]
'x' => [0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11]
'y' => [0x00, 0x00, 0x11, 0x11, 0x0F, 0x01, 0x0E]
'z' => [0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F]
'{' => [0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02]
'|' => [0x04, 0x04, 0x04, 0x00, 0x04, 0x04, 0x04]
'}' => [0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08]
'~' => [0x00, 0x08, 0x15, 0x02, 0x00, 0x00, 0x00]
_ => [0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F] // Default: box
}
}
///|
/// Render text using bitmap font
pub fn raster_text(
x : Int,
y : Int,
text : String,
font_size : Int,
color : Color,
setter : PixelSetter,
) -> Unit {
// Base font is 5x7 pixels
// Scale factor based on font_size (base is 7 pixels high)
let scale = if font_size <= 7 { 1 } else { font_size / 7 }
let char_width = 5 * scale
let char_height = 7 * scale
let spacing = scale // Space between characters
let mut cursor_x = x
// y is the baseline, so move up by font height
let start_y = y - char_height
for i = 0; i < text.length(); i = i + 1 {
let c = text[i].unsafe_to_char()
let bitmap = get_char_bitmap(c)
// Draw the character
for row = 0; row < 7; row = row + 1 {
let row_bits = bitmap[row]
for col = 0; col < 5; col = col + 1 {
// Check if bit is set (MSB is left side)
let bit_mask = 0x10 >> col
if (row_bits & bit_mask) != 0 {
// Draw scaled pixel
for sy = 0; sy < scale; sy = sy + 1 {
for sx = 0; sx < scale; sx = sx + 1 {
let px = cursor_x + col * scale + sx
let py = start_y + row * scale + sy
setter.pixel(px, py, color)
}
}
}
}
}
cursor_x = cursor_x + char_width + spacing
}
}