mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00: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:
parent
18a7c6a289
commit
9f76315bdc
30 changed files with 20185 additions and 2 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -24,7 +24,8 @@
|
|||
// ESLint config
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": [
|
||||
"./frontend"
|
||||
"./frontend",
|
||||
"./bezier-rs/docs/interactive-docs",
|
||||
],
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
|
|
|
|||
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -34,6 +34,28 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bezier-rs"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"glam",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bezier-rs-wasm"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bezier-rs",
|
||||
"glam",
|
||||
"js-sys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
|
@ -482,6 +504,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ members = [
|
|||
"graphene",
|
||||
"proc-macros",
|
||||
"frontend/wasm",
|
||||
"bezier-rs/lib",
|
||||
"bezier-rs/docs/interactive-docs/wasm",
|
||||
]
|
||||
|
||||
[profile.release.package.graphite-wasm]
|
||||
|
|
|
|||
125
bezier-rs/docs/interactive-docs/.eslintrc.js
Normal file
125
bezier-rs/docs/interactive-docs/.eslintrc.js
Normal 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"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
24
bezier-rs/docs/interactive-docs/.gitignore
vendored
Normal file
24
bezier-rs/docs/interactive-docs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
/wasm/pkg
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
24
bezier-rs/docs/interactive-docs/README.md
Normal file
24
bezier-rs/docs/interactive-docs/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# interactive-docs
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### 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/).
|
||||
20
bezier-rs/docs/interactive-docs/jsconfig.json
Normal file
20
bezier-rs/docs/interactive-docs/jsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
18961
bezier-rs/docs/interactive-docs/package-lock.json
generated
Normal file
18961
bezier-rs/docs/interactive-docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
38
bezier-rs/docs/interactive-docs/package.json
Normal file
38
bezier-rs/docs/interactive-docs/package.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "interactive-docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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": "^9.1.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": "^3.1.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"typescript": "~4.5.5",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
}
|
||||
BIN
bezier-rs/docs/interactive-docs/public/favicon.ico
Normal file
BIN
bezier-rs/docs/interactive-docs/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
bezier-rs/docs/interactive-docs/public/index.html
Normal file
17
bezier-rs/docs/interactive-docs/public/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
114
bezier-rs/docs/interactive-docs/src/App.vue
Normal file
114
bezier-rs/docs/interactive-docs/src/App.vue
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div class="App">
|
||||
<h1>Bezier-rs Interactive Documentation</h1>
|
||||
<p>This is the interactive documentation for the <b>bezier-rs</b> library. Click and drag on the endpoints of the example curves to visualize the various Bezier utilities and functions.</p>
|
||||
<div v-for="feature in features" :key="feature.id">
|
||||
<ExamplePane :template="feature.template" :templateOptions="feature.templateOptions" :name="feature.name" :callback="feature.callback" />
|
||||
</div>
|
||||
<br />
|
||||
<div id="svg-test" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from "vue";
|
||||
|
||||
import { drawText, drawPoint, getContextFromCanvas } from "@/utils/drawing";
|
||||
import { WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
import ExamplePane from "@/components/ExamplePane.vue";
|
||||
import SliderExample from "@/components/SliderExample.vue";
|
||||
|
||||
// eslint-disable-next-line
|
||||
const testBezierLib = async () => {
|
||||
import("@/../wasm/pkg").then((wasm) => {
|
||||
const bezier = wasm.WasmBezier.new_quad([
|
||||
[0, 0],
|
||||
[50, 0],
|
||||
[100, 100],
|
||||
]);
|
||||
const svgContainer = document.getElementById("svg-test");
|
||||
if (svgContainer) {
|
||||
svgContainer.innerHTML = bezier.to_svg();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
components: {
|
||||
ExamplePane,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
features: [
|
||||
{
|
||||
id: 0,
|
||||
name: "Constructor",
|
||||
// eslint-disable-next-line
|
||||
callback: (): void => {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Length",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
drawText(getContextFromCanvas(canvas), `Length: ${bezier.length().toFixed(2)}`, 5, canvas.height - 7);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Compute",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||
const point = JSON.parse(bezier.compute(parseFloat(options)));
|
||||
point.r = 4;
|
||||
point.selected = false;
|
||||
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
default: 0.5,
|
||||
variable: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Lookup Table",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||
const lookupPoints = bezier.compute_lookup_table(Number(options));
|
||||
lookupPoints.forEach((serPoint, index) => {
|
||||
if (index !== 0 && index !== lookupPoints.length - 1) {
|
||||
const point = JSON.parse(serPoint);
|
||||
point.r = 3;
|
||||
point.selected = false;
|
||||
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
|
||||
}
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
min: 2,
|
||||
max: 15,
|
||||
step: 1,
|
||||
default: 5,
|
||||
variable: "Steps",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
117
bezier-rs/docs/interactive-docs/src/components/BezierDrawing.ts
Normal file
117
bezier-rs/docs/interactive-docs/src/components/BezierDrawing.ts
Normal file
|
|
@ -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;
|
||||
57
bezier-rs/docs/interactive-docs/src/components/Example.vue
Normal file
57
bezier-rs/docs/interactive-docs/src/components/Example.vue
Normal file
|
|
@ -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>
|
||||
5
bezier-rs/docs/interactive-docs/src/main.ts
Normal file
5
bezier-rs/docs/interactive-docs/src/main.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createApp } from "vue";
|
||||
|
||||
import App from "@/App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
6
bezier-rs/docs/interactive-docs/src/shims-vue.d.ts
vendored
Normal file
6
bezier-rs/docs/interactive-docs/src/shims-vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable */
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
81
bezier-rs/docs/interactive-docs/src/utils/drawing.ts
Normal file
81
bezier-rs/docs/interactive-docs/src/utils/drawing.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
15
bezier-rs/docs/interactive-docs/src/utils/types.ts
Normal file
15
bezier-rs/docs/interactive-docs/src/utils/types.ts
Normal file
|
|
@ -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;
|
||||
};
|
||||
2
bezier-rs/docs/interactive-docs/src/utils/wasm-comm.ts
Normal file
2
bezier-rs/docs/interactive-docs/src/utils/wasm-comm.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export type WasmRawInstance = typeof import("../../wasm/pkg");
|
||||
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
|
||||
38
bezier-rs/docs/interactive-docs/tsconfig.json
Normal file
38
bezier-rs/docs/interactive-docs/tsconfig.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
35
bezier-rs/docs/interactive-docs/vue.config.js
Normal file
35
bezier-rs/docs/interactive-docs/vue.config.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const path = require("path");
|
||||
|
||||
const { defineConfig } = require("@vue/cli-service");
|
||||
|
||||
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
||||
|
||||
module.exports = defineConfig({
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
43
bezier-rs/docs/interactive-docs/wasm/Cargo.toml
Normal file
43
bezier-rs/docs/interactive-docs/wasm/Cargo.toml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[package]
|
||||
name = "bezier-rs-wasm"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
rust-version = "1.56.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 = "../../../lib", 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
|
||||
74
bezier-rs/docs/interactive-docs/wasm/src/lib.rs
Normal file
74
bezier-rs/docs/interactive-docs/wasm/src/lib.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Point {
|
||||
x: f64,
|
||||
y: f64,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmBezier {
|
||||
internal: Bezier,
|
||||
}
|
||||
|
||||
pub fn vec_to_point(p: &DVec2) -> JsValue {
|
||||
JsValue::from_serde(&serde_json::to_string(&Point { x: p[0], y: p[1] }).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmBezier {
|
||||
/// Expect js_points to be a list of 3 pairs
|
||||
pub fn new_quad(js_points: &JsValue) -> WasmBezier {
|
||||
let points: [DVec2; 3] = js_points.into_serde().unwrap();
|
||||
WasmBezier {
|
||||
internal: 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 {
|
||||
internal: Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_start(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_start(DVec2::from((x, y)));
|
||||
}
|
||||
|
||||
pub fn set_end(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_end(DVec2::from((x, y)));
|
||||
}
|
||||
|
||||
pub fn set_handle1(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_handle1(DVec2::from((x, y)));
|
||||
}
|
||||
|
||||
pub fn set_handle2(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_handle2(DVec2::from((x, y)));
|
||||
}
|
||||
|
||||
pub fn get_points(&self) -> Vec<JsValue> {
|
||||
self.internal.get_points().iter().flatten().map(vec_to_point).collect()
|
||||
}
|
||||
|
||||
pub fn to_svg(&self) -> String {
|
||||
self.internal.to_svg()
|
||||
}
|
||||
|
||||
pub fn length(&self) -> f64 {
|
||||
self.internal.length()
|
||||
}
|
||||
|
||||
pub fn compute(&self, t: f64) -> JsValue {
|
||||
vec_to_point(&self.internal.compute(t))
|
||||
}
|
||||
|
||||
pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> {
|
||||
self.internal.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
|
||||
}
|
||||
}
|
||||
14
bezier-rs/lib/Cargo.toml
Normal file
14
bezier-rs/lib/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "bezier-rs"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
rust-version = "1.56.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"
|
||||
|
||||
[dependencies]
|
||||
glam = { version = "0.17", features = ["serde"] }
|
||||
1
bezier-rs/lib/README.md
Normal file
1
bezier-rs/lib/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Bezier-rs: Bezier Math Library
|
||||
211
bezier-rs/lib/src/lib.rs
Normal file
211
bezier-rs/lib/src/lib.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
use glam::DVec2;
|
||||
|
||||
/// Representation of the handle point(s) in a bezier segment
|
||||
pub enum BezierHandles {
|
||||
Quadratic { handle: DVec2 },
|
||||
Cubic { handle1: DVec2, handle2: DVec2 },
|
||||
}
|
||||
|
||||
/// Representation of a bezier segment with 2D points
|
||||
pub struct Bezier {
|
||||
/// Start point of the bezier segment
|
||||
start: DVec2,
|
||||
/// Start point of the bezier segment
|
||||
end: DVec2,
|
||||
/// Handles of the bezier segment
|
||||
handles: BezierHandles,
|
||||
}
|
||||
|
||||
impl Bezier {
|
||||
// TODO: Consider removing this function
|
||||
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points
|
||||
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::from((x1, y1)),
|
||||
handles: BezierHandles::Quadratic { handle: DVec2::from((x2, y2)) },
|
||||
end: DVec2::from((x3, y3)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a quadratc bezier using the provided DVec2s as the start, handle, and end points
|
||||
pub fn from_quadratic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2) -> Self {
|
||||
Bezier {
|
||||
start: p1,
|
||||
handles: BezierHandles::Quadratic { handle: p2 },
|
||||
end: p3,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider removing this function
|
||||
/// Create a cubic bezier using the provided coordinates as the start, handles, and end points
|
||||
pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::from((x1, y1)),
|
||||
handles: BezierHandles::Cubic {
|
||||
handle1: DVec2::from((x2, y2)),
|
||||
handle2: DVec2::from((x3, y3)),
|
||||
},
|
||||
end: DVec2::from((x4, y4)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cubic bezier using the provided DVec2s as the start, handles, and end points
|
||||
pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self {
|
||||
Bezier {
|
||||
start: p1,
|
||||
handles: BezierHandles::Cubic { handle1: p2, handle2: p3 },
|
||||
end: p4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a quadratic bezier curve that goes through 3 points
|
||||
// #[inline]
|
||||
pub fn quadratic_from_points(p1: DVec2, p2: DVec2, p3: DVec2, _t: f64) -> Self {
|
||||
// TODO: Implement logic to get actual curve through the points
|
||||
Bezier::from_quadratic_dvec2(p1, p2, p3)
|
||||
}
|
||||
|
||||
/// Create a cubic bezier curve that goes through 3 points. d1 represents the strut.
|
||||
// #[inline]
|
||||
pub fn cubic_from_points(p1: DVec2, p2: DVec2, p3: DVec2, _t: f64, _d1: f64) -> Self {
|
||||
// TODO: Implement logic to get actual curve through the points
|
||||
Bezier::from_quadratic_dvec2(p1, p2, p3)
|
||||
}
|
||||
|
||||
/// Convert to SVG
|
||||
// TODO: Allow modifying the viewport, width and height
|
||||
pub fn to_svg(&self) -> String {
|
||||
let m_path = format!("M {} {}", self.start.x, self.start.y);
|
||||
let handles_path = match self.handles {
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
format!("Q {} {}", handle.x, handle.y)
|
||||
}
|
||||
BezierHandles::Cubic { handle1, handle2 } => {
|
||||
format!("C {} {}, {} {}", handle1.x, handle1.y, handle2.x, handle2.y)
|
||||
}
|
||||
};
|
||||
let curve_path = format!("{}, {} {}", handles_path, self.end.x, self.end.y);
|
||||
format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}px"><path d="{} {} {}" stroke="black" fill="transparent"/></svg>"#,
|
||||
0, 0, 100, 100, 100, 100, "\n", m_path, curve_path
|
||||
)
|
||||
}
|
||||
|
||||
/// Set the coordinates of the start point
|
||||
pub fn set_start(&mut self, s: DVec2) {
|
||||
self.start = s;
|
||||
}
|
||||
|
||||
/// Set the coordinates of the end point
|
||||
pub fn set_end(&mut self, e: DVec2) {
|
||||
self.end = e;
|
||||
}
|
||||
|
||||
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment.
|
||||
pub fn set_handle1(&mut self, h1: DVec2) {
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { ref mut handle } => {
|
||||
*handle = h1;
|
||||
}
|
||||
BezierHandles::Cubic { ref mut handle1, .. } => {
|
||||
*handle1 = h1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the coordinates of the second handle point. This will convert a quadratic segment into a cubic one.
|
||||
pub fn set_handle2(&mut self, h2: DVec2) {
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
self.handles = BezierHandles::Cubic { handle1: handle, handle2: h2 };
|
||||
}
|
||||
BezierHandles::Cubic { ref mut handle2, .. } => {
|
||||
*handle2 = h2;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_start(&self) -> DVec2 {
|
||||
self.start
|
||||
}
|
||||
|
||||
pub fn get_end(&self) -> DVec2 {
|
||||
self.end
|
||||
}
|
||||
|
||||
pub fn get_handle1(&self) -> DVec2 {
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { handle } => handle,
|
||||
BezierHandles::Cubic { handle1, .. } => handle1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_handle2(&self) -> Option<DVec2> {
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { .. } => None,
|
||||
BezierHandles::Cubic { handle2, .. } => Some(handle2),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_points(&self) -> [Option<DVec2>; 4] {
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { handle } => [Some(self.start), Some(handle), Some(self.end), None],
|
||||
BezierHandles::Cubic { handle1, handle2 } => [Some(self.start), Some(handle1), Some(handle2), Some(self.end)],
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the point on the curve based on the t-value provided
|
||||
/// basis code based off of pseudocode found here: https://pomax.github.io/bezierinfo/#explanation
|
||||
pub fn compute(&self, t: f64) -> DVec2 {
|
||||
assert!((0.0..=1.0).contains(&t));
|
||||
|
||||
let t_squared = t * t;
|
||||
let one_minus_t = 1.0 - t;
|
||||
let squared_one_minus_t = one_minus_t * one_minus_t;
|
||||
|
||||
match self.handles {
|
||||
BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2.0 * one_minus_t * t * handle + t_squared * self.end,
|
||||
BezierHandles::Cubic { handle1, handle2 } => {
|
||||
let t_cubed = t_squared * t;
|
||||
let cubed_one_minus_t = squared_one_minus_t * one_minus_t;
|
||||
cubed_one_minus_t * self.start + 3.0 * squared_one_minus_t * t * handle1 + 3.0 * one_minus_t * t_squared * handle2 + t_cubed * self.end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a selection of equidistant points on the bezier curve
|
||||
/// If no value is provided for `steps`, then the function will default `steps` to be 10
|
||||
pub fn compute_lookup_table(&self, steps: Option<i32>) -> Vec<DVec2> {
|
||||
let steps_unwrapped = steps.unwrap_or(10);
|
||||
let ratio: f64 = 1.0 / (steps_unwrapped as f64);
|
||||
let mut steps_array = Vec::with_capacity((steps_unwrapped + 1) as usize);
|
||||
|
||||
for t in 0..steps_unwrapped + 1 {
|
||||
steps_array.push(self.compute(f64::from(t) * ratio))
|
||||
}
|
||||
|
||||
steps_array
|
||||
}
|
||||
|
||||
/// Return an approximation of the length of the bezier curve
|
||||
/// code example taken from: https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427
|
||||
pub fn length(&self) -> f64 {
|
||||
// We will use an approximate approach where
|
||||
// we split the curve into many subdivisions
|
||||
// and calculate the euclidean distance between the two endpoints of the subdivision
|
||||
const SUBDIVISIONS: i32 = 1000;
|
||||
|
||||
let lookup_table = self.compute_lookup_table(Some(SUBDIVISIONS));
|
||||
let mut approx_curve_length = 0.0;
|
||||
let mut prev_point = lookup_table[0];
|
||||
// calculate approximate distance between subdivision
|
||||
for curr_point in lookup_table.iter().skip(1) {
|
||||
// calculate distance of subdivision
|
||||
approx_curve_length += (*curr_point - prev_point).length();
|
||||
// update the prev point
|
||||
prev_point = *curr_point;
|
||||
}
|
||||
|
||||
approx_curve_length
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ license = "Apache-2.0"
|
|||
log = "0.4"
|
||||
|
||||
kurbo = { git = "https://github.com/linebender/kurbo.git", features = [
|
||||
"serde",
|
||||
"serde",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
glam = { version = "0.17", features = ["serde"] }
|
||||
|
|
|
|||
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "./bezier-rs/docs/interactive-docs/tsconfig.json"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue