diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs
index cb612f8f..8134cb8b 100644
--- a/libraries/bezier-rs/src/bezier/transform.rs
+++ b/libraries/bezier-rs/src/bezier/transform.rs
@@ -1,4 +1,5 @@
use super::*;
+use crate::utils::f64_compare;
use glam::DMat2;
use std::f64::consts::PI;
@@ -41,12 +42,20 @@ 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`.
pub fn trim(&self, t1: f64, t2: f64) -> Bezier {
+ if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) {
+ let point = self.evaluate(t1);
+ return match self.handles {
+ BezierHandles::Linear => Bezier::from_linear_dvec2(point, point),
+ BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point),
+ BezierHandles::Cubic { handle_start: _, handle_end: _ } => Bezier::from_cubic_dvec2(point, point, point, point),
+ };
+ }
// 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 };
let t2_split_side = if t1 <= t2 { 0 } else { 1 };
let bezier_starting_at_t1 = self.split(t1)[t1_split_side];
// Adjust the ratio `t2` to its corresponding value on the new curve that was split on `t1`
- let adjusted_t2 = if t1 < t2 || (t1 == t2 && t1 == 0.) {
+ let adjusted_t2 = if t1 < t2 || t1 == 0. {
// Case where we took the split from t1 to the end
// Also cover the `t1` == t2 case where there would otherwise be a divide by 0
(t2 - t1) / (1. - t1)
diff --git a/website/other/bezier-rs-demos/src/App.vue b/website/other/bezier-rs-demos/src/App.vue
index 1b9f4444..285f8d22 100644
--- a/website/other/bezier-rs-demos/src/App.vue
+++ b/website/other/bezier-rs-demos/src/App.vue
@@ -4,7 +4,7 @@
This is the interactive documentation for the bezier-rs library. Click and drag on the endpoints of the example curves to visualize the various Bezier utilities and functions.
Beziers
-
+
{
- drawText(getContextFromCanvas(canvas), `Length: ${bezier.length().toFixed(2)}`, 5, canvas.height - 7);
- },
+ callback: (bezier: WasmBezierInstance, _: Record): string => bezier.length(),
},
{
name: "Evaluate",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
- const point = JSON.parse(bezier.evaluate(options.t));
- drawPoint(getContextFromCanvas(canvas), point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.evaluate(options.t),
+ exampleOptions: {
+ [BezierCurveType.Quadratic]: {
+ sliderOptions: [tSliderOptions],
+ },
},
- template: markRaw(SliderExample),
- templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Lookup Table",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
- const lookupPoints: Point[] = JSON.parse(bezier.compute_lookup_table(options.steps));
- lookupPoints.forEach((point, index) => {
- if (index !== 0 && index !== lookupPoints.length - 1) {
- drawPoint(getContextFromCanvas(canvas), point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
- }
- });
- },
- template: markRaw(SliderExample),
- templateOptions: {
- sliders: [
- {
- min: 2,
- max: 15,
- step: 1,
- default: 5,
- variable: "steps",
- },
- ],
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.compute_lookup_table(options.steps),
+ exampleOptions: {
+ [BezierCurveType.Quadratic]: {
+ sliderOptions: [
+ {
+ min: 2,
+ max: 15,
+ step: 1,
+ default: 5,
+ variable: "steps",
+ },
+ ],
+ },
},
},
{
name: "Derivative",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
- const context = getContextFromCanvas(canvas);
-
- const derivativeBezier = bezier.derivative();
- if (derivativeBezier) {
- const points: Point[] = JSON.parse(derivativeBezier.get_points());
- if (points.length === 2) {
- drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
- } else {
- drawBezier(context, points, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
- }
- }
- },
- curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
- customPoints: {
- [BezierCurveType.Quadratic]: [
- [30, 40],
- [110, 50],
- [120, 130],
- ],
- [BezierCurveType.Cubic]: [
- [50, 50],
- [60, 100],
- [100, 140],
- [140, 150],
- ],
+ callback: (bezier: WasmBezierInstance, _: Record): string => bezier.derivative(),
+ exampleOptions: {
+ [BezierCurveType.Linear]: {
+ disabled: true,
+ },
+ [BezierCurveType.Quadratic]: {
+ customPoints: [
+ [30, 40],
+ [110, 50],
+ [120, 130],
+ ],
+ },
+ [BezierCurveType.Cubic]: {
+ customPoints: [
+ [50, 50],
+ [60, 100],
+ [100, 140],
+ [140, 150],
+ ],
+ },
},
},
{
name: "Tangent",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
- const context = getContextFromCanvas(canvas);
-
- const intersection = JSON.parse(bezier.evaluate(options.t));
- const tangent = JSON.parse(bezier.tangent(options.t));
-
- const tangentEnd = {
- x: intersection.x + tangent.x * SCALE_UNIT_VECTOR_FACTOR,
- y: intersection.y + tangent.y * SCALE_UNIT_VECTOR_FACTOR,
- };
-
- drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
- drawLine(context, intersection, tangentEnd, COLORS.NON_INTERACTIVE.STROKE_1);
- drawPoint(context, tangentEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.tangent(options.t),
+ exampleOptions: {
+ [BezierCurveType.Quadratic]: {
+ sliderOptions: [tSliderOptions],
+ },
},
- template: markRaw(SliderExample),
- templateOptions: { sliders: [tSliderOptions] },
},
+
{
name: "Normal",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
- const context = getContextFromCanvas(canvas);
-
- const intersection = JSON.parse(bezier.evaluate(options.t));
- const normal = JSON.parse(bezier.normal(options.t));
-
- const normalEnd = {
- x: intersection.x + normal.x * SCALE_UNIT_VECTOR_FACTOR,
- y: intersection.y + normal.y * SCALE_UNIT_VECTOR_FACTOR,
- };
-
- drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
- drawLine(context, intersection, normalEnd, COLORS.NON_INTERACTIVE.STROKE_1);
- drawPoint(context, normalEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.normal(options.t),
+ exampleOptions: {
+ [BezierCurveType.Quadratic]: {
+ sliderOptions: [tSliderOptions],
+ },
},
- template: markRaw(SliderExample),
- templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Curvature",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
- const context = getContextFromCanvas(canvas);
- const point = JSON.parse(bezier.evaluate(options.t));
- const normal = JSON.parse(bezier.normal(options.t));
- const curvature = bezier.curvature(options.t);
- const radius = 1 / curvature;
-
- const curvatureCenter = { x: point.x + normal.x * radius, y: point.y + normal.y * radius };
-
- drawCircle(context, curvatureCenter, Math.abs(radius), COLORS.NON_INTERACTIVE.STROKE_1);
- drawLine(context, point, curvatureCenter, COLORS.NON_INTERACTIVE.STROKE_1);
- drawPoint(context, point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
- drawPoint(context, curvatureCenter, 3, COLORS.NON_INTERACTIVE.STROKE_1);
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.curvature(options.t),
+ exampleOptions: {
+ [BezierCurveType.Linear]: {
+ disabled: true,
+ },
+ [BezierCurveType.Quadratic]: {
+ sliderOptions: [tSliderOptions],
+ },
},
- curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
- template: markRaw(SliderExample),
- templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Split",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
- const context = getContextFromCanvas(canvas);
- const bezierPairPoints = JSON.parse(bezier.split(options.t));
-
- drawBezier(context, bezierPairPoints[0], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_2, radius: 3.5 });
- drawBezier(context, bezierPairPoints[1], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.split(options.t),
+ exampleOptions: {
+ [BezierCurveType.Quadratic]: {
+ sliderOptions: [tSliderOptions],
+ },
},
- template: markRaw(SliderExample),
- templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Trim",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
- const context = getContextFromCanvas(canvas);
- const trimmedBezier = bezier.trim(options.t1, options.t2);
- drawBezierHelper(context, trimmedBezier, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
- },
- template: markRaw(SliderExample),
- templateOptions: {
- sliders: [
- {
- variable: "t1",
- min: 0,
- max: 1,
- step: 0.01,
- default: 0.25,
- },
- {
- variable: "t2",
- min: 0,
- max: 1,
- step: 0.01,
- default: 0.75,
- },
- ],
+ callback: (bezier: WasmBezierInstance, options: Record): string => bezier.trim(options.t1, options.t2),
+ exampleOptions: {
+ [BezierCurveType.Quadratic]: {
+ sliderOptions: [
+ {
+ variable: "t1",
+ min: 0,
+ max: 1,
+ step: 0.01,
+ default: 0.25,
+ },
+ {
+ variable: "t2",
+ min: 0,
+ max: 1,
+ step: 0.01,
+ default: 0.75,
+ },
+ ],
+ },
},
},
{
name: "Project",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record, mouseLocation?: Point): void => {
- if (mouseLocation != null) {
- const context = getContextFromCanvas(canvas);
- const t = bezier.project(mouseLocation.x, mouseLocation.y);
- const closestPoint = JSON.parse(bezier.evaluate(t));
- drawLine(context, mouseLocation, closestPoint, COLORS.NON_INTERACTIVE.STROKE_1);
- }
- },
+ callback: (bezier: WasmBezierInstance, _: Record, mouseLocation: Point): string =>
+ mouseLocation ? bezier.project(mouseLocation.x, mouseLocation.y) : bezier.to_svg(),
+ triggerOnMouseMove: true,
},
{
name: "Local Extrema",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
- const context = getContextFromCanvas(canvas);
- const dimensionColors = ["red", "green"];
- const extrema: number[][] = JSON.parse(bezier.local_extrema());
- extrema.forEach((tValues, index) => {
- tValues.forEach((t) => {
- const point: Point = JSON.parse(bezier.evaluate(t));
- drawPoint(context, point, 4, dimensionColors[index]);
- });
- });
- drawText(getContextFromCanvas(canvas), "X extrema", 5, canvas.height - 20, dimensionColors[0]);
- drawText(getContextFromCanvas(canvas), "Y extrema", 5, canvas.height - 5, dimensionColors[1]);
- },
- customPoints: {
- [BezierCurveType.Quadratic]: [
- [40, 40],
- [160, 30],
- [110, 150],
- ],
- [BezierCurveType.Cubic]: [
- [160, 180],
- [170, 10],
- [30, 90],
- [180, 160],
- ],
+ callback: (bezier: WasmBezierInstance, _: Record): string => bezier.local_extrema(),
+ exampleOptions: {
+ [BezierCurveType.Quadratic]: {
+ customPoints: [
+ [40, 40],
+ [160, 30],
+ [110, 150],
+ ],
+ },
+ [BezierCurveType.Cubic]: {
+ customPoints: [
+ [160, 180],
+ [170, 10],
+ [30, 90],
+ [180, 160],
+ ],
+ },
},
},
{
name: "Bounding Box",
- callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
- const context = getContextFromCanvas(canvas);
- const bboxPoints: Point[] = JSON.parse(bezier.bounding_box());
- const minPoint = bboxPoints[0];
- const maxPoint = bboxPoints[1];
- drawLine(context, minPoint, { x: minPoint.x, y: maxPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
- drawLine(context, minPoint, { x: maxPoint.x, y: minPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
- drawLine(context, maxPoint, { x: minPoint.x, y: maxPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
- drawLine(context, maxPoint, { x: maxPoint.x, y: minPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
- },
+ callback: (bezier: WasmBezierInstance, _: Record): string => bezier.bounding_box(),
},
{
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);
- });
+ callback: (bezier: WasmBezierInstance, _: Record): string => bezier.inflections(),
+ exampleOptions: {
+ [BezierCurveType.Linear]: {
+ disabled: true,
+ },
+ [BezierCurveType.Quadratic]: {
+ disabled: true,
+ },
},
- curveDegrees: new Set([BezierCurveType.Cubic]),
},
+ ],
+ features: [
{
name: "De Casteljau Points",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => {
@@ -398,7 +325,7 @@ export default defineComponent({
drawLine(context, line[0], line[1], COLORS.NON_INTERACTIVE.STROKE_1);
const intersections: Float64Array = bezier.intersect_line_segment(mappedLine);
intersections.forEach((t: number) => {
- const p = JSON.parse(bezier.evaluate(t));
+ const p = JSON.parse(bezier.evaluate_value(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
@@ -416,7 +343,7 @@ export default defineComponent({
drawCurve(context, points, COLORS.NON_INTERACTIVE.STROKE_1, 1);
const intersections: Float64Array = bezier.intersect_quadratic_segment(mappedPoints, options.error);
intersections.forEach((t: number) => {
- const p = JSON.parse(bezier.evaluate(t));
+ const p = JSON.parse(bezier.evaluate_value(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
@@ -447,7 +374,7 @@ export default defineComponent({
drawCurve(context, points, COLORS.NON_INTERACTIVE.STROKE_1, 1);
const intersections: Float64Array = bezier.intersect_cubic_segment(mappedPoints, options.error);
intersections.forEach((t: number) => {
- const p = JSON.parse(bezier.evaluate(t));
+ const p = JSON.parse(bezier.evaluate_value(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
@@ -470,7 +397,7 @@ export default defineComponent({
const context = getContextFromCanvas(canvas);
const intersections: number[][] = JSON.parse(bezier.intersect_self(options.error));
intersections.forEach((tValues: number[]) => {
- const p = JSON.parse(bezier.evaluate(tValues[0]));
+ const p = JSON.parse(bezier.evaluate_value(tValues[0]));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
diff --git a/website/other/bezier-rs-demos/src/components/BezierExample.vue b/website/other/bezier-rs-demos/src/components/BezierExample.vue
index fa745d7f..bb7e0eef 100644
--- a/website/other/bezier-rs-demos/src/components/BezierExample.vue
+++ b/website/other/bezier-rs-demos/src/components/BezierExample.vue
@@ -41,6 +41,10 @@ export default defineComponent({
type: Object as PropType>,
default: () => ({}),
},
+ triggerOnMouseMove: {
+ type: Boolean,
+ default: false,
+ },
},
data() {
const curveType = getCurveType(this.points.length);
@@ -81,6 +85,8 @@ export default defineComponent({
this.bezier[this.manipulatorKeys[this.activeIndex]](mx, my);
this.mutablePoints[this.activeIndex] = [mx, my];
this.bezierSVG = this.callback(this.bezier, this.sliderData);
+ } else if (this.triggerOnMouseMove) {
+ this.bezierSVG = this.callback(this.bezier, this.sliderData, { x: mx, y: my });
}
},
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
diff --git a/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue b/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue
index ac92434a..8aad4d64 100644
--- a/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue
+++ b/website/other/bezier-rs-demos/src/components/BezierExamplePane.vue
@@ -3,7 +3,14 @@
@@ -12,7 +19,7 @@