Move bezier-rs into libraries folder and deploy its interactive docs

This commit is contained in:
Keavon Chambers
2022-08-15 00:17:31 -07:00
parent 44eb83c9b3
commit 763cdbb100
44 changed files with 352 additions and 544 deletions
+125
View File
@@ -0,0 +1,125 @@
const webpackConfigPath = require.resolve("@vue/cli-service/webpack.config.js");
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2020: true,
},
parserOptions: {
ecmaVersion: 2020,
// parser: '@typescript-eslint/parser'
},
extends: [
// Vue-specific defaults
"plugin:vue/vue3-essential",
// Vue-compatible JS defaults
"@vue/airbnb",
// Vue-compatible TS defaults
"@vue/typescript/recommended",
// Vue-compatible Prettier defaults
"plugin:prettier-vue/recommended",
// General Prettier defaults
"prettier",
],
settings: {
// https://github.com/import-js/eslint-plugin-import#resolvers
"import/resolver": {
// `node` must be listed first!
node: {},
webpack: { config: webpackConfigPath },
},
// https://github.com/meteorlxy/eslint-plugin-prettier-vue
"prettier-vue": {
// Use Prettier to format the HTML, CSS, and JS blocks of .vue single-file components
SFCBlocks: {
template: true,
style: true,
script: true,
},
},
},
ignorePatterns: [
// Ignore generated directories
"node_modules/",
"dist/",
"pkg/",
"wasm/pkg/",
// Don't ignore JS and TS dotfiles in this folder
"!.*.js",
"!.*.ts",
],
rules: {
// Standard ESLint config
indent: "off",
quotes: ["error", "double"],
camelcase: ["error", { properties: "always" }],
"linebreak-style": ["error", "unix"],
"eol-last": ["error", "always"],
"max-len": ["error", { code: 200, tabWidth: 4 }],
"prefer-destructuring": "off",
"no-console": "warn",
"no-debugger": "warn",
"no-param-reassign": ["error", { props: false }],
"no-bitwise": "off",
"no-shadow": "off",
"no-use-before-define": "off",
"no-restricted-imports": ["error", { patterns: [".*", "!@/*"] }],
// TypeScript plugin config
"@typescript-eslint/indent": "off",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", ignoreRestSiblings: true }],
"@typescript-eslint/explicit-function-return-type": ["error"],
// Import plugin config (used to intelligently validate module import statements)
"import/prefer-default-export": "off",
"import/no-relative-packages": "error",
"import/order": [
"error",
{
alphabetize: {
order: "asc",
caseInsensitive: true,
},
warnOnUnassignedImports: true,
"newlines-between": "always-and-inside-groups",
pathGroups: [
{
pattern: "**/*.vue",
group: "unknown",
position: "after",
},
],
},
],
// Prettier plugin config (used to enforce HTML, CSS, and JS formatting styles as an ESLint plugin, where fixes are reported to ESLint to be applied when linting)
"prettier-vue/prettier": [
"error",
{
tabWidth: 4,
tabs: true,
printWidth: 200,
},
],
// Vue plugin config (used to validate Vue single-file components)
"vue/multi-word-component-names": "off",
// Vue Accessibility plugin config (included by airbnb defaults but undesirable for a web app project)
"vuejs-accessibility/form-control-has-label": "off",
"vuejs-accessibility/label-has-for": "off",
"vuejs-accessibility/click-events-have-key-events": "off",
},
overrides: [
{
files: ["*.js"],
rules: {
"@typescript-eslint/explicit-function-return-type": ["off"],
},
},
],
};
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
/node_modules
/public
/dist
/wasm/pkg
+24
View File
@@ -0,0 +1,24 @@
# Bezier-rs interactive docs
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm start
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
{
"name": "bezier-rs-demos",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "vue-cli-service serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^3.2.13"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-eslint": "^5.0.4",
"@vue/cli-plugin-typescript": "^5.0.0",
"@vue/cli-service": "^5.0.4",
"@vue/compiler-sfc": "^3.2.31",
"@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
"eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier-vue": "^4.2.0",
"eslint-plugin-vue": "^9.2.0",
"typescript": "^4.7.4",
"vue-template-compiler": "^2.6.14"
},
"optionalDependencies": {
"wasm-pack": "^0.10.3"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
+602
View File
@@ -0,0 +1,602 @@
<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>
<h2>Beziers</h2>
<div v-for="(feature, index) in bezierFeatures" :key="index">
<ExamplePane
:template="feature.template"
:templateOptions="feature.templateOptions"
:name="feature.name"
:callback="feature.callback"
:createThroughPoints="feature.createThroughPoints"
:curveDegrees="feature.curveDegrees"
:customPoints="feature.customPoints"
: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>
<script lang="ts">
import { defineComponent, markRaw } from "vue";
import { drawBezier, drawBezierHelper, drawCircle, drawCircleSector, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
import { BezierCurveType, CircleSector, 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,
max: 1,
step: 0.01,
default: 0.5,
variable: "t",
};
const SCALE_UNIT_VECTOR_FACTOR = 50;
export default defineComponent({
data() {
return {
bezierFeatures: [
{
name: "Constructor",
// eslint-disable-next-line
callback: (): void => {},
},
{
name: "Bezier Through Points",
// eslint-disable-next-line
callback: (): void => {},
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
createThroughPoints: true,
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
variable: "t",
},
],
},
customOptions: {
[BezierCurveType.Cubic]: {
sliders: [
{
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
variable: "t",
},
{
min: 0,
max: 100,
step: 2,
default: 30,
variable: "midpoint separation",
},
],
},
},
customPoints: {
[BezierCurveType.Quadratic]: [
[30, 50],
[120, 70],
[160, 170],
],
},
},
{
name: "Length",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
drawText(getContextFromCanvas(canvas), `Length: ${bezier.length().toFixed(2)}`, 5, canvas.height - 7);
},
},
{
name: "Evaluate",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const point = JSON.parse(bezier.evaluate(options.t));
drawPoint(getContextFromCanvas(canvas), point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
},
template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Lookup Table",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const lookupPoints: Point[] = JSON.parse(bezier.compute_lookup_table(options.steps));
lookupPoints.forEach((point, index) => {
if (index !== 0 && index !== lookupPoints.length - 1) {
drawPoint(getContextFromCanvas(canvas), point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
}
});
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
min: 2,
max: 15,
step: 1,
default: 5,
variable: "steps",
},
],
},
},
{
name: "Derivative",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const derivativeBezier = bezier.derivative();
if (derivativeBezier) {
const points: Point[] = JSON.parse(derivativeBezier.get_points());
if (points.length === 2) {
drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
} else {
drawBezier(context, points, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
}
}
},
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
customPoints: {
[BezierCurveType.Quadratic]: [
[30, 40],
[110, 50],
[120, 130],
],
[BezierCurveType.Cubic]: [
[50, 50],
[60, 100],
[100, 140],
[140, 150],
],
},
},
{
name: "Tangent",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const intersection = JSON.parse(bezier.evaluate(options.t));
const tangent = JSON.parse(bezier.tangent(options.t));
const tangentEnd = {
x: intersection.x + tangent.x * SCALE_UNIT_VECTOR_FACTOR,
y: intersection.y + tangent.y * SCALE_UNIT_VECTOR_FACTOR,
};
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
drawLine(context, intersection, tangentEnd, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, tangentEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
},
template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Normal",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const intersection = JSON.parse(bezier.evaluate(options.t));
const normal = JSON.parse(bezier.normal(options.t));
const normalEnd = {
x: intersection.x + normal.x * SCALE_UNIT_VECTOR_FACTOR,
y: intersection.y + normal.y * SCALE_UNIT_VECTOR_FACTOR,
};
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
drawLine(context, intersection, normalEnd, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, normalEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
},
template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Curvature",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const point = JSON.parse(bezier.evaluate(options.t));
const normal = JSON.parse(bezier.normal(options.t));
const curvature = bezier.curvature(options.t);
const radius = 1 / curvature;
const curvatureCenter = { x: point.x + normal.x * radius, y: point.y + normal.y * radius };
drawCircle(context, curvatureCenter, Math.abs(radius), COLORS.NON_INTERACTIVE.STROKE_1);
drawLine(context, point, curvatureCenter, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
drawPoint(context, curvatureCenter, 3, COLORS.NON_INTERACTIVE.STROKE_1);
},
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Split",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const bezierPairPoints = JSON.parse(bezier.split(options.t));
drawBezier(context, bezierPairPoints[0], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_2, radius: 3.5 });
drawBezier(context, bezierPairPoints[1], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
},
template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Trim",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const trimmedBezier = bezier.trim(options.t1, options.t2);
drawBezierHelper(context, trimmedBezier, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "t1",
min: 0,
max: 1,
step: 0.01,
default: 0.25,
},
{
variable: "t2",
min: 0,
max: 1,
step: 0.01,
default: 0.75,
},
],
},
},
{
name: "Project",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point): void => {
if (mouseLocation != null) {
const context = getContextFromCanvas(canvas);
const t = bezier.project(mouseLocation.x, mouseLocation.y);
const closestPoint = JSON.parse(bezier.evaluate(t));
drawLine(context, mouseLocation, closestPoint, COLORS.NON_INTERACTIVE.STROKE_1);
}
},
},
{
name: "Local Extrema",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const dimensionColors = ["red", "green"];
const extrema: number[][] = JSON.parse(bezier.local_extrema());
extrema.forEach((tValues, index) => {
tValues.forEach((t) => {
const point: Point = JSON.parse(bezier.evaluate(t));
drawPoint(context, point, 4, dimensionColors[index]);
});
});
drawText(getContextFromCanvas(canvas), "X extrema", 5, canvas.height - 20, dimensionColors[0]);
drawText(getContextFromCanvas(canvas), "Y extrema", 5, canvas.height - 5, dimensionColors[1]);
},
customPoints: {
[BezierCurveType.Quadratic]: [
[40, 40],
[160, 30],
[110, 150],
],
[BezierCurveType.Cubic]: [
[160, 180],
[170, 10],
[30, 90],
[180, 160],
],
},
},
{
name: "Bounding Box",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const bboxPoints: Point[] = JSON.parse(bezier.bounding_box());
const minPoint = bboxPoints[0];
const maxPoint = bboxPoints[1];
drawLine(context, minPoint, { x: minPoint.x, y: maxPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
drawLine(context, minPoint, { x: maxPoint.x, y: minPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
drawLine(context, maxPoint, { x: minPoint.x, y: maxPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
drawLine(context, maxPoint, { x: maxPoint.x, y: minPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
},
},
{
name: "Inflections",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const inflections: number[] = JSON.parse(bezier.inflections());
inflections.forEach((t) => {
const point = JSON.parse(bezier.evaluate(t));
drawPoint(context, point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
});
},
curveDegrees: new Set([BezierCurveType.Cubic]),
},
{
name: "De Casteljau Points",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const hullPoints: Point[][] = JSON.parse(bezier.de_casteljau_points(options.t));
hullPoints.reverse().forEach((iteration: Point[], iterationIndex) => {
const colorLight = `hsl(${90 * iterationIndex}, 100%, 50%)`;
iteration.forEach((point: Point, index) => {
// Skip the anchor and handle points which are already drawn in black
if (iterationIndex !== hullPoints.length - 1) {
drawPoint(getContextFromCanvas(canvas), point, 4, colorLight);
}
if (index !== 0) {
const prevPoint: Point = iteration[index - 1];
drawLine(getContextFromCanvas(canvas), point, prevPoint, colorLight);
}
});
});
},
template: markRaw(SliderExample),
templateOptions: { sliders: [tSliderOptions] },
},
{
name: "Rotate",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const rotatedBezier = JSON.parse(bezier.rotate(options.angle * Math.PI).get_points());
drawBezier(context, rotatedBezier, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "angle",
min: 0,
max: 2,
step: 1 / 50,
default: 0.12,
unit: "π",
},
],
},
},
{
name: "Intersect (Line Segment)",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const line = [
{ x: 150, y: 150 },
{ x: 20, y: 20 },
];
const mappedLine = line.map((p) => [p.x, p.y]);
drawLine(context, line[0], line[1], COLORS.NON_INTERACTIVE.STROKE_1);
const intersections: Float64Array = bezier.intersect_line_segment(mappedLine);
intersections.forEach((t: number) => {
const p = JSON.parse(bezier.evaluate(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
},
{
name: "Intersect (Quadratic Segment)",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const points = [
{ x: 20, y: 80 },
{ x: 180, y: 10 },
{ x: 90, y: 120 },
];
const mappedPoints = points.map((p) => [p.x, p.y]);
drawCurve(context, points, COLORS.NON_INTERACTIVE.STROKE_1, 1);
const intersections: Float64Array = bezier.intersect_quadratic_segment(mappedPoints, options.error);
intersections.forEach((t: number) => {
const p = JSON.parse(bezier.evaluate(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "error",
min: 0.1,
max: 2,
step: 0.1,
default: 0.5,
},
],
},
},
{
name: "Intersect (Cubic Segment)",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const points = [
{ x: 40, y: 20 },
{ x: 100, y: 40 },
{ x: 40, y: 120 },
{ x: 175, y: 140 },
];
const mappedPoints = points.map((p) => [p.x, p.y]);
drawCurve(context, points, COLORS.NON_INTERACTIVE.STROKE_1, 1);
const intersections: Float64Array = bezier.intersect_cubic_segment(mappedPoints, options.error);
intersections.forEach((t: number) => {
const p = JSON.parse(bezier.evaluate(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "error",
min: 0.1,
max: 2,
step: 0.1,
default: 0.5,
},
],
},
},
{
name: "Intersect (Self)",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const intersections: number[][] = JSON.parse(bezier.intersect_self(options.error));
intersections.forEach((tValues: number[]) => {
const p = JSON.parse(bezier.evaluate(tValues[0]));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "error",
min: 0.01,
max: 1,
step: 0.05,
default: 0.5,
},
],
},
customPoints: {
[BezierCurveType.Cubic]: [
[160, 180],
[170, 10],
[30, 90],
[180, 140],
],
},
curveDegrees: new Set([BezierCurveType.Cubic]),
},
{
name: "Reduce",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const curves: Point[][] = JSON.parse(bezier.reduce());
curves.forEach((points, index) => {
drawBezier(context, points, null, { curveStrokeColor: `hsl(${40 * index}, 100%, 50%)`, radius: 3.5, drawHandles: false });
});
},
},
{
name: "Arcs",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const arcs: CircleSector[] = JSON.parse(bezier.arcs(options.error, options.max_iterations, options.strategy));
arcs.forEach((circleSector, index) => {
drawCircleSector(context, circleSector, `hsl(${40 * index}, 100%, 50%, 75%)`, `hsl(${40 * index}, 100%, 50%, 37.5%)`);
});
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "strategy",
min: 0,
max: 2,
step: 1,
default: 0,
unit: [": Automatic", ": FavorLargerArcs", ": FavorCorrectness"],
},
{
variable: "error",
min: 0.05,
max: 1,
step: 0.05,
default: 0.5,
},
{
variable: "max_iterations",
min: 50,
max: 200,
step: 1,
default: 100,
},
],
},
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
customPoints: {
[BezierCurveType.Quadratic]: [
[50, 50],
[85, 65],
[100, 100],
],
[BezierCurveType.Cubic]: [
[160, 180],
[170, 10],
[30, 90],
[180, 160],
],
},
},
{
name: "Offset",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const curves: Point[][] = JSON.parse(bezier.offset(options.distance));
curves.forEach((points, index) => {
if (points.length === 2) {
drawLine(context, points[0], points[1], `hsl(${40 * index}, 100%, 50%)`);
} else {
drawCurve(context, points, `hsl(${40 * index}, 100%, 50%)`);
}
});
drawPoint(context, curves[0][0], 4, "hsl(0, 100%, 50%)");
drawPoint(context, curves[curves.length - 1][curves[0].length - 1], 4, `hsl(${40 * (curves.length - 1)}, 100%, 50%)`);
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "distance",
min: -50,
max: 50,
step: 1,
default: 20,
},
],
},
},
],
subpathFeatures: [
{
name: "Constructor",
callback: (subpath: WasmSubpathInstance): string => subpath.to_svg(),
},
{
name: "Length",
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
},
],
};
},
components: {
ExamplePane,
SubpathExamplePane,
},
});
</script>
<style>
#app {
font-family: Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
@@ -0,0 +1,161 @@
import { WasmBezier } from "@/../wasm/pkg";
import { COLORS, drawBezier, drawPoint, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing";
import { BezierCallback, 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: BezierCallback;
options: Record<string, number>;
createThroughPoints: boolean;
constructor(bezier: WasmBezierInstance, callback: BezierCallback, 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 actualBezierPointLength = JSON.parse(this.bezier.get_points()).length;
let pointsToDraw = this.points;
let styleConfig: Partial<BezierStyleConfig> = {
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_2,
};
let dragIndex = this.dragIndex;
if (this.createThroughPoints) {
let bezierThroughPoints;
const pointList = this.points.map((p) => [p.x, p.y]);
if (actualBezierPointLength === 3) {
bezierThroughPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
} else {
bezierThroughPoints = WasmBezier.cubic_through_points(pointList, this.options.t, this.options["midpoint separation"]);
}
pointsToDraw = JSON.parse(bezierThroughPoints.get_points());
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;
@@ -0,0 +1,63 @@
<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, WasmBezierInstance } from "@/utils/types";
export default defineComponent({
props: {
title: String,
bezier: {
type: Object as PropType<WasmBezierInstance>,
required: true,
},
callback: {
type: Function as PropType<BezierCallback>,
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>
@@ -0,0 +1,133 @@
<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>
@@ -0,0 +1,54 @@
<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>
@@ -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>
@@ -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,160 @@
import { BezierStyleConfig, CircleSector, Point, WasmBezierInstance } 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 drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, bezierStyleConfig: Partial<BezierStyleConfig> = {}): void => {
const points = JSON.parse(bezier.get_points());
drawBezier(ctx, points, null, bezierStyleConfig);
};
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);
}
});
};
@@ -0,0 +1,55 @@
export type WasmRawInstance = typeof import("@/../wasm/pkg");
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
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",
Cubic = "Cubic",
}
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;
max: number;
step: number;
default: number;
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;
};
@@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"node"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue",
"tests/**/*.ts",
],
"exclude": [
"node_modules"
]
}
@@ -0,0 +1,36 @@
const path = require("path");
const { defineConfig } = require("@vue/cli-service");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = defineConfig({
publicPath: ".",
transpileDependencies: true,
// https://cli.vuejs.org/guide/webpack.html
chainWebpack: (config) => {
// WASM Pack Plugin integrates compiled Rust code (.wasm) and generated wasm-bindgen code (.js) with the webpack bundle
// Use this JS to import the bundled Rust entry points: const wasm = import("@/../wasm/pkg").then(panicProxy);
// Then call WASM functions with: (await wasm).function_name()
// https://github.com/wasm-tool/wasm-pack-plugin
config
// https://cli.vuejs.org/guide/webpack.html#modifying-options-of-a-plugin
.plugin("wasm-pack")
.use(WasmPackPlugin)
.init(
(Plugin) =>
new Plugin({
crateDirectory: path.resolve(__dirname, "wasm"),
// Remove when this issue is resolved: https://github.com/wasm-tool/wasm-pack-plugin/issues/93
outDir: path.resolve(__dirname, "wasm/pkg"),
watchDirectories: ["../../lib"].map((folder) => path.resolve(__dirname, folder)),
})
)
.end();
},
configureWebpack: {
experiments: {
asyncWebAssembly: true,
},
},
});
@@ -0,0 +1,43 @@
[package]
name = "bezier-rs-wasm"
publish = false
version = "0.0.0"
rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021"
readme = "../../README.md"
homepage = "https://graphite.rs"
repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
bezier-rs = { path = "../../../../libraries/bezier-rs", package = "bezier-rs" }
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2.73", features = ["serde-serialize"] }
serde_json = "*"
serde-wasm-bindgen = "0.4.1"
js-sys = "0.3.55"
glam = { version = "0.17", features = ["serde"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.22"
[package.metadata.wasm-pack.profile.dev]
wasm-opt = false
[package.metadata.wasm-pack.profile.dev.wasm-bindgen]
debug-js-glue = true
demangle-name-section = true
dwarf-debug-info = true
[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Os"]
[package.metadata.wasm-pack.profile.release.wasm-bindgen]
debug-js-glue = false
demangle-name-section = false
dwarf-debug-info = false
@@ -0,0 +1,258 @@
pub mod subpath;
mod svg_drawing;
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions};
use glam::DVec2;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Serialize, Deserialize)]
struct CircleSector {
center: Point,
radius: f64,
#[serde(rename = "startAngle")]
start_angle: f64,
#[serde(rename = "endAngle")]
end_angle: f64,
}
#[derive(Serialize, Deserialize)]
struct Point {
x: f64,
y: f64,
}
#[wasm_bindgen]
pub enum WasmMaximizeArcs {
Automatic, // 0
On, // 1
Off, // 2
}
/// Wrapper of the `Bezier` struct to be used in JS.
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmBezier(Bezier);
/// Convert a `DVec2` into a `Point`.
fn vec_to_point(p: &DVec2) -> Point {
Point { x: p.x, y: p.y }
}
/// Convert a bezier to a list of points.
fn bezier_to_points(bezier: Bezier) -> Vec<Point> {
bezier.get_points().map(|point| Point { x: point.x, y: point.y }).collect()
}
/// Serialize some data and then convert it to a JsValue.
fn to_js_value<T: Serialize>(data: T) -> JsValue {
JsValue::from_serde(&serde_json::to_string(&data).unwrap()).unwrap()
}
fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy {
match wasm_enum_value {
WasmMaximizeArcs::Automatic => ArcStrategy::Automatic,
WasmMaximizeArcs::On => ArcStrategy::FavorLargerArcs,
WasmMaximizeArcs::Off => ArcStrategy::FavorCorrectness,
}
}
#[wasm_bindgen]
impl WasmBezier {
/// Expect js_points to be a list of 2 pairs.
pub fn new_linear(js_points: &JsValue) -> WasmBezier {
let points: [DVec2; 2] = js_points.into_serde().unwrap();
WasmBezier(Bezier::from_linear_dvec2(points[0], points[1]))
}
/// Expect js_points to be a list of 3 pairs.
pub fn new_quadratic(js_points: &JsValue) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
WasmBezier(Bezier::from_quadratic_dvec2(points[0], points[1], points[2]))
}
/// Expect js_points to be a list of 4 pairs.
pub fn new_cubic(js_points: &JsValue) -> WasmBezier {
let points: [DVec2; 4] = js_points.into_serde().unwrap();
WasmBezier(Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]))
}
pub fn quadratic_through_points(js_points: &JsValue, t: f64) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
WasmBezier(Bezier::quadratic_through_points(points[0], points[1], points[2], Some(t)))
}
pub fn cubic_through_points(js_points: &JsValue, t: f64, midpoint_separation: f64) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
WasmBezier(Bezier::cubic_through_points(points[0], points[1], points[2], Some(t), Some(midpoint_separation)))
}
pub fn set_start(&mut self, x: f64, y: f64) {
self.0.set_start(DVec2::new(x, y));
}
pub fn set_end(&mut self, x: f64, y: f64) {
self.0.set_end(DVec2::new(x, y));
}
pub fn set_handle_start(&mut self, x: f64, y: f64) {
self.0.set_handle_start(DVec2::new(x, y));
}
pub fn set_handle_end(&mut self, x: f64, y: f64) {
self.0.set_handle_end(DVec2::new(x, y));
}
/// The wrapped return type is `Vec<Point>`.
pub fn get_points(&self) -> JsValue {
let points: Vec<Point> = self.0.get_points().map(|point| vec_to_point(&point)).collect();
to_js_value(points)
}
pub fn to_svg(&self) -> String {
self.0.to_svg()
}
pub fn length(&self) -> f64 {
self.0.length(None)
}
/// The wrapped return type is `Point`.
pub fn evaluate(&self, t: f64) -> JsValue {
let point: Point = vec_to_point(&self.0.evaluate(t));
to_js_value(point)
}
/// The wrapped return type is `Vec<Point>`.
pub fn compute_lookup_table(&self, steps: usize) -> JsValue {
let table_values: Vec<Point> = self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect();
to_js_value(table_values)
}
pub fn derivative(&self) -> Option<WasmBezier> {
self.0.derivative().map(WasmBezier)
}
/// The wrapped return type is `Point`.
pub fn tangent(&self, t: f64) -> JsValue {
let tangent_point: Point = vec_to_point(&self.0.tangent(t));
to_js_value(tangent_point)
}
/// The wrapped return type is `Point`.
pub fn normal(&self, t: f64) -> JsValue {
let normal_point: Point = vec_to_point(&self.0.normal(t));
to_js_value(normal_point)
}
pub fn curvature(&self, t: f64) -> f64 {
self.0.curvature(t)
}
/// The wrapped return type is `[Vec<Point>; 2]`.
pub fn split(&self, t: f64) -> JsValue {
let bezier_points: [Vec<Point>; 2] = self.0.split(t).map(bezier_to_points);
to_js_value(bezier_points)
}
pub fn trim(&self, t1: f64, t2: f64) -> WasmBezier {
WasmBezier(self.0.trim(t1, t2))
}
pub fn project(&self, x: f64, y: f64) -> f64 {
self.0.project(DVec2::new(x, y), ProjectionOptions::default())
}
/// The wrapped return type is `[Vec<f64>; 2]`.
pub fn local_extrema(&self) -> JsValue {
let local_extrema: [Vec<f64>; 2] = self.0.local_extrema();
to_js_value(local_extrema)
}
/// The wrapped return type is `[Point; 2]`.
pub fn bounding_box(&self) -> JsValue {
let bbox_points: [Point; 2] = self.0.bounding_box().map(|p| Point { x: p.x, y: p.y });
to_js_value(bbox_points)
}
/// The wrapped return type is `Vec<f64>`.
pub fn inflections(&self) -> JsValue {
let inflections: Vec<f64> = self.0.inflections();
to_js_value(inflections)
}
/// The wrapped return type is `Vec<Vec<Point>>`.
pub fn de_casteljau_points(&self, t: f64) -> JsValue {
let points: Vec<Vec<Point>> = self
.0
.de_casteljau_points(t)
.iter()
.map(|level| level.iter().map(|&point| Point { x: point.x, y: point.y }).collect::<Vec<Point>>())
.collect();
to_js_value(points)
}
pub fn rotate(&self, angle: f64) -> WasmBezier {
WasmBezier(self.0.rotate(angle))
}
fn intersect(&self, curve: &Bezier, error: Option<f64>) -> Vec<f64> {
self.0.intersections(curve, error)
}
pub fn intersect_line_segment(&self, js_points: &JsValue) -> Vec<f64> {
let points: [DVec2; 2] = js_points.into_serde().unwrap();
let line = Bezier::from_linear_dvec2(points[0], points[1]);
self.intersect(&line, None)
}
pub fn intersect_quadratic_segment(&self, js_points: &JsValue, error: f64) -> Vec<f64> {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
let quadratic = Bezier::from_quadratic_dvec2(points[0], points[1], points[2]);
self.intersect(&quadratic, Some(error))
}
pub fn intersect_cubic_segment(&self, js_points: &JsValue, error: f64) -> Vec<f64> {
let points: [DVec2; 4] = js_points.into_serde().unwrap();
let cubic = Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]);
self.intersect(&cubic, Some(error))
}
/// The wrapped return type is `Vec<[f64; 2]>`.
pub fn intersect_self(&self, error: f64) -> JsValue {
let points: Vec<[f64; 2]> = self.0.self_intersections(Some(error));
to_js_value(points)
}
pub fn reduce(&self) -> JsValue {
let bezier_points: Vec<Vec<Point>> = self.0.reduce(None).into_iter().map(bezier_to_points).collect();
to_js_value(bezier_points)
}
/// The wrapped return type is `Vec<Vec<Point>>`.
pub fn offset(&self, distance: f64) -> JsValue {
let bezier_points: Vec<Vec<Point>> = self.0.offset(distance).into_iter().map(bezier_to_points).collect();
to_js_value(bezier_points)
}
/// The wrapped return type is `Vec<CircleSector>`.
pub fn arcs(&self, error: f64, max_iterations: usize, maximize_arcs: WasmMaximizeArcs) -> JsValue {
let strategy = convert_wasm_maximize_arcs(maximize_arcs);
let options = ArcsOptions { error, max_iterations, strategy };
let circle_sectors: Vec<CircleSector> = self
.0
.arcs(options)
.iter()
.map(|sector| CircleSector {
center: Point {
x: sector.center.x,
y: sector.center.y,
},
radius: sector.radius,
start_angle: sector.start_angle,
end_angle: sector.end_angle,
})
.collect();
to_js_value(circle_sectors)
}
}
@@ -0,0 +1,47 @@
use bezier_rs::subpath::{ManipulatorGroup, Subpath, ToSVGOptions};
use glam::DVec2;
use wasm_bindgen::prelude::*;
use crate::svg_drawing::*;
/// Wrapper of the `Subpath` struct to be used in JS.
#[wasm_bindgen]
pub struct WasmSubpath(Subpath);
#[wasm_bindgen]
impl WasmSubpath {
/// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats.
pub fn from_triples(js_points: &JsValue, closed: bool) -> WasmSubpath {
let point_triples: Vec<[Option<DVec2>; 3]> = js_points.into_serde().unwrap();
let manipulator_groups = point_triples
.into_iter()
.map(|point_triple| ManipulatorGroup {
anchor: point_triple[0].unwrap(),
in_handle: point_triple[1],
out_handle: point_triple[2],
})
.collect();
WasmSubpath(Subpath::new(manipulator_groups, closed))
}
pub fn set_anchor(&mut self, index: usize, x: f64, y: f64) {
self.0[index].anchor = DVec2::new(x, y);
}
pub fn set_in_handle(&mut self, index: usize, x: f64, y: f64) {
self.0[index].in_handle = Some(DVec2::new(x, y));
}
pub fn set_out_handle(&mut self, index: usize, x: f64, y: f64) {
self.0[index].out_handle = Some(DVec2::new(x, y));
}
pub fn to_svg(&self) -> String {
format!("{}{}{}", SVG_OPEN_TAG, self.0.to_svg(ToSVGOptions::default()), SVG_CLOSE_TAG)
}
pub fn length(&self) -> String {
let length_text = draw_text(format!("Length: {:.2}", self.0.length(None)), 5., 193., BLACK);
format!("{}{}{}{}", SVG_OPEN_TAG, self.0.to_svg(ToSVGOptions::default()), length_text, SVG_CLOSE_TAG)
}
}
@@ -0,0 +1,11 @@
// SVG drawing constants
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200px" height="200px">"#;
pub const SVG_CLOSE_TAG: &str = "</svg>";
// Stylistic constants
pub const BLACK: &str = "black";
/// Helper function to create an SVG text entitty.
pub fn draw_text(text: String, x_pos: f64, y_pos: f64, fill: &str) -> String {
format!(r#"<text x="{x_pos}" y="{y_pos}" fill="{fill}">{text}</text>"#)
}
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
echo 🔧 Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- -y
export PATH=$PATH:/opt/buildhome/.cargo/bin
echo rustc version:
rustc --version
echo 🚧 Install Node dependencies
echo node version:
node --version
echo npm version:
npm --version
cd bezier-rs-demos
npm ci
echo 👷 Build Bezier-rs Demos
export NODE_ENV=production
npm run build
mv dist ../../public/bezier-rs-demos