~raph/bezoid

53554c0d54ced565fb6433449239545ee2f285e7 — Raph Levien 1 year, 6 months ago 297c825
Draggable control handles

For the first time, actually able to use bezier-like UX.
4 files changed, 115 insertions(+), 4 deletions(-)

A src/bez.rs
M src/grapher.rs
M src/main.rs
M src/solve.rs
A src/bez.rs => src/bez.rs +106 -0
@@ 0,0 1,106 @@
//! Custom widget for manipulating a bezier.

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

use crate::appdata::AppData;
use crate::grapher;

#[derive(Default)]
pub struct Bez {
    grab: Option<u8>,
}

const OFFSET_X: f64 = 100.0;
const OFFSET_Y: f64 = 250.0;
const SCALE: f64 = 300.0;

const HIT_RADIUS: f64 = 5.0;

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 {
                    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 {
                    self.grab = Some(1);
                    ctx.set_active(true);
                }
            }
            Event::MouseMove(e) => {
                if let Some(g) = self.grab {
                    let p = self.screen_to_design(e.pos);
                    if g == 0 {
                        data.px1 = p.x;
                        data.py1 = p.y;
                    } else if g == 1 {
                        data.px2 = p.x;
                        data.py2 = p.y;
                    }
                    println!("dragging {}", g);
                }
            }
            Event::MouseUp(_e) => {
                self.grab = None;
                ctx.set_active(false);
            }
            _ => (),
        }
    }

    fn lifecycle(
        &mut self,
        _ctx: &mut LifeCycleCtx,
        _event: &LifeCycle,
        _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();
        }
    }

    fn layout(
        &mut self,
        _ctx: &mut LayoutCtx,
        bc: &BoxConstraints,
        _data: &AppData,
        _env: &Env,
    ) -> Size {
        bc.max()
    }

    fn paint(&mut self, ctx: &mut PaintCtx, data: &AppData, _env: &Env) {
        let a2 = Affine::translate(Vec2::new(OFFSET_X, OFFSET_Y)) * Affine::FLIP_Y * Affine::scale(SCALE);
        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();
        grapher::plot_xy(ctx, &curve.pts, Point::new(OFFSET_X, OFFSET_Y), SCALE);
    }
}

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);
        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);
        a.inverse() * design
    }
}

M src/grapher.rs => src/grapher.rs +1 -1
@@ 24,7 24,7 @@ fn plot(ctx: &mut PaintCtx, seq: &[f64], origin: Point, width: f64, scale: f64) 
    ctx.stroke(path, &Color::WHITE, 1.0);
}

fn plot_xy(ctx: &mut PaintCtx, seq: &[Point], origin: Point, scale: f64) {
pub fn plot_xy(ctx: &mut PaintCtx, seq: &[Point], origin: Point, scale: f64) {
    let mut path = BezPath::new();
    for i in 0..seq.len() {
        let p = origin + scale * seq[i].to_vec2();

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

mod appdata;
mod bez;
mod bezoid;
mod grapher;
mod solve;


@@ 63,7 64,10 @@ fn ui_builder() -> impl Widget<AppData> {
        .with_range(-1.0, 1.0)
        .lens(AppData::py2)
        .padding(5.0);
    let grapher = grapher::Grapher;

    // TODO: set up tabs widget or suchlike
    //let main_widget = grapher::Grapher;
    let main_widget = bez::Bez::default();

    Flex::column()
        .with_child(label0)


@@ 78,6 82,6 @@ fn ui_builder() -> impl Widget<AppData> {
        .with_child(sliderpy1)
        .with_child(sliderpx2)
        .with_child(sliderpy2)
        .with_flex_child(grapher, 1.0)
        .with_flex_child(main_widget, 1.0)
        .must_fill_main_axis(true)
}

M src/solve.rs => src/solve.rs +2 -1
@@ 15,7 15,8 @@ impl Solver {
        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 bias = 2.0 - a.powf(1.0 / BEZ_BIAS_EXP);
            bias.max(-1.0)
        }
        let v1 = p1.to_vec2();
        let v2 = Point::new(1.0, 0.0) - p2;