diff --git a/bezier-rs/docs/interactive-docs/src/App.vue b/bezier-rs/docs/interactive-docs/src/App.vue index 400147d6..4a9de38b 100644 --- a/bezier-rs/docs/interactive-docs/src/App.vue +++ b/bezier-rs/docs/interactive-docs/src/App.vue @@ -228,7 +228,7 @@ export default defineComponent({ }, { name: "Project", - callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record, mouseLocation: Point | null): void => { + callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record, mouseLocation?: Point): void => { if (mouseLocation != null) { const context = getContextFromCanvas(canvas); const closestPoint = JSON.parse(bezier.project(mouseLocation.x, mouseLocation.y)); @@ -236,6 +236,20 @@ export default defineComponent({ } }, }, + { + name: "Local Extrema", + callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => { + const context = getContextFromCanvas(canvas); + const dimensionColors = [COLORS.NON_INTERACTIVE.STROKE_1, COLORS.NON_INTERACTIVE.STROKE_2]; + const extrema: number[][] = JSON.parse(bezier.local_extrema()); + extrema.forEach((tValues, index) => { + tValues.forEach((t) => { + const point = JSON.parse(bezier.compute(t)); + drawPoint(context, point, 4, dimensionColors[index]); + }); + }); + }, + }, ], }; }, diff --git a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs index 5d69d893..54961d6a 100644 --- a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs +++ b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs @@ -14,20 +14,20 @@ struct Point { #[derive(Clone)] pub struct WasmBezier(Bezier); -/// Convert a `DVec2` into a `JsValue` +/// Convert a `DVec2` into a `JsValue`. pub fn vec_to_point(p: &DVec2) -> JsValue { JsValue::from_serde(&serde_json::to_string(&Point { x: p.x, y: p.y }).unwrap()).unwrap() } #[wasm_bindgen] impl WasmBezier { - /// Expect js_points to be a list of 3 pairs + /// Expect js_points to be a list of 3 pairs. pub fn new_quadratic(js_points: &JsValue) -> WasmBezier { let points: [DVec2; 3] = js_points.into_serde().unwrap(); WasmBezier(Bezier::from_quadratic_dvec2(points[0], points[1], points[2])) } - /// Expect js_points to be a list of 4 pairs + /// Expect js_points to be a list of 4 pairs. pub fn new_cubic(js_points: &JsValue) -> WasmBezier { let points: [DVec2; 4] = js_points.into_serde().unwrap(); WasmBezier(Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3])) @@ -102,4 +102,9 @@ impl WasmBezier { pub fn project(&self, x: f64, y: f64) -> JsValue { vec_to_point(&self.0.project(DVec2::new(x, y), 20, 1e-4, 3, 10)) } + + pub fn local_extrema(&self) -> JsValue { + let local_extrema = self.0.local_extrema(); + JsValue::from_serde(&serde_json::to_string(&local_extrema).unwrap()).unwrap() + } } diff --git a/bezier-rs/lib/src/lib.rs b/bezier-rs/lib/src/lib.rs index 8346249d..dbc6e0f9 100644 --- a/bezier-rs/lib/src/lib.rs +++ b/bezier-rs/lib/src/lib.rs @@ -2,37 +2,37 @@ use glam::DVec2; mod utils; -/// Representation of the handle point(s) in a bezier segment +/// Representation of the handle point(s) in a bezier segment. #[derive(Copy, Clone)] pub enum BezierHandles { - /// Handles for a quadratic segment + /// Handles for a quadratic segment. Quadratic { - /// Point representing the location of the single handle + /// Point representing the location of the single handle. handle: DVec2, }, - /// Handles for a cubic segment + /// Handles for a cubic segment. Cubic { - /// Point representing the location of the handle associated to the start point + /// Point representing the location of the handle associated to the start point. handle_start: DVec2, - /// Point representing the location of the handle associated to the end point + /// Point representing the location of the handle associated to the end point. handle_end: DVec2, }, } -/// Representation of a bezier segment with 2D points +/// Representation of a bezier segment with 2D points. #[derive(Copy, Clone)] pub struct Bezier { - /// Start point of the bezier segment + /// Start point of the bezier segment. start: DVec2, - /// Start point of the bezier segment + /// Start point of the bezier segment. end: DVec2, - /// Handles of the bezier segment + /// Handles of the bezier segment. handles: BezierHandles, } impl Bezier { // TODO: Consider removing this function - /// Create a quadratic bezier using the provided coordinates as the start, handle, and end points + /// Create a quadratic bezier using the provided coordinates as the start, handle, and end points. pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self { Bezier { start: DVec2::new(x1, y1), @@ -41,7 +41,7 @@ impl Bezier { } } - /// Create a quadratc bezier using the provided DVec2s as the start, handle, and end points + /// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points. pub fn from_quadratic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2) -> Self { Bezier { start: p1, @@ -51,7 +51,7 @@ impl Bezier { } // TODO: Consider removing this function - /// Create a cubic bezier using the provided coordinates as the start, handles, and end points + /// Create a cubic bezier using the provided coordinates as the start, handles, and end points. pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self { Bezier { start: DVec2::new(x1, y1), @@ -63,7 +63,7 @@ impl Bezier { } } - /// Create a cubic bezier using the provided DVec2s as the start, handles, and end points + /// Create a cubic bezier using the provided DVec2s as the start, handles, and end points. pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self { Bezier { start: p1, @@ -110,7 +110,7 @@ impl Bezier { Bezier::from_cubic_dvec2(start, handle_start, handle_end, end) } - /// Convert to SVG + /// Convert to SVG. // TODO: Allow modifying the viewport, width and height pub fn to_svg(&self) -> String { let m_path = format!("M {} {}", self.start.x, self.start.y); @@ -129,12 +129,12 @@ impl Bezier { ) } - /// Set the coordinates of the start point + /// Set the coordinates of the start point. pub fn set_start(&mut self, s: DVec2) { self.start = s; } - /// Set the coordinates of the end point + /// Set the coordinates of the end point. pub fn set_end(&mut self, e: DVec2) { self.end = e; } @@ -199,8 +199,8 @@ impl Bezier { } } - /// Calculate the point on the curve based on the `t`-value provided. - /// Basis code based off of pseudocode found here: + /// Calculate the point on the curve based on the `t`-value provided. + /// Basis code based off of pseudocode found here: . pub fn compute(&self, t: f64) -> DVec2 { assert!((0.0..=1.0).contains(&t)); @@ -218,8 +218,8 @@ impl Bezier { } } - /// Return a selection of equidistant points on the bezier curve - /// If no value is provided for `steps`, then the function will default `steps` to be 10 + /// Return a selection of equidistant points on the bezier curve. + /// If no value is provided for `steps`, then the function will default `steps` to be 10. pub fn compute_lookup_table(&self, steps: Option) -> Vec { let steps_unwrapped = steps.unwrap_or(10); let ratio: f64 = 1.0 / (steps_unwrapped as f64); @@ -232,8 +232,8 @@ impl Bezier { steps_array } - /// Return an approximation of the length of the bezier curve - /// code example taken from: + /// Return an approximation of the length of the bezier curve. + /// Code example from . pub fn length(&self) -> f64 { // We will use an approximate approach where // we split the curve into many subdivisions @@ -254,7 +254,7 @@ impl Bezier { approx_curve_length } - /// Returns a vector representing the derivative at the point designated by `t` on the curve + /// Returns a vector representing the derivative at the point designated by `t` on the curve. pub fn derivative(&self, t: f64) -> DVec2 { let one_minus_t = 1. - t; match self.handles { @@ -272,18 +272,18 @@ impl Bezier { } } - /// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve + /// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve. pub fn tangent(&self, t: f64) -> DVec2 { self.derivative(t).normalize() } - /// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve + /// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve. pub fn normal(&self, t: f64) -> DVec2 { let derivative = self.derivative(t); derivative.normalize().perp() } - /// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t` + /// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t`. pub fn split(&self, t: f64) -> [Bezier; 2] { let split_point = self.compute(t); @@ -314,7 +314,7 @@ impl Bezier { } } - /// Returns the Bezier curve representing the sub-curve starting at the point corresponding to `t1` and ending at the point corresponding to `t2` + /// Returns the Bezier curve representing the sub-curve starting at the point corresponding to `t1` and ending at the point corresponding to `t2`. pub fn trim(&self, t1: f64, t2: f64) -> Bezier { // Depending on the order of `t1` and `t2`, determine which half of the split we need to keep let t1_split_side = if t1 <= t2 { 1 } else { 0 }; @@ -334,10 +334,10 @@ impl Bezier { /// Returns the closest point on the curve to the provided point. /// Uses a searching algorithm akin to binary search that can be customized using the following parameters: - /// - `lut_size` - Size of the lookup table for the initial passthrough - /// - `convergence_epsilon` - Difference used between floating point numbers to be considered as equal - /// - `convergence_limit` - Controls the number of iterations needed to consider that minimum distance to have converged - /// - `iteration_limit` - Controls the maximum total number of iterations to be used + /// - `lut_size` - Size of the lookup table for the initial passthrough. + /// - `convergence_epsilon` - Difference used between floating point numbers to be considered as equal. + /// - `convergence_limit` - Controls the number of iterations needed to consider that minimum distance to have converged. + /// - `iteration_limit` - Controls the maximum total number of iterations to be used. pub fn project(&self, point: DVec2, lut_size: i32, convergence_epsilon: f64, convergence_limit: i32, iteration_limit: i32) -> DVec2 { // First find the closest point from the results of a lookup table let lut = self.compute_lookup_table(Some(lut_size)); @@ -418,6 +418,41 @@ impl Bezier { self.compute(final_t) } + + /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively. + /// The local extrema are defined to be points at which the derivative of the curve is equal to zero. + fn unrestricted_local_extrema(&self) -> [Vec; 2] { + match self.handles { + BezierHandles::Quadratic { handle } => { + let a = handle - self.start; + let b = self.end - handle; + let b_minus_a = b - a; + [utils::solve_linear(b_minus_a.x, a.x), utils::solve_linear(b_minus_a.y, a.y)] + } + BezierHandles::Cubic { handle_start, handle_end } => { + let a = 3. * (-self.start + 3. * handle_start - 3. * handle_end + self.end); + let b = 6. * (self.start - 2. * handle_start + handle_end); + let c = 3. * (handle_start - self.start); + let discriminant = b * b - 4. * a * c; + let two_times_a = 2. * a; + [ + utils::solve_quadratic(discriminant.x, two_times_a.x, b.x, c.x), + utils::solve_quadratic(discriminant.y, two_times_a.y, b.y, c.y), + ] + } + } + } + + /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively. + /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. + pub fn local_extrema(&self) -> [Vec; 2] { + self.unrestricted_local_extrema() + .into_iter() + .map(|t_values| t_values.into_iter().filter(|&t| t > 0. && t < 1.).collect::>()) + .collect::>>() + .try_into() + .unwrap() + } } #[cfg(test)] diff --git a/bezier-rs/lib/src/utils.rs b/bezier-rs/lib/src/utils.rs index 21505a90..3bb7587b 100644 --- a/bezier-rs/lib/src/utils.rs +++ b/bezier-rs/lib/src/utils.rs @@ -20,8 +20,8 @@ pub fn compute_abc_for_quadratic_through_points(start_point: DVec2, point_on_cur compute_abc_through_points(start_point, point_on_curve, end_point, t_squared, squared_one_minus_t) } -/// Compute a, b, and c for a cubic curve that fits the start, end and point on curve at `t`. -/// The definition for the a, b, c points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. +/// Compute `a`, `b`, and `c` for a cubic curve that fits the start, end and point on curve at `t`. +/// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] { let t_cubed = t * t * t; let one_minus_t = 1. - t; @@ -37,3 +37,30 @@ pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (i32, f64) { .min_by(|x, y| (&(x.1)).partial_cmp(&(y.1)).unwrap()) .unwrap() } + +/// Find the roots of the linear equation `ax + b` +pub fn solve_linear(a: f64, b: f64) -> Vec { + let mut roots = Vec::new(); + if a != 0. { + roots.push(-b / a); + } + roots +} + +/// Find the roots of the linear equation `ax^2 + bx + c` +/// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes +pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> Vec { + let mut roots = Vec::new(); + if two_times_a != 0. { + if discriminant > 0. { + let root_discriminant = discriminant.sqrt(); + roots.push((-b + root_discriminant) / (two_times_a)); + roots.push((-b - root_discriminant) / (two_times_a)); + } else if discriminant == 0. { + roots.push(-b / (two_times_a)); + } + } else { + roots = solve_linear(b, c); + } + roots +}