Added Subpaths to bezier-rs (#730)

* Added Subpath constructor, iterator and length

* Asserted that subpaths of len < 2 cannot be closed. Added len() and comments

* Added subpath to_svg(), made structs public, made quadratic use either in_handle or out_handle

* add bezier handles

* Added basic interactivity and index traits

* Added SubPath interactivity

* Added svg styling

* Broke subpath impl across multiple files. More sylistic changes per review

* Fixed format error

* Added closed subpath to documentation page

* Modified subpath to_svg to use functional style

* Stylistic changes per review

* Fixed build errors

* More sylistic changes per review

* Moved svg commands to constants

* Moved formatting for svg arguments to ToSVGOptions

* Renamed files in git

* Even more stylistic changes per review
This commit is contained in:
Rob Nadal
2022-07-27 17:30:08 -04:00
committed by GitHub
parent 8c888e39c7
commit aeb9e85726
17 changed files with 634 additions and 36 deletions
+23 -7
View File
@@ -2,7 +2,8 @@
<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, index) in features" :key="index">
<h2>Beziers</h2>
<div v-for="(feature, index) in bezierFeatures" :key="index">
<ExamplePane
:template="feature.template"
:templateOptions="feature.templateOptions"
@@ -14,6 +15,10 @@
:customOptions="feature.customOptions"
/>
</div>
<h2>Subpaths</h2>
<div v-for="(feature, index) in subpathFeatures" :key="index">
<SubpathExamplePane :name="feature.name" :callback="feature.callback" />
</div>
</div>
</template>
@@ -21,10 +26,11 @@
import { defineComponent, markRaw } from "vue";
import { drawText, drawPoint, drawBezier, drawLine, getContextFromCanvas, drawBezierHelper, COLORS } from "@/utils/drawing";
import { BezierCurveType, Point, WasmBezierInstance } from "@/utils/types";
import { BezierCurveType, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
import ExamplePane from "@/components/ExamplePane.vue";
import SliderExample from "@/components/SliderExample.vue";
import SubpathExamplePane from "@/components/SubpathExamplePane.vue";
const tSliderOptions = {
min: 0,
@@ -37,13 +43,9 @@ const tSliderOptions = {
const SCALE_UNIT_VECTOR_FACTOR = 50;
export default defineComponent({
name: "App",
components: {
ExamplePane,
},
data() {
return {
features: [
bezierFeatures: [
{
name: "Constructor",
// eslint-disable-next-line
@@ -355,8 +357,22 @@ export default defineComponent({
},
},
],
subpathFeatures: [
{
name: "Constructor",
callback: (subpath: WasmSubpathInstance): string => subpath.to_svg(),
},
{
name: "Length",
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
},
],
};
},
components: {
ExamplePane,
SubpathExamplePane,
},
});
</script>
@@ -12,12 +12,6 @@ import BezierDrawing from "@/components/BezierDrawing";
import { BezierCallback, WasmBezierInstance } from "@/utils/types";
export default defineComponent({
name: "ExampleComponent",
data() {
return {
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
};
},
props: {
title: String,
bezier: {
@@ -37,6 +31,11 @@ export default defineComponent({
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());
@@ -1,6 +1,6 @@
<template>
<div>
<h2 class="example-pane-header">{{ name }}</h2>
<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" />
@@ -58,10 +58,6 @@ const CurveTypeMapping = {
};
export default defineComponent({
name: "ExamplePane",
components: {
Example,
},
props: {
name: {
type: String as PropType<string>,
@@ -118,12 +114,15 @@ export default defineComponent({
});
});
},
components: {
Example,
},
});
</script>
<style>
.example-row {
display: flex; /* or inline-flex */
display: flex;
flex-direction: row;
justify-content: center;
}
@@ -16,10 +16,6 @@ import { BezierCallback, TemplateOption, WasmBezierInstance } from "@/utils/type
import Example from "@/components/Example.vue";
export default defineComponent({
name: "SliderExample",
components: {
Example,
},
props: {
title: String,
bezier: {
@@ -46,6 +42,9 @@ export default defineComponent({
sliderUnits: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.unit }))),
};
},
components: {
Example,
},
});
</script>
@@ -0,0 +1,79 @@
<template>
<div>
<h4 class="example-header">{{ title }}</h4>
<figure @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove" class="example-figure" v-html="subpathSVG"></figure>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { WasmSubpath } from "@/../wasm/pkg";
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey } from "@/utils/types";
const SELECTABLE_RANGE = 10;
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
export default defineComponent({
props: {
title: String,
triples: {
type: Array as PropType<Array<Array<number[] | undefined>>>,
required: true,
mutable: true,
},
closed: {
type: Boolean as PropType<boolean>,
default: false,
},
callback: {
type: Function as PropType<SubpathCallback>,
required: true,
},
},
data() {
const subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
return {
subpath,
subpathSVG: this.callback(subpath),
activeIndex: undefined as number[] | undefined,
mutableTriples: JSON.parse(JSON.stringify(this.triples)),
};
},
methods: {
onMouseDown(event: MouseEvent) {
const mx = event.offsetX;
const my = event.offsetY;
for (let controllerIndex = 0; controllerIndex < this.mutableTriples.length; controllerIndex += 1) {
for (let pointIndex = 0; pointIndex < 3; pointIndex += 1) {
const point = this.mutableTriples[controllerIndex][pointIndex];
if (point && Math.abs(mx - point[0]) < SELECTABLE_RANGE && Math.abs(my - point[1]) < SELECTABLE_RANGE) {
this.activeIndex = [controllerIndex, pointIndex];
return;
}
}
}
},
onMouseUp() {
this.activeIndex = undefined;
},
onMouseMove(event: MouseEvent) {
const mx = event.offsetX;
const my = event.offsetY;
if (this.activeIndex) {
this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my);
this.mutableTriples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my];
this.subpathSVG = this.callback(this.subpath);
}
},
},
});
</script>
<style scoped>
.example-figure {
border: solid 1px black;
width: 200px;
height: 200px;
}
</style>
@@ -0,0 +1,72 @@
<template>
<div>
<h3 class="example-pane-header">{{ name }}</h3>
<div class="example-row">
<div v-for="(example, index) in examples" :key="index">
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { SubpathCallback } from "@/utils/types";
import SubpathExample from "@/components/SubpathExample.vue";
export default defineComponent({
props: {
name: String,
callback: {
type: Function as PropType<SubpathCallback>,
required: true,
},
},
data() {
return {
examples: [
{
title: "Open Subpath",
triples: [
[[20, 20], undefined, [10, 90]],
[[150, 40], [60, 40], undefined],
[[175, 175], undefined, undefined],
[[100, 100], [40, 120], undefined],
],
closed: false,
},
{
title: "Closed Subpath",
triples: [
[[35, 125], undefined, [40, 40]],
[[130, 30], [120, 120], undefined],
[
[145, 150],
[175, 90],
[70, 185],
],
],
closed: true,
},
],
};
},
components: {
SubpathExample,
},
});
</script>
<style scoped>
.example-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.example-pane-header {
margin-bottom: 0;
}
</style>
@@ -7,7 +7,7 @@ export const COLORS = {
CANVAS: "white",
INTERACTIVE: {
STROKE_1: "black",
STROKE_2: "grey",
STROKE_2: "gray",
SELECTED: "blue",
},
NON_INTERACTIVE: {
@@ -5,6 +5,9 @@ export type WasmBezierKey = keyof WasmBezierInstance;
export type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
export type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
export enum BezierCurveType {
Linear = "Linear",
Quadratic = "Quadratic",
@@ -12,6 +15,7 @@ export enum BezierCurveType {
}
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point) => void;
export type SubpathCallback = (subpath: WasmSubpathInstance) => string;
export type SliderOption = {
min: number;