Implement bezier local_extrema function (#693)
* Implement backend of extrema in bezier-rs * Added extrema frontend * Added extrema interface * Wrapped extrema in filter function to remove points not on the curve * Saved intermediate results while computing extrema * Fixed extrema bug when a in cubic formula is 0 * Removed extra prints * Fixed quadratic extrema regression * Moved helper functions to utils file * Fixed bug in solve linear * Stylistic changes per review * Sentence comments Co-authored-by: Linda Zheng <thelindazheng@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
3ab47418d2
commit
c343aaa3ab
|
|
@ -228,7 +228,7 @@ export default defineComponent({
|
|||
},
|
||||
{
|
||||
name: "Project",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation: Point | null): void => {
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, 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]);
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <https://pomax.github.io/bezierinfo/#explanation>
|
||||
/// Calculate the point on the curve based on the `t`-value provided.
|
||||
/// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>.
|
||||
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<i32>) -> Vec<DVec2> {
|
||||
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: <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>
|
||||
/// Return an approximation of the length of the bezier curve.
|
||||
/// Code example from <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>.
|
||||
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<f64>; 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<f64>; 2] {
|
||||
self.unrestricted_local_extrema()
|
||||
.into_iter()
|
||||
.map(|t_values| t_values.into_iter().filter(|&t| t > 0. && t < 1.).collect::<Vec<f64>>())
|
||||
.collect::<Vec<Vec<f64>>>()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -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<f64> {
|
||||
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<f64> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue