From 55f6d13dafc2bd0ae666da5701fb43c99c120391 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Tue, 27 Sep 2022 17:37:30 -0700 Subject: [PATCH] Bezier-rs: continue converting demos from canvas to SVG (#779) * Convert constructor to use svg * Convert the through_points functions to svg * Convert length, lut, derivative, and tangent from canvas to svg * Fixed bug when t1 == t2 in split * Converted split and trim to use svg representation, and swapped slider options default to use quadratic options * Convert normal and curvature to use svg representation in bezier-rs-demos * Convert the project function to use svg representation in bezier-rs-demos * Convert the local_extrema, bbox, and inflections to use svgs * Add text offset constants * Fix typo Co-authored-by: Robert Nadal Co-authored-by: Keavon Chambers --- libraries/bezier-rs/src/bezier/transform.rs | 11 +- website/other/bezier-rs-demos/src/App.vue | 313 +++++++----------- .../src/components/BezierExample.vue | 6 + .../src/components/BezierExamplePane.vue | 22 +- .../bezier-rs-demos/src/utils/drawing.ts | 7 +- .../other/bezier-rs-demos/src/utils/types.ts | 2 +- .../other/bezier-rs-demos/wasm/src/bezier.rs | 220 +++++++++--- .../bezier-rs-demos/wasm/src/svg_drawing.rs | 20 +- 8 files changed, 358 insertions(+), 243 deletions(-) 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 @@

{{ name }}

- +
@@ -12,7 +19,7 @@