Beginnings of the bezier-rs math library (#662)

Co-authored-by: Thomas Cheng <35661641+Androxium@users.noreply.github.com>
Co-authored-by: Robert Nadal <Robnadal44@gmail.com>
Co-authored-by: ll2zheng <ll2zheng@uwaterloo.ca>
This commit is contained in:
Hannah Li
2022-06-16 20:50:58 -04:00
committed by Keavon Chambers
co-authored by Thomas Cheng Robert Nadal ll2zheng
parent 18a7c6a289
commit 9f76315bdc
30 changed files with 20185 additions and 2 deletions
+114
View File
@@ -0,0 +1,114 @@
<template>
<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">
<ExamplePane :template="feature.template" :templateOptions="feature.templateOptions" :name="feature.name" :callback="feature.callback" />
</div>
<br />
<div id="svg-test" />
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw } from "vue";
import { drawText, drawPoint, getContextFromCanvas } from "@/utils/drawing";
import { WasmBezierInstance } from "@/utils/types";
import ExamplePane from "@/components/ExamplePane.vue";
import SliderExample from "@/components/SliderExample.vue";
// eslint-disable-next-line
const testBezierLib = async () => {
import("@/../wasm/pkg").then((wasm) => {
const bezier = wasm.WasmBezier.new_quad([
[0, 0],
[50, 0],
[100, 100],
]);
const svgContainer = document.getElementById("svg-test");
if (svgContainer) {
svgContainer.innerHTML = bezier.to_svg();
}
});
};
export default defineComponent({
name: "App",
components: {
ExamplePane,
},
data() {
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)));
point.r = 4;
point.selected = false;
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
},
template: markRaw(SliderExample),
templateOptions: {
min: 0,
max: 1,
step: 0.01,
default: 0.5,
variable: "t",
},
},
{
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) => {
if (index !== 0 && index !== lookupPoints.length - 1) {
const point = JSON.parse(serPoint);
point.r = 3;
point.selected = false;
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
}
});
},
template: markRaw(SliderExample),
templateOptions: {
min: 2,
max: 15,
step: 1,
default: 5,
variable: "Steps",
},
},
],
};
},
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
@@ -0,0 +1,117 @@
import { drawBezier, getContextFromCanvas } from "@/utils/drawing";
import { BezierCallback, Point, WasmBezierMutatorKey } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
class BezierDrawing {
static indexToMutator: WasmBezierMutatorKey[] = ["set_start", "set_handle1", "set_handle2", "set_end"];
points: Point[];
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
dragIndex: number | null;
bezier: WasmBezierInstance;
callback: BezierCallback;
options: string;
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: string) {
this.bezier = bezier;
this.callback = callback;
this.options = options;
this.points = bezier
.get_points()
.map((p) => JSON.parse(p))
.map((p, i, points) => ({
x: p.x,
y: p.y,
r: i === 0 || i === points.length - 1 ? 5 : 3,
selected: false,
mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i],
}));
const canvas = document.createElement("canvas");
if (canvas === null) {
throw Error("Failed to create canvas");
}
this.canvas = canvas;
this.canvas.width = 200;
this.canvas.height = 200;
this.ctx = getContextFromCanvas(this.canvas);
this.dragIndex = null; // Index of the point being moved
this.canvas.addEventListener("mousedown", this.mouseDownHandler.bind(this));
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();
}
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();
}
}
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
) {
this.dragIndex = i;
this.points[this.dragIndex].selected = true;
break;
}
}
}
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();
this.dragIndex = null;
}
}
updateBezier(options = ""): void {
if (options !== "") {
this.options = options;
}
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
drawBezier(this.ctx, this.points);
this.callback(this.canvas, this.bezier, this.options);
}
getCanvas(): HTMLCanvasElement {
return this.canvas;
}
}
export default BezierDrawing;
@@ -0,0 +1,57 @@
<template>
<div>
<h4 class="example_header">{{ title }}</h4>
<figure class="example_figure" ref="drawing"></figure>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import BezierDrawing from "@/components/BezierDrawing";
import { BezierCallback } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
export default defineComponent({
name: "ExampleComponent",
data() {
return {
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options),
};
},
props: {
title: String,
bezier: {
type: Object as PropType<WasmBezierInstance>,
required: true,
},
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
options: {
type: String,
default: "",
},
},
mounted() {
const drawing = this.$refs.drawing as HTMLElement;
drawing.appendChild(this.bezierDrawing.getCanvas());
this.bezierDrawing.updateBezier();
},
watch: {
options() {
this.bezierDrawing.updateBezier(this.options);
},
},
});
</script>
<style scoped>
.example_header {
margin-bottom: 0;
}
.example_figure {
margin-top: 0.5em;
}
</style>
@@ -0,0 +1,86 @@
<template>
<div>
<h2 class="example_pane_header">{{ name }}</h2>
<div class="example_row">
<div v-for="example in exampleData" :key="example.id">
<component :is="template" :templateOptions="templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Component } from "vue";
import { BezierCallback } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
import Example from "@/components/Example.vue";
type ExampleData = {
id: number;
title: string;
bezier: WasmBezierInstance;
};
export default defineComponent({
name: "ExamplePane",
components: {
Example,
},
props: {
name: String,
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
template: {
type: Object as PropType<Component>,
default: Example,
},
templateOptions: Object,
},
data() {
return {
exampleData: [] as ExampleData[],
};
},
mounted() {
import("@/../wasm/pkg").then((wasm) => {
this.exampleData = [
{
id: 0,
title: "Quadratic",
bezier: wasm.WasmBezier.new_quad([
[30, 30],
[140, 20],
[160, 170],
]),
},
{
id: 1,
title: "Cubic",
bezier: wasm.WasmBezier.new_cubic([
[30, 30],
[60, 140],
[150, 30],
[160, 160],
]),
},
];
});
},
});
</script>
<style>
.example_row {
display: flex; /* or inline-flex */
flex-direction: row;
justify-content: center;
}
.example_pane_header {
margin-bottom: 0;
}
</style>
@@ -0,0 +1,45 @@
<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" />
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { BezierCallback } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
import Example from "@/components/Example.vue";
export default defineComponent({
name: "SliderExample",
components: {
Example,
},
props: {
title: String,
bezier: {
type: Object as PropType<WasmBezierInstance>,
required: true,
},
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
templateOptions: {
type: Object,
default: () => ({}),
},
},
data() {
return {
value: this.templateOptions.default,
};
},
});
</script>
<style scoped></style>
@@ -0,0 +1,5 @@
import { createApp } from "vue";
import App from "@/App.vue";
createApp(App).mount("#app");
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable */
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
@@ -0,0 +1,81 @@
import { Point } from "@/utils/types";
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
const ctx = canvas.getContext("2d");
if (ctx === null) {
throw Error("Failed to fetch context");
}
return ctx;
};
export const drawLine = (ctx: CanvasRenderingContext2D, p1: Point, p2: Point): void => {
ctx.strokeStyle = "grey";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
};
export const drawPoint = (ctx: CanvasRenderingContext2D, p: Point, stroke = "black"): void => {
// Outline the point
ctx.strokeStyle = p.selected ? "blue" : stroke;
ctx.lineWidth = p.r / 3;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 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.fill();
};
export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number): void => {
ctx.fillStyle = "black";
ctx.font = "16px Arial";
ctx.fillText(text, x, y);
};
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[]): 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[3] = end point
*/
const start = points[0];
let end = null;
let handle1 = null;
let handle2 = null;
if (points.length === 4) {
handle1 = points[1];
handle2 = points[2];
end = points[3];
} else {
handle1 = points[1];
handle2 = handle1;
end = points[2];
}
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
if (points.length === 3) {
ctx.quadraticCurveTo(handle1.x, handle1.y, end.x, end.y);
} else {
ctx.bezierCurveTo(handle1.x, handle1.y, handle2.x, handle2.y, end.x, end.y);
}
ctx.stroke();
drawLine(ctx, start, handle1);
drawLine(ctx, end, handle2);
points.forEach((point) => {
drawPoint(ctx, point);
});
};
@@ -0,0 +1,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 BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string) => void;
export type Point = {
x: number;
y: number;
r: number;
mutator: WasmBezierMutatorKey;
selected: boolean;
};
@@ -0,0 +1,2 @@
export type WasmRawInstance = typeof import("../../wasm/pkg");
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;