From 416932a21af86060e6dcfd90fc5c52e1b76f7cd6 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sun, 12 Nov 2023 09:56:45 -0800 Subject: [PATCH 01/11] Add interactive Bezier toy This contains a prototype of Euler spiral based offset and flatten, and will become an interactive figure in the corresponding blog post. --- _figures/.gitignore | 2 + _figures/beztoy/.gitignore | 2 + _figures/beztoy/Cargo.lock | 209 +++++++++++++++++++++++++ _figures/beztoy/Cargo.toml | 11 ++ _figures/beztoy/README.md | 5 + _figures/beztoy/index.html | 7 + _figures/beztoy/src/euler.rs | 255 +++++++++++++++++++++++++++++++ _figures/beztoy/src/flatten.rs | 91 +++++++++++ _figures/beztoy/src/main.rs | 138 +++++++++++++++++ _figures/grid_robustness.drawio | 1 + _figures/simplify_figs/.DS_Store | Bin 0 -> 6148 bytes _figures/xilem_view.drawio | 1 + 12 files changed, 722 insertions(+) create mode 100644 _figures/.gitignore create mode 100644 _figures/beztoy/.gitignore create mode 100644 _figures/beztoy/Cargo.lock create mode 100644 _figures/beztoy/Cargo.toml create mode 100644 _figures/beztoy/README.md create mode 100644 _figures/beztoy/index.html create mode 100644 _figures/beztoy/src/euler.rs create mode 100644 _figures/beztoy/src/flatten.rs create mode 100644 _figures/beztoy/src/main.rs create mode 100644 _figures/grid_robustness.drawio create mode 100644 _figures/simplify_figs/.DS_Store create mode 100644 _figures/xilem_view.drawio diff --git a/_figures/.gitignore b/_figures/.gitignore new file mode 100644 index 0000000..4f96631 --- /dev/null +++ b/_figures/.gitignore @@ -0,0 +1,2 @@ +/target +/dist diff --git a/_figures/beztoy/.gitignore b/_figures/beztoy/.gitignore new file mode 100644 index 0000000..4f96631 --- /dev/null +++ b/_figures/beztoy/.gitignore @@ -0,0 +1,2 @@ +/target +/dist diff --git a/_figures/beztoy/Cargo.lock b/_figures/beztoy/Cargo.lock new file mode 100644 index 0000000..2ca0d99 --- /dev/null +++ b/_figures/beztoy/Cargo.lock @@ -0,0 +1,209 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "beztoy" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "wasm-bindgen", + "web-sys", + "xilem_svg", +] + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "peniko" +version = "0.1.0" +source = "git+https://github.com/linebender/peniko?rev=629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa#629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa" +dependencies = [ + "kurbo", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "xilem_core" +version = "0.1.0" +source = "git+https://github.com/linebender/xilem?rev=71d1db04dce2dce2264817177575067bec803932#71d1db04dce2dce2264817177575067bec803932" + +[[package]] +name = "xilem_svg" +version = "0.1.0" +source = "git+https://github.com/linebender/xilem?rev=71d1db04dce2dce2264817177575067bec803932#71d1db04dce2dce2264817177575067bec803932" +dependencies = [ + "bitflags", + "peniko", + "wasm-bindgen", + "web-sys", + "xilem_core", +] diff --git a/_figures/beztoy/Cargo.toml b/_figures/beztoy/Cargo.toml new file mode 100644 index 0000000..1e35dc7 --- /dev/null +++ b/_figures/beztoy/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "beztoy" +license = "Apache-2.0" +version = "0.1.0" +edition = "2021" + +[dependencies] +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2.87" +web-sys = "0.3.64" +xilem_svg = { git = "https://github.com/linebender/xilem", rev = "71d1db04dce2dce2264817177575067bec803932"} diff --git a/_figures/beztoy/README.md b/_figures/beztoy/README.md new file mode 100644 index 0000000..f54a358 --- /dev/null +++ b/_figures/beztoy/README.md @@ -0,0 +1,5 @@ +# beztoy + +A toy for experimenting with Bézier curves, intended to become a testbed for Euler spiral based stroke expansion. + +Run with `trunk serve`, then navigate to the webpage. diff --git a/_figures/beztoy/index.html b/_figures/beztoy/index.html new file mode 100644 index 0000000..693ecd3 --- /dev/null +++ b/_figures/beztoy/index.html @@ -0,0 +1,7 @@ + + + + + + diff --git a/_figures/beztoy/src/euler.rs b/_figures/beztoy/src/euler.rs new file mode 100644 index 0000000..3b62200 --- /dev/null +++ b/_figures/beztoy/src/euler.rs @@ -0,0 +1,255 @@ +// Copyright 2023 the raphlinus.github.io Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Calculations and utilities for Euler spirals + +use xilem_svg::kurbo::{CubicBez, ParamCurve, Point, Vec2}; + +#[derive(Debug)] +pub struct CubicParams { + pub th0: f64, + pub th1: f64, + pub d0: f64, + pub d1: f64, +} + +#[derive(Debug)] +pub struct EulerParams { + pub th0: f64, + pub th1: f64, + pub k0: f64, + pub k1: f64, + pub ch: f64, +} + +#[derive(Debug)] +pub struct EulerSeg { + pub p0: Point, + pub p1: Point, + pub params: EulerParams, +} + +pub struct CubicToEulerIter { + c: CubicBez, + tolerance: f64, + // [t0 * dt .. (t0 + 1) * dt] is the range we're currently considering + t0: u64, + dt: f64, +} + +impl CubicParams { + pub fn from_cubic(c: CubicBez) -> Self { + let chord = c.p3 - c.p0; + // TODO: if chord is 0, we have a problem + let d01 = c.p1 - c.p0; + let h0 = Vec2::new( + d01.x * chord.x + d01.y * chord.y, + d01.y * chord.x - d01.x * chord.y, + ); + let th0 = h0.atan2(); + let d0 = h0.hypot() / chord.hypot2(); + let d23 = c.p3 - c.p2; + let h1 = Vec2::new( + d23.x * chord.x + d23.y * chord.y, + d23.x * chord.y - d23.y * chord.x, + ); + let th1 = h1.atan2(); + let d1 = h1.hypot() / chord.hypot2(); + CubicParams { th0, th1, d0, d1 } + } + + // Estimated error of GH to Euler spiral + // + // Return value is normalized to chord - to get actual error, multiply + // by chord. + pub fn est_euler_err(&self) -> f64 { + // Potential optimization: work with unit vector rather than angle + let e0 = (2. / 3.) / (1.0 + self.th0.cos()); + let e1 = (2. / 3.) / (1.0 + self.th1.cos()); + let s0 = self.th0.sin(); + let s1 = self.th1.sin(); + let s01 = (s0 + s1).sin(); + let amin = 0.15 * (2. * e0 * s0 + 2. * e1 * s1 - e0 * e1 * s01); + let a = 0.15 * (2. * self.d0 * s0 + 2. * self.d1 * s1 - self.d0 * self.d1 * s01); + let aerr = (a - amin).abs(); + let symm = (self.th0 + self.th1).abs(); + let asymm = (self.th0 - self.th1).abs(); + let dist = (self.d0 - e0).hypot(self.d1 - e1); + let ctr = 3.7e-6 * symm.powi(5) + 6e-3 * asymm * symm.powi(2); + let halo_symm = 5e-3 * symm * dist; + let halo_asymm = 7e-2 * asymm * dist; + 1.25 * ctr + 1.55 * aerr + halo_symm + halo_asymm + } +} + +impl EulerParams { + pub fn from_angles(th0: f64, th1: f64) -> EulerParams { + let k0 = th0 + th1; + let dth = th1 - th0; + let d2 = dth * dth; + let k2 = k0 * k0; + let mut a = 6.0; + a -= d2 * (1. / 70.); + a -= (d2 * d2) * (1. / 10780.); + a += (d2 * d2 * d2) * 2.769178184818219e-07; + let b = -0.1 + d2 * (1. / 4200.) + d2 * d2 * 1.6959677820260655e-05; + let c = -1. / 1400. + d2 * 6.84915970574303e-05 - k2 * 7.936475029053326e-06; + a += (b + c * k2) * k2; + let k1 = dth * a; + + // calculation of chord + let mut ch = 1.0; + ch -= d2 * (1. / 40.); + ch += (d2 * d2) * 0.00034226190482569864; + ch -= (d2 * d2 * d2) * 1.9349474568904524e-06; + let b = -1. / 24. + d2 * 0.0024702380951963226 - d2 * d2 * 3.7297408997537985e-05; + let c = 1. / 1920. - d2 * 4.87350869747975e-05 - k2 * 3.1001936068463107e-06; + ch += (b + c * k2) * k2; + EulerParams { + th0, + th1, + k0, + k1, + ch, + } + } + + pub fn eval_th(&self, t: f64) -> f64 { + (self.k0 + 0.5 * self.k1 * (t - 1.0)) * t - self.th0 + } + + /// Evaluate the curve at the given parameter. + /// + /// The parameter is in the range 0..1, and the result goes from (0, 0) to (1, 0). + fn eval(&self, t: f64) -> Point { + let thm = self.eval_th(t * 0.5); + let k0 = self.k0; + let k1 = self.k1; + let (u, v) = integ_euler_10((k0 + k1 * (0.5 * t - 0.5)) * t, k1 * t * t); + let s = t / self.ch * thm.sin(); + let c = t / self.ch * thm.cos(); + let x = u * c - v * s; + let y = -v * c - u * s; + Point::new(x, y) + } + + fn eval_with_offset(&self, t: f64, offset: f64) -> Point { + let th = self.eval_th(t); + let v = Vec2::new(offset * th.sin(), offset * th.cos()); + self.eval(t) + v + } +} + +impl EulerSeg { + pub fn from_params(p0: Point, p1: Point, params: EulerParams) -> Self { + EulerSeg { p0, p1, params } + } + + /// Use two-parabola approximation. + pub fn to_cubic(&self) -> CubicBez { + let (s0, c0) = self.params.th0.sin_cos(); + let (s1, c1) = self.params.th1.sin_cos(); + let d0 = (2. / 3.) / (1.0 + c0); + let d1 = (2. / 3.) / (1.0 + c1); + let chord = self.p1 - self.p0; + let p1 = self.p0 + d0 * Vec2::new(chord.x * c0 - chord.y * s0, chord.y * c0 + chord.x * s0); + let p2 = self.p1 - d1 * Vec2::new(chord.x * c1 + chord.y * s1, chord.y * c1 - chord.x * s1); + CubicBez::new(self.p0, p1, p2, self.p1) + } + + #[allow(unused)] + pub fn eval(&self, t: f64) -> Point { + let Point { x, y } = self.params.eval(t); + let chord = self.p1 - self.p0; + Point::new( + self.p0.x + chord.x * x - chord.y * y, + self.p0.y + chord.x * y + chord.y * x, + ) + } + + pub fn eval_with_offset(&self, t: f64, offset: f64) -> Point { + let chord = self.p1 - self.p0; + let scaled = offset / chord.hypot(); + let Point { x, y } = self.params.eval_with_offset(t, scaled); + Point::new( + self.p0.x + chord.x * x - chord.y * y, + self.p0.y + chord.x * y + chord.y * x, + ) + } +} + +impl Iterator for CubicToEulerIter { + type Item = EulerSeg; + + fn next(&mut self) -> Option { + let t0 = (self.t0 as f64) * self.dt; + if t0 == 1.0 { + return None; + } + loop { + let t1 = t0 + self.dt; + let cubic = self.c.subsegment(t0..t1); + let cubic_params = CubicParams::from_cubic(cubic); + let est_err: f64 = cubic_params.est_euler_err(); + let err = est_err * cubic.p0.distance(cubic.p3); + if err <= self.tolerance { + self.t0 += 1; + let shift = self.t0.trailing_zeros(); + self.t0 >>= shift; + self.dt *= (1 << shift) as f64; + let euler_params = EulerParams::from_angles(cubic_params.th0, cubic_params.th1); + let es = EulerSeg::from_params(cubic.p0, cubic.p3, euler_params); + return Some(es); + } + self.t0 *= 2; + self.dt *= 0.5; + } + } +} + +impl CubicToEulerIter { + pub fn new(c: CubicBez, tolerance: f64) -> Self { + CubicToEulerIter { + c, + tolerance, + t0: 0, + dt: 1.0, + } + } +} + +/// Integrate Euler spiral. +/// +/// TODO: investigate needed accuracy. We might be able to get away +/// with 8th order. +fn integ_euler_10(k0: f64, k1: f64) -> (f64, f64) { + let t1_1 = k0; + let t1_2 = 0.5 * k1; + let t2_2 = t1_1 * t1_1; + let t2_3 = 2. * (t1_1 * t1_2); + let t2_4 = t1_2 * t1_2; + let t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + let t3_6 = t2_4 * t1_2; + let t4_4 = t2_2 * t2_2; + let t4_5 = 2. * (t2_2 * t2_3); + let t4_6 = 2. * (t2_2 * t2_4) + t2_3 * t2_3; + let t4_7 = 2. * (t2_3 * t2_4); + let t4_8 = t2_4 * t2_4; + let t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + let t5_8 = t4_6 * t1_2 + t4_7 * t1_1; + let t6_6 = t4_4 * t2_2; + let t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + let t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + let t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + let t8_8 = t6_6 * t2_2; + let mut u = 1.; + u -= (1. / 24.) * t2_2 + (1. / 160.) * t2_4; + u += (1. / 1920.) * t4_4 + (1. / 10752.) * t4_6 + (1. / 55296.) * t4_8; + u -= (1. / 322560.) * t6_6 + (1. / 1658880.) * t6_8; + u += (1. / 92897280.) * t8_8; + let mut v = (1. / 12.) * t1_2; + v -= (1. / 480.) * t3_4 + (1. / 2688.) * t3_6; + v += (1. / 53760.) * t5_6 + (1. / 276480.) * t5_8; + v -= (1. / 11612160.) * t7_8; + (u, v) +} diff --git a/_figures/beztoy/src/flatten.rs b/_figures/beztoy/src/flatten.rs new file mode 100644 index 0000000..52790b6 --- /dev/null +++ b/_figures/beztoy/src/flatten.rs @@ -0,0 +1,91 @@ +// Copyright 2023 the raphlinus.github.io Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Math for flattening of Euler spiral parallel curve + +use std::f64::consts::FRAC_PI_4; + +use xilem_svg::kurbo::BezPath; + +use crate::euler::EulerSeg; + +pub fn flatten_offset(iter: impl Iterator, offset: f64) -> BezPath { + let mut result = BezPath::new(); + let mut first = true; + for es in iter { + if core::mem::take(&mut first) { + result.move_to(es.eval_with_offset(0.0, offset)); + } + let scale = es.p0.distance(es.p1); + let tol = 1.0; + let (k0, k1) = (es.params.k0 - 0.5 * es.params.k1, es.params.k1); + // compute forward integral to determine number of subdivisions + let dist_scaled = offset / scale; + let a = -2.0 * dist_scaled * k1; + let b = -1.0 - 2.0 * dist_scaled * k0; + let int0 = espc_int_approx(b); + let int1 = espc_int_approx(a + b); + let integral = int1 - int0; + let k_peak = k0 - k1 * b / a; + let integrand_peak = (k_peak * (k_peak * dist_scaled + 1.0)).abs().sqrt(); + let scaled_int = integral * integrand_peak / a; + let n_frac = 0.5 * (scale / tol).sqrt() * scaled_int; + let n = n_frac.ceil(); + for i in 0..n as usize { + let t = (i + 1) as f64 / n; + let inv = espc_int_inv_approx(integral * t + int0); + let s = (inv - b) / a; + result.line_to(es.eval_with_offset(s, offset)); + } + } + result +} + +const BREAK1: f64 = 0.8; +const BREAK2: f64 = 1.25; +const BREAK3: f64 = 2.1; +const SIN_SCALE: f64 = 1.0976991822760038; +const QUAD_A1: f64 = 0.6406; +const QUAD_B1: f64 = -0.81; +const QUAD_C1: f64 = 0.9148117935952064; +const QUAD_A2: f64 = 0.5; +const QUAD_B2: f64 = -0.156; +const QUAD_C2: f64 = 0.16145779359520596; + +fn espc_int_approx(x: f64) -> f64 { + let y = x.abs(); + let a = if y < BREAK1 { + (SIN_SCALE * y).sin() * (1.0 / SIN_SCALE) + } else if y < BREAK2 { + (8.0f64.sqrt() / 3.0) * (y - 1.0) * (y - 1.0).abs().sqrt() + FRAC_PI_4 + } else { + let (a, b, c) = if y < BREAK3 { + (QUAD_A1, QUAD_B1, QUAD_C1) + } else { + (QUAD_A2, QUAD_B2, QUAD_C2) + }; + a * y * y + b * y + c + }; + a.copysign(x) +} + +fn espc_int_inv_approx(x: f64) -> f64 { + let y = x.abs(); + let a = if y < 0.7010707591262915 { + (x * SIN_SCALE).asin() * (1.0 / SIN_SCALE) + } else if y < 0.903249293595206 { + let b = y - FRAC_PI_4; + let u = b.abs().powf(2. / 3.).copysign(b); + u * (9.0f64 / 8.).cbrt() + 1.0 + } else { + let (u, v, w) = if y < 2.038857793595206 { + const B: f64 = 0.5 * QUAD_B1 / QUAD_A1; + (B * B - QUAD_C1 / QUAD_A1, 1.0 / QUAD_A1, B) + } else { + const B: f64 = 0.5 * QUAD_B2 / QUAD_A2; + (B * B - QUAD_C2 / QUAD_A2, 1.0 / QUAD_A2, B) + }; + (u + v * y).sqrt() - w + }; + a.copysign(x) +} diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs new file mode 100644 index 0000000..f3faf13 --- /dev/null +++ b/_figures/beztoy/src/main.rs @@ -0,0 +1,138 @@ +// Copyright 2023 the raphlinus.github.io Authors +// SPDX-License-Identifier: Apache-2.0 + +//! An interactive toy for experimenting with rendering of Bézier paths, +//! including Euler spiral based stroke expansion. + +mod euler; +mod flatten; + +use xilem_svg::{ + group, + kurbo::{BezPath, Circle, CubicBez, Line, Point, Shape}, + peniko::Color, + App, PointerMsg, View, ViewExt, +}; + +use crate::{ + euler::{CubicParams, CubicToEulerIter}, + flatten::flatten_offset, +}; + +#[derive(Default)] +struct AppState { + p0: Point, + p1: Point, + p2: Point, + p3: Point, + grab: GrabState, +} + +#[derive(Default)] +struct GrabState { + is_down: bool, + id: i32, + dx: f64, + dy: f64, +} + +impl GrabState { + fn handle(&mut self, pt: &mut Point, p: &PointerMsg) { + match p { + PointerMsg::Down(e) => { + if e.button == 0 { + self.dx = pt.x - e.x; + self.dy = pt.y - e.y; + self.id = e.id; + self.is_down = true; + } + } + PointerMsg::Move(e) => { + if self.is_down && self.id == e.id { + pt.x = self.dx + e.x; + pt.y = self.dy + e.y; + } + } + PointerMsg::Up(e) => { + if self.id == e.id { + self.is_down = false; + } + } + } + } +} + +// https://iamkate.com/data/12-bit-rainbow/ +const RAINBOW_PALETTE: [Color; 12] = [ + Color::rgb8(0x88, 0x11, 0x66), + Color::rgb8(0xaa, 0x33, 0x55), + Color::rgb8(0xcc, 0x66, 0x66), + Color::rgb8(0xee, 0x99, 0x44), + Color::rgb8(0xee, 0xdd, 0x00), + Color::rgb8(0x99, 0xdd, 0x55), + Color::rgb8(0x44, 0xdd, 0x88), + Color::rgb8(0x22, 0xcc, 0xbb), + Color::rgb8(0x00, 0xbb, 0xcc), + Color::rgb8(0x00, 0x99, 0xcc), + Color::rgb8(0x33, 0x66, 0xbb), + Color::rgb8(0x66, 0x33, 0x99), +]; + +fn app_logic(state: &mut AppState) -> impl View { + let mut path = BezPath::new(); + path.move_to(state.p0); + path.curve_to(state.p1, state.p2, state.p3); + let stroke = xilem_svg::kurbo::Stroke::new(2.0); + let stroke_thick = xilem_svg::kurbo::Stroke::new(8.0); + let stroke_thin = xilem_svg::kurbo::Stroke::new(1.0); + const NONE: Color = Color::rgba8(0, 0, 0, 0); + const HANDLE_RADIUS: f64 = 4.0; + let c = CubicBez::new(state.p0, state.p1, state.p2, state.p3); + let params = CubicParams::from_cubic(c); + let err = params.est_euler_err(); + let mut spirals = vec![]; + for (i, es) in CubicToEulerIter::new(c, 1.0).enumerate() { + for i in 0..10 { + let t = i as f64 * 0.1; + es.params.eval_th(t); + } + let path = es.to_cubic().into_path(1.0); + let color = RAINBOW_PALETTE[(i * 7) % 12]; + spirals.push(path.stroke(color, stroke_thick.clone())); + } + let offset = 40.0; + let flat = flatten_offset(CubicToEulerIter::new(c, 1.0), offset); + group(( + group(spirals).fill(NONE), + path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), + flat.stroke(Color::BLUE, stroke_thin).fill(NONE), + Line::new(state.p0, state.p1) + .stroke(Color::BLUE, stroke.clone()) + .fill(NONE), + Line::new(state.p2, state.p3) + .stroke(Color::BLUE, stroke.clone()) + .fill(NONE), + Line::new((790., 300.), (790., 300. - 1000. * err)) + .stroke(Color::RED, stroke.clone()) + .fill(NONE), + Circle::new(state.p0, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p0, &msg)), + Circle::new(state.p1, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p1, &msg)), + Circle::new(state.p2, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p2, &msg)), + Circle::new(state.p3, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p3, &msg)), + )) +} + +pub fn main() { + console_error_panic_hook::set_once(); + let mut state = AppState::default(); + state.p0 = Point::new(100.0, 100.0); + state.p1 = Point::new(300.0, 150.0); + state.p2 = Point::new(500.0, 150.0); + state.p3 = Point::new(700.0, 150.0); + let app = App::new(state, app_logic); + app.run(); +} diff --git a/_figures/grid_robustness.drawio b/_figures/grid_robustness.drawio new file mode 100644 index 0000000..9add686 --- /dev/null +++ b/_figures/grid_robustness.drawio @@ -0,0 +1 @@ +5VZdb5swFP01PK4CjEn62NJsU6d2raKozxa+AasGR45Tkv76mWADDkmzRam6dXmI7HM/bJ9zdIWHkmL9TZJFficocC/06dpDN14YXmJf/9fApgHiUdwAmWS0gYIOmLJXMKCpy1aMwtJJVEJwxRYumIqyhFQ5GJFSVG7aXHD31AXJYABMU8KH6BOjKm/QsX1WjX8HluX25MA3kYLYZAMsc0JF1YPQxEOJFEI1q2KdAK+5s7w0dV8PRNuLSSjV7xTMHuR88nr/YxYr/Pzz7v7xdnb7xXR5IXxlHmwuqzaWASlWJYW6ie+h6ypnCqYLktbRSkuusVwVXO8CvZwzzhPBhdzWIophTCONL5UUz9CL+NufjpgLgFSwPviyoOVL+wxEAUpudIotiA3FxmPh2OyrTrE2J++rZUFiXJK1vTsi9cJw+Qe8hsd5hZJe1QbVu1KU4PLokt7UAh1Y9ShDPQbwHgIsJoETxV7c9vtIMSc8CKYP7gQIXAEQ3uF1KVYyBVPV9+hOI+0LV8lwp5EiMgM1aLTVqH326bLFZ5VtYPokcUz/pqCHbfVhKuMdcS7xBT5N52B0tNU7K40+w+CLxn/d4Iv+q8EXnWvw4Q8efPh9B5/v16PvHxA0GEV2ELWj6URJ97QanUtUve0+HJv07usbTX4B \ No newline at end of file diff --git a/_figures/simplify_figs/.DS_Store b/_figures/simplify_figs/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..81229e8d26382bd3469cb3d9bb2de3f1954844ac GIT binary patch literal 6148 zcmeH~Jqp4=5QS&dB4Cr!avKle4VIuM@B*S@B?yZB9^E%TjnP_yyn&f-XEsBUS7b9H zqQmpN5$Q#wgBxXSVPuMYE)TiO>2iLYjtb*@FJ*K=2U&T%hcRwa*e@u>x3=Er<$CqZN!+^)bZi z-VT<$t|nVB+C_8t(7dzS6a&*}7cEF&S{)2jfC`Khm`C2*`M-mIoBu~GOsN1B_%j7` zvE6S6yi}g8AFpTiLso6w;GkcQ@b(jc#E#+>+ztE17GO=bASy8a2)GOkRN$uyya2`( B5q1Co literal 0 HcmV?d00001 diff --git a/_figures/xilem_view.drawio b/_figures/xilem_view.drawio new file mode 100644 index 0000000..e4dc1f3 --- /dev/null +++ b/_figures/xilem_view.drawio @@ -0,0 +1 @@ +zVZNc5swEP01HONByHz4mDiue/FMZtxpm6MKG9AEEBXC4P76Ckvi027STDu2L9a+Xa2W994ILLzOmi0nRbJjEaSWY0eNhR8tx0EIL+VfixwV4nm+AmJOI13UA3v6CzRoa7SiEZSjQsFYKmgxBkOW5xCKEUY4Z/W47IWl41MLEsMM2IcknaPfaCQShQau3eOfgcaJORnZOpMRU6yBMiERqwcQ3lh4zRkTapU1a0hb8gwvat+nC9luMA65eM+G7et2x35w3uyeANYlL3Jvc6e7HEha6Qe2HC+V/R5emGwrpxZHTYX3s2ImcVeehLqXBSgomj4pV3H7/3UvSPhqesmhVDuV1Hx0nR3OqjyCdk5bpuuECtgXJGyztbSVxBKRpTJC3e4DcAHNRSpQR7B0JrAMBD/KErNhpTUxptRh3SuMjGzJQF1TR7Sp4q5zz7tcaOr/QgbnogxlQfKPy/AFGjEQQTW7EREmGqDg2iLg/yTCQyUEy29VBie4NR2WZ3SYkAR5dN9e7jIKU1KWNBzzMiaxVUm/WVAgY8kMP35vkwvXhM+69hQ8NqPoqCM1BkSzN8aEazkqq3gIb9+7gvAYxFsXw1y7gTbuGWkMxiElgh7G457TS5/wxOjp0m8mVphaw7RQj6l3DV89k0YOnjTCk0aKh1mjk326x/64o/xbdRQ0VKhtvqvDZ31gu+53tcEVbYivaUMHowVeYWTj5TJwfHfl/tlM73Ul8v2FjVf9z5u43f9HJpVh/5mnyvuPZbz5DQ== \ No newline at end of file From 730ae074b3a0843a8528b7c214b58e26011541b8 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Mon, 13 Nov 2023 07:18:16 -0800 Subject: [PATCH 02/11] Add visualization of subdivision points --- _figures/beztoy/index.html | 1 + _figures/beztoy/src/main.rs | 31 ++++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/_figures/beztoy/index.html b/_figures/beztoy/index.html index 693ecd3..854c877 100644 --- a/_figures/beztoy/index.html +++ b/_figures/beztoy/index.html @@ -4,4 +4,5 @@ +

See raphlinus#100 for more context.

diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index f3faf13..2368c92 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -9,7 +9,7 @@ mod flatten; use xilem_svg::{ group, - kurbo::{BezPath, Circle, CubicBez, Line, Point, Shape}, + kurbo::{BezPath, Circle, CubicBez, Line, Point, Shape, PathEl}, peniko::Color, App, PointerMsg, View, ViewExt, }; @@ -102,10 +102,21 @@ fn app_logic(state: &mut AppState) -> impl View { } let offset = 40.0; let flat = flatten_offset(CubicToEulerIter::new(c, 1.0), offset); + let mut flat_pts = vec![]; + for seg in flat.elements() { + match seg { + PathEl::MoveTo(p) | PathEl::LineTo(p) => { + let circle = Circle::new(*p, 2.0).fill(Color::BLACK); + flat_pts.push(circle); + } + _ => (), + } + } group(( group(spirals).fill(NONE), path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), flat.stroke(Color::BLUE, stroke_thin).fill(NONE), + group(flat_pts), Line::new(state.p0, state.p1) .stroke(Color::BLUE, stroke.clone()) .fill(NONE), @@ -115,14 +126,16 @@ fn app_logic(state: &mut AppState) -> impl View { Line::new((790., 300.), (790., 300. - 1000. * err)) .stroke(Color::RED, stroke.clone()) .fill(NONE), - Circle::new(state.p0, HANDLE_RADIUS) - .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p0, &msg)), - Circle::new(state.p1, HANDLE_RADIUS) - .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p1, &msg)), - Circle::new(state.p2, HANDLE_RADIUS) - .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p2, &msg)), - Circle::new(state.p3, HANDLE_RADIUS) - .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p3, &msg)), + group(( + Circle::new(state.p0, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p0, &msg)), + Circle::new(state.p1, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p1, &msg)), + Circle::new(state.p2, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p2, &msg)), + Circle::new(state.p3, HANDLE_RADIUS) + .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p3, &msg)), + )), )) } From cc28060ea9b34ace9e658dbb37381c33b1c4d31b Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sun, 10 Dec 2023 08:02:12 -0800 Subject: [PATCH 03/11] Draw both sides of stroke --- _config.yml | 4 + _figures/beztoy/Cargo.lock | 209 ---------------------------------- _figures/beztoy/Trunk.toml | 4 + _figures/beztoy/beztoy.tar.gz | Bin 0 -> 72071 bytes _figures/beztoy/index.html | 6 + _figures/beztoy/src/main.rs | 11 +- 6 files changed, 21 insertions(+), 213 deletions(-) delete mode 100644 _figures/beztoy/Cargo.lock create mode 100644 _figures/beztoy/Trunk.toml create mode 100644 _figures/beztoy/beztoy.tar.gz diff --git a/_config.yml b/_config.yml index d6b2c72..73f5e6d 100644 --- a/_config.yml +++ b/_config.yml @@ -41,3 +41,7 @@ plugins: # - vendor/cache/ # - vendor/gems/ # - vendor/ruby/ + +exclude: + - Cargo.toml + - Cargo.lock diff --git a/_figures/beztoy/Cargo.lock b/_figures/beztoy/Cargo.lock deleted file mode 100644 index 2ca0d99..0000000 --- a/_figures/beztoy/Cargo.lock +++ /dev/null @@ -1,209 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "beztoy" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "wasm-bindgen", - "web-sys", - "xilem_svg", -] - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kurbo" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" -dependencies = [ - "arrayvec", - "smallvec", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "peniko" -version = "0.1.0" -source = "git+https://github.com/linebender/peniko?rev=629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa#629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa" -dependencies = [ - "kurbo", - "smallvec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "smallvec" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" - -[[package]] -name = "syn" -version = "2.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "xilem_core" -version = "0.1.0" -source = "git+https://github.com/linebender/xilem?rev=71d1db04dce2dce2264817177575067bec803932#71d1db04dce2dce2264817177575067bec803932" - -[[package]] -name = "xilem_svg" -version = "0.1.0" -source = "git+https://github.com/linebender/xilem?rev=71d1db04dce2dce2264817177575067bec803932#71d1db04dce2dce2264817177575067bec803932" -dependencies = [ - "bitflags", - "peniko", - "wasm-bindgen", - "web-sys", - "xilem_core", -] diff --git a/_figures/beztoy/Trunk.toml b/_figures/beztoy/Trunk.toml new file mode 100644 index 0000000..af4efa9 --- /dev/null +++ b/_figures/beztoy/Trunk.toml @@ -0,0 +1,4 @@ +[build] +#public_url = "https://levien.com/tmp/beztoy/" + + diff --git a/_figures/beztoy/beztoy.tar.gz b/_figures/beztoy/beztoy.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7c1fa8d771a08f19eece78223bf7eb22593141a1 GIT binary patch literal 72071 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

n_o%|qVx_{T>Qn4>#azSWJVy6XOlWkafwD%K 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 diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index 2368c92..78e8993 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -91,7 +91,8 @@ fn app_logic(state: &mut AppState) -> impl View { let params = CubicParams::from_cubic(c); let err = params.est_euler_err(); let mut spirals = vec![]; - for (i, es) in CubicToEulerIter::new(c, 1.0).enumerate() { + const TOL: f64 = 1.0; + for (i, es) in CubicToEulerIter::new(c, TOL).enumerate() { for i in 0..10 { let t = i as f64 * 0.1; es.params.eval_th(t); @@ -101,9 +102,10 @@ fn app_logic(state: &mut AppState) -> impl View { spirals.push(path.stroke(color, stroke_thick.clone())); } let offset = 40.0; - let flat = flatten_offset(CubicToEulerIter::new(c, 1.0), offset); + let flat = flatten_offset(CubicToEulerIter::new(c, TOL), offset); + let flat2 = flatten_offset(CubicToEulerIter::new(c, TOL), -offset); let mut flat_pts = vec![]; - for seg in flat.elements() { + for seg in flat.elements().iter().chain(flat2.elements().iter()) { match seg { PathEl::MoveTo(p) | PathEl::LineTo(p) => { let circle = Circle::new(*p, 2.0).fill(Color::BLACK); @@ -115,7 +117,8 @@ fn app_logic(state: &mut AppState) -> impl View { group(( group(spirals).fill(NONE), path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), - flat.stroke(Color::BLUE, stroke_thin).fill(NONE), + flat.stroke(Color::BLUE, stroke_thin.clone()).fill(NONE), + flat2.stroke(Color::PURPLE, stroke_thin).fill(NONE), group(flat_pts), Line::new(state.p0, state.p1) .stroke(Color::BLUE, stroke.clone()) From 9918b021684f398d808b29db0e63f75d88b3209b Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Fri, 15 Dec 2023 10:23:05 -0800 Subject: [PATCH 04/11] Update to xilem_web WIP, points to pre-merge commit. Also put NONE back on the group. --- _figures/beztoy/Cargo.toml | 3 ++- _figures/beztoy/src/euler.rs | 2 +- _figures/beztoy/src/flatten.rs | 2 +- _figures/beztoy/src/main.rs | 40 +++++++++++++++------------------- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/_figures/beztoy/Cargo.toml b/_figures/beztoy/Cargo.toml index 1e35dc7..93aacef 100644 --- a/_figures/beztoy/Cargo.toml +++ b/_figures/beztoy/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" console_error_panic_hook = "0.1" wasm-bindgen = "0.2.87" web-sys = "0.3.64" -xilem_svg = { git = "https://github.com/linebender/xilem", rev = "71d1db04dce2dce2264817177575067bec803932"} +#xilem_svg = { git = "https://github.com/linebender/xilem", rev = "71d1db04dce2dce2264817177575067bec803932"} +xilem_web = { git = "https://github.com/Philipp-M/xilem", rev = "01838144540210ea14a7b337584c2dd7ff7cf5a3" } diff --git a/_figures/beztoy/src/euler.rs b/_figures/beztoy/src/euler.rs index 3b62200..66420dd 100644 --- a/_figures/beztoy/src/euler.rs +++ b/_figures/beztoy/src/euler.rs @@ -3,7 +3,7 @@ //! Calculations and utilities for Euler spirals -use xilem_svg::kurbo::{CubicBez, ParamCurve, Point, Vec2}; +use xilem_web::svg::kurbo::{CubicBez, ParamCurve, Point, Vec2}; #[derive(Debug)] pub struct CubicParams { diff --git a/_figures/beztoy/src/flatten.rs b/_figures/beztoy/src/flatten.rs index 52790b6..13278b6 100644 --- a/_figures/beztoy/src/flatten.rs +++ b/_figures/beztoy/src/flatten.rs @@ -5,7 +5,7 @@ use std::f64::consts::FRAC_PI_4; -use xilem_svg::kurbo::BezPath; +use xilem_web::svg::kurbo::BezPath; use crate::euler::EulerSeg; diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index 78e8993..724619e 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -7,12 +7,7 @@ mod euler; mod flatten; -use xilem_svg::{ - group, - kurbo::{BezPath, Circle, CubicBez, Line, Point, Shape, PathEl}, - peniko::Color, - App, PointerMsg, View, ViewExt, -}; +use xilem_web::{svg::{kurbo::{Point, BezPath, CubicBez, PathEl, Circle, Line, Shape}, peniko::Color}, PointerMsg, View, App, elements::svg::{g, svg}, document_body, interfaces::*}; use crate::{ euler::{CubicParams, CubicToEulerIter}, @@ -82,10 +77,10 @@ fn app_logic(state: &mut AppState) -> impl View { let mut path = BezPath::new(); path.move_to(state.p0); path.curve_to(state.p1, state.p2, state.p3); - let stroke = xilem_svg::kurbo::Stroke::new(2.0); - let stroke_thick = xilem_svg::kurbo::Stroke::new(8.0); - let stroke_thin = xilem_svg::kurbo::Stroke::new(1.0); - const NONE: Color = Color::rgba8(0, 0, 0, 0); + let stroke = xilem_web::svg::kurbo::Stroke::new(2.0); + let stroke_thick = xilem_web::svg::kurbo::Stroke::new(8.0); + let stroke_thin = xilem_web::svg::kurbo::Stroke::new(1.0); + const NONE: Color = Color::TRANSPARENT; const HANDLE_RADIUS: f64 = 4.0; let c = CubicBez::new(state.p0, state.p1, state.p2, state.p3); let params = CubicParams::from_cubic(c); @@ -99,7 +94,7 @@ fn app_logic(state: &mut AppState) -> impl View { } let path = es.to_cubic().into_path(1.0); let color = RAINBOW_PALETTE[(i * 7) % 12]; - spirals.push(path.stroke(color, stroke_thick.clone())); + spirals.push(path.stroke(color, stroke_thick.clone()).fill(NONE)); } let offset = 40.0; let flat = flatten_offset(CubicToEulerIter::new(c, TOL), offset); @@ -114,22 +109,19 @@ fn app_logic(state: &mut AppState) -> impl View { _ => (), } } - group(( - group(spirals).fill(NONE), + svg(g(( + g(spirals), path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), flat.stroke(Color::BLUE, stroke_thin.clone()).fill(NONE), flat2.stroke(Color::PURPLE, stroke_thin).fill(NONE), - group(flat_pts), + g(flat_pts), Line::new(state.p0, state.p1) - .stroke(Color::BLUE, stroke.clone()) - .fill(NONE), + .stroke(Color::BLUE, stroke.clone()), Line::new(state.p2, state.p3) - .stroke(Color::BLUE, stroke.clone()) - .fill(NONE), + .stroke(Color::BLUE, stroke.clone()), Line::new((790., 300.), (790., 300. - 1000. * err)) - .stroke(Color::RED, stroke.clone()) - .fill(NONE), - group(( + .stroke(Color::RED, stroke.clone()), + g(( Circle::new(state.p0, HANDLE_RADIUS) .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p0, &msg)), Circle::new(state.p1, HANDLE_RADIUS) @@ -139,7 +131,9 @@ fn app_logic(state: &mut AppState) -> impl View { Circle::new(state.p3, HANDLE_RADIUS) .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p3, &msg)), )), - )) + ))) + .attr("width", 800) + .attr("height", 600) } pub fn main() { @@ -150,5 +144,5 @@ pub fn main() { state.p2 = Point::new(500.0, 150.0); state.p3 = Point::new(700.0, 150.0); let app = App::new(state, app_logic); - app.run(); + app.run(&document_body()); } From 554605e152eb5488cb9105b0522e05a57ba695e1 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 12 Mar 2024 10:33:05 -0700 Subject: [PATCH 05/11] Improve robustness Apply robustness logic borrowed from main Vello dev: switch to derivatives, tweak derivatives to be nonzero, etc. Also improve drawing of the Euler spiral in the 180 degree U-turn case; it was drawn as a single cubic segment which is fine for small angle deviations, but makes a colinear case. This drawing is very hacky but is sufficient for visual display. --- _figures/beztoy/src/euler.rs | 102 +++++++++++++++++++++++++++++++---- _figures/beztoy/src/main.rs | 20 +++++-- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/_figures/beztoy/src/euler.rs b/_figures/beztoy/src/euler.rs index 66420dd..723bf9c 100644 --- a/_figures/beztoy/src/euler.rs +++ b/_figures/beztoy/src/euler.rs @@ -3,7 +3,7 @@ //! Calculations and utilities for Euler spirals -use xilem_web::svg::kurbo::{CubicBez, ParamCurve, Point, Vec2}; +use xilem_web::svg::kurbo::{CubicBez, Point, Vec2}; #[derive(Debug)] pub struct CubicParams { @@ -35,9 +35,35 @@ pub struct CubicToEulerIter { // [t0 * dt .. (t0 + 1) * dt] is the range we're currently considering t0: u64, dt: f64, + last_p: Vec2, + last_q: Vec2, + last_t: f64, } impl CubicParams { + /// Compute parameters from endpoints and derivatives. + pub fn from_points_derivs(p0: Vec2, p1: Vec2, q0: Vec2, q1: Vec2, dt: f64) -> Self { + let chord = p1 - p0; + // Robustness note: we must protect this function from being called when the + // chord length is (near-)zero. + let scale = dt / chord.length_squared(); + let h0 = Vec2::new( + q0.x * chord.x + q0.y * chord.y, + q0.y * chord.x - q0.x * chord.y, + ); + let th0 = h0.atan2(); + let d0 = h0.length() * scale; + let h1 = Vec2::new( + q1.x * chord.x + q1.y * chord.y, + q1.x * chord.y - q1.y * chord.x, + ); + let th1 = h1.atan2(); + let d1 = h1.length() * scale; + // Robustness note: we may want to clamp the magnitude of the angles to + // a bit less than pi. Perhaps here, perhaps downstream. + CubicParams { th0, th1, d0, d1 } + } + pub fn from_cubic(c: CubicBez) -> Self { let chord = c.p3 - c.p0; // TODO: if chord is 0, we have a problem @@ -64,11 +90,21 @@ impl CubicParams { // by chord. pub fn est_euler_err(&self) -> f64 { // Potential optimization: work with unit vector rather than angle - let e0 = (2. / 3.) / (1.0 + self.th0.cos()); - let e1 = (2. / 3.) / (1.0 + self.th1.cos()); + let cth0 = self.th0.cos(); + let cth1 = self.th1.cos(); + if cth0 * cth1 < 0.0 { + // Rationale: this happens when fitting a cusp or near-cusp with + // a near 180 degree u-turn. The actual ES is bounded in that case. + // Further subdivision won't reduce the angles if actually a cusp. + return 2.0; + } + let e0 = (2. / 3.) / (1.0 + cth0); + let e1 = (2. / 3.) / (1.0 + cth1); let s0 = self.th0.sin(); let s1 = self.th1.sin(); - let s01 = (s0 + s1).sin(); + // Note: some other versions take sin of s0 + s1 instead. Those are incorrect. + // Strangely, calibration is the same, but more work could be done. + let s01 = cth0 * s1 + cth1 * s0; let amin = 0.15 * (2. * e0 * s0 + 2. * e1 * s1 - e0 * e1 * s01); let a = 0.15 * (2. * self.d0 * s0 + 2. * self.d1 * s1 - self.d0 * self.d1 * s01); let aerr = (a - amin).abs(); @@ -138,6 +174,11 @@ impl EulerParams { let v = Vec2::new(offset * th.sin(), offset * th.cos()); self.eval(t) + v } + + // Determine whether a render as a single cubic will be adequate + pub fn cubic_ok(&self) -> bool { + self.th0.abs() < 1.0 && self.th1.abs() < 1.0 + } } impl EulerSeg { @@ -178,6 +219,21 @@ impl EulerSeg { } } +/// Evaluate both the point and derivative of a cubic bezier. +fn eval_cubic_and_deriv(c: &CubicBez, t: f64) -> (Vec2, Vec2) { + let p0 = c.p0.to_vec2(); + let p1 = c.p1.to_vec2(); + let p2 = c.p2.to_vec2(); + let p3 = c.p3.to_vec2(); + let m = 1.0 - t; + let mm = m * m; + let mt = m * t; + let tt = t * t; + let p = p0 * (mm * m) + (p1 * (3.0 * mm) + p2 * (3.0 * mt) + p3 * tt) * t; + let q = (p1 - p0) * mm + (p2 - p1) * (2.0 * mt) + (p3 - p2) * tt; + (p, q) +} + impl Iterator for CubicToEulerIter { type Item = EulerSeg; @@ -187,18 +243,33 @@ impl Iterator for CubicToEulerIter { return None; } loop { - let t1 = t0 + self.dt; - let cubic = self.c.subsegment(t0..t1); - let cubic_params = CubicParams::from_cubic(cubic); + let mut t1 = t0 + self.dt; + let p0 = self.last_p; + let q0 = self.last_q; + let (mut p1, mut q1) = eval_cubic_and_deriv(&self.c, t1); + if q1.length_squared() < DERIV_THRESH.powi(2) { + let (new_p1, new_q1) = eval_cubic_and_deriv(&self.c, t1 - DERIV_EPS); + q1 = new_q1; + if t1 < 1. { + p1 = new_p1; + t1 -= DERIV_EPS; + } + } + // TODO: robustness + let actual_dt = t1 - self.last_t; + let cubic_params = CubicParams::from_points_derivs(p0, p1, q0, q1, actual_dt); let est_err: f64 = cubic_params.est_euler_err(); - let err = est_err * cubic.p0.distance(cubic.p3); + let err = est_err * (p0 - p1).hypot(); if err <= self.tolerance { self.t0 += 1; let shift = self.t0.trailing_zeros(); self.t0 >>= shift; self.dt *= (1 << shift) as f64; let euler_params = EulerParams::from_angles(cubic_params.th0, cubic_params.th1); - let es = EulerSeg::from_params(cubic.p0, cubic.p3, euler_params); + let es = EulerSeg::from_params(p0.to_point(), p1.to_point(), euler_params); + self.last_p = p1; + self.last_q = q1; + self.last_t = t1; return Some(es); } self.t0 *= 2; @@ -207,13 +278,26 @@ impl Iterator for CubicToEulerIter { } } +/// Threshold below which a derivative is considered too small. +const DERIV_THRESH: f64 = 1e-6; +/// Amount to nudge t when derivative is near-zero. +const DERIV_EPS: f64 = 1e-6; + impl CubicToEulerIter { pub fn new(c: CubicBez, tolerance: f64) -> Self { + let mut last_q = c.p1 - c.p0; + // TODO: tweak + if last_q.length_squared() < DERIV_THRESH.powi(2) { + last_q = eval_cubic_and_deriv(&c, DERIV_EPS).1; + } CubicToEulerIter { c, tolerance, t0: 0, dt: 1.0, + last_p: c.p0.to_vec2(), + last_q, + last_t: 0.0, } } } diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index 724619e..d24ae65 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -88,11 +88,21 @@ fn app_logic(state: &mut AppState) -> impl View { let mut spirals = vec![]; const TOL: f64 = 1.0; for (i, es) in CubicToEulerIter::new(c, TOL).enumerate() { - for i in 0..10 { - let t = i as f64 * 0.1; - es.params.eval_th(t); - } - let path = es.to_cubic().into_path(1.0); + let path = if es.params.cubic_ok() { + es.to_cubic().into_path(1.0) + } else { + // Janky rendering, we should be more sophisticated + // and subdivide into cubics with appropriate bounds + let mut path = BezPath::new(); + const N: usize = 20; + path.move_to(es.p0); + for i in 1..N { + let t = i as f64 / N as f64; + path.line_to(es.eval(t)); + } + path.line_to(es.p1); + path + }; let color = RAINBOW_PALETTE[(i * 7) % 12]; spirals.push(path.stroke(color, stroke_thick.clone()).fill(NONE)); } From 24e6cc4d61afbd2222993c3b73608cdd3108a3c5 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sat, 30 Mar 2024 13:45:00 -0700 Subject: [PATCH 06/11] Lower cubic to arcs via Euler spirals Implement idea of converting Euler spirals to arcs using simple error metric. Visualize arcs in red, original curve in black. --- _figures/beztoy/src/arc.rs | 38 +++++++++++++++++++++++++ _figures/beztoy/src/main.rs | 55 ++++++++++++++++++++++++------------- 2 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 _figures/beztoy/src/arc.rs diff --git a/_figures/beztoy/src/arc.rs b/_figures/beztoy/src/arc.rs new file mode 100644 index 0000000..1bd5ed1 --- /dev/null +++ b/_figures/beztoy/src/arc.rs @@ -0,0 +1,38 @@ +// Copyright 2023 the raphlinus.github.io Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Convert an Euler spiral to a series of arcs. + +use xilem_web::svg::kurbo::{SvgArc, Vec2}; + +use crate::euler::EulerSeg; + +pub fn euler_to_arcs(es: &EulerSeg, tol: f64) -> Vec { + let arclen = es.p0.distance(es.p1) / es.params.ch; + let n_subdiv = ((1. / 120.) * arclen / tol * es.params.k1.abs()).cbrt(); + web_sys::console::log_1(&format!("n_subdiv = {n_subdiv}").into()); + let n = (n_subdiv.ceil() as usize).max(1); + let dt = 1.0 / n as f64; + let mut p0 = es.p0; + (0..n) + .map(|i| { + let t0 = i as f64 * dt; + let t1 = t0 + dt; + let p1 = if i + 1 == n { es.p1 } else { es.eval(t1) }; + let t = t0 + 0.5 * dt - 0.5; + let k = es.params.k0 + t * es.params.k1; + web_sys::console::log_1(&format!("{i}: k = {k} t = {t}").into()); + let r = arclen / k; + let arc = SvgArc { + from: p0, + to: p1, + radii: Vec2::new(r, r), + x_rotation: 0.0, + large_arc: false, + sweep: k < 0.0, + }; + p0 = p1; + arc + }) + .collect() +} diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index d24ae65..2b002bd 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -4,12 +4,23 @@ //! An interactive toy for experimenting with rendering of Bézier paths, //! including Euler spiral based stroke expansion. +mod arc; mod euler; mod flatten; -use xilem_web::{svg::{kurbo::{Point, BezPath, CubicBez, PathEl, Circle, Line, Shape}, peniko::Color}, PointerMsg, View, App, elements::svg::{g, svg}, document_body, interfaces::*}; +use xilem_web::{ + document_body, + elements::svg::{g, svg}, + interfaces::*, + svg::{ + kurbo::{Arc, BezPath, Circle, CubicBez, Line, PathEl, Point, Shape}, + peniko::Color, + }, + App, PointerMsg, View, +}; use crate::{ + arc::euler_to_arcs, euler::{CubicParams, CubicToEulerIter}, flatten::flatten_offset, }; @@ -73,12 +84,19 @@ const RAINBOW_PALETTE: [Color; 12] = [ Color::rgb8(0x66, 0x33, 0x99), ]; +fn lerp_color(a: Color, b: Color, t: f64) -> Color { + let r = (a.r as f64 + (b.r as f64 - a.r as f64) * t) * (1. / 255.); + let g = (a.g as f64 + (b.g as f64 - a.g as f64) * t) * (1. / 255.); + let b = (a.b as f64 + (b.b as f64 - a.b as f64) * t) * (1. / 255.); + Color::rgb(r, g, b) +} + fn app_logic(state: &mut AppState) -> impl View { let mut path = BezPath::new(); path.move_to(state.p0); path.curve_to(state.p1, state.p2, state.p3); let stroke = xilem_web::svg::kurbo::Stroke::new(2.0); - let stroke_thick = xilem_web::svg::kurbo::Stroke::new(8.0); + let stroke_thick = xilem_web::svg::kurbo::Stroke::new(15.0); let stroke_thin = xilem_web::svg::kurbo::Stroke::new(1.0); const NONE: Color = Color::TRANSPARENT; const HANDLE_RADIUS: f64 = 4.0; @@ -104,33 +122,32 @@ fn app_logic(state: &mut AppState) -> impl View { path }; let color = RAINBOW_PALETTE[(i * 7) % 12]; + let color = lerp_color(color, Color::WHITE, 0.5); spirals.push(path.stroke(color, stroke_thick.clone()).fill(NONE)); } - let offset = 40.0; - let flat = flatten_offset(CubicToEulerIter::new(c, TOL), offset); - let flat2 = flatten_offset(CubicToEulerIter::new(c, TOL), -offset); let mut flat_pts = vec![]; - for seg in flat.elements().iter().chain(flat2.elements().iter()) { - match seg { - PathEl::MoveTo(p) | PathEl::LineTo(p) => { - let circle = Circle::new(*p, 2.0).fill(Color::BLACK); - flat_pts.push(circle); + let mut flat = BezPath::new(); + flat.move_to(c.p0); + web_sys::console::log_1(&"---".into()); + for es in CubicToEulerIter::new(c, TOL) { + for arc in euler_to_arcs(&es, 1.0) { + let circle = Circle::new(arc.to, 2.0).fill(Color::BLACK); + flat_pts.push(circle); + if let Some(arc) = Arc::from_svg_arc(&arc) { + flat.extend(arc.append_iter(0.1)); + } else { + web_sys::console::log_1(&format!("conversion failed {arc:?}").into()); } - _ => (), } } svg(g(( g(spirals), path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), - flat.stroke(Color::BLUE, stroke_thin.clone()).fill(NONE), - flat2.stroke(Color::PURPLE, stroke_thin).fill(NONE), + flat.stroke(Color::RED, stroke_thin.clone()).fill(NONE), g(flat_pts), - Line::new(state.p0, state.p1) - .stroke(Color::BLUE, stroke.clone()), - Line::new(state.p2, state.p3) - .stroke(Color::BLUE, stroke.clone()), - Line::new((790., 300.), (790., 300. - 1000. * err)) - .stroke(Color::RED, stroke.clone()), + Line::new(state.p0, state.p1).stroke(Color::BLUE, stroke.clone()), + Line::new(state.p2, state.p3).stroke(Color::BLUE, stroke.clone()), + Line::new((790., 300.), (790., 300. - 1000. * err)).stroke(Color::RED, stroke.clone()), g(( Circle::new(state.p0, HANDLE_RADIUS) .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.p0, &msg)), From 2cc48d7934c027bba846ff7fdfd92875ea59c143 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sun, 31 Mar 2024 16:22:34 -0700 Subject: [PATCH 07/11] Checkpoint parallel curve Working drawing of parallel curves with good error. Missing cusp handling though. --- _figures/beztoy/src/arc.rs | 30 ++++++++++++++++++++++++++++++ _figures/beztoy/src/flatten.rs | 3 +-- _figures/beztoy/src/main.rs | 11 ++++++++--- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/_figures/beztoy/src/arc.rs b/_figures/beztoy/src/arc.rs index 1bd5ed1..9a547ab 100644 --- a/_figures/beztoy/src/arc.rs +++ b/_figures/beztoy/src/arc.rs @@ -36,3 +36,33 @@ pub fn euler_to_arcs(es: &EulerSeg, tol: f64) -> Vec { }) .collect() } + +pub fn espc_to_arcs(es: &EulerSeg, d: f64, tol: f64) -> Vec { + let arclen = es.p0.distance(es.p1) / es.params.ch; + let n_subdiv = ((1. / 120.) * arclen / tol * es.params.k1.abs()).cbrt(); + web_sys::console::log_1(&format!("n_subdiv = {n_subdiv}").into()); + let n = (n_subdiv.ceil() as usize).max(1); + let dt = 1.0 / n as f64; + let mut p0 = es.eval_with_offset(0.0, d); + (0..n) + .map(|i| { + let t0 = i as f64 * dt; + let t1 = t0 + dt; + let p1 = es.eval_with_offset(t1, d); + let t = t0 + 0.5 * dt - 0.5; + let k = es.params.k0 + t * es.params.k1; + let arclen_offset = arclen + d * k; + let r = arclen_offset / k; + let arc = SvgArc { + from: p0, + to: p1, + radii: Vec2::new(r, r), + x_rotation: 0.0, + large_arc: false, + sweep: k < 0.0, + }; + p0 = p1; + arc + }) + .collect() +} diff --git a/_figures/beztoy/src/flatten.rs b/_figures/beztoy/src/flatten.rs index 13278b6..dc8d0b9 100644 --- a/_figures/beztoy/src/flatten.rs +++ b/_figures/beztoy/src/flatten.rs @@ -9,7 +9,7 @@ use xilem_web::svg::kurbo::BezPath; use crate::euler::EulerSeg; -pub fn flatten_offset(iter: impl Iterator, offset: f64) -> BezPath { +pub fn flatten_offset(iter: impl Iterator, offset: f64, tol: f64) -> BezPath { let mut result = BezPath::new(); let mut first = true; for es in iter { @@ -17,7 +17,6 @@ pub fn flatten_offset(iter: impl Iterator, offset: f64) -> BezP result.move_to(es.eval_with_offset(0.0, offset)); } let scale = es.p0.distance(es.p1); - let tol = 1.0; let (k0, k1) = (es.params.k0 - 0.5 * es.params.k1, es.params.k1); // compute forward integral to determine number of subdivisions let dist_scaled = offset / scale; diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index 2b002bd..24d9bc9 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -20,7 +20,7 @@ use xilem_web::{ }; use crate::{ - arc::euler_to_arcs, + arc::{espc_to_arcs, euler_to_arcs}, euler::{CubicParams, CubicToEulerIter}, flatten::flatten_offset, }; @@ -125,12 +125,16 @@ fn app_logic(state: &mut AppState) -> impl View { let color = lerp_color(color, Color::WHITE, 0.5); spirals.push(path.stroke(color, stroke_thick.clone()).fill(NONE)); } + let offset = 100.0; + let flat_ref = flatten_offset(CubicToEulerIter::new(c, 0.01), offset, 0.01); let mut flat_pts = vec![]; let mut flat = BezPath::new(); - flat.move_to(c.p0); web_sys::console::log_1(&"---".into()); for es in CubicToEulerIter::new(c, TOL) { - for arc in euler_to_arcs(&es, 1.0) { + if flat.is_empty() { + flat.move_to(es.eval_with_offset(0.0, offset)); + } + for arc in espc_to_arcs(&es, offset, TOL) { let circle = Circle::new(arc.to, 2.0).fill(Color::BLACK); flat_pts.push(circle); if let Some(arc) = Arc::from_svg_arc(&arc) { @@ -143,6 +147,7 @@ fn app_logic(state: &mut AppState) -> impl View { svg(g(( g(spirals), path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), + flat_ref.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), flat.stroke(Color::RED, stroke_thin.clone()).fill(NONE), g(flat_pts), Line::new(state.p0, state.p1).stroke(Color::BLUE, stroke.clone()), From ed42b4ac46cfba7f50d3c6916dd4a27eb07ef4c0 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sun, 31 Mar 2024 17:47:19 -0700 Subject: [PATCH 08/11] Additional error term for offset Fix error underestimate for near-cusps. --- _figures/beztoy/src/arc.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/_figures/beztoy/src/arc.rs b/_figures/beztoy/src/arc.rs index 9a547ab..2a13523 100644 --- a/_figures/beztoy/src/arc.rs +++ b/_figures/beztoy/src/arc.rs @@ -39,7 +39,10 @@ pub fn euler_to_arcs(es: &EulerSeg, tol: f64) -> Vec { pub fn espc_to_arcs(es: &EulerSeg, d: f64, tol: f64) -> Vec { let arclen = es.p0.distance(es.p1) / es.params.ch; - let n_subdiv = ((1. / 120.) * arclen / tol * es.params.k1.abs()).cbrt(); + // TODO: determine if there needs to be a scaling parameter on d. But this + // seems to work well empirically. + let est_err = (1. / 120.) / tol * es.params.k1.abs() * (arclen + d.abs()); + let n_subdiv = est_err.cbrt(); web_sys::console::log_1(&format!("n_subdiv = {n_subdiv}").into()); let n = (n_subdiv.ceil() as usize).max(1); let dt = 1.0 / n as f64; From cd9cd24f7870005377f036f3c347b2ba19eb41eb Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sun, 31 Mar 2024 20:05:59 -0700 Subject: [PATCH 09/11] Tighter error bound More careful error measurement (making sure to match up arc length parameterization) lets us tighten the error bound for handling cusps. --- _figures/beztoy/src/arc.rs | 5 ++--- _figures/beztoy/src/main.rs | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/_figures/beztoy/src/arc.rs b/_figures/beztoy/src/arc.rs index 2a13523..0177578 100644 --- a/_figures/beztoy/src/arc.rs +++ b/_figures/beztoy/src/arc.rs @@ -39,9 +39,8 @@ pub fn euler_to_arcs(es: &EulerSeg, tol: f64) -> Vec { pub fn espc_to_arcs(es: &EulerSeg, d: f64, tol: f64) -> Vec { let arclen = es.p0.distance(es.p1) / es.params.ch; - // TODO: determine if there needs to be a scaling parameter on d. But this - // seems to work well empirically. - let est_err = (1. / 120.) / tol * es.params.k1.abs() * (arclen + d.abs()); + let est_err = + (1. / 120.) / tol * es.params.k1.abs() * (arclen + 0.4 * (es.params.k1 * d).abs()); let n_subdiv = est_err.cbrt(); web_sys::console::log_1(&format!("n_subdiv = {n_subdiv}").into()); let n = (n_subdiv.ceil() as usize).max(1); diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index 24d9bc9..6dc4abd 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -147,7 +147,9 @@ fn app_logic(state: &mut AppState) -> impl View { svg(g(( g(spirals), path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), - flat_ref.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), + flat_ref + .stroke(Color::BLACK, stroke_thin.clone()) + .fill(NONE), flat.stroke(Color::RED, stroke_thin.clone()).fill(NONE), g(flat_pts), Line::new(state.p0, state.p1).stroke(Color::BLUE, stroke.clone()), From 7c00b6031da4224bc4d5ac3199e382ad7e459e53 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Mon, 6 May 2024 17:11:39 +0200 Subject: [PATCH 10/11] :x --- _figures/beztoy/src/main.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index 6dc4abd..f6c1a06 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -8,19 +8,23 @@ mod arc; mod euler; mod flatten; +use wasm_bindgen::JsCast; use xilem_web::{ document_body, - elements::svg::{g, svg}, + elements::{ + html::{div, input}, + svg::{g, svg}, + }, interfaces::*, svg::{ - kurbo::{Arc, BezPath, Circle, CubicBez, Line, PathEl, Point, Shape}, + kurbo::{Arc, BezPath, Circle, CubicBez, Line, Point, Shape}, peniko::Color, }, App, PointerMsg, View, }; use crate::{ - arc::{espc_to_arcs, euler_to_arcs}, + arc::espc_to_arcs, euler::{CubicParams, CubicToEulerIter}, flatten::flatten_offset, }; @@ -32,6 +36,7 @@ struct AppState { p2: Point, p3: Point, grab: GrabState, + offset: f64, } #[derive(Default)] @@ -125,7 +130,7 @@ fn app_logic(state: &mut AppState) -> impl View { let color = lerp_color(color, Color::WHITE, 0.5); spirals.push(path.stroke(color, stroke_thick.clone()).fill(NONE)); } - let offset = 100.0; + let offset = state.offset; let flat_ref = flatten_offset(CubicToEulerIter::new(c, 0.01), offset, 0.01); let mut flat_pts = vec![]; let mut flat = BezPath::new(); @@ -144,7 +149,7 @@ fn app_logic(state: &mut AppState) -> impl View { } } } - svg(g(( + let svg_el = svg(g(( g(spirals), path.stroke(Color::BLACK, stroke_thin.clone()).fill(NONE), flat_ref @@ -167,7 +172,25 @@ fn app_logic(state: &mut AppState) -> impl View { )), ))) .attr("width", 800) - .attr("height", 600) + .attr("height", 600); + let slider_el = input(()) + .attr("type", "range") + .attr("min", "1") + .attr("max", "100") + .attr("value", "100") + .on_input(|state: &mut AppState, evt| { + if let Some(element) = evt + .target() + .and_then(|t| t.dyn_into::().ok()) + { + let value = element.value(); + if let Ok(val_f64) = value.parse::() { + state.offset = val_f64; + //web_sys::console::log_1(&format!("got input event {val_f64}").into()); + } + } + }); + div((div("Offset"), slider_el, div(svg_el))) } pub fn main() { @@ -177,6 +200,7 @@ pub fn main() { state.p1 = Point::new(300.0, 150.0); state.p2 = Point::new(500.0, 150.0); state.p3 = Point::new(700.0, 150.0); + state.offset = 100.0; let app = App::new(state, app_logic); app.run(&document_body()); } From 8a4d38ca76d604582a9cf5a720ba5d2b731c4b94 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Fri, 14 Jun 2024 14:00:13 -0700 Subject: [PATCH 11/11] Add tolerance slider This rolls up some changes for RustNL. The main substantive change is the additional slider for tolerance, but there's also some changes for style. I think this might also be the commit to snap to recent xilem_web, but I'm not 100% sure. --- _figures/beztoy/Cargo.toml | 3 +-- _figures/beztoy/src/arc.rs | 1 + _figures/beztoy/src/main.rs | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/_figures/beztoy/Cargo.toml b/_figures/beztoy/Cargo.toml index 93aacef..68a84eb 100644 --- a/_figures/beztoy/Cargo.toml +++ b/_figures/beztoy/Cargo.toml @@ -8,5 +8,4 @@ edition = "2021" console_error_panic_hook = "0.1" wasm-bindgen = "0.2.87" web-sys = "0.3.64" -#xilem_svg = { git = "https://github.com/linebender/xilem", rev = "71d1db04dce2dce2264817177575067bec803932"} -xilem_web = { git = "https://github.com/Philipp-M/xilem", rev = "01838144540210ea14a7b337584c2dd7ff7cf5a3" } +xilem_web = { git = "https://github.com/linebender/xilem", rev = "1ef39fc562d969cfc8548d16b2b72202a5d2196d" } diff --git a/_figures/beztoy/src/arc.rs b/_figures/beztoy/src/arc.rs index 0177578..845373d 100644 --- a/_figures/beztoy/src/arc.rs +++ b/_figures/beztoy/src/arc.rs @@ -7,6 +7,7 @@ use xilem_web::svg::kurbo::{SvgArc, Vec2}; use crate::euler::EulerSeg; +#[allow(unused)] pub fn euler_to_arcs(es: &EulerSeg, tol: f64) -> Vec { let arclen = es.p0.distance(es.p1) / es.params.ch; let n_subdiv = ((1. / 120.) * arclen / tol * es.params.k1.abs()).cbrt(); diff --git a/_figures/beztoy/src/main.rs b/_figures/beztoy/src/main.rs index f6c1a06..a250f40 100644 --- a/_figures/beztoy/src/main.rs +++ b/_figures/beztoy/src/main.rs @@ -37,6 +37,7 @@ struct AppState { p3: Point, grab: GrabState, offset: f64, + tolerance: f64, } #[derive(Default)] @@ -109,8 +110,8 @@ fn app_logic(state: &mut AppState) -> impl View { let params = CubicParams::from_cubic(c); let err = params.est_euler_err(); let mut spirals = vec![]; - const TOL: f64 = 1.0; - for (i, es) in CubicToEulerIter::new(c, TOL).enumerate() { + let tol = state.tolerance; + for (i, es) in CubicToEulerIter::new(c, tol).enumerate() { let path = if es.params.cubic_ok() { es.to_cubic().into_path(1.0) } else { @@ -135,11 +136,11 @@ fn app_logic(state: &mut AppState) -> impl View { let mut flat_pts = vec![]; let mut flat = BezPath::new(); web_sys::console::log_1(&"---".into()); - for es in CubicToEulerIter::new(c, TOL) { + for es in CubicToEulerIter::new(c, tol) { if flat.is_empty() { flat.move_to(es.eval_with_offset(0.0, offset)); } - for arc in espc_to_arcs(&es, offset, TOL) { + for arc in espc_to_arcs(&es, offset, tol) { let circle = Circle::new(arc.to, 2.0).fill(Color::BLACK); flat_pts.push(circle); if let Some(arc) = Arc::from_svg_arc(&arc) { @@ -155,7 +156,7 @@ fn app_logic(state: &mut AppState) -> impl View { flat_ref .stroke(Color::BLACK, stroke_thin.clone()) .fill(NONE), - flat.stroke(Color::RED, stroke_thin.clone()).fill(NONE), + flat.stroke(Color::GREEN, stroke_thin.clone()).fill(NONE), g(flat_pts), Line::new(state.p0, state.p1).stroke(Color::BLUE, stroke.clone()), Line::new(state.p2, state.p3).stroke(Color::BLUE, stroke.clone()), @@ -190,7 +191,30 @@ fn app_logic(state: &mut AppState) -> impl View { } } }); - div((div("Offset"), slider_el, div(svg_el))) + let tolerance_el = input(()) + .attr("type", "range") + .attr("min", "1") + .attr("max", "100") + .attr("value", "10") + .on_input(|state: &mut AppState, evt| { + if let Some(element) = evt + .target() + .and_then(|t| t.dyn_into::().ok()) + { + let value = element.value(); + if let Ok(val_f64) = value.parse::() { + state.tolerance = val_f64 * 0.1; + //web_sys::console::log_1(&format!("got input event {val_f64}").into()); + } + } + }); + div(( + div("Offset"), + slider_el, + div("Tolerance"), + tolerance_el, + div(svg_el), + )) } pub fn main() { @@ -201,6 +225,7 @@ pub fn main() { state.p2 = Point::new(500.0, 150.0); state.p3 = Point::new(700.0, 150.0); state.offset = 100.0; + state.tolerance = 1.0; let app = App::new(state, app_logic); app.run(&document_body()); }