~raph/bezoid

91d6c52e39dfe0d4dc307b794866fe1d9d074bec — Raph Levien 1 year, 6 months ago 3d6ffcd
Infer bezier from curve

The length of the control arms are not calibrated, but it lets you
compare the two curve families side by side.
3 files changed, 110 insertions(+), 79 deletions(-)

A src/bezoid.rs
M src/grapher.rs
M src/main.rs
A src/bezoid.rs => src/bezoid.rs +96 -0
@@ 0,0 1,96 @@
//! The math for the bezoid curve family.

use druid::kurbo::{Affine, CubicBez, Point, Vec2};

use crate::appdata::AppData;

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

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

fn compute_basis(bias: f64) -> Vec<f64> {
    let mut sum = 0.0;
    let mut result = (0..=N)
        .map(|i| {
            let x = (i as f64 + 0.5) * N_RECIP;
            let y1 = 1.0 - x;
            let y = if bias > 1.0 {
                y1 / (1.0 - (bias - 1.0) * y1).powi(2)
            } else {
                let y0 = 6.0 * (y1.powi(2) - y1.powi(3));
                y0 + bias * (y1 - y0)
            };
            let result = sum;
            if i < N {
                sum += y * N_RECIP;
            }
            result
        })
        .collect::<Vec<_>>();
    //println!("sum = {}", sum);
    if bias > 1.0 {
        // This basis function can be integrated analytically:
        fn integral(x: f64, a: f64) -> f64 {
            (1.0 / (1.0 - a * x) + (1.0 - a * x).ln()) / (a * a)
        }
        let a = bias - 1.0;
        let _sum_computed = integral(1.0, a) - integral(0.0, a);
        let _sum2 = (1.0 / (1.0 - a) + (1.0 - a).ln() - 1.0) / (a * a);
        //println!("sum = {}, integral = {}, {}", sum, _sum_computed, _sum2)
    }
    let sum_recip = sum.recip();
    for x in &mut result {
        *x *= sum_recip;
    }
    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
    }
    basis0
}

pub fn integrate_curve(thetas: &[f64]) -> CurveResult {
    let n = thetas.len();
    let mut p = Point::ORIGIN;
    let scale = 1.0 / (n - 1) as f64;
    let mut pts = (0..n)
        .map(|i| {
            let this_p = p;
            if i < thetas.len() - 1 {
                let th = 0.5 * (thetas[i] + thetas[i + 1]);
                p += scale * Vec2::from_angle(th);
            }
            this_p
        })
        .collect::<Vec<_>>();
    let a = Affine::new([p.x, p.y, -p.y, p.x, 0.0, 0.0]).inverse();
    for p in &mut pts {
        *p = a * *p;
    }
    let th_chord = p.to_vec2().atan2();
    let th0 = th_chord - thetas[0];
    let th1 = thetas[n - 1] - th_chord;
    CurveResult { pts, th0, th1 }
}

impl CurveResult {
    pub fn infer_bezier(&self, data: &AppData) -> CubicBez {
        let len0 = (2.0 - data.bias0) / 3.0;
        let p0 = Point::ORIGIN;
        let p1 = p0 + len0 * Vec2::from_angle(self.th0);
        let p3 = Point::new(1.0, 0.0);
        let len1 = (2.0 - data.bias1) / 3.0;
        let p2 = p3 - len1 * Vec2::from_angle(-self.th1);
        CubicBez::new(p0, p1, p2, p3)
    }
}

M src/grapher.rs => src/grapher.rs +13 -79
@@ 1,84 1,15 @@
//! Custom widget for drawing stuff

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

use crate::appdata::AppData;
use crate::bezoid;

pub struct Grapher;

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

fn compute_basis(bias: f64) -> Vec<f64> {
    let mut sum = 0.0;
    let mut result = (0..=N)
        .map(|i| {
            let x = (i as f64 + 0.5) * N_RECIP;
            let y1 = 1.0 - x;
            let y = if bias > 1.0 {
                y1 / (1.0 - (bias - 1.0) * y1).powi(2)
            } else {
                let y0 = 6.0 * (y1.powi(2) - y1.powi(3));
                y0 + bias * (y1 - y0)
            };
            let result = sum;
            if i < N {
                sum += y * N_RECIP;
            }
            result
        })
        .collect::<Vec<_>>();
    //println!("sum = {}", sum);
    if bias > 1.0 {
        // This basis function can be integrated analytically:
        fn integral(x: f64, a: f64) -> f64 {
            (1.0 / (1.0 - a * x) + (1.0 - a * x).ln()) / (a * a)
        }
        let a = bias - 1.0;
        let _sum_computed = integral(1.0, a) - integral(0.0, a);
        let _sum2 = (1.0 / (1.0 - a) + (1.0 - a).ln() - 1.0) / (a * a);
        //println!("sum = {}, integral = {}, {}", sum, _sum_computed, _sum2)
    }
    let sum_recip = sum.recip();
    for x in &mut result {
        *x *= sum_recip;
    }
    result
}

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
    }
    basis0
}

fn integrate_curve(thetas: &[f64]) -> Vec<Point> {
    let n = thetas.len();
    let mut p = Point::ORIGIN;
    let scale = 1.0 / (n - 1) as f64;
    let mut result = (0..n)
        .map(|i| {
            let this_p = p;
            if i < thetas.len() - 1 {
                let th = 0.5 * (thetas[i] + thetas[i + 1]);
                p += scale * Vec2::from_angle(th);
            }
            this_p
        })
        .collect::<Vec<_>>();
    let a = Affine::new([p.x, p.y, -p.y, p.x, 0.0, 0.0]).inverse();
    for p in &mut result {
        *p = a * *p;
    }
    result
}

fn plot(ctx: &mut PaintCtx, seq: &[f64], origin: Point, width: f64, scale: f64) {
    let mut path = BezPath::new();
    for i in 0..seq.len() {


@@ 107,18 38,18 @@ fn plot_xy(ctx: &mut PaintCtx, seq: &[Point], origin: Point, scale: f64) {
}

impl Widget<AppData> for Grapher {
    fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut AppData, env: &Env) {}
    fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut AppData, _env: &Env) {}

    fn lifecycle(
        &mut self,
        _ctx: &mut LifeCycleCtx,
        _event: &LifeCycle,
        _data: &AppData,
        env: &Env,
        _env: &Env,
    ) {
    }

    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &AppData, data: &AppData, env: &Env) {
    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &AppData, data: &AppData, _env: &Env) {
        if !old_data.same(data) {
            ctx.request_paint();
        }


@@ 135,12 66,15 @@ impl Widget<AppData> for Grapher {
    }

    fn paint(&mut self, ctx: &mut PaintCtx, data: &AppData, _env: &Env) {
        let line = Line::new((0.0, 0.0), (100.0, 100.0));
        ctx.stroke(line, &Color::WHITE, 1.0);
        //let basis = compute_basis(data.bias0);
        let thetas = compute_thetas(data);
        let thetas = bezoid::compute_thetas(data);
        plot(ctx, &thetas, Point::new(50.0, 400.0), 600.0, 100.0);
        let pts = integrate_curve(&thetas);
        plot_xy(ctx, &pts, Point::new(100.0, 300.0), 400.0);
        let curve = bezoid::integrate_curve(&thetas);
        plot_xy(ctx, &curve.pts, Point::new(100.0, 300.0), 400.0);
        let cb = curve.infer_bezier(data);
        let bez = cb.into_path(1e-3);
        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);
    }
}

M src/main.rs => src/main.rs +1 -0
@@ 2,6 2,7 @@ use druid::widget::{Flex, Label, Slider};
use druid::{AppLauncher, Env, PlatformError, Widget, WidgetExt, WindowDesc};

mod appdata;
mod bezoid;
mod grapher;

use appdata::AppData;