///|
/// A single command in a `Path2D`. Matches the subset of Canvas2D commands
/// needed for fill and stroke.
pub enum PathCmd {
MoveTo(Double, Double)
LineTo(Double, Double)
QuadTo(Double, Double, Double, Double)
CubicTo(Double, Double, Double, Double, Double, Double)
Close
} derive(Eq)
///|
pub impl Show for PathCmd with fn output(self, logger) {
match self {
MoveTo(x, y) => {
logger.write_string("MoveTo(")
logger.write_object(x)
logger.write_string(", ")
logger.write_object(y)
logger.write_string(")")
}
LineTo(x, y) => {
logger.write_string("LineTo(")
logger.write_object(x)
logger.write_string(", ")
logger.write_object(y)
logger.write_string(")")
}
QuadTo(cx, cy, x, y) => {
logger.write_string("QuadTo(")
logger.write_object(cx)
logger.write_string(", ")
logger.write_object(cy)
logger.write_string(", ")
logger.write_object(x)
logger.write_string(", ")
logger.write_object(y)
logger.write_string(")")
}
CubicTo(x1, y1, x2, y2, x, y) => {
logger.write_string("CubicTo(")
logger.write_object(x1)
logger.write_string(", ")
logger.write_object(y1)
logger.write_string(", ")
logger.write_object(x2)
logger.write_string(", ")
logger.write_object(y2)
logger.write_string(", ")
logger.write_object(x)
logger.write_string(", ")
logger.write_object(y)
logger.write_string(")")
}
Close => logger.write_string("Close")
}
}
///|
/// A path — an ordered sequence of drawing commands.
/// Mirrors `Path2D` from the HTML Canvas2D API.
pub struct Path2D {
commands : Array[PathCmd]
}
///|
pub fn Path2D::new() -> Path2D {
{ commands: [] }
}
///|
pub fn Path2D::move_to(self : Path2D, x : Double, y : Double) -> Unit {
self.commands.push(PathCmd::MoveTo(x, y))
}
///|
pub fn Path2D::line_to(self : Path2D, x : Double, y : Double) -> Unit {
self.commands.push(PathCmd::LineTo(x, y))
}
///|
pub fn Path2D::quadratic_curve_to(
self : Path2D,
cpx : Double,
cpy : Double,
x : Double,
y : Double,
) -> Unit {
self.commands.push(PathCmd::QuadTo(cpx, cpy, x, y))
}
///|
pub fn Path2D::bezier_curve_to(
self : Path2D,
cp1x : Double,
cp1y : Double,
cp2x : Double,
cp2y : Double,
x : Double,
y : Double,
) -> Unit {
self.commands.push(PathCmd::CubicTo(cp1x, cp1y, cp2x, cp2y, x, y))
}
///|
pub fn Path2D::rect(
self : Path2D,
x : Double,
y : Double,
w : Double,
h : Double,
) -> Unit {
self.commands.push(PathCmd::MoveTo(x, y))
self.commands.push(PathCmd::LineTo(x + w, y))
self.commands.push(PathCmd::LineTo(x + w, y + h))
self.commands.push(PathCmd::LineTo(x, y + h))
self.commands.push(PathCmd::Close)
}
///|
pub fn Path2D::close(self : Path2D) -> Unit {
self.commands.push(PathCmd::Close)
}
///|
/// Append cubic Bezier segments approximating an elliptical arc.
/// Caller is responsible for the preceding `MoveTo`. No `MoveTo` is emitted.
///
/// - `rx, ry` — unsigned radii (caller normalizes negative values)
/// - `rotation` — rotation of the x-axis in radians
/// - `ccw` — if true, sweep counter-clockwise
fn emit_arc_cubics(
path : Path2D,
cx : Double,
cy : Double,
rx : Double,
ry : Double,
rotation : Double,
start_angle : Double,
end_angle : Double,
ccw : Bool,
) -> Unit {
let tau = 2.0 * @math.PI
let half_pi = @math.PI / 2.0
let a0 = start_angle
let mut a1 = end_angle
if ccw {
while a1 > a0 {
a1 = a1 - tau
}
} else {
while a1 < a0 {
a1 = a1 + tau
}
}
let step = if ccw { -half_pi } else { half_pi }
let cos_rot = @math.cos(rotation)
let sin_rot = @math.sin(rotation)
let mut a = a0
while (!ccw && a < a1) || (ccw && a > a1) {
let next = if ccw {
if a + step < a1 {
a1
} else {
a + step
}
} else if a + step > a1 {
a1
} else {
a + step
}
let delta = next - a
let k = 4.0 / 3.0 * @math.tan(delta / 4.0)
let cos_a = @math.cos(a)
let sin_a = @math.sin(a)
let cos_n = @math.cos(next)
let sin_n = @math.sin(next)
let q1x = rx * (cos_a - k * sin_a)
let q1y = ry * (sin_a + k * cos_a)
let q2x = rx * (cos_n + k * sin_n)
let q2y = ry * (sin_n - k * cos_n)
let q3x = rx * cos_n
let q3y = ry * sin_n
let p1x = cx + q1x * cos_rot - q1y * sin_rot
let p1y = cy + q1x * sin_rot + q1y * cos_rot
let p2x = cx + q2x * cos_rot - q2y * sin_rot
let p2y = cy + q2x * sin_rot + q2y * cos_rot
let p3x = cx + q3x * cos_rot - q3y * sin_rot
let p3y = cy + q3x * sin_rot + q3y * cos_rot
path.commands.push(PathCmd::CubicTo(p1x, p1y, p2x, p2y, p3x, p3y))
a = next
}
}
///|
/// Return the current pen position — the endpoint of the last command.
/// For `Close`, scans backward to the matching `MoveTo` (DOM semantics).
/// Returns `None` for an empty path.
fn current_point(path : Path2D) -> (Double, Double)? {
let len = path.commands.length()
if len == 0 {
return None
}
match path.commands[len - 1] {
MoveTo(x, y) => Some((x, y))
LineTo(x, y) => Some((x, y))
QuadTo(_, _, x, y) => Some((x, y))
CubicTo(_, _, _, _, x, y) => Some((x, y))
Close => {
// Scan backward for most recent MoveTo
for i = len - 2; i >= 0; i = i - 1 {
match path.commands[i] {
MoveTo(x, y) => return Some((x, y))
_ => continue
}
}
None
}
}
}
///|
/// Append an elliptical arc. Mirrors HTML Canvas `ellipse()`.
/// Emits `MoveTo` to the arc start, then ≤90° cubic Bezier segments.
/// Silent normalization: negative `rx`/`ry` → absolute value.
pub fn Path2D::ellipse(
self : Path2D,
cx : Double,
cy : Double,
rx : Double,
ry : Double,
rotation : Double,
start_angle : Double,
end_angle : Double,
counterclockwise? : Bool = false,
) -> Unit {
let rx_abs = if rx < 0.0 { -rx } else { rx }
let ry_abs = if ry < 0.0 { -ry } else { ry }
let cos_rot = @math.cos(rotation)
let sin_rot = @math.sin(rotation)
let qx = rx_abs * @math.cos(start_angle)
let qy = ry_abs * @math.sin(start_angle)
let sx = cx + qx * cos_rot - qy * sin_rot
let sy = cy + qx * sin_rot + qy * cos_rot
self.commands.push(PathCmd::MoveTo(sx, sy))
emit_arc_cubics(
self, cx, cy, rx_abs, ry_abs, rotation, start_angle, end_angle, counterclockwise,
)
}
///|
/// Append a tangent arc between two lines. Mirrors HTML Canvas `arcTo()`.
/// Silent normalization: negative `radius` → absolute value.
/// Degenerate cases (no current point, collinear, r=0) → `LineTo(x1, y1)`.
pub fn Path2D::arc_to(
self : Path2D,
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
radius : Double,
) -> Unit {
let r = if radius < 0.0 { -radius } else { radius }
// 1. Current point
let p0 = match current_point(self) {
Some(pt) => pt
None => {
self.commands.push(PathCmd::MoveTo(x1, y1))
return
}
}
// 2. Vectors from p1 to p0, p1 to p2
let v1x = p0.0 - x1
let v1y = p0.1 - y1
let v2x = x2 - x1
let v2y = y2 - y1
let len1 = (v1x * v1x + v1y * v1y).sqrt()
let len2 = (v2x * v2x + v2y * v2y).sqrt()
// 3. Degenerate checks
if len1 == 0.0 || len2 == 0.0 || r == 0.0 {
self.commands.push(PathCmd::LineTo(x1, y1))
return
}
let cross = v1x * v2y - v1y * v2x
if cross.abs() < 1.0e-12 * len1 * len2 {
self.commands.push(PathCmd::LineTo(x1, y1))
return
}
// 4. Normalize
let n1x = v1x / len1
let n1y = v1y / len1
let n2x = v2x / len2
let n2y = v2y / len2
// 5. Half-angle geometry
let cos_theta = n1x * n2x + n1y * n2y
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt()
let d = r * (1.0 + cos_theta) / sin_theta
// 6. Tangent points
let t1x = x1 + d * n1x
let t1y = y1 + d * n1y
let t2x = x1 + d * n2x
let t2y = y1 + d * n2y
// 7. Arc center via perpendicular from t1
let sign = if cross > 0.0 { 1.0 } else { -1.0 }
let perp_x = -n1y
let perp_y = n1x
let cx = t1x + r * sign * perp_x
let cy = t1y + r * sign * perp_y
// 8. Start/end angles
let a0 = @math.atan2(t1y - cy, t1x - cx)
let a1 = @math.atan2(t2y - cy, t2x - cx)
let ccw = cross > 0.0
// 9. Emit
self.commands.push(PathCmd::LineTo(t1x, t1y))
emit_arc_cubics(self, cx, cy, r, r, 0.0, a0, a1, ccw)
}
///|
/// Append a rounded rectangle sub-path. Mirrors HTML Canvas `roundRect()`.
///
/// `radii` length: 1 = uniform, 2 = (tl/br, tr/bl), 3 = (tl, tr/bl, br),
/// 4 = (tl, tr, br, bl). Invalid lengths → nothing emitted.
/// Negative radii → abs(). Negative w/h → rect flips.
/// Radii exceeding min(|w|,|h|)/2 are proportionally scaled.
pub fn Path2D::round_rect(
self : Path2D,
x : Double,
y : Double,
w : Double,
h : Double,
radii : Array[Double],
) -> Unit {
// 1. Expand to 4 corners (tl, tr, br, bl)
let n = radii.length()
if n != 1 && n != 2 && n != 3 && n != 4 {
return // invalid length — emit nothing
}
let mut r_tl = radii[0]
let mut r_tr = if n >= 2 { radii[1] } else { radii[0] }
let mut r_br = if n >= 3 { radii[2] } else { radii[0] }
let mut r_bl = if n == 4 {
radii[3]
} else if n == 2 {
radii[1]
} else if n == 3 {
radii[1]
} else {
radii[0]
}
// 2. Absolute value
if r_tl < 0.0 {
r_tl = -r_tl
}
if r_tr < 0.0 {
r_tr = -r_tr
}
if r_br < 0.0 {
r_br = -r_br
}
if r_bl < 0.0 {
r_bl = -r_bl
}
// 3. Normalize negative dimensions
let mut x = x
let mut y = y
let mut w = w
let mut h = h
if w < 0.0 {
x = x + w
w = -w
}
if h < 0.0 {
y = y + h
h = -h
}
// 4. Proportional scale-down (HTML Canvas spec)
let mut scale = 1.0
let top = r_tl + r_tr
if top > 0.0 && w / top < scale {
scale = w / top
}
let right = r_tr + r_br
if right > 0.0 && h / right < scale {
scale = h / right
}
let bottom = r_br + r_bl
if bottom > 0.0 && w / bottom < scale {
scale = w / bottom
}
let left = r_bl + r_tl
if left > 0.0 && h / left < scale {
scale = h / left
}
if scale < 1.0 {
r_tl = r_tl * scale
r_tr = r_tr * scale
r_br = r_br * scale
r_bl = r_bl * scale
}
// 5. Emit sub-path (clockwise, top edge first)
let half_pi = @math.PI / 2.0
let pi = @math.PI
// Top edge
self.commands.push(PathCmd::MoveTo(x + r_tl, y))
self.commands.push(PathCmd::LineTo(x + w - r_tr, y))
// Top-right corner
emit_arc_cubics(
self,
x + w - r_tr,
y + r_tr,
r_tr,
r_tr,
0.0,
-half_pi,
0.0,
false,
)
// Right edge
self.commands.push(PathCmd::LineTo(x + w, y + h - r_br))
// Bottom-right corner
emit_arc_cubics(
self,
x + w - r_br,
y + h - r_br,
r_br,
r_br,
0.0,
0.0,
half_pi,
false,
)
// Bottom edge
self.commands.push(PathCmd::LineTo(x + r_bl, y + h))
// Bottom-left corner
emit_arc_cubics(
self,
x + r_bl,
y + h - r_bl,
r_bl,
r_bl,
0.0,
half_pi,
pi,
false,
)
// Left edge
self.commands.push(PathCmd::LineTo(x, y + r_tl))
// Top-left corner
emit_arc_cubics(
self,
x + r_tl,
y + r_tl,
r_tl,
r_tl,
0.0,
pi,
pi + half_pi,
false,
)
self.commands.push(PathCmd::Close)
}
///|
/// Approximate a circular arc with cubic Beziers of at most 90° per segment.
/// Angles are in radians, measured from the +X axis. The arc is traversed
/// counter-clockwise when `counterclockwise=true`, clockwise otherwise
/// (matching HTML Canvas semantics).
pub fn Path2D::arc(
self : Path2D,
cx : Double,
cy : Double,
radius : Double,
start_angle : Double,
end_angle : Double,
counterclockwise? : Bool = false,
) -> Unit {
self.ellipse(
cx,
cy,
radius,
radius,
0.0,
start_angle,
end_angle,
counterclockwise~,
)
}
///|
/// Flatten a Path2D command stream into polylines in destination (post-matrix)
/// space. Each `Close` or a new `MoveTo` starts a new sub-polyline. Control
/// points are subdivided adaptively until each segment is within `flatness`
/// destination pixels of the ideal curve. `flatness` must be positive; the
/// caller is responsible for guarding against NaN and ≤0 values.
///
/// Empty polylines (produced by e.g. consecutive `Close` commands) are
/// discarded from the output.
pub fn flatten(
cmds : Array[PathCmd],
matrix : Matrix2D,
flatness : Double,
) -> Array[Array[(Double, Double)]] {
let polylines : Array[Array[(Double, Double)]] = []
let mut current : Array[(Double, Double)] = []
let mut cx = 0.0
let mut cy = 0.0
let mut start_x = 0.0
let mut start_y = 0.0
for cmd in cmds {
match cmd {
PathCmd::MoveTo(x, y) => {
if current.length() > 0 {
polylines.push(current)
}
current = []
let (px, py) = matrix.transform_point(x, y)
current.push((px, py))
cx = x
cy = y
start_x = x
start_y = y
}
PathCmd::LineTo(x, y) => {
let (px, py) = matrix.transform_point(x, y)
current.push((px, py))
cx = x
cy = y
}
PathCmd::QuadTo(x1, y1, x, y) => {
flatten_quad(cx, cy, x1, y1, x, y, matrix, flatness, current)
cx = x
cy = y
}
PathCmd::CubicTo(x1, y1, x2, y2, x, y) => {
flatten_cubic(cx, cy, x1, y1, x2, y2, x, y, matrix, flatness, current)
cx = x
cy = y
}
PathCmd::Close => {
if current.length() > 0 {
let (px, py) = matrix.transform_point(start_x, start_y)
current.push((px, py))
polylines.push(current)
current = []
}
cx = start_x
cy = start_y
}
}
}
if current.length() > 0 {
polylines.push(current)
}
polylines
}
///|
/// Adaptive de Casteljau subdivision for a quadratic Bezier.
/// Appends the transformed endpoint and any intermediate split points to `out`.
fn flatten_quad(
x0 : Double,
y0 : Double,
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
matrix : Matrix2D,
flatness : Double,
out : Array[(Double, Double)],
) -> Unit {
let (tx0, ty0) = matrix.transform_point(x0, y0)
let (tx1, ty1) = matrix.transform_point(x1, y1)
let (tx2, ty2) = matrix.transform_point(x2, y2)
// Error estimate: distance of the control point from the chord midpoint.
let mx = (tx0 + tx2) * 0.5
let my = (ty0 + ty2) * 0.5
let dx = tx1 - mx
let dy = ty1 - my
let err_sq = dx * dx + dy * dy
if err_sq <= flatness * flatness {
out.push((tx2, ty2))
return
}
let x01 = (x0 + x1) * 0.5
let y01 = (y0 + y1) * 0.5
let x12 = (x1 + x2) * 0.5
let y12 = (y1 + y2) * 0.5
let xm = (x01 + x12) * 0.5
let ym = (y01 + y12) * 0.5
flatten_quad(x0, y0, x01, y01, xm, ym, matrix, flatness, out)
flatten_quad(xm, ym, x12, y12, x2, y2, matrix, flatness, out)
}
///|
/// Adaptive de Casteljau subdivision for a cubic Bezier.
fn flatten_cubic(
x0 : Double,
y0 : Double,
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
x3 : Double,
y3 : Double,
matrix : Matrix2D,
flatness : Double,
out : Array[(Double, Double)],
) -> Unit {
let (tx0, ty0) = matrix.transform_point(x0, y0)
let (tx1, ty1) = matrix.transform_point(x1, y1)
let (tx2, ty2) = matrix.transform_point(x2, y2)
let (tx3, ty3) = matrix.transform_point(x3, y3)
// Use distance of control points from the chord as a flatness estimate.
let dx = tx3 - tx0
let dy = ty3 - ty0
let chord_sq = dx * dx + dy * dy
let max_err_sq = if chord_sq == 0.0 {
let ex = tx1 - tx0
let ey = ty1 - ty0
let fx = tx2 - tx0
let fy = ty2 - ty0
let e = ex * ex + ey * ey
let f = fx * fx + fy * fy
if e > f {
e
} else {
f
}
} else {
let d1 = (tx1 - tx0) * dy - (ty1 - ty0) * dx
let d2 = (tx2 - tx0) * dy - (ty2 - ty0) * dx
let d1_sq = d1 * d1 / chord_sq
let d2_sq = d2 * d2 / chord_sq
if d1_sq > d2_sq {
d1_sq
} else {
d2_sq
}
}
if max_err_sq <= flatness * flatness {
out.push((tx3, ty3))
return
}
// de Casteljau subdivision at t = 0.5
let x01 = (x0 + x1) * 0.5
let y01 = (y0 + y1) * 0.5
let x12 = (x1 + x2) * 0.5
let y12 = (y1 + y2) * 0.5
let x23 = (x2 + x3) * 0.5
let y23 = (y2 + y3) * 0.5
let x012 = (x01 + x12) * 0.5
let y012 = (y01 + y12) * 0.5
let x123 = (x12 + x23) * 0.5
let y123 = (y12 + y23) * 0.5
let xm = (x012 + x123) * 0.5
let ym = (y012 + y123) * 0.5
flatten_cubic(x0, y0, x01, y01, x012, y012, xm, ym, matrix, flatness, out)
flatten_cubic(xm, ym, x123, y123, x23, y23, x3, y3, matrix, flatness, out)
}