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:
Hannah Li
2022-06-16 20:50:58 -04:00
committed by Keavon Chambers
co-authored by Thomas Cheng Robert Nadal ll2zheng
parent 18a7c6a289
commit 9f76315bdc
30 changed files with 20185 additions and 2 deletions
@@ -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"],
},
},
],
};
@@ -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
View 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/).
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "preserve",
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
File diff suppressed because it is too large Load Diff
@@ -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"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -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
View 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>
@@ -0,0 +1,117 @@
import { drawBezier, getContextFromCanvas } from "@/utils/drawing";
import { BezierCallback, Point, WasmBezierMutatorKey } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
class BezierDrawing {
static indexToMutator: WasmBezierMutatorKey[] = ["set_start", "set_handle1", "set_handle2", "set_end"];
points: Point[];
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
dragIndex: number | null;
bezier: WasmBezierInstance;
callback: BezierCallback;
options: string;
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: string) {
this.bezier = bezier;
this.callback = callback;
this.options = options;
this.points = bezier
.get_points()
.map((p) => JSON.parse(p))
.map((p, i, points) => ({
x: p.x,
y: p.y,
r: i === 0 || i === points.length - 1 ? 5 : 3,
selected: false,
mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i],
}));
const canvas = document.createElement("canvas");
if (canvas === null) {
throw Error("Failed to create canvas");
}
this.canvas = canvas;
this.canvas.width = 200;
this.canvas.height = 200;
this.ctx = getContextFromCanvas(this.canvas);
this.dragIndex = null; // Index of the point being moved
this.canvas.addEventListener("mousedown", this.mouseDownHandler.bind(this));
this.canvas.addEventListener("mousemove", this.mouseMoveHandler.bind(this));
this.canvas.addEventListener("mouseup", this.deselectPointHandler.bind(this));
this.canvas.addEventListener("mouseout", this.deselectPointHandler.bind(this));
this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
this.updateBezier();
}
mouseMoveHandler(evt: MouseEvent): void {
const mx = evt.offsetX;
const my = evt.offsetY;
if (
this.dragIndex != null &&
mx - this.points[this.dragIndex].r > 0 &&
my - this.points[this.dragIndex].r > 0 &&
mx + this.points[this.dragIndex].r < this.canvas.width &&
my + this.points[this.dragIndex].r < this.canvas.height
) {
const selectedPoint = this.points[this.dragIndex];
selectedPoint.x = mx;
selectedPoint.y = my;
this.bezier[selectedPoint.mutator](selectedPoint.x, selectedPoint.y);
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
this.updateBezier();
}
}
mouseDownHandler(evt: MouseEvent): void {
const mx = evt.offsetX;
const my = evt.offsetY;
for (let i = 0; i < this.points.length; i += 1) {
if (
Math.abs(mx - this.points[i].x) < this.points[i].r + 3 &&
Math.abs(my - this.points[i].y) < this.points[i].r + 3 // Fudge factor makes the points easier to grab
) {
this.dragIndex = i;
this.points[this.dragIndex].selected = true;
break;
}
}
}
deselectPointHandler(): void {
if (this.dragIndex != null) {
this.points[this.dragIndex].selected = false;
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
this.updateBezier();
this.dragIndex = null;
}
}
updateBezier(options = ""): void {
if (options !== "") {
this.options = options;
}
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
drawBezier(this.ctx, this.points);
this.callback(this.canvas, this.bezier, this.options);
}
getCanvas(): HTMLCanvasElement {
return this.canvas;
}
}
export default BezierDrawing;
@@ -0,0 +1,57 @@
<template>
<div>
<h4 class="example_header">{{ title }}</h4>
<figure class="example_figure" ref="drawing"></figure>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import BezierDrawing from "@/components/BezierDrawing";
import { BezierCallback } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
export default defineComponent({
name: "ExampleComponent",
data() {
return {
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options),
};
},
props: {
title: String,
bezier: {
type: Object as PropType<WasmBezierInstance>,
required: true,
},
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
options: {
type: String,
default: "",
},
},
mounted() {
const drawing = this.$refs.drawing as HTMLElement;
drawing.appendChild(this.bezierDrawing.getCanvas());
this.bezierDrawing.updateBezier();
},
watch: {
options() {
this.bezierDrawing.updateBezier(this.options);
},
},
});
</script>
<style scoped>
.example_header {
margin-bottom: 0;
}
.example_figure {
margin-top: 0.5em;
}
</style>
@@ -0,0 +1,86 @@
<template>
<div>
<h2 class="example_pane_header">{{ name }}</h2>
<div class="example_row">
<div v-for="example in exampleData" :key="example.id">
<component :is="template" :templateOptions="templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Component } from "vue";
import { BezierCallback } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
import Example from "@/components/Example.vue";
type ExampleData = {
id: number;
title: string;
bezier: WasmBezierInstance;
};
export default defineComponent({
name: "ExamplePane",
components: {
Example,
},
props: {
name: String,
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
template: {
type: Object as PropType<Component>,
default: Example,
},
templateOptions: Object,
},
data() {
return {
exampleData: [] as ExampleData[],
};
},
mounted() {
import("@/../wasm/pkg").then((wasm) => {
this.exampleData = [
{
id: 0,
title: "Quadratic",
bezier: wasm.WasmBezier.new_quad([
[30, 30],
[140, 20],
[160, 170],
]),
},
{
id: 1,
title: "Cubic",
bezier: wasm.WasmBezier.new_cubic([
[30, 30],
[60, 140],
[150, 30],
[160, 160],
]),
},
];
});
},
});
</script>
<style>
.example_row {
display: flex; /* or inline-flex */
flex-direction: row;
justify-content: center;
}
.example_pane_header {
margin-bottom: 0;
}
</style>
@@ -0,0 +1,45 @@
<template>
<div>
<Example :title="title" :bezier="bezier" :callback="callback" :options="value.toString()" />
<div class="slider_label">{{ templateOptions.variable }} = {{ value }}</div>
<input class="slider" v-model="value" type="range" :step="templateOptions.step" :min="templateOptions.min" :max="templateOptions.max" />
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { BezierCallback } from "@/utils/types";
import { WasmBezierInstance } from "@/utils/wasm-comm";
import Example from "@/components/Example.vue";
export default defineComponent({
name: "SliderExample",
components: {
Example,
},
props: {
title: String,
bezier: {
type: Object as PropType<WasmBezierInstance>,
required: true,
},
callback: {
type: Function as PropType<BezierCallback>,
required: true,
},
templateOptions: {
type: Object,
default: () => ({}),
},
},
data() {
return {
value: this.templateOptions.default,
};
},
});
</script>
<style scoped></style>
@@ -0,0 +1,5 @@
import { createApp } from "vue";
import App from "@/App.vue";
createApp(App).mount("#app");
+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,81 @@
import { Point } from "@/utils/types";
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
const ctx = canvas.getContext("2d");
if (ctx === null) {
throw Error("Failed to fetch context");
}
return ctx;
};
export const drawLine = (ctx: CanvasRenderingContext2D, p1: Point, p2: Point): void => {
ctx.strokeStyle = "grey";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
};
export const drawPoint = (ctx: CanvasRenderingContext2D, p: Point, stroke = "black"): void => {
// Outline the point
ctx.strokeStyle = p.selected ? "blue" : stroke;
ctx.lineWidth = p.r / 3;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, 2 * Math.PI, false);
ctx.stroke();
// Fill the point (hiding any overlapping lines)
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * (2 / 3), 0, 2 * Math.PI, false);
ctx.fill();
};
export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number): void => {
ctx.fillStyle = "black";
ctx.font = "16px Arial";
ctx.fillText(text, x, y);
};
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[]): void => {
/* Until a bezier representation is finalized, treat the points as follows
points[0] = start point
points[1] = handle 1
points[2] = (optional) handle 2
points[3] = end point
*/
const start = points[0];
let end = null;
let handle1 = null;
let handle2 = null;
if (points.length === 4) {
handle1 = points[1];
handle2 = points[2];
end = points[3];
} else {
handle1 = points[1];
handle2 = handle1;
end = points[2];
}
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
if (points.length === 3) {
ctx.quadraticCurveTo(handle1.x, handle1.y, end.x, end.y);
} else {
ctx.bezierCurveTo(handle1.x, handle1.y, handle2.x, handle2.y, end.x, end.y);
}
ctx.stroke();
drawLine(ctx, start, handle1);
drawLine(ctx, end, handle2);
points.forEach((point) => {
drawPoint(ctx, point);
});
};
@@ -0,0 +1,15 @@
export type WasmRawInstance = typeof import("../../wasm/pkg");
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
export type WasmBezierKey = keyof WasmBezierInstance;
export type WasmBezierMutatorKey = "set_start" | "set_handle1" | "set_handle2" | "set_end";
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string) => void;
export type Point = {
x: number;
y: number;
r: number;
mutator: WasmBezierMutatorKey;
selected: boolean;
};
@@ -0,0 +1,2 @@
export type WasmRawInstance = typeof import("../../wasm/pkg");
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
@@ -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"
]
}
@@ -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,
},
},
});
@@ -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
@@ -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()
}
}