Bezier derivative and normal implementation (#679)

* added ui for derivative impl

* Add derivative computation for bezier-rs library

* integrate devivative ui with library

* Add implementation for the normal function

* Update rustdoc comments

* Rename handles and getters, add tangent function

* Rename variables, address nits

Co-authored-by: Rob Nadal <robnadal44@gmail.com>
Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com>
Co-authored-by: ll2zheng <Linda Zheng>
This commit is contained in:
Hannah Li
2022-06-23 17:03:48 -04:00
committed by GitHub
co-authored by Rob Nadal Thomas Cheng ll2zheng
parent 2052c00010
commit a4a174db04
7 changed files with 224 additions and 108 deletions
+68 -16
View File
@@ -13,7 +13,7 @@
<script lang="ts">
import { defineComponent, markRaw } from "vue";
import { drawText, drawPoint, getContextFromCanvas } from "@/utils/drawing";
import { drawText, drawPoint, drawLine, getContextFromCanvas } from "@/utils/drawing";
import { WasmBezierInstance } from "@/utils/types";
import ExamplePane from "@/components/ExamplePane.vue";
@@ -34,6 +34,14 @@ const testBezierLib = async () => {
});
};
const tSliderOptions = {
min: 0,
max: 1,
step: 0.01,
default: 0.5,
variable: "t",
};
export default defineComponent({
name: "App",
components: {
@@ -60,30 +68,19 @@ export default defineComponent({
name: "Compute",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const point = JSON.parse(bezier.compute(parseFloat(options)));
point.r = 4;
point.selected = false;
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
drawPoint(getContextFromCanvas(canvas), point, 4, "Red");
},
template: markRaw(SliderExample),
templateOptions: {
min: 0,
max: 1,
step: 0.01,
default: 0.5,
variable: "t",
},
templateOptions: tSliderOptions,
},
{
id: 4,
name: "Lookup Table",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const lookupPoints = bezier.compute_lookup_table(Number(options));
lookupPoints.forEach((serPoint, index) => {
lookupPoints.forEach((serialisedPoint, index) => {
if (index !== 0 && index !== lookupPoints.length - 1) {
const point = JSON.parse(serPoint);
point.r = 3;
point.selected = false;
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
drawPoint(getContextFromCanvas(canvas), JSON.parse(serialisedPoint), 3, "Red");
}
});
},
@@ -96,6 +93,61 @@ export default defineComponent({
variable: "Steps",
},
},
{
id: 5,
name: "Derivative",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const t = parseFloat(options);
const context = getContextFromCanvas(canvas);
const intersection = JSON.parse(bezier.compute(t));
const derivative = JSON.parse(bezier.derivative(t));
const curveFactor = bezier.get_points().length - 1;
const tangentStart = {
x: intersection.x - derivative.x / curveFactor,
y: intersection.y - derivative.y / curveFactor,
};
const tangentEnd = {
x: intersection.x + derivative.x / curveFactor,
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");
},
template: markRaw(SliderExample),
templateOptions: tSliderOptions,
},
{
id: 6,
name: "Normal",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
const t = parseFloat(options);
const context = getContextFromCanvas(canvas);
const intersection = JSON.parse(bezier.compute(t));
const normal = JSON.parse(bezier.normal(t));
const normalStart = {
x: intersection.x - normal.x * 20,
y: intersection.y - normal.y * 20,
};
const normalEnd = {
x: intersection.x + normal.x * 20,
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");
},
template: markRaw(SliderExample),
templateOptions: tSliderOptions,
},
],
};
},
@@ -1,11 +1,14 @@
import { drawBezier, getContextFromCanvas } from "@/utils/drawing";
import { BezierCallback, Point, WasmBezierMutatorKey } from "@/utils/types";
import { drawBezier, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing";
import { BezierCallback, BezierPoint, WasmBezierMutatorKey } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
class BezierDrawing {
static indexToMutator: WasmBezierMutatorKey[] = ["set_start", "set_handle1", "set_handle2", "set_end"];
// Offset to increase selectable range, used to make points easier to grab
const FUDGE_FACTOR = 3;
points: Point[];
class BezierDrawing {
static indexToMutator: WasmBezierMutatorKey[] = ["set_start", "set_handle_start", "set_handle_end", "set_end"];
points: BezierPoint[];
canvas: HTMLCanvasElement;
@@ -29,7 +32,7 @@ class BezierDrawing {
.map((p, i, points) => ({
x: p.x,
y: p.y,
r: i === 0 || i === points.length - 1 ? 5 : 3,
r: getPointSizeByIndex(i, points.length),
selected: false,
mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i],
}));
@@ -40,6 +43,7 @@ class BezierDrawing {
}
this.canvas = canvas;
this.canvas.style.border = "solid 1px black";
this.canvas.width = 200;
this.canvas.height = 200;
@@ -51,52 +55,48 @@ class BezierDrawing {
this.canvas.addEventListener("mousemove", this.mouseMoveHandler.bind(this));
this.canvas.addEventListener("mouseup", this.deselectPointHandler.bind(this));
this.canvas.addEventListener("mouseout", this.deselectPointHandler.bind(this));
this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
this.updateBezier();
}
clearFigure(): void {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
mouseMoveHandler(evt: MouseEvent): void {
const mx = evt.offsetX;
const my = evt.offsetY;
if (
this.dragIndex != null &&
mx - this.points[this.dragIndex].r > 0 &&
my - this.points[this.dragIndex].r > 0 &&
mx + this.points[this.dragIndex].r < this.canvas.width &&
my + this.points[this.dragIndex].r < this.canvas.height
) {
const selectedPoint = this.points[this.dragIndex];
selectedPoint.x = mx;
selectedPoint.y = my;
this.bezier[selectedPoint.mutator](selectedPoint.x, selectedPoint.y);
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
this.updateBezier();
if (this.dragIndex !== null) {
const selectableRange = getPointSizeByIndex(this.dragIndex, this.points.length);
if (mx - selectableRange > 0 && my - selectableRange > 0 && mx + selectableRange < this.canvas.width && my + selectableRange < this.canvas.height) {
const selectedPoint = this.points[this.dragIndex];
selectedPoint.x = mx;
selectedPoint.y = my;
this.bezier[selectedPoint.mutator](selectedPoint.x, selectedPoint.y);
this.clearFigure();
}
}
this.updateBezier();
}
mouseDownHandler(evt: MouseEvent): void {
const mx = evt.offsetX;
const my = evt.offsetY;
for (let i = 0; i < this.points.length; i += 1) {
if (
Math.abs(mx - this.points[i].x) < this.points[i].r + 3 &&
Math.abs(my - this.points[i].y) < this.points[i].r + 3 // Fudge factor makes the points easier to grab
) {
const selectableRange = getPointSizeByIndex(i, this.points.length) + FUDGE_FACTOR;
if (Math.abs(mx - this.points[i].x) < selectableRange && Math.abs(my - this.points[i].y) < selectableRange) {
this.dragIndex = i;
this.points[this.dragIndex].selected = true;
break;
}
}
this.updateBezier();
}
deselectPointHandler(): void {
if (this.dragIndex != null) {
this.points[this.dragIndex].selected = false;
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
this.updateBezier();
if (this.dragIndex !== undefined) {
this.clearFigure();
this.dragIndex = null;
this.updateBezier();
}
}
@@ -104,8 +104,8 @@ class BezierDrawing {
if (options !== "") {
this.options = options;
}
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
drawBezier(this.ctx, this.points);
this.clearFigure();
drawBezier(this.ctx, this.points, this.dragIndex);
this.callback(this.canvas, this.bezier, this.options);
}
@@ -52,8 +52,8 @@ export default defineComponent({
id: 0,
title: "Quadratic",
bezier: wasm.WasmBezier.new_quad([
[30, 30],
[140, 20],
[30, 50],
[140, 30],
[160, 170],
]),
},
@@ -1,5 +1,12 @@
import { Point } from "@/utils/types";
const RADIUS_SIZE = {
large: 5,
small: 3,
};
export const getPointSizeByIndex = (index: number, numPoints: number): number => RADIUS_SIZE[index === 0 || index === numPoints - 1 ? "large" : "small"];
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
const ctx = canvas.getContext("2d");
if (ctx === null) {
@@ -8,28 +15,28 @@ export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRendering
return ctx;
};
export const drawLine = (ctx: CanvasRenderingContext2D, p1: Point, p2: Point): void => {
ctx.strokeStyle = "grey";
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = "gray"): void => {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.moveTo(point1.x, point1.y);
ctx.lineTo(point2.x, point2.y);
ctx.stroke();
};
export const drawPoint = (ctx: CanvasRenderingContext2D, p: Point, stroke = "black"): void => {
export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = "black"): void => {
// Outline the point
ctx.strokeStyle = p.selected ? "blue" : stroke;
ctx.lineWidth = p.r / 3;
ctx.strokeStyle = strokeColor;
ctx.lineWidth = radius / 3;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, 2 * Math.PI, false);
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI, false);
ctx.stroke();
// Fill the point (hiding any overlapping lines)
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * (2 / 3), 0, 2 * Math.PI, false);
ctx.arc(point.x, point.y, radius * (2 / 3), 0, 2 * Math.PI, false);
ctx.fill();
};
@@ -39,24 +46,24 @@ export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number,
ctx.fillText(text, x, y);
};
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[]): void => {
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 1
points[2] = (optional) handle 2
points[1] = handle start
points[2] = (optional) handle end
points[3] = end point
*/
const start = points[0];
let end = null;
let handle1 = null;
let handle2 = null;
let handleStart = null;
let handleEnd = null;
if (points.length === 4) {
handle1 = points[1];
handle2 = points[2];
handleStart = points[1];
handleEnd = points[2];
end = points[3];
} else {
handle1 = points[1];
handle2 = handle1;
handleStart = points[1];
handleEnd = handleStart;
end = points[2];
}
@@ -66,16 +73,16 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[]): void
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
if (points.length === 3) {
ctx.quadraticCurveTo(handle1.x, handle1.y, end.x, end.y);
ctx.quadraticCurveTo(handleStart.x, handleStart.y, end.x, end.y);
} else {
ctx.bezierCurveTo(handle1.x, handle1.y, handle2.x, handle2.y, end.x, end.y);
ctx.bezierCurveTo(handleStart.x, handleStart.y, handleEnd.x, handleEnd.y, end.x, end.y);
}
ctx.stroke();
drawLine(ctx, start, handle1);
drawLine(ctx, end, handle2);
drawLine(ctx, start, handleStart);
drawLine(ctx, end, handleEnd);
points.forEach((point) => {
drawPoint(ctx, point);
points.forEach((point, index) => {
drawPoint(ctx, point, getPointSizeByIndex(index, points.length), index === dragIndex ? "Blue" : "Black");
});
};
@@ -2,14 +2,15 @@ export type WasmRawInstance = typeof import("../../wasm/pkg");
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
export type WasmBezierKey = keyof WasmBezierInstance;
export type WasmBezierMutatorKey = "set_start" | "set_handle1" | "set_handle2" | "set_end";
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 Point = {
x: number;
y: number;
r: number;
};
export type BezierPoint = Point & {
mutator: WasmBezierMutatorKey;
selected: boolean;
};