## ~raph/bezoid

91d6c52e39dfe0d4dc307b794866fe1d9d074bec — Raph Levien 1 year, 6 months ago
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;