Implement functions to create a Bezier that goes through 3 specified points (#687)

* Implement quadratic and cubic from points

* Catch edge cases and integrate `t` slider

* Add 2 sliders for cubic

* Create utils file for bezier-rs and address other PR comments

* Rename variable and remove unnecessary ids

* Update rustdoc comments and rename variables

* Remove unnecessary file and refactor options for drawing beziers

* Address PR comments

* Update quadratic_through_points description

* Add wasm-pack to dependencies and change from spaces to tabs for indents

* Change strut to midpoint_separation, adjust sliders and section name

* Minor refactor
This commit is contained in:
Hannah Li 2022-06-29 20:52:09 -04:00 committed by Keavon Chambers
parent 4eaffd0e5a
commit 2e3e079982
13 changed files with 19471 additions and 19055 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,41 @@
{ {
"name": "interactive-docs", "name": "interactive-docs",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"vue": "^3.2.13" "vue": "^3.2.13"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-eslint": "^5.0.4", "@vue/cli-plugin-eslint": "^5.0.4",
"@vue/cli-plugin-typescript": "~5.0.0", "@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-service": "^5.0.4", "@vue/cli-service": "^5.0.4",
"@vue/compiler-sfc": "^3.2.31", "@vue/compiler-sfc": "^3.2.31",
"@vue/eslint-config-airbnb": "^6.0.0", "@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^9.1.0", "@vue/eslint-config-typescript": "^9.1.0",
"@wasm-tool/wasm-pack-plugin": "^1.6.0", "@wasm-tool/wasm-pack-plugin": "^1.6.0",
"eslint": "^8.14.0", "eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier-vue": "^3.1.0", "eslint-plugin-prettier-vue": "^3.1.0",
"eslint-plugin-vue": "^8.7.1", "eslint-plugin-vue": "^8.7.1",
"typescript": "~4.5.5", "typescript": "~4.5.5",
"vue-template-compiler": "^2.6.14" "vue-template-compiler": "^2.6.14"
}, },
"browserslist": [ "optionalDependencies": {
"> 1%", "wasm-pack": "^0.10.3"
"last 2 versions", },
"not dead", "browserslist": [
"not ie 11" "> 1%",
] "last 2 versions",
"not dead",
"not ie 11"
]
} }

View File

@ -3,7 +3,14 @@
<h1>Bezier-rs Interactive Documentation</h1> <h1>Bezier-rs Interactive Documentation</h1>
<p>This is the interactive documentation for the <b>bezier-rs</b> library. Click and drag on the endpoints of the example curves to visualize the various Bezier utilities and functions.</p> <p>This is the interactive documentation for the <b>bezier-rs</b> library. Click and drag on the endpoints of the example curves to visualize the various Bezier utilities and functions.</p>
<div v-for="(feature, index) in features" :key="index"> <div v-for="(feature, index) in features" :key="index">
<ExamplePane :template="feature.template" :templateOptions="feature.templateOptions" :name="feature.name" :callback="feature.callback" /> <ExamplePane
:template="feature.template"
:templateOptions="feature.templateOptions"
:name="feature.name"
:callback="feature.callback"
:createThroughPoints="feature.createThroughPoints"
:cubicOptions="feature.cubicOptions"
/>
</div> </div>
<br /> <br />
<div id="svg-test" /> <div id="svg-test" />
@ -22,7 +29,7 @@ import SliderExample from "@/components/SliderExample.vue";
// eslint-disable-next-line // eslint-disable-next-line
const testBezierLib = async () => { const testBezierLib = async () => {
import("@/../wasm/pkg").then((wasm) => { import("@/../wasm/pkg").then((wasm) => {
const bezier = wasm.WasmBezier.new_quad([ const bezier = wasm.WasmBezier.new_quadratic([
[0, 0], [0, 0],
[50, 0], [50, 0],
[100, 100], [100, 100],
@ -55,6 +62,42 @@ export default defineComponent({
// eslint-disable-next-line // eslint-disable-next-line
callback: (): void => {}, callback: (): void => {},
}, },
{
name: "Bezier through points",
// eslint-disable-next-line
callback: (): void => {},
createThroughPoints: true,
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
variable: "t",
},
],
},
cubicOptions: {
sliders: [
{
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
variable: "t",
},
{
min: 0,
max: 100,
step: 5,
default: 10,
variable: "midpoint separation",
},
],
},
},
{ {
name: "Length", name: "Length",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => { callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
@ -150,8 +193,8 @@ export default defineComponent({
const context = getContextFromCanvas(canvas); const context = getContextFromCanvas(canvas);
const bezierPairPoints = JSON.parse(bezier.split(options.t)); const bezierPairPoints = JSON.parse(bezier.split(options.t));
drawBezier(context, bezierPairPoints[0], null, COLORS.NON_INTERACTIVE.STROKE_2, 3.5); drawBezier(context, bezierPairPoints[0], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_2, radius: 3.5 });
drawBezier(context, bezierPairPoints[1], null, COLORS.NON_INTERACTIVE.STROKE_1, 3.5); drawBezier(context, bezierPairPoints[1], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
}, },
template: markRaw(SliderExample), template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] }, templateOptions: { sliders: [tSliderOptions] },
@ -161,7 +204,7 @@ export default defineComponent({
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => { callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas); const context = getContextFromCanvas(canvas);
const trimmedBezier = bezier.trim(options.t1, options.t2); const trimmedBezier = bezier.trim(options.t1, options.t2);
drawBezierHelper(context, trimmedBezier, COLORS.NON_INTERACTIVE.STROKE_1, 3.5); drawBezierHelper(context, trimmedBezier, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
}, },
template: markRaw(SliderExample), template: markRaw(SliderExample),
templateOptions: { templateOptions: {

View File

@ -1,6 +1,6 @@
import { drawBezier, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing"; import { WasmBezier } from "@/../wasm/pkg";
import { BezierCallback, BezierPoint, WasmBezierMutatorKey } from "@/utils/types"; import { COLORS, drawBezier, drawPoint, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing";
import { WasmBezierInstance } from "@/utils/wasm-comm"; import { BezierCallback, BezierPoint, BezierStyleConfig, WasmBezierMutatorKey, WasmBezierInstance } from "@/utils/types";
// Offset to increase selectable range, used to make points easier to grab // Offset to increase selectable range, used to make points easier to grab
const FUDGE_FACTOR = 3; const FUDGE_FACTOR = 3;
@ -22,10 +22,13 @@ class BezierDrawing {
options: Record<string, number>; options: Record<string, number>;
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: Record<string, number>) { createThroughPoints: boolean;
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: Record<string, number>, createThroughPoints = false) {
this.bezier = bezier; this.bezier = bezier;
this.callback = callback; this.callback = callback;
this.options = options; this.options = options;
this.createThroughPoints = createThroughPoints;
this.points = bezier this.points = bezier
.get_points() .get_points()
.map((p) => JSON.parse(p)) .map((p) => JSON.parse(p))
@ -37,6 +40,11 @@ class BezierDrawing {
mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i], mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i],
})); }));
if (this.createThroughPoints && this.points.length === 4) {
// Use the first handler as the middle point
this.points = [this.points[0], this.points[1], this.points[3]];
}
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
if (canvas === null) { if (canvas === null) {
throw Error("Failed to create canvas"); throw Error("Failed to create canvas");
@ -105,7 +113,39 @@ class BezierDrawing {
this.options = options; this.options = options;
} }
this.clearFigure(); this.clearFigure();
drawBezier(this.ctx, this.points, this.dragIndex);
// For the create through points cases, we store a bezier where the handle is actually the point that the curve should pass through
// This is so that we can re-use the drag and drop logic, while simply drawing the desired bezier instead
const actualBezierPointLength = this.bezier.get_points().length;
let pointsToDraw = this.points;
let styleConfig: Partial<BezierStyleConfig> = {
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_2,
};
let dragIndex = this.dragIndex;
if (this.createThroughPoints) {
let serializedPoints;
const pointList = this.points.map((p) => [p.x, p.y]);
if (actualBezierPointLength === 3) {
serializedPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
} else {
serializedPoints = WasmBezier.cubic_through_points(pointList, this.options.t, this.options["midpoint separation"]);
}
pointsToDraw = serializedPoints.get_points().map((p) => JSON.parse(p));
if (this.dragIndex === 1) {
// Do not propagate dragIndex when the the non-endpoint is moved
dragIndex = null;
} else if (this.dragIndex === 2 && pointsToDraw.length === 4) {
// For the cubic case, we want to propagate the drag index when the end point is moved, but need to adjust the index
dragIndex = 3;
}
styleConfig = { handleLineStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, handleStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1 };
}
drawBezier(this.ctx, pointsToDraw, dragIndex, styleConfig);
if (this.createThroughPoints) {
// Draw the point that the curve was drawn through
drawPoint(this.ctx, this.points[1], getPointSizeByIndex(1, this.points.length), this.dragIndex === 1 ? COLORS.INTERACTIVE.SELECTED : COLORS.INTERACTIVE.STROKE_1);
}
this.callback(this.canvas, this.bezier, this.options); this.callback(this.canvas, this.bezier, this.options);
} }

View File

@ -9,14 +9,13 @@
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import BezierDrawing from "@/components/BezierDrawing"; import BezierDrawing from "@/components/BezierDrawing";
import { BezierCallback } from "@/utils/types"; import { BezierCallback, WasmBezierInstance } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
export default defineComponent({ export default defineComponent({
name: "ExampleComponent", name: "ExampleComponent",
data() { data() {
return { return {
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options), bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
}; };
}, },
props: { props: {
@ -33,6 +32,10 @@ export default defineComponent({
type: Object as PropType<Record<string, number>>, type: Object as PropType<Record<string, number>>,
default: () => ({}), default: () => ({}),
}, },
createThroughPoints: {
type: Boolean as PropType<boolean>,
default: false,
},
}, },
mounted() { mounted() {
const drawing = this.$refs.drawing as HTMLElement; const drawing = this.$refs.drawing as HTMLElement;

View File

@ -2,8 +2,8 @@
<div> <div>
<h2 class="example_pane_header">{{ name }}</h2> <h2 class="example_pane_header">{{ name }}</h2>
<div class="example_row"> <div class="example_row">
<div v-for="example in exampleData" :key="example.id"> <div v-for="(example, index) in exampleData" :key="index">
<component :is="template" :templateOptions="templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" /> <component :is="template" :templateOptions="example.templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" :createThroughPoints="createThroughPoints" />
</div> </div>
</div> </div>
</div> </div>
@ -12,15 +12,14 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, Component } from "vue"; import { defineComponent, PropType, Component } from "vue";
import { BezierCallback } from "@/utils/types"; import { BezierCallback, TemplateOption, WasmBezierInstance, WasmRawInstance } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
import Example from "@/components/Example.vue"; import Example from "@/components/Example.vue";
type ExampleData = { type ExampleData = {
id: number;
title: string; title: string;
bezier: WasmBezierInstance; bezier: WasmBezierInstance;
templateOptions: TemplateOption;
}; };
export default defineComponent({ export default defineComponent({
@ -38,7 +37,15 @@ export default defineComponent({
type: Object as PropType<Component>, type: Object as PropType<Component>,
default: Example, default: Example,
}, },
templateOptions: Object, templateOptions: Object as PropType<TemplateOption>,
cubicOptions: {
type: Object as PropType<TemplateOption>,
default: null,
},
createThroughPoints: {
type: Boolean as PropType<boolean>,
default: false,
},
}, },
data() { data() {
return { return {
@ -46,26 +53,28 @@ export default defineComponent({
}; };
}, },
mounted() { mounted() {
import("@/../wasm/pkg").then((wasm) => { import("@/../wasm/pkg").then((wasm: WasmRawInstance) => {
const quadraticPoints = [
[30, 50],
[140, 30],
[160, 170],
];
const cubicPoints = [
[30, 30],
[60, 140],
[150, 30],
[160, 160],
];
this.exampleData = [ this.exampleData = [
{ {
id: 0,
title: "Quadratic", title: "Quadratic",
bezier: wasm.WasmBezier.new_quad([ bezier: wasm.WasmBezier.new_quadratic(quadraticPoints),
[30, 50], templateOptions: this.templateOptions as TemplateOption,
[140, 30],
[160, 170],
]),
}, },
{ {
id: 1,
title: "Cubic", title: "Cubic",
bezier: wasm.WasmBezier.new_cubic([ bezier: wasm.WasmBezier.new_cubic(cubicPoints),
[30, 30], templateOptions: (this.cubicOptions || this.templateOptions) as TemplateOption,
[60, 140],
[150, 30],
[160, 160],
]),
}, },
]; ];
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" /> <Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" :createThroughPoints="createThroughPoints" />
<div v-for="(slider, index) in templateOptions.sliders" :key="index"> <div v-for="(slider, index) in templateOptions.sliders" :key="index">
<div class="slider_label">{{ slider.variable }} = {{ sliderData[slider.variable] }}</div> <div class="slider_label">{{ slider.variable }} = {{ sliderData[slider.variable] }}</div>
<input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" /> <input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" />
@ -11,8 +11,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import { BezierCallback, SliderOption } from "@/utils/types"; import { BezierCallback, TemplateOption, WasmBezierInstance } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
import Example from "@/components/Example.vue"; import Example from "@/components/Example.vue";
@ -32,12 +31,16 @@ export default defineComponent({
required: true, required: true,
}, },
templateOptions: { templateOptions: {
type: Object, type: Object as PropType<TemplateOption>,
default: () => ({}), default: () => ({}),
}, },
createThroughPoints: {
type: Boolean as PropType<boolean>,
default: false,
},
}, },
data() { data() {
const sliders: SliderOption[] = this.templateOptions.sliders; const sliders = this.templateOptions.sliders;
return { return {
sliderData: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.default }))), sliderData: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.default }))),
}; };

View File

@ -1,4 +1,4 @@
import { Point, WasmBezierInstance } from "@/utils/types"; import { BezierStyleConfig, Point, WasmBezierInstance } from "@/utils/types";
const HANDLE_RADIUS_FACTOR = 2 / 3; const HANDLE_RADIUS_FACTOR = 2 / 3;
const DEFAULT_ENDPOINT_RADIUS = 5; const DEFAULT_ENDPOINT_RADIUS = 5;
@ -16,7 +16,9 @@ export const COLORS = {
}, },
}; };
export const getPointSizeByIndex = (index: number, numPoints: number, radius = DEFAULT_ENDPOINT_RADIUS): number => (index === 0 || index === numPoints - 1 ? radius : radius * HANDLE_RADIUS_FACTOR); export const isIndexFirstOrLast = (index: number, arrayLength: number): boolean => index === 0 || index === arrayLength - 1;
export const getPointSizeByIndex = (index: number, numPoints: number, radius = DEFAULT_ENDPOINT_RADIUS): number => (isIndexFirstOrLast(index, numPoints) ? radius : radius * HANDLE_RADIUS_FACTOR);
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => { export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
@ -57,12 +59,28 @@ export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number,
ctx.fillText(text, x, y); ctx.fillText(text, x, y);
}; };
export const drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, strokeColor = COLORS.INTERACTIVE.STROKE_1, radius = DEFAULT_ENDPOINT_RADIUS): void => { export const drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, bezierStyleConfig: Partial<BezierStyleConfig> = {}): void => {
const points = bezier.get_points().map((p: string) => JSON.parse(p)); const points = bezier.get_points().map((p: string) => JSON.parse(p));
drawBezier(ctx, points, null, strokeColor, radius); drawBezier(ctx, points, null, bezierStyleConfig);
}; };
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null, strokeColor = COLORS.INTERACTIVE.STROKE_1, radius = DEFAULT_ENDPOINT_RADIUS): void => { export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null, bezierStyleConfig: Partial<BezierStyleConfig> = {}): void => {
const styleConfig: BezierStyleConfig = {
curveStrokeColor: COLORS.INTERACTIVE.STROKE_1,
handleStrokeColor: COLORS.INTERACTIVE.STROKE_1,
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_1,
radius: DEFAULT_ENDPOINT_RADIUS,
...bezierStyleConfig,
};
// if the handle or handle line colors are not specified, use the same colour as the rest of the curve
if (bezierStyleConfig.curveStrokeColor) {
if (!bezierStyleConfig.handleStrokeColor) {
styleConfig.handleStrokeColor = bezierStyleConfig.curveStrokeColor;
}
if (!bezierStyleConfig.handleLineStrokeColor) {
styleConfig.handleLineStrokeColor = bezierStyleConfig.curveStrokeColor;
}
}
// Points passed to drawBezier are interpreted as follows // Points passed to drawBezier are interpreted as follows
// points[0] = start point // points[0] = start point
// points[1] = handle start // points[1] = handle start
@ -82,7 +100,7 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
end = points[2]; end = points[2];
} }
ctx.strokeStyle = strokeColor; ctx.strokeStyle = styleConfig.curveStrokeColor;
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.beginPath(); ctx.beginPath();
@ -94,10 +112,11 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
} }
ctx.stroke(); ctx.stroke();
drawLine(ctx, start, handleStart, strokeColor); drawLine(ctx, start, handleStart, styleConfig.handleLineStrokeColor);
drawLine(ctx, end, handleEnd, strokeColor); drawLine(ctx, end, handleEnd, styleConfig.handleLineStrokeColor);
points.forEach((point, index) => { points.forEach((point, index) => {
drawPoint(ctx, point, getPointSizeByIndex(index, points.length, radius), index === dragIndex ? COLORS.INTERACTIVE.SELECTED : strokeColor); const strokeColor = isIndexFirstOrLast(index, points.length) ? styleConfig.curveStrokeColor : styleConfig.handleStrokeColor;
drawPoint(ctx, point, getPointSizeByIndex(index, points.length, styleConfig.radius), index === dragIndex ? COLORS.INTERACTIVE.SELECTED : strokeColor);
}); });
}; };

View File

@ -14,6 +14,10 @@ export type SliderOption = {
variable: string; variable: string;
}; };
export type TemplateOption = {
sliders: SliderOption[];
};
export type Point = { export type Point = {
x: number; x: number;
y: number; y: number;
@ -22,3 +26,10 @@ export type Point = {
export type BezierPoint = Point & { export type BezierPoint = Point & {
mutator: WasmBezierMutatorKey; mutator: WasmBezierMutatorKey;
}; };
export type BezierStyleConfig = {
curveStrokeColor: string;
handleStrokeColor: string;
handleLineStrokeColor: string;
radius: number;
};

View File

@ -1,2 +0,0 @@
export type WasmRawInstance = typeof import("../../wasm/pkg");
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;

View File

@ -22,7 +22,7 @@ pub fn vec_to_point(p: &DVec2) -> JsValue {
#[wasm_bindgen] #[wasm_bindgen]
impl WasmBezier { 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_quad(js_points: &JsValue) -> WasmBezier { pub fn new_quadratic(js_points: &JsValue) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap(); let points: [DVec2; 3] = js_points.into_serde().unwrap();
WasmBezier(Bezier::from_quadratic_dvec2(points[0], points[1], points[2])) WasmBezier(Bezier::from_quadratic_dvec2(points[0], points[1], points[2]))
} }
@ -33,6 +33,16 @@ impl WasmBezier {
WasmBezier(Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3])) WasmBezier(Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]))
} }
pub fn quadratic_through_points(js_points: &JsValue, t: f64) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
WasmBezier(Bezier::quadratic_through_points(points[0], points[1], points[2], t))
}
pub fn cubic_through_points(js_points: &JsValue, t: f64, midpoint_separation: f64) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
WasmBezier(Bezier::cubic_through_points(points[0], points[1], points[2], t, midpoint_separation))
}
pub fn set_start(&mut self, x: f64, y: f64) { pub fn set_start(&mut self, x: f64, y: f64) {
self.0.set_start(DVec2::new(x, y)); self.0.set_start(DVec2::new(x, y));
} }

View File

@ -1,5 +1,7 @@
use glam::DVec2; 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)] #[derive(Copy, Clone)]
pub enum BezierHandles { pub enum BezierHandles {
@ -70,18 +72,42 @@ impl Bezier {
} }
} }
/// Create a quadratic bezier curve that goes through 3 points /// Create a quadratic bezier curve that goes through 3 points, where the middle point will be at the corresponding position `t` on the curve.
// #[inline] /// Note that when `t = 0` or `t = 1`, the expectation is that the `point_on_curve` should be equal to `start` and `end` respectively.
pub fn quadratic_from_points(p1: DVec2, p2: DVec2, p3: DVec2, _t: f64) -> Self { /// In these cases, if the provided values are not equal, this function will use the `point_on_curve` as the `start`/`end` instead.
// TODO: Implement logic to get actual curve through the points pub fn quadratic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: f64) -> Self {
Bezier::from_quadratic_dvec2(p1, p2, p3) if t == 0. {
return Bezier::from_quadratic_dvec2(point_on_curve, point_on_curve, end);
}
if t == 1. {
return Bezier::from_quadratic_dvec2(start, point_on_curve, point_on_curve);
}
let [a, _, _] = utils::compute_abc_for_quadratic_through_points(start, point_on_curve, end, t);
Bezier::from_quadratic_dvec2(start, a, end)
} }
/// Create a cubic bezier curve that goes through 3 points. d1 represents the strut. /// Create a cubic bezier curve that goes through 3 points, where the middle point will be at the corresponding position `t` on the curve.
// #[inline] /// Note that when `t = 0` or `t = 1`, the expectation is that the `point_on_curve` should be equal to `start` and `end` respectively.
pub fn cubic_from_points(p1: DVec2, p2: DVec2, p3: DVec2, _t: f64, _d1: f64) -> Self { /// In these cases, if the provided values are not equal, this function will use the `point_on_curve` as the `start`/`end` instead.
// TODO: Implement logic to get actual curve through the points /// - `midpoint_separation` is a representation of the how wide the resulting curve will be around `t` on the curve. This parameter designates the distance between the `e1` and `e2` defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
Bezier::from_quadratic_dvec2(p1, p2, p3) pub fn cubic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: f64, midpoint_separation: f64) -> Self {
if t == 0. {
return Bezier::from_cubic_dvec2(point_on_curve, point_on_curve, end, end);
}
if t == 1. {
return Bezier::from_cubic_dvec2(start, start, point_on_curve, point_on_curve);
}
let [a, b, _] = utils::compute_abc_for_cubic_through_points(start, point_on_curve, end, t);
let distance_between_start_and_end = (end - start) / (start.distance(end));
let e1 = b - (distance_between_start_and_end * midpoint_separation);
let e2 = b + (distance_between_start_and_end * midpoint_separation * (1. - t) / t);
// TODO: these functions can be changed to helpers, but need to come up with an appropriate name first
let v1 = (e1 - t * a) / (1. - t);
let v2 = (e2 - (1. - t) * a) / t;
let handle_start = (v1 - (1. - t) * start) / t;
let handle_end = (v2 - t * end) / (1. - t);
Bezier::from_cubic_dvec2(start, handle_start, handle_end, end)
} }
/// Convert to SVG /// Convert to SVG
@ -306,3 +332,45 @@ impl Bezier {
bezier_starting_at_t1.split(adjusted_t2)[t2_split_side] bezier_starting_at_t1.split(adjusted_t2)[t2_split_side]
} }
} }
#[cfg(test)]
mod tests {
use crate::Bezier;
use glam::DVec2;
fn compare_points(p1: DVec2, p2: DVec2) -> bool {
DVec2::new(0.001, 0.001).cmpge(p1 - p2).all()
}
#[test]
fn quadratic_from_points() {
let p1 = DVec2::new(30., 50.);
let p2 = DVec2::new(140., 30.);
let p3 = DVec2::new(160., 170.);
let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, 0.5);
assert!(compare_points(bezier1.compute(0.5), p2));
let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, 0.8);
assert!(compare_points(bezier2.compute(0.8), p2));
let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, 0.);
assert!(compare_points(bezier3.compute(0.), p2));
}
#[test]
fn cubic_through_points() {
let p1 = DVec2::new(30., 30.);
let p2 = DVec2::new(60., 140.);
let p3 = DVec2::new(160., 160.);
let bezier1 = Bezier::cubic_through_points(p1, p2, p3, 0.3, 10.);
assert!(compare_points(bezier1.compute(0.3), p2));
let bezier2 = Bezier::cubic_through_points(p1, p2, p3, 0.8, 91.7);
assert!(compare_points(bezier2.compute(0.8), p2));
let bezier3 = Bezier::cubic_through_points(p1, p2, p3, 0., 91.7);
assert!(compare_points(bezier3.compute(0.), p2));
}
}

View File

@ -0,0 +1,31 @@
use glam::DVec2;
/// Helper to perform the computation of a and c, where b is the provided point on the curve.
/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases.
/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
fn compute_abc_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t_to_nth_power: f64, nth_power_of_one_minus_t: f64) -> [DVec2; 3] {
let point_c_ratio = nth_power_of_one_minus_t / (t_to_nth_power + nth_power_of_one_minus_t);
let c = point_c_ratio * start_point + (1. - point_c_ratio) * end_point;
let ab_bc_ratio = (t_to_nth_power + nth_power_of_one_minus_t - 1.).abs() / (t_to_nth_power + nth_power_of_one_minus_t);
let a = point_on_curve + (point_on_curve - c) / ab_bc_ratio;
[a, point_on_curve, c]
}
/// Compute a, b, and c for a quadratic 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_quadratic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] {
let t_squared = t * t;
let one_minus_t = 1. - t;
let squared_one_minus_t = one_minus_t * one_minus_t;
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.
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;
let cubed_one_minus_t = one_minus_t * one_minus_t * one_minus_t;
compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t)
}