Bezier-rs: Remove unused legacy drawing components and structs (#853)

* Removed unused legacy drawing components

* Removed Point abstraction from frontend code

* Code review fixes

* Helper lambdas to functions

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Rob Nadal
2022-11-18 00:34:52 -08:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 2994afa6b8
commit a220bfa759
11 changed files with 47 additions and 660 deletions
+6 -20
View File
@@ -6,17 +6,6 @@
<div v-for="(feature, index) in bezierFeatures" :key="index">
<BezierExamplePane :name="feature.name" :callback="feature.callback" :exampleOptions="feature.exampleOptions" :triggerOnMouseMove="feature.triggerOnMouseMove" />
</div>
<!-- TODO: Remove the below and all associated canvas-related code, then rename `bezierFeatures` to `features` -->
<div v-for="(feature, index) in ([] as any)" :key="index">
<ExamplePane
:template="feature.template"
:templateOptions="feature.templateOptions"
:name="feature.name"
:callback="feature.callback"
:curveDegrees="feature.curveDegrees"
:customPoints="feature.customPoints"
/>
</div>
<h2>Subpaths</h2>
<div v-for="(feature, index) in subpathFeatures" :key="index">
<SubpathExamplePane :name="feature.name" :callback="feature.callback" />
@@ -28,10 +17,9 @@
import { defineComponent } from "vue";
import { WasmBezier } from "@/../wasm/pkg";
import { BezierCurveType, ExampleOptions, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
import { BezierCurveType, ExampleOptions, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
import BezierExamplePane from "@/components/BezierExamplePane.vue";
import ExamplePane from "@/components/ExamplePane.vue";
import SubpathExamplePane from "@/components/SubpathExamplePane.vue";
const tSliderOptions = {
@@ -61,12 +49,11 @@ export default defineComponent({
{
name: "Bezier Through Points",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => {
const points: Point[] = JSON.parse(bezier.get_points());
const formattedPoints: number[][] = points.map((p) => [p.x, p.y]);
const points = JSON.parse(bezier.get_points());
if (Object.values(options).length === 1) {
return WasmBezier.quadratic_through_points(formattedPoints, options.t);
return WasmBezier.quadratic_through_points(points, options.t);
}
return WasmBezier.cubic_through_points(formattedPoints, options.t, options["midpoint separation"]);
return WasmBezier.cubic_through_points(points, options.t, options["midpoint separation"]);
},
exampleOptions: {
[BezierCurveType.Linear]: {
@@ -233,8 +220,8 @@ export default defineComponent({
},
{
name: "Project",
callback: (bezier: WasmBezierInstance, _: Record<string, number>, mouseLocation: Point): string =>
mouseLocation ? bezier.project(mouseLocation.x, mouseLocation.y) : bezier.to_svg(),
callback: (bezier: WasmBezierInstance, _: Record<string, number>, mouseLocation?: [number, number]): string =>
mouseLocation ? bezier.project(mouseLocation[0], mouseLocation[1]) : bezier.to_svg(),
triggerOnMouseMove: true,
},
{
@@ -529,7 +516,6 @@ export default defineComponent({
},
components: {
BezierExamplePane,
ExamplePane,
SubpathExamplePane,
},
});
@@ -1,150 +0,0 @@
import { COLORS, drawBezier, drawPoint, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing";
import { Callback, BezierPoint, BezierStyleConfig, Point, WasmBezierManipulatorKey, WasmBezierInstance } from "@/utils/types";
// Offset to increase selectable range, used to make points easier to grab
const FUDGE_FACTOR = 3;
// Given the number of points in the curve, map the index of a point to the correct manipulator key
const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [k: number]: WasmBezierManipulatorKey[] } = {
2: ["set_start", "set_end"],
3: ["set_start", "set_handle_start", "set_end"],
4: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
};
class BezierDrawing {
points: BezierPoint[];
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
dragIndex: number | null;
bezier: WasmBezierInstance;
callback: Callback;
options: Record<string, number>;
createThroughPoints: boolean;
constructor(bezier: WasmBezierInstance, callback: Callback, options: Record<string, number>, createThroughPoints = false) {
this.bezier = bezier;
this.callback = callback;
this.options = options;
this.createThroughPoints = createThroughPoints;
const bezierPoints: Point[] = JSON.parse(bezier.get_points());
this.points = bezierPoints.map((p, i, points) => ({
x: p.x,
y: p.y,
r: getPointSizeByIndex(i, points.length),
selected: false,
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][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");
if (canvas === null) {
throw Error("Failed to create canvas");
}
this.canvas = canvas;
this.canvas.style.border = "solid 1px black";
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", (e) => this.mouseDownHandler(e));
this.canvas.addEventListener("mousemove", (e) => this.mouseMoveHandler(e));
this.canvas.addEventListener("mouseup", () => this.deselectPointHandler());
this.updateBezier();
}
clearFigure(): void {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
mouseMoveHandler(evt: MouseEvent): void {
if (evt.buttons === 0) this.deselectPointHandler();
const mx = evt.offsetX;
const my = evt.offsetY;
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.manipulator](selectedPoint.x, selectedPoint.y);
}
}
this.updateBezier({ x: mx, y: my });
}
mouseDownHandler(evt: MouseEvent): void {
const mx = evt.offsetX;
const my = evt.offsetY;
for (let i = 0; i < this.points.length; i += 1) {
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;
break;
}
}
this.updateBezier();
}
deselectPointHandler(): void {
if (this.dragIndex !== undefined) {
this.dragIndex = null;
this.updateBezier();
}
}
updateBezier(mouseLocation?: Point, options: Record<string, number> = {}): void {
this.clearFigure();
if (Object.values(options).length !== 0) {
this.options = options;
}
this.clearFigure();
// 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 pointsToDraw = this.points;
let styleConfig: Partial<BezierStyleConfig> = {
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_2,
};
let dragIndex = this.dragIndex;
if (this.createThroughPoints) {
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, mouseLocation);
}
getCanvas(): HTMLCanvasElement {
return this.canvas;
}
}
export default BezierDrawing;
@@ -86,7 +86,7 @@ export default defineComponent({
this.mutablePoints[this.activeIndex] = [mx, my];
this.bezierSVG = this.callback(this.bezier, this.sliderData);
} else if (this.triggerOnMouseMove) {
this.bezierSVG = this.callback(this.bezier, this.sliderData, { x: mx, y: my });
this.bezierSVG = this.callback(this.bezier, this.sliderData, [mx, my]);
}
},
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
@@ -1,63 +0,0 @@
<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 { Callback, WasmBezierInstance } from "@/utils/types";
export default defineComponent({
props: {
title: String,
bezier: {
type: Object as PropType<WasmBezierInstance>,
required: true,
},
callback: {
type: Function as PropType<Callback>,
required: true,
},
options: {
type: Object as PropType<Record<string, number>>,
default: () => ({}),
},
createThroughPoints: {
type: Boolean as PropType<boolean>,
default: false,
},
},
data() {
return {
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
};
},
mounted() {
const drawing = this.$refs.drawing as HTMLElement;
drawing.appendChild(this.bezierDrawing.getCanvas());
this.bezierDrawing.updateBezier();
},
watch: {
options: {
deep: true,
handler() {
this.bezierDrawing.updateBezier(undefined, this.options);
},
},
},
});
</script>
<style scoped>
.example-header {
margin-bottom: 0;
}
.example-figure {
margin-top: 0.5em;
}
</style>
@@ -1,133 +0,0 @@
<template>
<div>
<h3 class="example-pane-header">{{ name }}</h3>
<div class="example-row">
<div v-for="(example, index) in exampleData" :key="index">
<component :is="template" :templateOptions="example.templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" :createThroughPoints="createThroughPoints" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Component } from "vue";
import { BezierCallback, BezierCurveType, TemplateOption, WasmBezierConstructorKey, WasmBezierInstance, WasmRawInstance } from "@/utils/types";
import Example from "@/components/Example.vue";
type ExampleData = {
title: string;
bezier: WasmBezierInstance;
templateOptions: TemplateOption;
};
type CustomTemplateOptions = {
[key in BezierCurveType]?: TemplateOption;
};
type CustomPoints = {
[key in BezierCurveType]?: number[][];
};
const CurveTypeMapping = {
[BezierCurveType.Linear]: {
points: [
[30, 60],
[140, 120],
],
constructor: "new_linear" as WasmBezierConstructorKey,
},
[BezierCurveType.Quadratic]: {
points: [
[30, 50],
[140, 30],
[160, 170],
],
constructor: "new_quadratic" as WasmBezierConstructorKey,
},
[BezierCurveType.Cubic]: {
points: [
[30, 30],
[60, 140],
[150, 30],
[160, 160],
],
constructor: "new_cubic" as WasmBezierConstructorKey,
},
};
export default defineComponent({
props: {
name: {
type: String as PropType<string>,
required: true,
},
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
template: {
type: Object as PropType<Component>,
default: Example,
},
templateOptions: {
type: Object as PropType<TemplateOption>,
required: false,
},
customOptions: {
type: Object as PropType<CustomTemplateOptions>,
default: () => ({}),
},
createThroughPoints: {
type: Boolean as PropType<boolean>,
default: false,
},
curveDegrees: {
type: Set as PropType<Set<BezierCurveType>>,
default: () => new Set(Object.values(BezierCurveType)),
},
customPoints: {
type: Object as PropType<CustomPoints>,
default: () => ({}),
},
},
data() {
return {
exampleData: [] as ExampleData[],
};
},
mounted() {
import("@/../wasm/pkg").then((wasm: WasmRawInstance) => {
this.exampleData = [];
// Only add example for BezierCurveType that is in the curveDegrees set
Object.values(BezierCurveType).forEach((bezierType) => {
if (this.curveDegrees.has(bezierType)) {
const { points, constructor } = CurveTypeMapping[bezierType];
this.exampleData.push({
title: bezierType,
// Use custom options if they were provided for the current BezierCurveType
bezier: wasm.WasmBezier[constructor](this.customPoints[bezierType] || points),
templateOptions: (this.customOptions[bezierType] || this.templateOptions) as TemplateOption,
});
}
});
});
},
components: {
Example,
},
});
</script>
<style>
.example-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.example-pane-header {
margin-bottom: 0;
}
</style>
@@ -1,54 +0,0 @@
<template>
<div>
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" :createThroughPoints="createThroughPoints" />
<div v-for="(slider, index) in templateOptions.sliders" :key="index">
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ getSliderValue(sliderData[slider.variable], sliderUnits[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, TemplateOption, WasmBezierInstance } from "@/utils/types";
import Example from "@/components/Example.vue";
export default defineComponent({
props: {
title: String,
bezier: {
type: Object as PropType<WasmBezierInstance>,
required: true,
},
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
templateOptions: {
type: Object as PropType<TemplateOption>,
default: () => ({}),
},
createThroughPoints: {
type: Boolean as PropType<boolean>,
default: false,
},
},
data() {
const sliders = this.templateOptions.sliders;
return {
sliderData: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.default }))),
sliderUnits: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.unit }))),
};
},
components: {
Example,
},
methods: {
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
},
});
</script>
<style scoped></style>
@@ -1,155 +0,0 @@
import { BezierStyleConfig, CircleSector, Point } from "@/utils/types";
const HANDLE_RADIUS_FACTOR = 2 / 3;
const DEFAULT_ENDPOINT_RADIUS = 5;
export const COLORS = {
CANVAS: "white",
INTERACTIVE: {
STROKE_1: "black",
STROKE_2: "gray",
SELECTED: "blue",
},
NON_INTERACTIVE: {
STROKE_1: "red",
STROKE_2: "orange",
},
};
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 => {
const ctx = canvas.getContext("2d");
if (ctx === null) {
throw Error("Failed to fetch context");
}
return ctx;
};
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = COLORS.INTERACTIVE.STROKE_2, lineWidth = 1): void => {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.moveTo(point1.x, point1.y);
ctx.lineTo(point2.x, point2.y);
ctx.stroke();
};
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;
ctx.beginPath();
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI, false);
ctx.stroke();
// Fill the point (hiding any overlapping lines)
ctx.fillStyle = COLORS.CANVAS;
ctx.beginPath();
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, textColor = COLORS.INTERACTIVE.STROKE_1): void => {
ctx.fillStyle = textColor;
ctx.font = "16px Arial";
ctx.fillText(text, x, y);
};
export const drawCurve = (ctx: CanvasRenderingContext2D, points: Point[], strokeColor = COLORS.INTERACTIVE.STROKE_1, lineWidth = 2): void => {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
if (points.length === 3) {
ctx.quadraticCurveTo(points[1].x, points[1].y, points[2].x, points[2].y);
} else {
ctx.bezierCurveTo(points[1].x, points[1].y, points[2].x, points[2].y, points[3].x, points[3].y);
}
ctx.stroke();
};
export const drawCircle = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = COLORS.INTERACTIVE.STROKE_1): void => {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI, false);
ctx.stroke();
};
export const drawCircleSector = (ctx: CanvasRenderingContext2D, circleSector: CircleSector, strokeColor = COLORS.INTERACTIVE.STROKE_1, fillColor = COLORS.NON_INTERACTIVE.STROKE_1): void => {
ctx.strokeStyle = strokeColor;
ctx.fillStyle = fillColor;
ctx.lineWidth = 2;
const { center, radius, startAngle, endAngle } = circleSector;
ctx.beginPath();
ctx.moveTo(center.x, center.y);
ctx.arc(center.x, center.y, radius, startAngle, endAngle);
ctx.lineTo(center.x, center.y);
ctx.closePath();
ctx.fill();
};
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,
drawHandles: true,
...bezierStyleConfig,
};
// If the handle or handle line colors are not specified, use the same color 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[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;
let handleEnd = null;
if (points.length === 4) {
handleStart = points[1];
handleEnd = points[2];
end = points[3];
} else if (points.length === 3) {
handleStart = points[1];
handleEnd = handleStart;
end = points[2];
} else {
handleStart = start;
handleEnd = points[1];
end = handleEnd;
}
if (points.length === 2) {
drawLine(ctx, start, end, styleConfig.curveStrokeColor, 2);
} else {
drawCurve(ctx, points, styleConfig.curveStrokeColor);
if (styleConfig.drawHandles) {
drawLine(ctx, start, handleStart, styleConfig.handleLineStrokeColor);
drawLine(ctx, end, handleEnd, styleConfig.handleLineStrokeColor);
}
}
points.forEach((point, index) => {
if (styleConfig.drawHandles || isIndexFirstOrLast(index, points.length)) {
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);
}
});
};
@@ -1,6 +1,6 @@
import { BezierCurveType, WasmBezierConstructorKey } from "@/utils/types";
export const getCurveType = (numPoints: number): BezierCurveType => {
export function getCurveType(numPoints: number): BezierCurveType {
switch (numPoints) {
case 2:
return BezierCurveType.Linear;
@@ -11,9 +11,9 @@ export const getCurveType = (numPoints: number): BezierCurveType => {
default:
throw new Error("Invalid number of points for a bezier");
}
};
}
export const getConstructorKey = (bezierCurveType: BezierCurveType): WasmBezierConstructorKey => {
export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey {
switch (bezierCurveType) {
case BezierCurveType.Linear:
return "new_linear";
@@ -24,4 +24,4 @@ export const getConstructorKey = (bezierCurveType: BezierCurveType): WasmBezierC
default:
throw new Error("Invalid value for a BezierCurveType");
}
};
}
@@ -14,8 +14,7 @@ export enum BezierCurveType {
Cubic = "Cubic",
}
export type Callback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point) => void;
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point) => string;
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance) => string;
export type ExampleOptions = {
@@ -34,31 +33,3 @@ export type SliderOption = {
variable: string;
unit?: string | string[];
};
export type TemplateOption = {
sliders: SliderOption[];
};
export type Point = {
x: number;
y: number;
};
export type BezierPoint = Point & {
manipulator: WasmBezierManipulatorKey;
};
export type BezierStyleConfig = {
curveStrokeColor: string;
handleStrokeColor: string;
handleLineStrokeColor: string;
radius: number;
drawHandles: boolean;
};
export type CircleSector = {
center: Point;
radius: number;
startAngle: number;
endAngle: number;
};