use crate::{Color, Point, Rect};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathVerb {
MoveTo(Point),
LineTo(Point),
QuadTo {
ctrl: Point,
to: Point,
},
CubicTo {
c1: Point,
c2: Point,
to: Point,
},
Close,
}
impl PathVerb {
fn scaled(self, f: f32) -> PathVerb {
let s = |p: Point| Point::new(p.x * f, p.y * f);
match self {
PathVerb::MoveTo(p) => PathVerb::MoveTo(s(p)),
PathVerb::LineTo(p) => PathVerb::LineTo(s(p)),
PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
ctrl: s(ctrl),
to: s(to),
},
PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
c1: s(c1),
c2: s(c2),
to: s(to),
},
PathVerb::Close => PathVerb::Close,
}
}
fn rotated(self, center: Point, sin: f32, cos: f32) -> PathVerb {
let r = |p: Point| {
let (dx, dy) = (p.x - center.x, p.y - center.y);
Point::new(
center.x + dx * cos - dy * sin,
center.y + dx * sin + dy * cos,
)
};
match self {
PathVerb::MoveTo(p) => PathVerb::MoveTo(r(p)),
PathVerb::LineTo(p) => PathVerb::LineTo(r(p)),
PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
ctrl: r(ctrl),
to: r(to),
},
PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
c1: r(c1),
c2: r(c2),
to: r(to),
},
PathVerb::Close => PathVerb::Close,
}
}
fn mirrored_x(self, axis: f32) -> PathVerb {
let m = |p: Point| Point::new(2.0 * axis - p.x, p.y);
match self {
PathVerb::MoveTo(p) => PathVerb::MoveTo(m(p)),
PathVerb::LineTo(p) => PathVerb::LineTo(m(p)),
PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
ctrl: m(ctrl),
to: m(to),
},
PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
c1: m(c1),
c2: m(c2),
to: m(to),
},
PathVerb::Close => PathVerb::Close,
}
}
fn translated(self, dx: f32, dy: f32) -> PathVerb {
let t = |p: Point| Point::new(p.x + dx, p.y + dy);
match self {
PathVerb::MoveTo(p) => PathVerb::MoveTo(t(p)),
PathVerb::LineTo(p) => PathVerb::LineTo(t(p)),
PathVerb::QuadTo { ctrl, to } => PathVerb::QuadTo {
ctrl: t(ctrl),
to: t(to),
},
PathVerb::CubicTo { c1, c2, to } => PathVerb::CubicTo {
c1: t(c1),
c2: t(c2),
to: t(to),
},
PathVerb::Close => PathVerb::Close,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum StrokeCap {
#[default]
Butt,
Square,
Round,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Stroke {
pub color: Color,
pub width: f32,
pub cap: StrokeCap,
}
impl Stroke {
pub const fn new(color: Color, width: f32) -> Self {
Self {
color,
width,
cap: StrokeCap::Butt,
}
}
#[must_use]
pub const fn with_cap(mut self, cap: StrokeCap) -> Self {
self.cap = cap;
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FillRule {
#[default]
EvenOdd,
NonZero,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Path {
verbs: Vec<PathVerb>,
fill_rule: FillRule,
}
impl Path {
pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
self.fill_rule = rule;
self
}
pub fn fill_rule(&self) -> FillRule {
self.fill_rule
}
pub fn new() -> Self {
Self::default()
}
pub fn move_to(mut self, p: Point) -> Self {
self.verbs.push(PathVerb::MoveTo(p));
self
}
pub fn line_to(mut self, p: Point) -> Self {
self.verbs.push(PathVerb::LineTo(p));
self
}
pub fn quad_to(mut self, ctrl: Point, to: Point) -> Self {
self.verbs.push(PathVerb::QuadTo { ctrl, to });
self
}
pub fn cubic_to(mut self, c1: Point, c2: Point, to: Point) -> Self {
self.verbs.push(PathVerb::CubicTo { c1, c2, to });
self
}
pub fn close(mut self) -> Self {
self.verbs.push(PathVerb::Close);
self
}
#[must_use]
pub fn append(mut self, other: Path) -> Self {
self.verbs.extend(other.verbs);
self
}
pub fn verbs(&self) -> &[PathVerb] {
&self.verbs
}
pub fn is_empty(&self) -> bool {
self.verbs.is_empty()
}
pub fn rect(r: Rect) -> Self {
Self::new()
.move_to(Point::new(r.x, r.y))
.line_to(Point::new(r.x + r.width, r.y))
.line_to(Point::new(r.x + r.width, r.y + r.height))
.line_to(Point::new(r.x, r.y + r.height))
.close()
}
pub fn arc_to(mut self, center: Point, radius: f32, start: f32, end: f32) -> Self {
let sweep = end - start;
if sweep.abs() < 1e-6 || radius <= 0.0 {
return self;
}
let pieces = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0);
let step = sweep / pieces;
let k = 4.0 / 3.0 * (step / 4.0).tan();
let mut angle = start;
for _ in 0..pieces as usize {
let next = angle + step;
let (s0, c0) = angle.sin_cos();
let (s1, c1) = next.sin_cos();
let p0 = Point::new(center.x + radius * c0, center.y + radius * s0);
let p1 = Point::new(center.x + radius * c1, center.y + radius * s1);
self = self.cubic_to(
Point::new(p0.x - k * radius * s0, p0.y + k * radius * c0),
Point::new(p1.x + k * radius * s1, p1.y - k * radius * c1),
p1,
);
angle = next;
}
self
}
pub fn circle(center: Point, radius: f32) -> Self {
const K: f32 = 0.552_284_8;
let (cx, cy, r) = (center.x, center.y, radius);
let k = r * K;
Self::new()
.move_to(Point::new(cx, cy - r))
.cubic_to(
Point::new(cx + k, cy - r),
Point::new(cx + r, cy - k),
Point::new(cx + r, cy),
)
.cubic_to(
Point::new(cx + r, cy + k),
Point::new(cx + k, cy + r),
Point::new(cx, cy + r),
)
.cubic_to(
Point::new(cx - k, cy + r),
Point::new(cx - r, cy + k),
Point::new(cx - r, cy),
)
.cubic_to(
Point::new(cx - r, cy - k),
Point::new(cx - k, cy - r),
Point::new(cx, cy - r),
)
.close()
}
pub fn oval(r: Rect) -> Self {
const K: f32 = 0.552_284_8;
let (rx, ry) = (r.width * 0.5, r.height * 0.5);
let (cx, cy) = (r.x + rx, r.y + ry);
let (kx, ky) = (rx * K, ry * K);
Self::new()
.move_to(Point::new(cx, cy - ry))
.cubic_to(
Point::new(cx + kx, cy - ry),
Point::new(cx + rx, cy - ky),
Point::new(cx + rx, cy),
)
.cubic_to(
Point::new(cx + rx, cy + ky),
Point::new(cx + kx, cy + ry),
Point::new(cx, cy + ry),
)
.cubic_to(
Point::new(cx - kx, cy + ry),
Point::new(cx - rx, cy + ky),
Point::new(cx - rx, cy),
)
.cubic_to(
Point::new(cx - rx, cy - ky),
Point::new(cx - kx, cy - ry),
Point::new(cx, cy - ry),
)
.close()
}
pub fn scaled(&self, factor: f32) -> Path {
Path {
verbs: self.verbs.iter().map(|v| v.scaled(factor)).collect(),
fill_rule: self.fill_rule,
}
}
pub fn translated(&self, dx: f32, dy: f32) -> Path {
Path {
verbs: self.verbs.iter().map(|v| v.translated(dx, dy)).collect(),
fill_rule: self.fill_rule,
}
}
pub fn mirrored_x(&self, axis: f32) -> Path {
Path {
verbs: self.verbs.iter().map(|v| v.mirrored_x(axis)).collect(),
fill_rule: self.fill_rule,
}
}
pub fn rotated(&self, center: Point, radians: f32) -> Path {
let (sin, cos) = radians.sin_cos();
Path {
verbs: self
.verbs
.iter()
.map(|v| v.rotated(center, sin, cos))
.collect(),
fill_rule: self.fill_rule,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fill_rule_survives_every_transform() {
let p = Path::rect(Rect::new(0.0, 0.0, 4.0, 4.0)).with_fill_rule(FillRule::NonZero);
assert_eq!(
Path::new().fill_rule(),
FillRule::EvenOdd,
"even-odd by default"
);
assert_eq!(p.scaled(2.0).fill_rule(), FillRule::NonZero);
assert_eq!(p.translated(1.0, 1.0).fill_rule(), FillRule::NonZero);
assert_eq!(p.mirrored_x(2.0).fill_rule(), FillRule::NonZero);
assert_eq!(
p.rotated(Point::new(0.0, 0.0), 1.0).fill_rule(),
FillRule::NonZero
);
}
#[test]
fn mirroring_reflects_x_about_the_axis_and_nothing_else() {
let path = Path::new()
.move_to(Point::new(2.0, 5.0))
.line_to(Point::new(8.0, 5.0))
.quad_to(Point::new(10.0, 7.0), Point::new(6.0, 9.0))
.cubic_to(
Point::new(4.0, 9.0),
Point::new(3.0, 8.0),
Point::new(2.0, 5.0),
)
.close();
let flipped = path.mirrored_x(6.0);
assert_eq!(
flipped.verbs(),
&[
PathVerb::MoveTo(Point::new(10.0, 5.0)),
PathVerb::LineTo(Point::new(4.0, 5.0)),
PathVerb::QuadTo {
ctrl: Point::new(2.0, 7.0),
to: Point::new(6.0, 9.0),
},
PathVerb::CubicTo {
c1: Point::new(8.0, 9.0),
c2: Point::new(9.0, 8.0),
to: Point::new(10.0, 5.0),
},
PathVerb::Close,
]
);
assert_eq!(path.mirrored_x(6.0).mirrored_x(6.0).verbs(), path.verbs());
}
#[test]
fn rotating_turns_clockwise_about_the_centre() {
let centre = Point::new(10.0, 10.0);
let quarter = std::f32::consts::FRAC_PI_2;
let up = Path::new()
.move_to(centre)
.line_to(Point::new(10.0, 4.0))
.close();
let turned = up.rotated(centre, quarter);
let PathVerb::LineTo(tip) = turned.verbs()[1] else {
panic!("the second verb is still a line");
};
assert!(
(tip.x - 16.0).abs() < 1e-4 && (tip.y - 10.0).abs() < 1e-4,
"a quarter turn puts the tip to the right of the centre: {tip:?}"
);
let PathVerb::MoveTo(fixed) = turned.verbs()[0] else {
panic!("the first verb is still a move");
};
assert_eq!(fixed, centre, "the centre does not move");
let round_trip = up
.rotated(centre, quarter)
.rotated(centre, quarter)
.rotated(centre, quarter)
.rotated(centre, quarter);
for (a, b) in round_trip.verbs().iter().zip(up.verbs()) {
match (a, b) {
(PathVerb::MoveTo(a), PathVerb::MoveTo(b))
| (PathVerb::LineTo(a), PathVerb::LineTo(b)) => {
assert!(
(a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3,
"{a:?}"
);
}
(PathVerb::Close, PathVerb::Close) => {}
_ => panic!("the verbs changed kind"),
}
}
}
#[test]
fn rotating_carries_the_control_points() {
let centre = Point::new(0.0, 0.0);
let half = std::f32::consts::PI;
let curved = Path::new()
.move_to(Point::new(1.0, 0.0))
.quad_to(Point::new(2.0, 3.0), Point::new(4.0, 0.0))
.cubic_to(
Point::new(5.0, 1.0),
Point::new(6.0, 2.0),
Point::new(7.0, 0.0),
);
let turned = curved.rotated(centre, half);
let near = |a: Point, x: f32, y: f32| (a.x - x).abs() < 1e-4 && (a.y - y).abs() < 1e-4;
match turned.verbs()[1] {
PathVerb::QuadTo { ctrl, to } => {
assert!(near(ctrl, -2.0, -3.0), "the control point turned: {ctrl:?}");
assert!(near(to, -4.0, 0.0), "and so did the end: {to:?}");
}
ref other => panic!("expected a quad, got {other:?}"),
}
match turned.verbs()[2] {
PathVerb::CubicTo { c1, c2, to } => {
assert!(
near(c1, -5.0, -1.0) && near(c2, -6.0, -2.0),
"both controls"
);
assert!(near(to, -7.0, 0.0), "and the end: {to:?}");
}
ref other => panic!("expected a cubic, got {other:?}"),
}
}
#[test]
fn builder_records_verbs_in_order() {
let p = Path::new()
.move_to(Point::new(1.0, 2.0))
.line_to(Point::new(3.0, 4.0))
.quad_to(Point::new(5.0, 6.0), Point::new(7.0, 8.0))
.close();
assert_eq!(
p.verbs(),
&[
PathVerb::MoveTo(Point::new(1.0, 2.0)),
PathVerb::LineTo(Point::new(3.0, 4.0)),
PathVerb::QuadTo {
ctrl: Point::new(5.0, 6.0),
to: Point::new(7.0, 8.0)
},
PathVerb::Close,
]
);
}
#[test]
fn an_arc_ends_on_the_circle_it_was_given() {
use std::f32::consts::PI;
let centre = Point::new(50.0, 40.0);
let radius = 12.0;
for (start, end) in [
(0.0, PI), (PI, 0.0), (0.3, 0.5), (-PI, PI * 0.75), ] {
let on = |a: f32| Point::new(centre.x + radius * a.cos(), centre.y + radius * a.sin());
let path = Path::new()
.move_to(on(start))
.arc_to(centre, radius, start, end);
let last = match path.verbs().last() {
Some(PathVerb::CubicTo { to, .. }) => *to,
other => panic!("an arc ends on a cubic, got {other:?}"),
};
let want = on(end);
assert!(
(last.x - want.x).abs() < 0.01 && (last.y - want.y).abs() < 0.01,
"arc {start}..{end} ended at {last:?}, wanted {want:?}"
);
for verb in path.verbs() {
if let PathVerb::CubicTo { to, .. } = verb {
let d = ((to.x - centre.x).powi(2) + (to.y - centre.y).powi(2)).sqrt();
assert!((d - radius).abs() < 0.01, "node off the circle: {d}");
}
}
}
}
#[test]
fn rect_is_a_closed_quad() {
let p = Path::rect(Rect::new(0.0, 0.0, 10.0, 20.0));
assert_eq!(p.verbs().len(), 5);
assert!(matches!(p.verbs().last(), Some(PathVerb::Close)));
}
#[test]
fn scaled_and_translated_transform_points() {
let p = Path::new()
.move_to(Point::new(2.0, 3.0))
.line_to(Point::new(4.0, 5.0));
let s = p.scaled(2.0);
assert_eq!(s.verbs()[0], PathVerb::MoveTo(Point::new(4.0, 6.0)));
let t = p.translated(10.0, 100.0);
assert_eq!(t.verbs()[1], PathVerb::LineTo(Point::new(14.0, 105.0)));
}
#[test]
fn circle_starts_at_top_and_closes() {
let c = Path::circle(Point::new(12.0, 12.0), 10.0);
assert_eq!(c.verbs()[0], PathVerb::MoveTo(Point::new(12.0, 2.0)));
assert!(matches!(c.verbs().last(), Some(PathVerb::Close)));
}
}