~raph/bezoid

297c8252af7e1305fdb5070f33f3a423675ac383 — Raph Levien 1 year, 6 months ago 7f05cc1
Crude solver

Not robust at all, and the slider-based UX is bad (should be able to
drag points around), but a first cut it kinda works.
5 files changed, 141 insertions(+), 11 deletions(-)

M src/appdata.rs
M src/bezoid.rs
M src/grapher.rs
M src/main.rs
A src/solve.rs
M src/appdata.rs => src/appdata.rs +5 -0
@@ 6,4 6,9 @@ pub struct AppData {
    pub bias0: f64,
    pub k1: f64,
    pub bias1: f64,

    pub px1: f64,
    pub py1: f64,
    pub px2: f64,
    pub py2: f64,
}

M src/bezoid.rs => src/bezoid.rs +42 -8
@@ 4,6 4,14 @@ use druid::kurbo::{Affine, CubicBez, Point, Vec2};

use crate::appdata::AppData;

/// Raw parameters for the curve.
pub struct CurveParams {
    pub k0: f64,
    pub bias0: f64,
    pub k1: f64,
    pub bias1: f64,
}

pub struct CurveResult {
    pub pts: Vec<Point>,
    pub th0: f64,


@@ 11,6 19,9 @@ pub struct CurveResult {
    pub chord: f64,
}

pub const BEZ_BIAS_EXP: f64 = 0.35;
pub const BEZ_CHORD_EXP: f64 = 1.4;

const N: usize = 1000;
const N_RECIP: f64 = 1.0 / (N as f64);



@@ 51,13 62,20 @@ fn compute_basis(bias: f64) -> Vec<f64> {
    result
}

pub fn compute_thetas(data: &AppData) -> Vec<f64> {
    let mut basis0 = compute_basis(data.bias0);
    let basis1 = compute_basis(data.bias1);
    for (x, y) in basis0.iter_mut().zip(basis1.iter().rev()) {
        *x = *x * data.k0 - *y * data.k1
impl CurveParams {
    pub fn compute_thetas(&self) -> Vec<f64> {
        let mut basis0 = compute_basis(self.bias0);
        let basis1 = compute_basis(self.bias1);
        for (x, y) in basis0.iter_mut().zip(basis1.iter().rev()) {
            *x = *x * self.k0 - *y * self.k1
        }
        basis0
    }

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

pub fn integrate_curve(thetas: &[f64]) -> CurveResult {


@@ 82,14 100,19 @@ pub fn integrate_curve(thetas: &[f64]) -> CurveResult {
    let chord = p.to_vec2().hypot();
    let th0 = th_chord - thetas[0];
    let th1 = thetas[n - 1] - th_chord;
    CurveResult { pts, th0, th1, chord }
    CurveResult {
        pts,
        th0,
        th1,
        chord,
    }
}

impl CurveResult {
    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.
            (2.0 - bias).powf(0.35) / (3.0 * chord.powf(1.4))
            (2.0 - bias).powf(BEZ_BIAS_EXP) / (3.0 * chord.powf(BEZ_CHORD_EXP))
        }
        let len0 = arm_len(data.bias0, self.chord);
        let p0 = Point::ORIGIN;


@@ 100,3 123,14 @@ impl CurveResult {
        CubicBez::new(p0, p1, p2, p3)
    }
}

impl<'a> From<&'a AppData> for CurveParams {
    fn from(data: &'a AppData) -> CurveParams {
        CurveParams {
            k0: data.k0,
            bias0: data.bias0,
            k1: data.k1,
            bias1: data.bias1,
        }
    }
}

M src/grapher.rs => src/grapher.rs +16 -3
@@ 1,12 1,12 @@
//! Custom widget for drawing stuff

use druid::kurbo::{Affine, BezPath, Point, Shape};
use druid::kurbo::{Affine, BezPath, Line, Point, Shape, Vec2};
use druid::piet::Color;
use druid::widget::prelude::*;
use druid::Data;

use crate::appdata::AppData;
use crate::bezoid;
use crate::bezoid::{self, CurveParams};

pub struct Grapher;



@@ 67,7 67,8 @@ impl Widget<AppData> for Grapher {

    fn paint(&mut self, ctx: &mut PaintCtx, data: &AppData, _env: &Env) {
        //let basis = compute_basis(data.bias0);
        let thetas = bezoid::compute_thetas(data);
        let params: CurveParams = data.into();
        let thetas = params.compute_thetas();
        plot(ctx, &thetas, Point::new(50.0, 400.0), 600.0, 100.0);
        let curve = bezoid::integrate_curve(&thetas);
        plot_xy(ctx, &curve.pts, Point::new(100.0, 300.0), 400.0);


@@ 76,5 77,17 @@ impl Widget<AppData> for Grapher {
        let a = Affine::translate((100.0, 300.0)) * Affine::FLIP_Y * Affine::scale(400.0);
        let bez = a * bez;
        ctx.stroke(bez, &Color::rgb8(128, 128, 255), 1.0);

        let a2 = Affine::translate(Vec2::new(800.0, 250.0)) * Affine::FLIP_Y * Affine::scale(100.0);
        let solver = crate::solve::Solver;
        let p1 = Point::new(data.px1, data.py1);
        let p2 = Point::new(data.px2, data.py2);
        let l1 = Line::new(Point::ORIGIN, p1);
        ctx.stroke(a2 * l1, &Color::WHITE, 1.0);
        let l2 = Line::new(Point::new(1.0, 0.0), p2);
        ctx.stroke(a2 * l2, &Color::WHITE, 1.0);
        let params = solver.solve(p1, p2);
        let curve = params.compute();
        plot_xy(ctx, &curve.pts, a2 * Point::ORIGIN, 100.0);
    }
}

M src/main.rs => src/main.rs +22 -0
@@ 4,6 4,7 @@ use druid::{AppLauncher, Env, PlatformError, Widget, WidgetExt, WindowDesc};
mod appdata;
mod bezoid;
mod grapher;
mod solve;

use appdata::AppData;



@@ 45,6 46,23 @@ fn ui_builder() -> impl Widget<AppData> {
        .with_range(-1.0, 2.0)
        .lens(AppData::bias1)
        .padding(5.0);

    let sliderpx1 = Slider::new()
        .with_range(-1.0, 1.0)
        .lens(AppData::px1)
        .padding(5.0);
    let sliderpy1 = Slider::new()
        .with_range(-1.0, 1.0)
        .lens(AppData::py1)
        .padding(5.0);
    let sliderpx2 = Slider::new()
        .with_range(-1.0, 1.0)
        .lens(AppData::px2)
        .padding(5.0);
    let sliderpy2 = Slider::new()
        .with_range(-1.0, 1.0)
        .lens(AppData::py2)
        .padding(5.0);
    let grapher = grapher::Grapher;

    Flex::column()


@@ 56,6 74,10 @@ fn ui_builder() -> impl Widget<AppData> {
        .with_child(slider1)
        .with_child(labelb1)
        .with_child(sliderb1)
        .with_child(sliderpx1)
        .with_child(sliderpy1)
        .with_child(sliderpx2)
        .with_child(sliderpy2)
        .with_flex_child(grapher, 1.0)
        .must_fill_main_axis(true)
}

A src/solve.rs => src/solve.rs +56 -0
@@ 0,0 1,56 @@
//! Solver from cubic params to curve params.

use druid::kurbo::Point;

use crate::bezoid::{CurveParams, BEZ_BIAS_EXP, BEZ_CHORD_EXP};

pub struct Solver;

impl Solver {
    /// Solve for curve params, given bezier control points.
    ///
    /// The points are given relative to p0 at (0, 0) and p3 at
    /// (1, 0).
    pub fn solve(&self, p1: Point, p2: Point) -> CurveParams {
        println!("({:.3}, {:.3}) ({:.3}, {:.3})", p1.x, p1.y, p2.x, p2.y);
        fn inv_arm_len(h: f64, chord: f64) -> f64 {
            let a = h * 3.0 * chord.powf(BEZ_CHORD_EXP);
            2.0 - a.powf(1.0 / BEZ_BIAS_EXP)
        }
        let v1 = p1.to_vec2();
        let v2 = Point::new(1.0, 0.0) - p2;
        let th0 = v1.atan2();
        let th1 = -v2.atan2();
        let mut dth = 0.0;
        let mut chord = 1.0f64;
        const N: usize = 10;
        for i in 0..N {
            let bias0 = inv_arm_len(v1.hypot(), chord);
            let bias1 = inv_arm_len(v2.hypot(), chord);
            let params = CurveParams {
                k0: th0 + dth,
                bias0,
                k1: th1 - dth,
                bias1,
            };
            if i == N - 1 {
                return params;
            }
            let result = params.compute();
            chord = result.chord;
            let th_err = mod_tau(th0 - th1 - (result.th0 - result.th1));
            // TODO: use Newton or secant methods, this should be close to linear.
            dth += 0.5 * th_err;
            println!(
                "result th0={:.3}, th1={:.3}, err {:.3}",
                result.th0, result.th1, th_err
            );
        }
        unreachable!()
    }
}

fn mod_tau(x: f64) -> f64 {
    // Do this in terms of euclidean remainder instead?
    x - std::f64::consts::TAU * (x * (1.0 / std::f64::consts::TAU)).round()
}