use crate::path_builder::{Path, PathBuilder, PathOp};
use crate::{Point, Vector};
pub struct StrokeStyle {
pub width: f32,
pub cap: LineCap,
pub join: LineJoin,
pub mitre_limit: f32,
pub dash_array: Vec<f32>,
pub dash_offset: f32,
}
pub enum LineCap {
Round,
Square,
Butt,
}
pub enum LineJoin {
Round,
Mitre,
Bevel,
}
fn compute_normal(p0: Point, p1: Point) -> Vector {
let ux = p1.x - p0.x;
let uy = p1.y - p0.y;
let ulen = ux.hypot(uy);
assert!(ulen != 0.);
Vector::new(-uy / ulen, ux / ulen)
}
fn flip(v: Vector) -> Vector {
Vector::new(-v.x, -v.y)
}
fn arc_segment(path: &mut PathBuilder, xc: f32, yc: f32, radius: f32, a: Vector, b: Vector) {
let r_sin_a = radius * a.y;
let r_cos_a = radius * a.x;
let r_sin_b = radius * b.y;
let r_cos_b = radius * b.x;
let mut mid = a + b;
mid /= mid.length();
let mid2 = a + mid;
let h = (4. / 3.) * dot(perp(a), mid2) / dot(a, mid2);
path.cubic_to(
xc + r_cos_a - h * r_sin_a,
yc + r_sin_a + h * r_cos_a,
xc + r_cos_b + h * r_sin_b,
yc + r_sin_b - h * r_cos_b,
xc + r_cos_b,
yc + r_sin_b,
);
}
fn bisect(a: Vector, b: Vector) -> Vector {
let mut mid;
if dot(a, b) >= 0. {
mid = a + b;
} else {
mid = flip(a) + b;
mid = perp(mid);
}
let mid_len = mid.x * mid.x + mid.y * mid.y;
let len = mid_len.sqrt();
return mid / len;
}
fn arc(path: &mut PathBuilder, xc: f32, yc: f32, radius: f32, a: Vector, b: Vector) {
let mid_v = bisect(a, b);
arc_segment(path, xc, yc, radius, a, mid_v);
arc_segment(path, xc, yc, radius, mid_v, b);
}
fn join_round(path: &mut PathBuilder, center: Point, a: Vector, b: Vector, radius: f32) {
arc(path, center.x, center.y, radius, a, b);
}
fn cap_line(dest: &mut PathBuilder, style: &StrokeStyle, pt: Point, normal: Vector) {
let offset = style.width / 2.;
match style.cap {
LineCap::Butt => { }
LineCap::Round => {
dest.move_to(pt.x + normal.x * offset, pt.y + normal.y * offset);
arc(dest, pt.x, pt.y, offset, normal, flip(normal));
dest.close();
}
LineCap::Square => {
let v = Vector::new(normal.y, -normal.x);
let end = pt + v * offset;
dest.move_to(pt.x + normal.x * offset, pt.y + normal.y * offset);
dest.line_to(end.x + normal.x * offset, end.y + normal.y * offset);
dest.line_to(end.x + -normal.x * offset, end.y + -normal.y * offset);
dest.line_to(pt.x - normal.x * offset, pt.y - normal.y * offset);
dest.close();
}
}
}
fn bevel(
dest: &mut PathBuilder,
style: &StrokeStyle,
pt: Point,
s1_normal: Vector,
s2_normal: Vector,
) {
let offset = style.width / 2.;
dest.move_to(pt.x + s1_normal.x * offset, pt.y + s1_normal.y * offset);
dest.line_to(pt.x + s2_normal.x * offset, pt.y + s2_normal.y * offset);
dest.line_to(pt.x, pt.y);
dest.close();
}
fn swap(a: Vector) -> Vector {
return Vector::new(a.y, -a.x);
}
fn unperp(a: Vector) -> Vector {
swap(a)
}
fn perp(v: Vector) -> Vector {
Vector::new(-v.y, v.x)
}
fn dot(a: Vector, b: Vector) -> f32 {
a.x * b.x + a.y * b.y
}
fn line_intersection(a: Point, a_perp: Vector, b: Point, b_perp: Vector) -> Point {
let a_parallel = unperp(a_perp);
let c = b - a;
let denom = dot(b_perp, a_parallel);
if denom == 0.0 {
panic!("trouble")
}
let t = dot(b_perp, c) / denom;
let intersection = Point::new(a.x + t * (a_parallel.x), a.y + t * (a_parallel.y));
intersection
}
fn is_interior_angle(a: Vector, b: Vector) -> bool {
dot(perp(a), b) > 0. || a == b
}
fn join_line(
dest: &mut PathBuilder,
style: &StrokeStyle,
pt: Point,
mut s1_normal: Vector,
mut s2_normal: Vector,
) {
if is_interior_angle(s1_normal, s2_normal) {
s2_normal = flip(s2_normal);
s1_normal = flip(s1_normal);
std::mem::swap(&mut s1_normal, &mut s2_normal);
}
let offset = style.width / 2.;
match style.join {
LineJoin::Round => {
dest.move_to(pt.x + s1_normal.x * offset, pt.y + s1_normal.y * offset);
join_round(dest, pt, s1_normal, s2_normal, offset);
dest.line_to(pt.x, pt.y);
dest.close();
}
LineJoin::Mitre => {
let in_dot_out = -s1_normal.x * s2_normal.x + -s1_normal.y * s2_normal.y;
if 2. <= style.mitre_limit * style.mitre_limit * (1. - in_dot_out) {
let start = pt + s1_normal * offset;
let end = pt + s2_normal * offset;
let intersection = line_intersection(start, s1_normal, end, s2_normal);
dest.move_to(pt.x + s1_normal.x * offset, pt.y + s1_normal.y * offset);
dest.line_to(intersection.x, intersection.y);
dest.line_to(pt.x + s2_normal.x * offset, pt.y + s2_normal.y * offset);
dest.line_to(pt.x, pt.y);
dest.close();
} else {
bevel(dest, style, pt, s1_normal, s2_normal);
}
}
LineJoin::Bevel => {
bevel(dest, style, pt, s1_normal, s2_normal);
}
}
}
pub fn stroke_to_path(path: &Path, style: &StrokeStyle) -> Path {
let mut cur_pt = Point::zero();
let mut stroked_path = PathBuilder::new();
let mut last_normal = Vector::zero();
let half_width = style.width / 2.;
let mut start_point = None;
for op in &path.ops {
match *op {
PathOp::MoveTo(pt) => {
if let Some((point, normal)) = start_point {
cap_line(&mut stroked_path, style, cur_pt, last_normal);
cap_line(&mut stroked_path, style, point, flip(normal));
}
start_point = None;
cur_pt = pt;
}
PathOp::LineTo(pt) => {
let normal = compute_normal(cur_pt, pt);
if start_point.is_none() {
start_point = Some((cur_pt, normal));
} else {
join_line(&mut stroked_path, style, cur_pt, last_normal, normal);
}
stroked_path.move_to(
cur_pt.x + normal.x * half_width,
cur_pt.y + normal.y * half_width,
);
stroked_path.line_to(pt.x + normal.x * half_width, pt.y + normal.y * half_width);
stroked_path.line_to(pt.x + -normal.x * half_width, pt.y + -normal.y * half_width);
stroked_path.line_to(
cur_pt.x - normal.x * half_width,
cur_pt.y - normal.y * half_width,
);
stroked_path.close();
last_normal = normal;
cur_pt = pt;
}
PathOp::Close => {
if let Some((point, normal)) = start_point {
let last_normal = compute_normal(cur_pt, point);
stroked_path.move_to(
cur_pt.x + normal.x * half_width,
cur_pt.y + normal.y * half_width,
);
stroked_path.line_to(
point.x + normal.x * half_width,
point.y + normal.y * half_width,
);
stroked_path.line_to(
point.x + -normal.x * half_width,
point.y + -normal.y * half_width,
);
stroked_path.line_to(
cur_pt.x - normal.x * half_width,
cur_pt.y - normal.y * half_width,
);
stroked_path.close();
join_line(&mut stroked_path, style, point, last_normal, normal);
}
}
PathOp::QuadTo(..) => panic!("Only flat paths handled"),
PathOp::CubicTo(..) => panic!("Only flat paths handled"),
}
}
if let Some((point, normal)) = start_point {
cap_line(&mut stroked_path, style, cur_pt, last_normal);
cap_line(&mut stroked_path, style, point, flip(normal));
}
stroked_path.finish()
}