///|
/// 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 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)
}
///|
/// Approximate a circular arc with cubic Beziers of at most 90° per segment.
/// Uses the standard control-point distance `k = 4 * tan(delta/4) / 3`.
///
/// Angles are in radians, measured from the +X axis. The arc is traversed
/// counter-clockwise when `counterclockwise=true`, clockwise otherwise
/// (matching HTML Canvas semantics). If `start_angle == end_angle`, only a
/// `MoveTo` to the start point is emitted; no cubic segments are appended.
pub fn Path2D::arc(
self : Path2D,
cx : Double,
cy : Double,
radius : Double,
start_angle : Double,
end_angle : Double,
counterclockwise? : Bool = false,
) -> Unit {
let tau = 2.0 * @math.PI
let half_pi = @math.PI / 2.0
let a0 = start_angle
let mut a1 = end_angle
if counterclockwise {
// Ensure a1 < a0 for CCW drawing.
while a1 > a0 {
a1 = a1 - tau
}
} else {
while a1 < a0 {
a1 = a1 + tau
}
}
let x0 = cx + radius * @math.cos(a0)
let y0 = cy + radius * @math.sin(a0)
self.commands.push(PathCmd::MoveTo(x0, y0))
let step = if counterclockwise { -half_pi } else { half_pi }
let mut a = a0
// Emit ≤ 90° cubic segments until we reach or pass a1.
while (!counterclockwise && a < a1) || (counterclockwise && a > a1) {
let next = if counterclockwise {
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 p1x = cx + radius * (cos_a - k * sin_a)
let p1y = cy + radius * (sin_a + k * cos_a)
let p2x = cx + radius * (cos_n + k * sin_n)
let p2y = cy + radius * (sin_n - k * cos_n)
let p3x = cx + radius * cos_n
let p3y = cy + radius * sin_n
self.commands.push(PathCmd::CubicTo(p1x, p1y, p2x, p2y, p3x, p3y))
a = next
}
}
///|
/// 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)
}