Simplify the Bezier-rs interactive web demo code (#2020)

* Change demo pane classes into simpler group functions

* Eliminate classes

* Use template strings for HTML

* Reduce files and flatten directories

* Restructuring to reduce redundant code

* Eliminate the module pattern and consolidate both demo types

* Further consolidate into main.ts
This commit is contained in:
Keavon Chambers
2024-10-03 17:20:50 -07:00
committed by GitHub
parent e6d8c4743d
commit a2465f40b3
12 changed files with 598 additions and 755 deletions
@@ -23,7 +23,7 @@ body {
color: var(--color-navy); color: var(--color-navy);
} }
.class-header { .category-header {
color: var(--color-navy); color: var(--color-navy);
font-family: "Bona Nova", serif; font-family: "Bona Nova", serif;
margin-bottom: 0 margin-bottom: 0
@@ -48,7 +48,7 @@ body > h2 {
margin-top: 40px; margin-top: 40px;
} }
/* Demo Pane styles */ /* Demo group styles */
.demo-row { .demo-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -59,7 +59,7 @@ body > h2 {
margin-top: 20px; margin-top: 20px;
} }
.demo-pane-header { .demo-group-header {
display: inline-block; display: inline-block;
position: relative; position: relative;
margin-top: 2em; margin-top: 2em;
@@ -69,7 +69,7 @@ body > h2 {
color: var(--color-navy); color: var(--color-navy);
} }
.demo-pane-header a { .demo-group-header a {
display: none; display: none;
position: absolute; position: absolute;
left: 0; left: 0;
@@ -78,11 +78,11 @@ body > h2 {
opacity: 0.5; opacity: 0.5;
} }
.demo-pane-header:hover a { .demo-group-header:hover a {
display: inline-block; display: inline-block;
} }
.demo-pane-container { .demo-group-container {
position: relative; position: relative;
width: fit-content; width: fit-content;
margin: auto; margin: auto;
@@ -103,7 +103,7 @@ body > h2 {
border: solid 1px black; border: solid 1px black;
} }
.parent-slider-container { .parent-input-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
@@ -123,7 +123,7 @@ svg text {
padding-bottom: 5px; padding-bottom: 5px;
} }
.slider-label { .input-label {
font-family: monospace; font-family: monospace;
display: flex; display: flex;
justify-content: left; justify-content: left;
@@ -139,7 +139,6 @@ input[type="range"] {
border-radius: 5px; border-radius: 5px;
background: linear-gradient(var(--range-fill-dark), var(--range-fill-dark)) 0 / calc(0.5 * var(--range-thumb-height) + var(--range-ratio) * (100% - var(--range-thumb-height))) var(--range-fill-light); background: linear-gradient(var(--range-fill-dark), var(--range-fill-dark)) 0 / calc(0.5 * var(--range-thumb-height) + var(--range-ratio) * (100% - var(--range-thumb-height))) var(--range-fill-light);
background-repeat: no-repeat; background-repeat: no-repeat;
} }
/* Input Thumb */ /* Input Thumb */
@@ -1,114 +0,0 @@
import { WasmBezier } from "@/../wasm/pkg";
import type { BezierFeatureKey } from "@/features/bezier-features";
import bezierFeatures from "@/features/bezier-features";
import { renderDemo } from "@/utils/render";
import type { BezierCallback, BezierCurveType, InputOption, WasmBezierManipulatorKey, Demo } from "@/utils/types";
import { getConstructorKey, getCurveType } from "@/utils/types";
const SELECTABLE_RANGE = 10;
// 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: { [key in BezierCurveType]: WasmBezierManipulatorKey[] } = {
Linear: ["set_start", "set_end"],
Quadratic: ["set_start", "set_handle_start", "set_end"],
Cubic: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
};
class BezierDemo extends HTMLElement implements Demo {
// Props
title!: string;
points!: number[][];
key!: BezierFeatureKey;
inputOptions!: InputOption[];
triggerOnMouseMove!: boolean;
// Data
bezier!: WasmBezier;
callback!: BezierCallback;
manipulatorKeys!: WasmBezierManipulatorKey[];
activeIndex!: number | undefined;
sliderData!: Record<string, number>;
sliderUnits!: Record<string, string | string[]>;
// Avoids "recursive use of an object detected which would lead to unsafe aliasing in rust" error when moving mouse fast.
locked!: boolean;
async connectedCallback() {
this.title = this.getAttribute("title") || "";
this.points = JSON.parse(this.getAttribute("points") || "[]");
this.key = this.getAttribute("key") as BezierFeatureKey;
this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.callback = bezierFeatures[this.key].callback as BezierCallback;
const curveType = getCurveType(this.points.length);
this.manipulatorKeys = MANIPULATOR_KEYS_FROM_BEZIER_TYPE[curveType];
this.activeIndex = undefined as number | undefined;
this.sliderData = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.default })));
this.sliderUnits = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.unit })));
this.render();
const figure = this.querySelector("figure") as HTMLElement;
this.bezier = WasmBezier[getConstructorKey(curveType)](this.points);
this.drawDemo(figure);
}
render() {
renderDemo(this);
}
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]) {
figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation);
}
onMouseDown(event: MouseEvent) {
const mx = event.offsetX;
const my = event.offsetY;
for (let pointIndex = 0; pointIndex < this.points.length; pointIndex += 1) {
const point = this.points[pointIndex];
if (point && Math.abs(mx - point[0]) < SELECTABLE_RANGE && Math.abs(my - point[1]) < SELECTABLE_RANGE) {
this.activeIndex = pointIndex;
return;
}
}
}
onMouseUp() {
this.activeIndex = undefined;
}
onMouseMove(event: MouseEvent) {
if (this.locked) return;
this.locked = true;
const mx = event.offsetX;
const my = event.offsetY;
const figure = event.currentTarget as HTMLElement;
if (this.activeIndex !== undefined) {
this.bezier[this.manipulatorKeys[this.activeIndex]](mx, my);
this.points[this.activeIndex] = [mx, my];
this.drawDemo(figure);
} else if (this.triggerOnMouseMove) {
this.drawDemo(figure, [mx, my]);
}
this.locked = false;
}
getSliderUnit(sliderValue: number, variable: string): string {
const _ = sliderValue;
const sliderUnit = this.sliderUnits[variable];
return (Array.isArray(sliderUnit) ? "" : sliderUnit) || "";
}
}
export default BezierDemo;
@@ -1,82 +0,0 @@
import type { BezierFeatureKey } from "@/features/bezier-features";
import bezierFeatures from "@/features/bezier-features";
import { renderDemoPane } from "@/utils/render";
import type { BezierCurveType, BezierDemoOptions, InputOption, DemoPane, BezierDemoArgs } from "@/utils/types";
import { BEZIER_CURVE_TYPE } from "@/utils/types";
const demoDefaults = {
Linear: {
points: [
[55, 60],
[165, 120],
],
},
Quadratic: {
points: [
[55, 50],
[165, 30],
[185, 170],
],
},
Cubic: {
points: [
[55, 30],
[85, 140],
[175, 30],
[185, 160],
],
},
};
class BezierDemoPane extends HTMLElement implements DemoPane {
// Props
key!: BezierFeatureKey;
name!: string;
demoOptions!: BezierDemoOptions;
triggerOnMouseMove!: boolean;
// Data
demos!: BezierDemoArgs[];
id!: string;
connectedCallback() {
this.key = (this.getAttribute("name") || "") as BezierFeatureKey;
this.id = `bezier/${this.key}`;
this.name = bezierFeatures[this.key].name;
this.demoOptions = JSON.parse(this.getAttribute("demoOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
// Use quadratic slider options as a default if sliders are not provided for the other curve types.
const defaultSliderOptions: InputOption[] = this.demoOptions.Quadratic?.inputOptions || [];
this.demos = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => {
const givenData = this.demoOptions[curveType];
const defaultData = demoDefaults[curveType];
return {
title: curveType,
disabled: givenData?.disabled || false,
points: givenData?.customPoints || defaultData.points,
inputOptions: givenData?.inputOptions || defaultSliderOptions,
};
});
this.render();
}
render() {
renderDemoPane(this);
}
buildDemo(demo: BezierDemoArgs): HTMLElement {
const bezierDemo = document.createElement("bezier-demo");
bezierDemo.setAttribute("title", demo.title);
bezierDemo.setAttribute("points", JSON.stringify(demo.points));
bezierDemo.setAttribute("key", this.key);
bezierDemo.setAttribute("inputOptions", JSON.stringify(demo.inputOptions));
bezierDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
return bezierDemo;
}
}
export default BezierDemoPane;
@@ -1,101 +0,0 @@
import { WasmSubpath } from "@/../wasm/pkg";
import type { SubpathFeatureKey } from "@/features/subpath-features";
import subpathFeatures from "@/features/subpath-features";
import { renderDemo } from "@/utils/render";
import type { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption } from "@/utils/types";
const SELECTABLE_RANGE = 10;
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
class SubpathDemo extends HTMLElement {
// Props
title!: string;
triples!: (number[] | undefined)[][];
key!: SubpathFeatureKey;
closed!: boolean;
inputOptions!: InputOption[];
triggerOnMouseMove!: boolean;
// Data
subpath!: WasmSubpath;
callback!: SubpathCallback;
manipulatorKeys!: WasmSubpathManipulatorKey[];
activeIndex!: number[] | undefined;
sliderData!: Record<string, number>;
sliderUnits!: Record<string, string | string[]>;
async connectedCallback() {
this.title = this.getAttribute("title") || "";
this.triples = JSON.parse(this.getAttribute("triples") || "[]");
this.key = this.getAttribute("key") as SubpathFeatureKey;
this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.closed = this.getAttribute("closed") === "true";
this.callback = subpathFeatures[this.key].callback as SubpathCallback;
this.sliderData = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.default })));
this.sliderUnits = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.unit })));
this.render();
const figure = this.querySelector("figure") as HTMLElement;
this.subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
this.drawDemo(figure);
}
render() {
renderDemo(this);
}
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]) {
figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation);
}
onMouseDown(event: MouseEvent) {
const mx = event.offsetX;
const my = event.offsetY;
for (let controllerIndex = 0; controllerIndex < this.triples.length; controllerIndex += 1) {
for (let pointIndex = 0; pointIndex < 3; pointIndex += 1) {
const point = this.triples[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;
const figure = event.currentTarget as HTMLElement;
if (this.activeIndex) {
this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my);
this.triples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my];
this.drawDemo(figure);
} else if (this.triggerOnMouseMove) {
this.drawDemo(figure, [mx, my]);
}
}
getSliderUnit(sliderValue: number, variable: string): string {
const _ = sliderValue;
const sliderUnit = this.sliderUnits[variable];
return (Array.isArray(sliderUnit) ? "" : sliderUnit) || "";
}
}
export default SubpathDemo;
@@ -1,77 +0,0 @@
import type { SubpathFeatureKey } from "@/features/subpath-features";
import subpathFeatures from "@/features/subpath-features";
import { renderDemoPane } from "@/utils/render";
import type { DemoPane, SubpathDemoArgs, SubpathInputOption } from "@/utils/types";
class SubpathDemoPane extends HTMLElement implements DemoPane {
// Props
key!: SubpathFeatureKey;
name!: string;
inputOptions!: SubpathInputOption[];
triggerOnMouseMove!: boolean;
// Data
demos!: SubpathDemoArgs[];
id!: string;
connectedCallback() {
this.demos = [
{
title: "Open Subpath",
triples: [
[[45, 20], undefined, [35, 90]],
[[175, 40], [85, 40], undefined],
[[200, 175], undefined, undefined],
[[125, 100], [65, 120], undefined],
],
closed: false,
},
{
title: "Closed Subpath",
triples: [
[[60, 125], undefined, [65, 40]],
[[155, 30], [145, 120], undefined],
[
[170, 150],
[200, 90],
[95, 185],
],
],
closed: true,
},
];
this.key = (this.getAttribute("name") || "") as SubpathFeatureKey;
this.id = `subpath/${this.key}`;
this.name = subpathFeatures[this.key].name;
this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.render();
}
render() {
renderDemoPane(this);
}
buildDemo(demo: SubpathDemoArgs): HTMLElement {
const subpathDemo = document.createElement("subpath-demo");
subpathDemo.setAttribute("title", demo.title);
subpathDemo.setAttribute("triples", JSON.stringify(demo.triples));
subpathDemo.setAttribute("closed", String(demo.closed));
subpathDemo.setAttribute("key", this.key);
const inputOptions = this.inputOptions.map((option) => ({
...option,
disabled: option.isDisabledForClosed && demo.closed,
}));
subpathDemo.setAttribute("inputOptions", JSON.stringify(inputOptions));
subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
return subpathDemo;
}
}
export default SubpathDemoPane;
@@ -1,7 +1,6 @@
import { WasmBezier } from "@/../wasm/pkg"; import { WasmBezier } from "@/../wasm/pkg";
import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@/utils/options"; import type { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption } from "@/types";
import type { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption } from "@/utils/types"; import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions, BEZIER_T_VALUE_VARIANTS } from "@/types";
import { BEZIER_T_VALUE_VARIANTS } from "@/utils/types";
const bezierFeatures = { const bezierFeatures = {
constructor: { constructor: {
@@ -29,11 +28,12 @@ const bezierFeatures = {
], ],
inputOptions: [ inputOptions: [
{ {
variable: "t",
inputType: "slider",
min: 0.01, min: 0.01,
max: 0.99, max: 0.99,
step: 0.01, step: 0.01,
default: 0.5, default: 0.5,
variable: "t",
}, },
], ],
}, },
@@ -45,18 +45,20 @@ const bezierFeatures = {
], ],
inputOptions: [ inputOptions: [
{ {
variable: "t",
inputType: "slider",
min: 0.01, min: 0.01,
max: 0.99, max: 0.99,
step: 0.01, step: 0.01,
default: 0.5, default: 0.5,
variable: "t",
}, },
{ {
variable: "midpoint separation",
inputType: "slider",
min: 0, min: 0,
max: 100, max: 100,
step: 2, step: 2,
default: 30, default: 30,
variable: "midpoint separation",
}, },
], ],
}, },
@@ -87,11 +89,12 @@ const bezierFeatures = {
inputOptions: [ inputOptions: [
bezierTValueVariantOptions, bezierTValueVariantOptions,
{ {
variable: "steps",
inputType: "slider",
min: 2, min: 2,
max: 15, max: 15,
step: 1, step: 1,
default: 5, default: 5,
variable: "steps",
}, },
], ],
}, },
@@ -185,6 +188,7 @@ const bezierFeatures = {
bezierTValueVariantOptions, bezierTValueVariantOptions,
{ {
variable: "t1", variable: "t1",
inputType: "slider",
min: 0, min: 0,
max: 1, max: 1,
step: 0.01, step: 0.01,
@@ -192,6 +196,7 @@ const bezierFeatures = {
}, },
{ {
variable: "t2", variable: "t2",
inputType: "slider",
min: 0, min: 0,
max: 1, max: 1,
step: 0.01, step: 0.01,
@@ -259,6 +264,7 @@ const bezierFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
inputType: "slider",
min: -30, min: -30,
max: 30, max: 30,
step: 1, step: 1,
@@ -276,6 +282,7 @@ const bezierFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
inputType: "slider",
min: 0, min: 0,
max: 30, max: 30,
step: 1, step: 1,
@@ -294,6 +301,7 @@ const bezierFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "start_distance", variable: "start_distance",
inputType: "slider",
min: 0, min: 0,
max: 30, max: 30,
step: 1, step: 1,
@@ -301,6 +309,7 @@ const bezierFeatures = {
}, },
{ {
variable: "end_distance", variable: "end_distance",
inputType: "slider",
min: 0, min: 0,
max: 30, max: 30,
step: 1, step: 1,
@@ -328,6 +337,7 @@ const bezierFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "distance1", variable: "distance1",
inputType: "slider",
min: 0, min: 0,
max: 30, max: 30,
step: 1, step: 1,
@@ -335,6 +345,7 @@ const bezierFeatures = {
}, },
{ {
variable: "distance2", variable: "distance2",
inputType: "slider",
min: 0, min: 0,
max: 30, max: 30,
step: 1, step: 1,
@@ -342,6 +353,7 @@ const bezierFeatures = {
}, },
{ {
variable: "distance3", variable: "distance3",
inputType: "slider",
min: 0, min: 0,
max: 30, max: 30,
step: 1, step: 1,
@@ -349,6 +361,7 @@ const bezierFeatures = {
}, },
{ {
variable: "distance4", variable: "distance4",
inputType: "slider",
min: 0, min: 0,
max: 30, max: 30,
step: 1, step: 1,
@@ -366,12 +379,13 @@ const bezierFeatures = {
const inputOptions: InputOption[] = [ const inputOptions: InputOption[] = [
{ {
variable: "strategy", variable: "strategy",
default: 0,
inputType: "dropdown", inputType: "dropdown",
default: 0,
options: ["Automatic", "FavorLargerArcs", "FavorCorrectness"], options: ["Automatic", "FavorLargerArcs", "FavorCorrectness"],
}, },
{ {
variable: "error", variable: "error",
inputType: "slider",
min: 0.05, min: 0.05,
max: 1, max: 1,
step: 0.05, step: 0.05,
@@ -379,6 +393,7 @@ const bezierFeatures = {
}, },
{ {
variable: "max_iterations", variable: "max_iterations",
inputType: "slider",
min: 50, min: 50,
max: 200, max: 200,
step: 1, step: 1,
@@ -492,6 +507,7 @@ const bezierFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "angle", variable: "angle",
inputType: "slider",
min: 0, min: 0,
max: 2, max: 2,
step: 1 / 50, step: 1 / 50,
@@ -1,6 +1,5 @@
import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions, separationDiskDiameter } from "@/utils/options"; import type { SubpathCallback, SubpathInputOption, WasmSubpathInstance } from "@/types";
import type { SubpathCallback, SubpathInputOption, WasmSubpathInstance } from "@/utils/types"; import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions, separationDiskDiameter, SUBPATH_T_VALUE_VARIANTS } from "@/types";
import { SUBPATH_T_VALUE_VARIANTS } from "@/utils/types";
const subpathFeatures = { const subpathFeatures = {
constructor: { constructor: {
@@ -46,11 +45,12 @@ const subpathFeatures = {
inputOptions: [ inputOptions: [
subpathTValueVariantOptions, subpathTValueVariantOptions,
{ {
variable: "steps",
inputType: "slider",
min: 2, min: 2,
max: 30, max: 30,
step: 1, step: 1,
default: 5, default: 5,
variable: "steps",
}, },
], ],
}, },
@@ -163,6 +163,7 @@ const subpathFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
inputType: "slider",
min: -25, min: -25,
max: 25, max: 25,
step: 1, step: 1,
@@ -171,6 +172,7 @@ const subpathFeatures = {
joinOptions, joinOptions,
{ {
variable: "join: Miter - limit", variable: "join: Miter - limit",
inputType: "slider",
min: 1, min: 1,
max: 10, max: 10,
step: 0.25, step: 0.25,
@@ -184,6 +186,7 @@ const subpathFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
inputType: "slider",
min: 0, min: 0,
max: 25, max: 25,
step: 1, step: 1,
@@ -192,6 +195,7 @@ const subpathFeatures = {
joinOptions, joinOptions,
{ {
variable: "join: Miter - limit", variable: "join: Miter - limit",
inputType: "slider",
min: 1, min: 1,
max: 10, max: 10,
step: 0.25, step: 0.25,
@@ -206,6 +210,7 @@ const subpathFeatures = {
inputOptions: [ inputOptions: [
{ {
variable: "angle", variable: "angle",
inputType: "slider",
min: 0, min: 0,
max: 2, max: 2,
step: 1 / 50, step: 1 / 50,
+316 -70
View File
@@ -1,74 +1,60 @@
import { default as init } from "@/../wasm/pkg"; import { default as init, WasmSubpath, WasmBezier } from "@/../wasm/pkg";
import BezierDemo from "@/components/BezierDemo"; import bezierFeatures from "@/features-bezier";
import BezierDemoPane from "@/components/BezierDemoPane"; import type { BezierFeatureKey, BezierFeatureOptions } from "@/features-bezier";
import SubpathDemo from "@/components/SubpathDemo"; import subpathFeatures from "@/features-subpath";
import SubpathDemoPane from "@/components/SubpathDemoPane"; import type { SubpathFeatureKey, SubpathFeatureOptions } from "@/features-subpath";
import type { BezierFeatureKey } from "@/features/bezier-features"; import type { DemoArgs, BezierCurveType, BezierDemoArgs, SubpathDemoArgs, DemoData, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption, DemoDataBezier, DemoDataSubpath } from "@/types";
import bezierFeatures from "@/features/bezier-features"; import { BEZIER_CURVE_TYPE, getBezierDemoPointDefaults, getSubpathDemoArgs, POINT_INDEX_TO_MANIPULATOR, getConstructorKey, getCurveType, MANIPULATOR_KEYS_FROM_BEZIER_TYPE } from "@/types";
import type { SubpathFeatureKey } from "@/features/subpath-features";
import subpathFeatures from "@/features/subpath-features";
(async () => { init().then(renderPage);
await init();
window.customElements.define("bezier-demo", BezierDemo); function renderPage() {
window.customElements.define("bezier-demo-pane", BezierDemoPane); // Determine whether the page needs to recompute which examples to show
window.customElements.define("subpath-demo", SubpathDemo); window.addEventListener("hashchange", (e: HashChangeEvent) => {
window.customElements.define("subpath-demo-pane", SubpathDemoPane); const isUrlSolo = (url: string) => {
const splitHash = url.split("#")?.[1]?.split("/");
return splitHash?.length === 3 && splitHash?.[2] === "solo";
};
window.addEventListener("hashchange", (e: Event) => { const isOldHashSolo = isUrlSolo(e.oldURL);
const hashChangeEvent = e as HashChangeEvent; const isNewHashSolo = isUrlSolo(e.newURL);
const isOldHashSolo = isUrlSolo(hashChangeEvent.oldURL);
const isNewHashSolo = isUrlSolo(hashChangeEvent.newURL);
const target = document.getElementById(window.location.hash.substring(1)); const target = document.getElementById(window.location.hash.substring(1));
// Determine whether the page needs to recompute which examples to show if (!target || isOldHashSolo !== isNewHashSolo) renderPage();
if (!target || isOldHashSolo !== isNewHashSolo) {
renderExamples();
}
}); });
renderExamples(); // Get the hash from the URL
})();
function renderBezierPane(featureName: BezierFeatureKey, container?: HTMLElement) {
const feature = bezierFeatures[featureName];
const demo = document.createElement("bezier-demo-pane");
demo.setAttribute("name", featureName);
demo.setAttribute("demoOptions", JSON.stringify(feature.demoOptions || {}));
demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove));
container?.append(demo);
}
function renderSubpathPane(featureName: SubpathFeatureKey, container?: HTMLElement) {
const feature = subpathFeatures[featureName];
const demo = document.createElement("subpath-demo-pane");
demo.setAttribute("name", featureName);
demo.setAttribute("inputOptions", JSON.stringify(feature.inputOptions || []));
demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove));
container?.append(demo);
}
function isUrlSolo(url: string): boolean {
const hash = url.split("#")?.[1];
const splitHash = hash?.split("/");
return splitHash?.length === 3 && splitHash?.[2] === "solo";
}
function renderExamples() {
const hash = window.location.hash; const hash = window.location.hash;
const splitHash = hash.split("/"); const splitHash = hash.split("/");
// Scroll to specified hash if it exists
if (hash) document.getElementById(hash.substring(1))?.scrollIntoView();
// Determine which examples to render based on hash // Determine which examples to render based on hash
if (splitHash[0] === "#bezier" && splitHash[1] in bezierFeatures && splitHash[2] === "solo") { if (splitHash[0] === "#bezier" && splitHash[1] in bezierFeatures && splitHash[2] === "solo") {
window.document.body.innerHTML = `<div id="bezier-demos"></div>`; window.document.body.innerHTML = `<div id="bezier-demos"></div>`;
renderBezierPane(splitHash[1] as BezierFeatureKey, document.getElementById("bezier-demos") || undefined); const container = document.getElementById("bezier-demos");
} else if (splitHash[0] === "#subpath" && splitHash[1] in subpathFeatures && splitHash[2] === "solo") { if (!container) return;
const key = splitHash[1];
const value = (bezierFeatures as Record<string, BezierFeatureOptions>)[key];
if (value) container.append(bezierDemoGroup(key as BezierFeatureKey, value));
return;
}
if (splitHash[0] === "#subpath" && splitHash[1] in subpathFeatures && splitHash[2] === "solo") {
window.document.body.innerHTML = `<div id="subpath-demos"></div>`; window.document.body.innerHTML = `<div id="subpath-demos"></div>`;
renderSubpathPane(splitHash[1] as SubpathFeatureKey, document.getElementById("subpath-demos") || undefined); const container = document.getElementById("subpath-demos");
} else { if (!container) return;
window.document.body.innerHTML = `
const key = splitHash[1];
const value = (subpathFeatures as Record<string, SubpathFeatureOptions>)[key];
if (value) container.append(subpathDemoGroup(key as SubpathFeatureKey, value));
return;
}
window.document.body.innerHTML = `
<h1 class="website-header">Bezier-rs Interactive Documentation</h1> <h1 class="website-header">Bezier-rs Interactive Documentation</h1>
<p class="website-description"> <p class="website-description">
This is the interactive documentation for the <a href="https://crates.io/crates/bezier-rs">Bezier-rs</a> library. View the This is the interactive documentation for the <a href="https://crates.io/crates/bezier-rs">Bezier-rs</a> library. View the
@@ -76,24 +62,284 @@ function renderExamples() {
for detailed function descriptions and API usage. Click and drag on the endpoints of the demo curves to visualize the various Bezier utilities and functions. for detailed function descriptions and API usage. Click and drag on the endpoints of the demo curves to visualize the various Bezier utilities and functions.
</p> </p>
<h2 class="class-header">Beziers</h2> <h2 class="category-header">Beziers</h2>
<div id="bezier-demos"></div> <div id="bezier-demos"></div>
<h2 class="class-header">Subpaths</h2>
<h2 class="category-header">Subpaths</h2>
<div id="subpath-demos"></div> <div id="subpath-demos"></div>
`.trim(); `.trim();
const bezierDemos = document.getElementById("bezier-demos") || undefined; const bezierDemos = document.getElementById("bezier-demos") || undefined;
const subpathDemos = document.getElementById("subpath-demos") || undefined; if (bezierDemos) Object.entries(bezierFeatures).forEach(([key, options]) => bezierDemos.appendChild(bezierDemoGroup(key as BezierFeatureKey, options)));
(Object.keys(bezierFeatures) as BezierFeatureKey[]).forEach((feature) => renderBezierPane(feature, bezierDemos)); const subpathDemos = document.getElementById("subpath-demos") || undefined;
(Object.keys(subpathFeatures) as SubpathFeatureKey[]).forEach((feature) => renderSubpathPane(feature, subpathDemos)); if (subpathDemos) Object.entries(subpathFeatures).forEach(([key, options]) => subpathDemos.appendChild(subpathDemoGroup(key as SubpathFeatureKey, options)));
}
function bezierDemoGroup(key: BezierFeatureKey, options: BezierFeatureOptions): HTMLDivElement {
const demoOptions = options.demoOptions || {};
const demos: BezierDemoArgs[] = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => ({
title: curveType,
disabled: demoOptions[curveType]?.disabled || false,
points: demoOptions[curveType]?.customPoints || getBezierDemoPointDefaults()[curveType],
inputOptions: demoOptions[curveType]?.inputOptions || demoOptions.Quadratic?.inputOptions || [],
}));
return renderDemoGroup(`bezier/${key}`, bezierFeatures[key].name, demos, (demo: BezierDemoArgs) =>
demoBezier(demo.title, demo.points, key, demo.inputOptions, options.triggerOnMouseMove || false),
);
}
function subpathDemoGroup(key: SubpathFeatureKey, options: SubpathFeatureOptions): HTMLDivElement {
const buildDemo = (demo: SubpathDemoArgs) => {
const newInputOptions = (options.inputOptions || []).map((option) => ({
...option,
disabled: option.isDisabledForClosed && demo.closed,
}));
return demoSubpath(demo.title, demo.triples, key, demo.closed, newInputOptions, options.triggerOnMouseMove || false);
};
return renderDemoGroup(`subpath/${key}`, subpathFeatures[key].name, getSubpathDemoArgs(), buildDemo);
}
function demoBezier(title: string, points: number[][], key: BezierFeatureKey, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataBezier {
return {
kind: "bezier",
title,
element: document.createElement("div"),
inputOptions,
locked: false,
triggerOnMouseMove,
sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))),
sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))),
activePointIndex: undefined as number | undefined,
manipulatorKeys: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[getCurveType(points.length)],
bezier: WasmBezier[getConstructorKey(getCurveType(points.length))](points),
points,
callback: bezierFeatures[key].callback,
};
}
function demoSubpath(title: string, triples: (number[] | undefined)[][], key: SubpathFeatureKey, closed: boolean, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataSubpath {
return {
kind: "subpath",
title,
element: document.createElement("div"),
inputOptions,
locked: false,
triggerOnMouseMove,
sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))),
sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))),
activePointIndex: undefined as number | undefined,
activeManipulatorIndex: undefined as number | undefined,
manipulatorKeys: undefined as undefined | WasmSubpathManipulatorKey[],
subpath: WasmSubpath.from_triples(triples, closed) as WasmSubpathInstance,
triples,
callback: subpathFeatures[key].callback,
};
}
function updateDemoSVG(data: DemoData, figure: HTMLElement, mouseLocation?: [number, number]) {
if (data.kind === "subpath") figure.innerHTML = data.callback(data.subpath, data.sliderData, mouseLocation);
if (data.kind === "bezier") figure.innerHTML = data.callback(data.bezier, data.sliderData, mouseLocation);
}
function onMouseDown(data: DemoData, e: MouseEvent) {
const SELECTABLE_RANGE = 10;
let distances;
if (data.kind === "bezier") {
distances = data.points.flatMap((point, pointIndex) => {
if (!point) return [];
const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2));
return distance < SELECTABLE_RANGE ? [{ manipulatorIndex: undefined, pointIndex, distance }] : [];
});
} else if (data.kind === "subpath") {
distances = data.triples.flatMap((triple, manipulatorIndex) =>
triple.flatMap((point, pointIndex) => {
if (!point) return [];
const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2));
return distance < SELECTABLE_RANGE ? [{ manipulatorIndex, pointIndex, distance }] : [];
}),
);
} else {
return;
} }
// Scroll to specified hash if it exists const closest = distances.sort((a, b) => a.distance - b.distance)[0];
if (hash) { if (closest) {
const target = document.getElementById(hash.substring(1)); if (data.kind === "subpath") data.activeManipulatorIndex = closest.manipulatorIndex;
if (target) { data.activePointIndex = closest.pointIndex;
target.scrollIntoView();
}
} }
} }
function onMouseMove(data: DemoData, e: MouseEvent) {
if (data.locked || !(e.currentTarget instanceof HTMLElement)) return;
data.locked = true;
if (data.kind === "bezier" && data.activePointIndex !== undefined) {
data.bezier[data.manipulatorKeys[data.activePointIndex]](e.offsetX, e.offsetY);
data.points[data.activePointIndex] = [e.offsetX, e.offsetY];
updateDemoSVG(data, e.currentTarget);
} else if (data.kind === "subpath" && data.activePointIndex !== undefined && data.activeManipulatorIndex !== undefined) {
data.subpath[POINT_INDEX_TO_MANIPULATOR[data.activePointIndex]](data.activeManipulatorIndex, e.offsetX, e.offsetY);
data.triples[data.activeManipulatorIndex][data.activePointIndex] = [e.offsetX, e.offsetY];
updateDemoSVG(data, e.currentTarget);
} else if (data.triggerOnMouseMove) {
updateDemoSVG(data, e.currentTarget, [e.offsetX, e.offsetY]);
}
data.locked = false;
}
function onMouseUp(data: DemoData) {
data.activePointIndex = undefined;
if (data.kind === "subpath") data.activeManipulatorIndex = undefined;
}
function renderDemoGroup<T extends DemoArgs>(id: string, name: string, demos: T[], buildDemo: (demo: T) => DemoData): HTMLDivElement {
const demoGroup = document.createElement("div");
demoGroup.className = "demo-group-container";
demoGroup.insertAdjacentHTML(
"beforeend",
`
${(() => {
// Add header and href anchor if not on a solo example page
const currentHash = window.location.hash.split("/");
if (currentHash.length === 3 || currentHash[2] === "solo") return "";
return `
<h3 class="demo-group-header">
<a href="#${id}">#</a>
${name}
</h3>
`.trim();
})()}
<div class="demo-row" data-demo-row></div>
`.trim(),
);
const demoRow = demoGroup.querySelector("[data-demo-row]");
if (!demoRow) return demoGroup;
demos.forEach((demo) => {
if (demo.disabled) return;
const data = buildDemo(demo);
renderDemo(data);
const figure = data.element.querySelector("[data-demo-figure]");
if (figure instanceof HTMLElement) updateDemoSVG(data, figure);
demoRow.append(data.element);
});
return demoGroup;
}
function renderDemo(demo: DemoData) {
const getSliderUnit = (data: DemoData, variable: string): string => {
return (Array.isArray(data.sliderUnits[variable]) ? "" : data.sliderUnits[variable]) || "";
};
demo.element.insertAdjacentHTML(
"beforeend",
`
<h4 class="demo-header">${demo.title}</h4>
<div class="demo-figure" data-demo-figure></div>
<div class="parent-input-container" data-parent-input-container>
${(() =>
demo.inputOptions
.map((inputOption) =>
`
<div
class="${(() => {
if (inputOption.inputType === "dropdown") return "select-container";
if (inputOption.inputType === "slider") return "slider-container";
return "";
})()}"
data-input-container
>
<div class="input-label" data-input-label>
${inputOption.variable}: ${inputOption.inputType === "dropdown" ? "" : demo.sliderData[inputOption.variable]}${getSliderUnit(demo, inputOption.variable)}
</div>
${(() => {
if (inputOption.inputType !== "dropdown") return "";
return `
<select class="select-input" value="${inputOption.default}" ${inputOption.disabled ? "disabled" : ""} data-select>
${inputOption.options?.map((value, idx) => `<option value="${idx}" id="${idx}-${value}">${value}</option>`).join("\n")}
</select>
`.trim();
})()}
${(() => {
if (inputOption.inputType !== "slider") return "";
const ratio = (Number(inputOption.default) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0));
return `
<input
class="slider-input"
type="range"
max="${inputOption.max}"
min="${inputOption.min}"
step="${inputOption.step}"
value="${inputOption.default}"
style="--range-ratio: ${ratio}"
data-slider-input
/>
`.trim();
})()}
</div>
`.trim(),
)
.join("\n"))()}
</div>
`.trim(),
);
const figure = demo.element.querySelector(`[data-demo-figure]`);
if (!(figure instanceof HTMLElement)) return;
figure.addEventListener("mousedown", (e) => onMouseDown(demo, e));
figure.addEventListener("mouseup", () => onMouseUp(demo));
figure.addEventListener("mousemove", (e) => onMouseMove(demo, e));
demo.inputOptions.forEach((inputOption, index) => {
const inputContainer = demo.element.querySelectorAll(`[data-parent-input-container] [data-input-container]`)[index];
if (!(inputContainer instanceof HTMLDivElement)) return;
if (inputOption.inputType === "dropdown") {
const selectElement = inputContainer.querySelector("[data-select]");
if (!(selectElement instanceof HTMLSelectElement)) return;
selectElement.addEventListener("change", (e: Event) => {
if (!(e.target instanceof HTMLSelectElement)) return;
demo.sliderData[inputOption.variable] = Number(e.target.value);
updateDemoSVG(demo, figure);
});
}
if (inputOption.inputType === "slider") {
const sliderInput = inputContainer.querySelector("[data-slider-input]");
if (!(sliderInput instanceof HTMLInputElement)) return;
sliderInput.addEventListener("input", (e: Event) => {
const target = e.target;
if (!(target instanceof HTMLInputElement)) return;
// Set the slider label text
const variable = inputOption.variable;
const data = demo.sliderData[variable];
const unit = getSliderUnit(demo, variable);
const label = inputContainer.querySelector("[data-input-label]");
if (!(label instanceof HTMLDivElement)) return;
label.innerText = `${variable}: ${data}${unit}`;
// Set the slider input range percentage
sliderInput.style.setProperty("--range-ratio", String((Number(target.value) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0))));
// Update the slider data and redraw the demo
demo.sliderData[variable] = Number(target.value);
updateDemoSVG(demo, figure);
});
}
});
}
+241
View File
@@ -0,0 +1,241 @@
import type * as WasmPkg from "@/../wasm/pkg";
type WasmRawInstance = typeof WasmPkg;
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
type DemoDataCommon = {
title: string;
element: HTMLDivElement;
inputOptions: InputOption[];
locked: boolean;
triggerOnMouseMove: boolean;
sliderData: Record<string, number>;
sliderUnits: Record<string, string | string[]>;
activePointIndex: number | undefined;
};
export type DemoDataBezier = DemoDataCommon & {
kind: "bezier";
manipulatorKeys: WasmBezierManipulatorKey[];
bezier: WasmBezierInstance;
points: number[][];
callback: BezierCallback;
};
export type DemoDataSubpath = DemoDataCommon & {
kind: "subpath";
activeManipulatorIndex: number | undefined;
manipulatorKeys: WasmSubpathManipulatorKey[] | undefined;
subpath: WasmSubpathInstance;
triples: (number[] | undefined)[][];
callback: SubpathCallback;
};
export type DemoData = DemoDataBezier | DemoDataSubpath;
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
export type BezierCurveType = (typeof BEZIER_CURVE_TYPE)[number];
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
export type BezierDemoOptions = {
[key in BezierCurveType]: {
disabled?: boolean;
inputOptions?: InputOption[];
customPoints?: number[][];
};
};
export type InputOption = {
variable: string;
min?: number;
max?: number;
step?: number;
default?: number;
unit?: string | string[];
inputType?: "slider" | "dropdown";
options?: string[];
disabled?: boolean;
};
export type SubpathInputOption = InputOption & {
isDisabledForClosed?: boolean;
};
export function getCurveType(numPoints: number): BezierCurveType {
const mapping: Record<number, BezierCurveType> = {
2: "Linear",
3: "Quadratic",
4: "Cubic",
};
if (!(numPoints in mapping)) throw new Error("Invalid number of points for a bezier");
return mapping[numPoints];
}
export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey {
const mapping: Record<BezierCurveType, WasmBezierConstructorKey> = {
Linear: "new_linear",
Quadratic: "new_quadratic",
Cubic: "new_cubic",
};
return mapping[bezierCurveType];
}
export type DemoArgs = {
title: string;
disabled?: boolean;
};
export type BezierDemoArgs = {
points: number[][];
inputOptions: InputOption[];
} & DemoArgs;
export type SubpathDemoArgs = {
triples: (number[] | undefined)[][];
closed: boolean;
} & DemoArgs;
export const BEZIER_T_VALUE_VARIANTS = ["Parametric", "Euclidean"] as const;
export const SUBPATH_T_VALUE_VARIANTS = ["GlobalParametric", "GlobalEuclidean"] as const;
const CAP_VARIANTS = ["Butt", "Round", "Square"] as const;
const JOIN_VARIANTS = ["Bevel", "Miter", "Round"] as const;
export const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
// Given the number of points in the curve, map the index of a point to the correct manipulator key
export const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [key in BezierCurveType]: WasmBezierManipulatorKey[] } = {
Linear: ["set_start", "set_end"],
Quadratic: ["set_start", "set_handle_start", "set_end"],
Cubic: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
};
export function getBezierDemoPointDefaults() {
// We use a function to generate a new object each time it is called
// to prevent one instance from being shared and modified across demos
return {
Linear: [
[55, 60],
[165, 120],
],
Quadratic: [
[55, 50],
[165, 30],
[185, 170],
],
Cubic: [
[55, 30],
[85, 140],
[175, 30],
[185, 160],
],
};
}
export function getSubpathDemoArgs(): SubpathDemoArgs[] {
// We use a function to generate a new object each time it is called
// to prevent one instance from being shared and modified across demos
return [
{
title: "Open Subpath",
triples: [
[[45, 20], undefined, [35, 90]],
[[175, 40], [85, 40], undefined],
[[200, 175], undefined, undefined],
[[125, 100], [65, 120], undefined],
],
closed: false,
},
{
title: "Closed Subpath",
triples: [
[[60, 125], undefined, [65, 40]],
[[155, 30], [145, 120], undefined],
[
[170, 150],
[200, 90],
[95, 185],
],
],
closed: true,
},
];
}
export const tSliderOptions = {
variable: "t",
inputType: "slider",
min: 0,
max: 1,
step: 0.01,
default: 0.5,
};
export const errorOptions = {
variable: "error",
inputType: "slider",
min: 0.1,
max: 2,
step: 0.1,
default: 0.5,
};
export const minimumSeparationOptions = {
variable: "minimum_separation",
inputType: "slider",
min: 0.001,
max: 0.25,
step: 0.001,
default: 0.05,
};
export const intersectionErrorOptions = {
variable: "error",
inputType: "slider",
min: 0.001,
max: 0.525,
step: 0.0025,
default: 0.02,
};
export const separationDiskDiameter = {
variable: "separation_disk_diameter",
inputType: "slider",
min: 2.5,
max: 25,
step: 0.1,
default: 5,
};
export const bezierTValueVariantOptions = {
variable: "TVariant",
inputType: "dropdown",
default: 0,
options: BEZIER_T_VALUE_VARIANTS,
};
export const subpathTValueVariantOptions = {
variable: "TVariant",
inputType: "dropdown",
default: 0,
options: SUBPATH_T_VALUE_VARIANTS,
};
export const joinOptions = {
variable: "join",
inputType: "dropdown",
default: 0,
options: JOIN_VARIANTS,
};
export const capOptions = {
variable: "cap",
inputType: "dropdown",
default: 0,
options: CAP_VARIANTS,
};
@@ -1,69 +0,0 @@
import { BEZIER_T_VALUE_VARIANTS, CAP_VARIANTS, JOIN_VARIANTS, SUBPATH_T_VALUE_VARIANTS } from "@/utils/types";
export const tSliderOptions = {
variable: "t",
min: 0,
max: 1,
step: 0.01,
default: 0.5,
};
export const errorOptions = {
variable: "error",
min: 0.1,
max: 2,
step: 0.1,
default: 0.5,
};
export const minimumSeparationOptions = {
variable: "minimum_separation",
min: 0.001,
max: 0.25,
step: 0.001,
default: 0.05,
};
export const intersectionErrorOptions = {
variable: "error",
min: 0.001,
max: 0.525,
step: 0.0025,
default: 0.02,
};
export const separationDiskDiameter = {
variable: "separation_disk_diameter",
min: 2.5,
max: 25,
step: 0.1,
default: 5,
};
export const bezierTValueVariantOptions = {
variable: "TVariant",
default: 0,
inputType: "dropdown",
options: BEZIER_T_VALUE_VARIANTS,
};
export const subpathTValueVariantOptions = {
variable: "TVariant",
default: 0,
inputType: "dropdown",
options: SUBPATH_T_VALUE_VARIANTS,
};
export const joinOptions = {
variable: "join",
default: 0,
inputType: "dropdown",
options: JOIN_VARIANTS,
};
export const capOptions = {
variable: "cap",
default: 0,
inputType: "dropdown",
options: CAP_VARIANTS,
};
@@ -1,119 +0,0 @@
import type { Demo, DemoPane, InputOption } from "@/utils/types";
export function renderDemo(demo: Demo) {
const header = document.createElement("h4");
header.className = "demo-header";
header.innerText = demo.title;
const figure = document.createElement("figure");
figure.className = "demo-figure";
figure.addEventListener("mousedown", demo.onMouseDown.bind(demo));
figure.addEventListener("mouseup", demo.onMouseUp.bind(demo));
figure.addEventListener("mousemove", demo.onMouseMove.bind(demo));
demo.append(header);
demo.append(figure);
const parentSliderContainer = document.createElement("div");
parentSliderContainer.className = "parent-slider-container";
demo.inputOptions.forEach((inputOption: InputOption) => {
const isDropdown = inputOption.inputType === "dropdown";
const sliderContainer = document.createElement("div");
sliderContainer.className = isDropdown ? "select-container" : "slider-container";
const sliderLabel = document.createElement("div");
const sliderData = demo.sliderData[inputOption.variable];
const sliderUnit = demo.getSliderUnit(sliderData, inputOption.variable);
sliderLabel.className = "slider-label";
sliderLabel.innerText = `${inputOption.variable}: ${isDropdown ? "" : sliderData}${sliderUnit}`;
sliderContainer.appendChild(sliderLabel);
if (isDropdown) {
const selectInput = document.createElement("select");
selectInput.className = "select-input";
selectInput.value = String(inputOption.default);
inputOption.options?.forEach((value, idx) => {
const id = `${idx}-${value}`;
const option = document.createElement("option");
option.value = String(idx);
option.id = id;
option.text = value;
selectInput.append(option);
});
if (inputOption.disabled) {
selectInput.disabled = true;
}
selectInput.addEventListener("change", (event: Event) => {
demo.sliderData[inputOption.variable] = Number((event.target as HTMLInputElement).value);
demo.drawDemo(figure);
});
sliderContainer.appendChild(selectInput);
} else {
const sliderInput = document.createElement("input");
sliderInput.className = "slider-input";
sliderInput.type = "range";
sliderInput.max = String(inputOption.max);
sliderInput.min = String(inputOption.min);
sliderInput.step = String(inputOption.step);
sliderInput.value = String(inputOption.default);
const range = Number(inputOption.max) - Number(inputOption.min);
const ratio = (Number(inputOption.default) - Number(inputOption.min)) / range;
sliderInput.style.setProperty("--range-ratio", String(ratio));
sliderInput.addEventListener("input", (event: Event) => {
const target = event.target as HTMLInputElement;
demo.sliderData[inputOption.variable] = Number(target.value);
const data = demo.sliderData[inputOption.variable];
const unit = demo.getSliderUnit(demo.sliderData[inputOption.variable], inputOption.variable);
sliderLabel.innerText = `${inputOption.variable}: ${data}${unit}`;
const ratio = (Number(target.value) - Number(inputOption.min)) / range;
sliderInput.style.setProperty("--range-ratio", String(ratio));
demo.drawDemo(figure);
});
sliderContainer.appendChild(sliderInput);
}
parentSliderContainer.append(sliderContainer);
});
demo.append(parentSliderContainer);
}
export function renderDemoPane(demoPane: DemoPane) {
const container = document.createElement("div");
container.className = "demo-pane-container";
const headerAnchorLink = document.createElement("a");
headerAnchorLink.innerText = "#";
const currentHash = window.location.hash.split("/");
// Add header and href anchor if not on a solo example page
if (currentHash.length !== 3 && currentHash[2] !== "solo") {
headerAnchorLink.href = `#${demoPane.id}`;
const header = document.createElement("h3");
header.innerText = demoPane.name;
header.className = "demo-pane-header";
header.append(headerAnchorLink);
container.append(header);
}
const demoRow = document.createElement("div");
demoRow.className = "demo-row";
demoPane.demos.forEach((demo) => {
if (demo.disabled) {
return;
}
const demoComponent = demoPane.buildDemo(demo);
demoRow.append(demoComponent);
});
container.append(demoRow);
demoPane.append(container);
}
@@ -1,102 +0,0 @@
import type * as WasmPkg from "@/../wasm/pkg";
export type WasmRawInstance = typeof WasmPkg;
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 const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
export type BezierCurveType = (typeof BEZIER_CURVE_TYPE)[number];
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
export type BezierDemoOptions = {
[key in BezierCurveType]: {
disabled?: boolean;
inputOptions?: InputOption[];
customPoints?: number[][];
};
};
export type SubpathInputOption = InputOption & {
isDisabledForClosed?: boolean;
};
export type InputOption = {
variable: string;
min?: number;
max?: number;
step?: number;
default?: number;
unit?: string | string[];
inputType?: "slider" | "dropdown";
options?: string[];
disabled?: boolean;
};
export function getCurveType(numPoints: number): BezierCurveType {
const mapping: Record<number, BezierCurveType> = {
2: "Linear",
3: "Quadratic",
4: "Cubic",
};
if (!(numPoints in mapping)) throw new Error("Invalid number of points for a bezier");
return mapping[numPoints];
}
export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey {
const mapping: Record<BezierCurveType, WasmBezierConstructorKey> = {
Linear: "new_linear",
Quadratic: "new_quadratic",
Cubic: "new_cubic",
};
return mapping[bezierCurveType];
}
export type DemoArgs = {
title: string;
disabled?: boolean;
};
export type BezierDemoArgs = {
points: number[][];
inputOptions: InputOption[];
} & DemoArgs;
export type SubpathDemoArgs = {
triples: (number[] | undefined)[][];
closed: boolean;
} & DemoArgs;
export type Demo = {
inputOptions: InputOption[];
sliderData: Record<string, number>;
sliderUnits: Record<string, string | string[]>;
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void;
onMouseDown(event: MouseEvent): void;
onMouseUp(): void;
onMouseMove(event: MouseEvent): void;
getSliderUnit(sliderValue: number, variable: string): string;
} & HTMLElement;
export type DemoPane = {
name: string;
demos: DemoArgs[];
id: string;
buildDemo(demo: DemoArgs): HTMLElement;
} & HTMLElement;
export const BEZIER_T_VALUE_VARIANTS = ["Parametric", "Euclidean"] as const;
export const SUBPATH_T_VALUE_VARIANTS = ["GlobalParametric", "GlobalEuclidean"] as const;
export const CAP_VARIANTS = ["Butt", "Round", "Square"] as const;
export const JOIN_VARIANTS = ["Bevel", "Miter", "Round"] as const;