pub use kurbo::Affine;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl Rect {
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self { x, y, width, height }
}
pub fn from_points(p1: Point, p2: Point) -> Self {
let x = p1.x.min(p2.x);
let y = p1.y.min(p2.y);
let width = (p1.x - p2.x).abs();
let height = (p1.y - p2.y).abs();
Self { x, y, width, height }
}
pub fn contains(&self, p: Point) -> bool {
p.x >= self.x
&& p.x <= self.x + self.width
&& p.y >= self.y
&& p.y <= self.y + self.height
}
pub fn center(&self) -> Point {
Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
}
pub fn right(&self) -> f64 {
self.x + self.width
}
pub fn bottom(&self) -> f64 {
self.y + self.height
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
pub fn with_alpha(self, a: u8) -> Self {
Self { a, ..self }
}
pub fn from_hex(hex: &str) -> Option<Self> {
let hex = hex.strip_prefix('#').unwrap_or(hex);
if hex.len() != 6 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(Self::rgb(r, g, b))
}
pub const TAB_BLUE: Self = Self::rgb(0x4E, 0x79, 0xA7);
pub const TAB_ORANGE: Self = Self::rgb(0xF2, 0x8E, 0x2B);
pub const TAB_GREEN: Self = Self::rgb(0x59, 0xA1, 0x4F);
pub const TAB_RED: Self = Self::rgb(0xE1, 0x57, 0x59);
pub const TAB_PURPLE: Self = Self::rgb(0xB0, 0x7A, 0xA1);
pub const TAB_BROWN: Self = Self::rgb(0x9C, 0x75, 0x5F);
pub const TAB_PINK: Self = Self::rgb(0xFF, 0x9D, 0xA7);
pub const TAB_GREY: Self = Self::rgb(0xBA, 0xB0, 0xAC);
pub const TAB_OLIVE: Self = Self::rgb(0xED, 0xC9, 0x48);
pub const TAB_CYAN: Self = Self::rgb(0x76, 0xB7, 0xB2);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const TRANSPARENT: Self = Self::new(0, 0, 0, 0);
pub const TABLEAU_10: [Self; 10] = [
Self::TAB_BLUE,
Self::TAB_ORANGE,
Self::TAB_GREEN,
Self::TAB_RED,
Self::TAB_PURPLE,
Self::TAB_BROWN,
Self::TAB_PINK,
Self::TAB_GREY,
Self::TAB_OLIVE,
Self::TAB_CYAN,
];
}
#[derive(Debug, Clone, Copy)]
pub struct Paint {
pub color: Color,
pub anti_alias: bool,
}
impl Paint {
pub fn new(color: Color) -> Self {
Self {
color,
anti_alias: true,
}
}
}
#[derive(Debug, Clone)]
pub struct Stroke {
pub width: f64,
pub cap: StrokeCap,
pub join: StrokeJoin,
pub dash: Option<DashPattern>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StrokeCap {
Butt,
Round,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StrokeJoin {
Miter,
Round,
Bevel,
}
#[derive(Debug, Clone)]
pub struct DashPattern {
pub dashes: Vec<f64>,
pub offset: f64,
}
impl Stroke {
pub fn new(width: f64) -> Self {
Self {
width,
cap: StrokeCap::Butt,
join: StrokeJoin::Miter,
dash: None,
}
}
pub fn with_dash(mut self, pattern: DashPattern) -> Self {
self.dash = Some(pattern);
self
}
}
#[derive(Debug, Clone)]
pub struct TextStyle {
pub size: f64,
pub color: Color,
pub weight: FontWeight,
pub family: Option<String>,
pub halign: HAlign,
pub valign: VAlign,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontWeight {
Normal,
Bold,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VAlign {
Top,
Middle,
Bottom,
Baseline,
}
impl TextStyle {
pub fn new(size: f64) -> Self {
Self {
size,
color: Color::BLACK,
weight: FontWeight::Normal,
family: None,
halign: HAlign::Left,
valign: VAlign::Baseline,
}
}
}
#[derive(Debug, Clone)]
pub struct Image {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Default)]
pub struct Path {
pub elements: Vec<PathEl>,
}
#[derive(Debug, Clone, Copy)]
pub enum PathEl {
MoveTo(Point),
LineTo(Point),
QuadTo(Point, Point),
CurveTo(Point, Point, Point),
ClosePath,
}
impl Path {
pub fn new() -> Self {
Self {
elements: Vec::new(),
}
}
pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
self.elements.push(PathEl::MoveTo(Point::new(x, y)));
self
}
pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
self.elements.push(PathEl::LineTo(Point::new(x, y)));
self
}
pub fn quad_to(&mut self, x1: f64, y1: f64, x: f64, y: f64) -> &mut Self {
self.elements.push(PathEl::QuadTo(
Point::new(x1, y1),
Point::new(x, y),
));
self
}
pub fn curve_to(
&mut self,
x1: f64,
y1: f64,
x2: f64,
y2: f64,
x: f64,
y: f64,
) -> &mut Self {
self.elements.push(PathEl::CurveTo(
Point::new(x1, y1),
Point::new(x2, y2),
Point::new(x, y),
));
self
}
pub fn close(&mut self) -> &mut Self {
self.elements.push(PathEl::ClosePath);
self
}
pub fn rect(r: Rect) -> Self {
let mut p = Self::new();
p.move_to(r.x, r.y)
.line_to(r.right(), r.y)
.line_to(r.right(), r.bottom())
.line_to(r.x, r.bottom())
.close();
p
}
pub fn circle(center: Point, radius: f64) -> Self {
const KAPPA: f64 = 0.552_284_749_8;
let k = radius * KAPPA;
let cx = center.x;
let cy = center.y;
let mut p = Self::new();
p.move_to(cx + radius, cy);
p.curve_to(cx + radius, cy - k, cx + k, cy - radius, cx, cy - radius);
p.curve_to(cx - k, cy - radius, cx - radius, cy - k, cx - radius, cy);
p.curve_to(cx - radius, cy + k, cx - k, cy + radius, cx, cy + radius);
p.curve_to(cx + k, cy + radius, cx + radius, cy + k, cx + radius, cy);
p.close();
p
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn point_new() {
let p = Point::new(1.0, 2.0);
assert_eq!(p.x, 1.0);
assert_eq!(p.y, 2.0);
}
#[test]
fn rect_basics() {
let r = Rect::new(10.0, 20.0, 100.0, 50.0);
assert_eq!(r.right(), 110.0);
assert_eq!(r.bottom(), 70.0);
assert_eq!(r.center(), Point::new(60.0, 45.0));
assert!(r.contains(Point::new(60.0, 45.0)));
assert!(!r.contains(Point::new(0.0, 0.0)));
}
#[test]
fn rect_from_points() {
let r = Rect::from_points(Point::new(10.0, 20.0), Point::new(5.0, 30.0));
assert_eq!(r.x, 5.0);
assert_eq!(r.y, 20.0);
assert_eq!(r.width, 5.0);
assert_eq!(r.height, 10.0);
}
#[test]
fn color_hex_parsing() {
assert_eq!(Color::from_hex("#4E79A7"), Some(Color::TAB_BLUE));
assert_eq!(Color::from_hex("4E79A7"), Some(Color::TAB_BLUE));
assert_eq!(Color::from_hex("invalid"), None);
assert_eq!(Color::from_hex("#FFF"), None);
}
#[test]
fn color_with_alpha() {
let c = Color::TAB_BLUE.with_alpha(128);
assert_eq!(c.r, 0x4E);
assert_eq!(c.a, 128);
}
#[test]
fn tableau_10_length() {
assert_eq!(Color::TABLEAU_10.len(), 10);
assert_eq!(Color::TABLEAU_10[0], Color::TAB_BLUE);
assert_eq!(Color::TABLEAU_10[9], Color::TAB_CYAN);
}
#[test]
fn stroke_defaults() {
let s = Stroke::new(2.0);
assert_eq!(s.width, 2.0);
assert_eq!(s.cap, StrokeCap::Butt);
assert_eq!(s.join, StrokeJoin::Miter);
assert!(s.dash.is_none());
}
#[test]
fn stroke_with_dash() {
let s = Stroke::new(1.0).with_dash(DashPattern {
dashes: vec![5.0, 3.0],
offset: 0.0,
});
assert!(s.dash.is_some());
assert_eq!(s.dash.as_ref().unwrap().dashes, vec![5.0, 3.0]);
}
#[test]
fn text_style_defaults() {
let ts = TextStyle::new(12.0);
assert_eq!(ts.size, 12.0);
assert_eq!(ts.color, Color::BLACK);
assert_eq!(ts.weight, FontWeight::Normal);
assert!(ts.family.is_none());
assert_eq!(ts.halign, HAlign::Left);
assert_eq!(ts.valign, VAlign::Baseline);
}
#[test]
fn path_rect() {
let p = Path::rect(Rect::new(0.0, 0.0, 10.0, 10.0));
assert_eq!(p.elements.len(), 5);
assert!(!p.is_empty());
}
#[test]
fn path_circle() {
let p = Path::circle(Point::new(0.0, 0.0), 50.0);
assert_eq!(p.elements.len(), 6);
}
#[test]
fn path_builder() {
let mut p = Path::new();
assert!(p.is_empty());
p.move_to(0.0, 0.0)
.line_to(10.0, 0.0)
.quad_to(15.0, 5.0, 10.0, 10.0)
.curve_to(5.0, 15.0, -5.0, 15.0, -10.0, 10.0)
.close();
assert_eq!(p.elements.len(), 5);
}
#[test]
fn paint_defaults() {
let p = Paint::new(Color::BLACK);
assert!(p.anti_alias);
assert_eq!(p.color, Color::BLACK);
}
}