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
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Anchor, TangentMode, VectorPath};
use glam::DVec2;
fn unit_square_path() -> VectorPath {
VectorPath {
closed: true,
anchors: vec![
Anchor::corner(DVec2::new(0.0, 0.0)),
Anchor::corner(DVec2::new(100.0, 0.0)),
Anchor::corner(DVec2::new(100.0, 100.0)),
Anchor::corner(DVec2::new(0.0, 100.0)),
],
}
}
#[test]
fn zero_amount_identity() {
let p = unit_square_path();
assert_eq!(pucker_bloat_vector_path(&p, 0.0), p);
}
#[test]
fn positive_moves_vertices_toward_center() {
let p = unit_square_path();
let out = pucker_bloat_vector_path(&p, 50.0);
assert!((out.anchors[0].pos - DVec2::new(25.0, 25.0)).length() < 1e-6);
}
#[test]
fn negative_moves_vertices_outward() {
let p = unit_square_path();
let out = pucker_bloat_vector_path(&p, -50.0);
assert!((out.anchors[0].pos - DVec2::new(-25.0, -25.0)).length() < 1e-6);
}
#[test]
fn handles_move_opposite_to_vertices() {
let mut p = unit_square_path();
p.anchors[0].mode = TangentMode::Smooth;
p.anchors[0].tan_out = DVec2::new(10.0, 0.0);
let out = pucker_bloat_vector_path(&p, 50.0);
let tan = out.anchors[0].tan_out;
assert!(tan.length() > 1e-3, "handle should remain non-zero");
}
}