Bezier split and trim implementation (#680)

* Added split implementation and UI

Co-authored-by: Thomas Cheng <Androxium@users.noreply.github.com>

* Added bezier split impl

* Adjust struct traits

* Implement trim and adjust FE code to handle multiple sliders per feature

Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com>
Co-authored-by: Rob Nadal <robnadal44@gmail.com>

* Fix edge case in trim and add rust doc comments

* Stylistic changes per review

* More stylistic changes per review

* replaced last explicit color usages

Co-authored-by: Robert Nadal <Robnadal44@gmail.com>
Co-authored-by: Thomas Cheng <Androxium@users.noreply.github.com>
Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com>
This commit is contained in:
Hannah Li
2022-06-27 19:33:56 -04:00
committed by Keavon Chambers
co-authored by Thomas Cheng Thomas Cheng Rob Nadal
parent 7f15cac5e2
commit 4eaffd0e5a
8 changed files with 224 additions and 102 deletions
+73 -38
View File
@@ -2,7 +2,7 @@
<div class="App">
<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>
<div v-for="feature in features" :key="feature.id">
<div v-for="(feature, index) in features" :key="index">
<ExamplePane :template="feature.template" :templateOptions="feature.templateOptions" :name="feature.name" :callback="feature.callback" />
</div>
<br />
@@ -13,7 +13,7 @@
<script lang="ts">
import { defineComponent, markRaw } from "vue";
import { drawText, drawPoint, drawLine, getContextFromCanvas } from "@/utils/drawing";
import { drawText, drawPoint, drawBezier, drawLine, getContextFromCanvas, drawBezierHelper, COLORS } from "@/utils/drawing";
import { WasmBezierInstance } from "@/utils/types";
import ExamplePane from "@/components/ExamplePane.vue";
@@ -51,57 +51,55 @@ export default defineComponent({
return {
features: [
{
id: 0,
name: "Constructor",
// eslint-disable-next-line
callback: (): void => {},
},
{
id: 2,
name: "Length",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
drawText(getContextFromCanvas(canvas), `Length: ${bezier.length().toFixed(2)}`, 5, canvas.height - 7);
},
},
{
id: 3,
name: "Compute",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const point = JSON.parse(bezier.compute(parseFloat(options)));
drawPoint(getContextFromCanvas(canvas), point, 4, "Red");
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const point = JSON.parse(bezier.compute(options.t));
drawPoint(getContextFromCanvas(canvas), point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
},
template: markRaw(SliderExample),
templateOptions: tSliderOptions,
templateOptions: { sliders: [tSliderOptions] },
},
{
id: 4,
name: "Lookup Table",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const lookupPoints = bezier.compute_lookup_table(Number(options));
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const lookupPoints = bezier.compute_lookup_table(options.steps);
lookupPoints.forEach((serialisedPoint, index) => {
if (index !== 0 && index !== lookupPoints.length - 1) {
drawPoint(getContextFromCanvas(canvas), JSON.parse(serialisedPoint), 3, "Red");
drawPoint(getContextFromCanvas(canvas), JSON.parse(serialisedPoint), 3, COLORS.NON_INTERACTIVE.STROKE_1);
}
});
},
template: markRaw(SliderExample),
templateOptions: {
min: 2,
max: 15,
step: 1,
default: 5,
variable: "Steps",
sliders: [
{
min: 2,
max: 15,
step: 1,
default: 5,
variable: "steps",
},
],
},
},
{
id: 5,
name: "Derivative",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const t = parseFloat(options);
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const intersection = JSON.parse(bezier.compute(t));
const derivative = JSON.parse(bezier.derivative(t));
const intersection = JSON.parse(bezier.compute(options.t));
const derivative = JSON.parse(bezier.derivative(options.t));
const curveFactor = bezier.get_points().length - 1;
const tangentStart = {
@@ -113,23 +111,21 @@ export default defineComponent({
y: intersection.y + derivative.y / curveFactor,
};
drawLine(context, tangentStart, tangentEnd, "Red");
drawPoint(context, tangentStart, 3, "Red");
drawPoint(context, intersection, 3, "Red");
drawPoint(context, tangentEnd, 3, "Red");
drawLine(context, tangentStart, tangentEnd, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, tangentStart, 3, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, tangentEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
},
template: markRaw(SliderExample),
templateOptions: tSliderOptions,
templateOptions: { sliders: [tSliderOptions] },
},
{
id: 6,
name: "Normal",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const t = parseFloat(options);
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const intersection = JSON.parse(bezier.compute(t));
const normal = JSON.parse(bezier.normal(t));
const intersection = JSON.parse(bezier.compute(options.t));
const normal = JSON.parse(bezier.normal(options.t));
const normalStart = {
x: intersection.x - normal.x * 20,
@@ -140,13 +136,52 @@ export default defineComponent({
y: intersection.y + normal.y * 20,
};
drawLine(context, normalStart, normalEnd, "Red");
drawPoint(context, normalStart, 3, "Red");
drawPoint(context, intersection, 3, "Red");
drawPoint(context, normalEnd, 3, "Red");
drawLine(context, normalStart, normalEnd, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, normalStart, 3, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, normalEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
},
template: markRaw(SliderExample),
templateOptions: tSliderOptions,
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Split",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const bezierPairPoints = JSON.parse(bezier.split(options.t));
drawBezier(context, bezierPairPoints[0], null, COLORS.NON_INTERACTIVE.STROKE_2, 3.5);
drawBezier(context, bezierPairPoints[1], null, COLORS.NON_INTERACTIVE.STROKE_1, 3.5);
},
template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Trim",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const trimmedBezier = bezier.trim(options.t1, options.t2);
drawBezierHelper(context, trimmedBezier, COLORS.NON_INTERACTIVE.STROKE_1, 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,
},
],
},
},
],
};
@@ -20,9 +20,9 @@ class BezierDrawing {
callback: BezierCallback;
options: string;
options: Record<string, number>;
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: string) {
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: Record<string, number>) {
this.bezier = bezier;
this.callback = callback;
this.options = options;
@@ -100,8 +100,8 @@ class BezierDrawing {
}
}
updateBezier(options = ""): void {
if (options !== "") {
updateBezier(options: Record<string, number> = {}): void {
if (Object.values(options).length !== 0) {
this.options = options;
}
this.clearFigure();
@@ -30,8 +30,8 @@ export default defineComponent({
required: true,
},
options: {
type: String,
default: "",
type: Object as PropType<Record<string, number>>,
default: () => ({}),
},
},
mounted() {
@@ -40,8 +40,11 @@ export default defineComponent({
this.bezierDrawing.updateBezier();
},
watch: {
options() {
this.bezierDrawing.updateBezier(this.options);
options: {
deep: true,
handler() {
this.bezierDrawing.updateBezier(this.options);
},
},
},
});
@@ -1,15 +1,17 @@
<template>
<div>
<Example :title="title" :bezier="bezier" :callback="callback" :options="value.toString()" />
<div class="slider_label">{{ templateOptions.variable }} = {{ value }}</div>
<input class="slider" v-model="value" type="range" :step="templateOptions.step" :min="templateOptions.min" :max="templateOptions.max" />
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" />
<div v-for="(slider, index) in templateOptions.sliders" :key="index">
<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" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { BezierCallback } from "@/utils/types";
import { BezierCallback, SliderOption } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
import Example from "@/components/Example.vue";
@@ -35,8 +37,9 @@ export default defineComponent({
},
},
data() {
const sliders: SliderOption[] = this.templateOptions.sliders;
return {
value: this.templateOptions.default,
sliderData: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.default }))),
};
},
});
@@ -1,11 +1,22 @@
import { Point } from "@/utils/types";
import { Point, WasmBezierInstance } from "@/utils/types";
const RADIUS_SIZE = {
large: 5,
small: 3,
const HANDLE_RADIUS_FACTOR = 2 / 3;
const DEFAULT_ENDPOINT_RADIUS = 5;
export const COLORS = {
CANVAS: "white",
INTERACTIVE: {
STROKE_1: "black",
STROKE_2: "grey",
SELECTED: "blue",
},
NON_INTERACTIVE: {
STROKE_1: "red",
STROKE_2: "orange",
},
};
export const getPointSizeByIndex = (index: number, numPoints: number): number => RADIUS_SIZE[index === 0 || index === numPoints - 1 ? "large" : "small"];
export const getPointSizeByIndex = (index: number, numPoints: number, radius = DEFAULT_ENDPOINT_RADIUS): number => (index === 0 || index === numPoints - 1 ? radius : radius * HANDLE_RADIUS_FACTOR);
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
const ctx = canvas.getContext("2d");
@@ -15,7 +26,7 @@ export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRendering
return ctx;
};
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = "gray"): void => {
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = COLORS.INTERACTIVE.STROKE_2): void => {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 1;
@@ -25,7 +36,7 @@ export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: P
ctx.stroke();
};
export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = "black"): void => {
export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = COLORS.INTERACTIVE.STROKE_1): void => {
// Outline the point
ctx.strokeStyle = strokeColor;
ctx.lineWidth = radius / 3;
@@ -34,25 +45,29 @@ export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: n
ctx.stroke();
// Fill the point (hiding any overlapping lines)
ctx.fillStyle = "white";
ctx.fillStyle = COLORS.CANVAS;
ctx.beginPath();
ctx.arc(point.x, point.y, radius * (2 / 3), 0, 2 * Math.PI, false);
ctx.arc(point.x, point.y, radius * HANDLE_RADIUS_FACTOR, 0, 2 * Math.PI, false);
ctx.fill();
};
export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number): void => {
ctx.fillStyle = "black";
export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number, textColor = COLORS.INTERACTIVE.STROKE_1): void => {
ctx.fillStyle = textColor;
ctx.font = "16px Arial";
ctx.fillText(text, x, y);
};
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null): void => {
/* Until a bezier representation is finalized, treat the points as follows
points[0] = start point
points[1] = handle start
points[2] = (optional) handle end
points[3] = end point
*/
export const drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, strokeColor = COLORS.INTERACTIVE.STROKE_1, radius = DEFAULT_ENDPOINT_RADIUS): void => {
const points = bezier.get_points().map((p: string) => JSON.parse(p));
drawBezier(ctx, points, null, strokeColor, radius);
};
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null, strokeColor = COLORS.INTERACTIVE.STROKE_1, radius = DEFAULT_ENDPOINT_RADIUS): void => {
// Points passed to drawBezier are interpreted as follows
// points[0] = start point
// points[1] = handle start
// points[2] = (optional) handle end
// points[3] = end point
const start = points[0];
let end = null;
let handleStart = null;
@@ -67,7 +82,7 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
end = points[2];
}
ctx.strokeStyle = "black";
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 2;
ctx.beginPath();
@@ -79,10 +94,10 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
}
ctx.stroke();
drawLine(ctx, start, handleStart);
drawLine(ctx, end, handleEnd);
drawLine(ctx, start, handleStart, strokeColor);
drawLine(ctx, end, handleEnd, strokeColor);
points.forEach((point, index) => {
drawPoint(ctx, point, getPointSizeByIndex(index, points.length), index === dragIndex ? "Blue" : "Black");
drawPoint(ctx, point, getPointSizeByIndex(index, points.length, radius), index === dragIndex ? COLORS.INTERACTIVE.SELECTED : strokeColor);
});
};
@@ -4,7 +4,15 @@ export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
export type WasmBezierKey = keyof WasmBezierInstance;
export type WasmBezierMutatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string) => void;
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>) => void;
export type SliderOption = {
min: number;
max: number;
step: number;
default: number;
variable: string;
};
export type Point = {
x: number;
+28 -21
View File
@@ -9,11 +9,10 @@ struct Point {
y: f64,
}
/// Wrapper of the `Bezier` struct to be used in JS.
#[wasm_bindgen]
/// Wrapper of the `Bezier` struct to be used in JS
pub struct WasmBezier {
internal: Bezier,
}
#[derive(Clone)]
pub struct WasmBezier(Bezier);
/// Convert a `DVec2` into a `JsValue`
pub fn vec_to_point(p: &DVec2) -> JsValue {
@@ -25,60 +24,68 @@ impl WasmBezier {
/// Expect js_points to be a list of 3 pairs
pub fn new_quad(js_points: &JsValue) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
WasmBezier {
internal: Bezier::from_quadratic_dvec2(points[0], points[1], points[2]),
}
WasmBezier(Bezier::from_quadratic_dvec2(points[0], points[1], points[2]))
}
/// 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 {
internal: 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 set_start(&mut self, x: f64, y: f64) {
self.internal.set_start(DVec2::from((x, y)));
self.0.set_start(DVec2::new(x, y));
}
pub fn set_end(&mut self, x: f64, y: f64) {
self.internal.set_end(DVec2::from((x, y)));
self.0.set_end(DVec2::new(x, y));
}
pub fn set_handle_start(&mut self, x: f64, y: f64) {
self.internal.set_handle_start(DVec2::from((x, y)));
self.0.set_handle_start(DVec2::new(x, y));
}
pub fn set_handle_end(&mut self, x: f64, y: f64) {
self.internal.set_handle_end(DVec2::from((x, y)));
self.0.set_handle_end(DVec2::new(x, y));
}
pub fn get_points(&self) -> Vec<JsValue> {
self.internal.get_points().iter().flatten().map(vec_to_point).collect()
self.0.get_points().iter().flatten().map(vec_to_point).collect()
}
pub fn to_svg(&self) -> String {
self.internal.to_svg()
self.0.to_svg()
}
pub fn length(&self) -> f64 {
self.internal.length()
self.0.length()
}
pub fn compute(&self, t: f64) -> JsValue {
vec_to_point(&self.internal.compute(t))
vec_to_point(&self.0.compute(t))
}
pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> {
self.internal.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
}
pub fn derivative(&self, t: f64) -> JsValue {
vec_to_point(&self.internal.derivative(t))
vec_to_point(&self.0.derivative(t))
}
pub fn normal(&self, t: f64) -> JsValue {
vec_to_point(&self.internal.normal(t))
vec_to_point(&self.0.normal(t))
}
pub fn split(&self, t: f64) -> JsValue {
let bezier_points: [Vec<Point>; 2] = self
.0
.split(t)
.map(|bezier| bezier.get_points().iter().flatten().map(|point| Point { x: point.x, y: point.y }).collect());
JsValue::from_serde(&serde_json::to_string(&bezier_points).unwrap()).unwrap()
}
pub fn trim(&self, t1: f64, t2: f64) -> WasmBezier {
WasmBezier(self.0.trim(t1, t2))
}
}