mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 15:28:11 +08:00
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:
committed by
Keavon Chambers
co-authored by
Thomas Cheng
Robert Nadal
ll2zheng
parent
18a7c6a289
commit
9f76315bdc
@@ -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");
|
||||
@@ -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"]>;
|
||||
Reference in New Issue
Block a user