~raph/bezoid

4645edd5ff6465ea6184ecb320ded1a3ffd3bc64 — Raph Levien 1 year, 6 months ago 0d19b67
Analytical computation of thetas

Use closed form analytical formula to compute thetas, rather than crude
numerical integration.
2 files changed, 55 insertions(+), 5 deletions(-)

M src/bez.rs
M src/bezoid.rs
M src/bez.rs => src/bez.rs +12 -5
@@ 24,10 24,14 @@ impl Widget<AppData> for Bez {
    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut AppData, _env: &Env) {
        match event {
            Event::MouseDown(e) => {
                if (self.design_to_screen(Point::new(data.px1, data.py1)) - e.pos).hypot() < HIT_RADIUS {
                if (self.design_to_screen(Point::new(data.px1, data.py1)) - e.pos).hypot()
                    < HIT_RADIUS
                {
                    self.grab = Some(0);
                    ctx.set_active(true);
                } else if (self.design_to_screen(Point::new(data.px2, data.py2)) - e.pos).hypot() < HIT_RADIUS {
                } else if (self.design_to_screen(Point::new(data.px2, data.py2)) - e.pos).hypot()
                    < HIT_RADIUS
                {
                    self.grab = Some(1);
                    ctx.set_active(true);
                }


@@ 96,13 100,16 @@ impl Widget<AppData> for Bez {

impl Bez {
    fn design_to_screen(&self, design: Point) -> Point {
        let a = Affine::translate(Vec2::new(OFFSET_X, OFFSET_Y)) * Affine::FLIP_Y * Affine::scale(SCALE);
        let a = Affine::translate(Vec2::new(OFFSET_X, OFFSET_Y))
            * Affine::FLIP_Y
            * Affine::scale(SCALE);
        a * design
    }


    fn screen_to_design(&self, design: Point) -> Point {
        let a = Affine::translate(Vec2::new(OFFSET_X, OFFSET_Y)) * Affine::FLIP_Y * Affine::scale(SCALE);
        let a = Affine::translate(Vec2::new(OFFSET_X, OFFSET_Y))
            * Affine::FLIP_Y
            * Affine::scale(SCALE);
        a.inverse() * design
    }


M src/bezoid.rs => src/bezoid.rs +43 -0
@@ 63,6 63,9 @@ fn compute_basis(bias: f64) -> Vec<f64> {
}

impl CurveParams {
    // This is a computation using crude numerical integration. We
    // don't need it anymore because we now do it analytically.
    /*
    pub fn compute_thetas(&self) -> Vec<f64> {
        let mut basis0 = compute_basis(self.bias0);
        let basis1 = compute_basis(self.bias1);


@@ 71,6 74,24 @@ impl CurveParams {
        }
        basis0
    }
    */

    /// An analytical approach to compute thetas.
    ///
    /// We won't keep this because we're going to do another stage of numerical
    /// integration, but it's useful as we develop.
    pub fn compute_thetas(&self) -> Vec<f64> {
        (0..=N)
            .map(|i| {
                let t = (i as f64) * N_RECIP;
                self.compute_theta(t)
            })
            .collect()
    }

    pub fn compute_theta(&self, t: f64) -> f64 {
        self.k1 * integrate_basis(self.bias1, t) - self.k0 * integrate_basis(self.bias0, 1.0 - t)
    }

    pub fn compute(&self) -> CurveResult {
        let thetas = self.compute_thetas();


@@ 108,7 129,29 @@ pub fn integrate_curve(thetas: &[f64]) -> CurveResult {
    }
}

/// Compute integral of basis function.
///
/// The integral of the basis function can be represented as a reasonably
/// simple closed-form analytic formula.
///
/// Note: this is normalized so that y(1) - y(0) = 1.
///
/// This is oriented for the rightmost control point.
fn integrate_basis(bias: f64, t: f64) -> f64 {
    if bias > 1.0 {
        let a = (bias - 1.0).min(1.0 - 1e-4);
        let norm = 1.0 / (1.0 - a) + (1.0 - a).ln() - 1.0;
        (1.0 / (1.0 - a * t) + (1.0 - a * t).ln()) / norm
    } else {
        let iy0 = 4.0 * t.powi(3) - 3.0 * t.powi(4);
        let iy1 = t.powi(2);
        iy0 + bias * (iy1 - iy0)
    }
}

impl CurveResult {
    // This is no longer in sync with the bez -> params direction
    // (in Solver).
    pub fn infer_bezier(&self, data: &AppData) -> CubicBez {
        fn arm_len(bias: f64, chord: f64) -> f64 {
            // This is a bit ad hoc but seems to basically work.