diff --git a/bezier-rs/docs/interactive-docs/src/App.vue b/bezier-rs/docs/interactive-docs/src/App.vue index 9112b841..d93c630c 100644 --- a/bezier-rs/docs/interactive-docs/src/App.vue +++ b/bezier-rs/docs/interactive-docs/src/App.vue @@ -369,6 +369,18 @@ export default defineComponent({ drawLine(context, maxPoint, { x: maxPoint.x, y: minPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1); }, }, + { + name: "Inflections", + callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => { + const context = getContextFromCanvas(canvas); + const inflections: number[] = JSON.parse(bezier.inflections()); + inflections.forEach((t) => { + const point = JSON.parse(bezier.evaluate(t)); + drawPoint(context, point, 4, COLORS.NON_INTERACTIVE.STROKE_1); + }); + }, + curveDegrees: new Set([BezierCurveType.Cubic]), + }, ], subpathFeatures: [ { diff --git a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs index acbc2862..90f1658c 100644 --- a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs +++ b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs @@ -156,4 +156,9 @@ impl WasmBezier { let bbox_points: [Point; 2] = self.0.bounding_box().map(|p| Point { x: p.x, y: p.y }); to_js_value(bbox_points) } + + pub fn inflections(&self) -> JsValue { + let inflections = self.0.inflections(); + to_js_value(inflections) + } } diff --git a/bezier-rs/lib/src/lib.rs b/bezier-rs/lib/src/lib.rs index 8a290204..89d3760b 100644 --- a/bezier-rs/lib/src/lib.rs +++ b/bezier-rs/lib/src/lib.rs @@ -793,6 +793,45 @@ impl Bezier { [endpoints_min, endpoints_max] } + + // TODO: Use an `impl Iterator` return type instead of a `Vec` + /// Returns list of `t`-values representing the inflection points of the curve. + /// The inflection points are defined to be points at which the second derivative of the curve is equal to zero. + pub fn unrestricted_inflections(&self) -> Vec { + match self.handles { + // There exists no inflection points for linear and quadratic beziers. + BezierHandles::Linear => Vec::new(), + BezierHandles::Quadratic { .. } => Vec::new(), + BezierHandles::Cubic { .. } => { + // Axis align the curve. + let translated_bezier = self.translate(-self.start); + let angle = translated_bezier.end.angle_between(DVec2::new(1., 0.)); + let rotated_bezier = translated_bezier.rotate(angle); + if let BezierHandles::Cubic { handle_start, handle_end } = rotated_bezier.handles { + // These formulas and naming conventions follows https://pomax.github.io/bezierinfo/#inflections + let a = handle_end.x * handle_start.y; + let b = rotated_bezier.end.x * handle_start.y; + let c = handle_start.x * handle_end.y; + let d = rotated_bezier.end.x * handle_end.y; + + let x = -3. * a + 2. * b + 3. * c - d; + let y = 3. * a - b - 3. * c; + let z = c - a; + + let discriminant = y * y - 4. * x * z; + utils::solve_quadratic(discriminant, 2. * x, y, z) + } else { + unreachable!("shouldn't happen") + } + } + } + } + + /// Returns list of `t`-values representing the inflection points of the curve. + /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. + pub fn inflections(&self) -> Vec { + self.unrestricted_inflections().into_iter().filter(|&t| t > 0. && t < 1.).collect::>() + } } #[cfg(test)]