renamite-geometry 0.3.1

VectorPath, kurbo adapters, and Graphite geometry facades for renamite.
Documentation
//! Pucker & Bloat: vertices toward/away from contour centroid. Handles opposite.
//! Matches AE / lottie-web `pb` semantics (amount is percent, 100 = extreme).

use kurbo::{BezPath, PathEl, Point};

/// `amount` in percent.
///
/// Positive = pucker: vertices move toward the centroid, handles move away
/// (so straight polygons shrink at the corners but curves bulge).
/// Negative = bloat: vertices move away, handles move toward the center.
/// This vertex-toward-center convention matches AE `pb` and lottie-web.
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();

        // Reconstruct as polyline (handles not stored on flattened path;
        // cubic paths are flattened first like offset). For VectorPath-aware
        // path, prefer pucker_bloat_vector_path below.
        out.move_to(warped[0]);
        for p in warped.iter().skip(1) {
            out.line_to(*p);
        }
        if closed {
            out.close_path();
        }
    }
    out
}

/// Anchor-level pucker/bloat (preserves cubic handles). Preferred for Path shapes.
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| {
            // Vertex toward center by +k; handles opposite (-k).
            let pos = a.pos + (center - a.pos) * k;
            // Absolute handle points, then convert back to relative.
            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 {
    // Vertex moves toward center by t: a + (b-a)*t
    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
}