use kurbo::{BezPath, PathEl, Point};
pub fn pucker_bloat_path(path: &BezPath, amount_pct: f64) -> BezPath {
if !amount_pct.is_finite() || amount_pct.abs() < 1e-9 {
return path.clone();
}
let k = amount_pct / 100.0;
let mut out = BezPath::new();
for (pts, closed) in flatten_subpaths(path) {
if pts.len() < 2 {
continue;
}
let center = centroid(&pts);
let warped: Vec<Point> = pts.iter().map(|p| lerp_pt(*p, center, k)).collect();
out.move_to(warped[0]);
for p in warped.iter().skip(1) {
out.line_to(*p);
}
if closed {
out.close_path();
}
}
out
}
pub fn pucker_bloat_vector_path(path: &crate::VectorPath, amount_pct: f64) -> crate::VectorPath {
use crate::{Anchor, VectorPath};
if !amount_pct.is_finite() || amount_pct.abs() < 1e-9 || path.anchors.is_empty() {
return path.clone();
}
let k = amount_pct / 100.0;
let center = {
let n = path.anchors.len() as f64;
let s: glam::DVec2 = path.anchors.iter().map(|a| a.pos).sum();
s / n.max(1.0)
};
let anchors: Vec<Anchor> = path
.anchors
.iter()
.map(|a| {
let pos = a.pos + (center - a.pos) * k;
let hin_abs = a.pos + a.tan_in;
let hout_abs = a.pos + a.tan_out;
let hin_w = hin_abs + (center - hin_abs) * (-k);
let hout_w = hout_abs + (center - hout_abs) * (-k);
Anchor {
pos,
tan_in: hin_w - pos,
tan_out: hout_w - pos,
mode: a.mode,
}
})
.collect();
VectorPath {
anchors,
closed: path.closed,
}
}
fn centroid(pts: &[Point]) -> Point {
let n = pts.len() as f64;
let (mut x, mut y) = (0.0, 0.0);
for p in pts {
x += p.x;
y += p.y;
}
Point::new(x / n, y / n)
}
fn lerp_pt(a: Point, b: Point, t: f64) -> Point {
Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
}
fn flatten_subpaths(path: &BezPath) -> Vec<(Vec<Point>, bool)> {
let mut result = Vec::new();
let mut cur = Vec::new();
let mut closed = false;
let mut start = Point::ZERO;
kurbo::flatten(path, 0.25, |el| match el {
PathEl::MoveTo(p) => {
if !cur.is_empty() {
result.push((std::mem::take(&mut cur), closed));
closed = false;
}
start = p;
cur.push(p);
}
PathEl::LineTo(p) => {
if cur.last().is_none_or(|q| (*q - p).hypot() > 1e-9) {
cur.push(p);
}
}
PathEl::ClosePath => {
if cur.len() >= 2 {
let last = *cur.last().unwrap();
if (last - start).hypot() < 1e-9 {
cur.pop();
}
}
closed = true;
}
_ => {}
});
if !cur.is_empty() {
result.push((cur, closed));
}
result
}