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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 598 additions and 755 deletions

View file

@ -23,7 +23,7 @@ body {
color: var(--color-navy);
}
.class-header {
.category-header {
color: var(--color-navy);
font-family: "Bona Nova", serif;
margin-bottom: 0
@ -48,7 +48,7 @@ body > h2 {
margin-top: 40px;
}
/* Demo Pane styles */
/* Demo group styles */
.demo-row {
display: flex;
flex-direction: row;
@ -59,7 +59,7 @@ body > h2 {
margin-top: 20px;
}
.demo-pane-header {
.demo-group-header {
display: inline-block;
position: relative;
margin-top: 2em;
@ -69,7 +69,7 @@ body > h2 {
color: var(--color-navy);
}
.demo-pane-header a {
.demo-group-header a {
display: none;
position: absolute;
left: 0;
@ -78,11 +78,11 @@ body > h2 {
opacity: 0.5;
}
.demo-pane-header:hover a {
.demo-group-header:hover a {
display: inline-block;
}
.demo-pane-container {
.demo-group-container {
position: relative;
width: fit-content;
margin: auto;
@ -103,7 +103,7 @@ body > h2 {
border: solid 1px black;
}
.parent-slider-container {
.parent-input-container {
display: flex;
justify-content: center;
flex-direction: column;
@ -123,7 +123,7 @@ svg text {
padding-bottom: 5px;
}
.slider-label {
.input-label {
font-family: monospace;
display: flex;
justify-content: left;
@ -139,7 +139,6 @@ input[type="range"] {
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-repeat: no-repeat;
}
/* Input Thumb */

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,7 +1,6 @@
import { WasmBezier } from "@/../wasm/pkg";
import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@/utils/options";
import type { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption } from "@/utils/types";
import { BEZIER_T_VALUE_VARIANTS } from "@/utils/types";
import type { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption } from "@/types";
import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions, BEZIER_T_VALUE_VARIANTS } from "@/types";
const bezierFeatures = {
constructor: {
@ -29,11 +28,12 @@ const bezierFeatures = {
],
inputOptions: [
{
variable: "t",
inputType: "slider",
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
variable: "t",
},
],
},
@ -45,18 +45,20 @@ const bezierFeatures = {
],
inputOptions: [
{
variable: "t",
inputType: "slider",
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
variable: "t",
},
{
variable: "midpoint separation",
inputType: "slider",
min: 0,
max: 100,
step: 2,
default: 30,
variable: "midpoint separation",
},
],
},
@ -87,11 +89,12 @@ const bezierFeatures = {
inputOptions: [
bezierTValueVariantOptions,
{
variable: "steps",
inputType: "slider",
min: 2,
max: 15,
step: 1,
default: 5,
variable: "steps",
},
],
},
@ -185,6 +188,7 @@ const bezierFeatures = {
bezierTValueVariantOptions,
{
variable: "t1",
inputType: "slider",
min: 0,
max: 1,
step: 0.01,
@ -192,6 +196,7 @@ const bezierFeatures = {
},
{
variable: "t2",
inputType: "slider",
min: 0,
max: 1,
step: 0.01,
@ -259,6 +264,7 @@ const bezierFeatures = {
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: -30,
max: 30,
step: 1,
@ -276,6 +282,7 @@ const bezierFeatures = {
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: 0,
max: 30,
step: 1,
@ -294,6 +301,7 @@ const bezierFeatures = {
inputOptions: [
{
variable: "start_distance",
inputType: "slider",
min: 0,
max: 30,
step: 1,
@ -301,6 +309,7 @@ const bezierFeatures = {
},
{
variable: "end_distance",
inputType: "slider",
min: 0,
max: 30,
step: 1,
@ -328,6 +337,7 @@ const bezierFeatures = {
inputOptions: [
{
variable: "distance1",
inputType: "slider",
min: 0,
max: 30,
step: 1,
@ -335,6 +345,7 @@ const bezierFeatures = {
},
{
variable: "distance2",
inputType: "slider",
min: 0,
max: 30,
step: 1,
@ -342,6 +353,7 @@ const bezierFeatures = {
},
{
variable: "distance3",
inputType: "slider",
min: 0,
max: 30,
step: 1,
@ -349,6 +361,7 @@ const bezierFeatures = {
},
{
variable: "distance4",
inputType: "slider",
min: 0,
max: 30,
step: 1,
@ -366,12 +379,13 @@ const bezierFeatures = {
const inputOptions: InputOption[] = [
{
variable: "strategy",
default: 0,
inputType: "dropdown",
default: 0,
options: ["Automatic", "FavorLargerArcs", "FavorCorrectness"],
},
{
variable: "error",
inputType: "slider",
min: 0.05,
max: 1,
step: 0.05,
@ -379,6 +393,7 @@ const bezierFeatures = {
},
{
variable: "max_iterations",
inputType: "slider",
min: 50,
max: 200,
step: 1,
@ -492,6 +507,7 @@ const bezierFeatures = {
inputOptions: [
{
variable: "angle",
inputType: "slider",
min: 0,
max: 2,
step: 1 / 50,

View file

@ -1,6 +1,5 @@
import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions, separationDiskDiameter } from "@/utils/options";
import type { SubpathCallback, SubpathInputOption, WasmSubpathInstance } from "@/utils/types";
import { SUBPATH_T_VALUE_VARIANTS } from "@/utils/types";
import type { SubpathCallback, SubpathInputOption, WasmSubpathInstance } from "@/types";
import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions, separationDiskDiameter, SUBPATH_T_VALUE_VARIANTS } from "@/types";
const subpathFeatures = {
constructor: {
@ -46,11 +45,12 @@ const subpathFeatures = {
inputOptions: [
subpathTValueVariantOptions,
{
variable: "steps",
inputType: "slider",
min: 2,
max: 30,
step: 1,
default: 5,
variable: "steps",
},
],
},
@ -163,6 +163,7 @@ const subpathFeatures = {
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: -25,
max: 25,
step: 1,
@ -171,6 +172,7 @@ const subpathFeatures = {
joinOptions,
{
variable: "join: Miter - limit",
inputType: "slider",
min: 1,
max: 10,
step: 0.25,
@ -184,6 +186,7 @@ const subpathFeatures = {
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: 0,
max: 25,
step: 1,
@ -192,6 +195,7 @@ const subpathFeatures = {
joinOptions,
{
variable: "join: Miter - limit",
inputType: "slider",
min: 1,
max: 10,
step: 0.25,
@ -206,6 +210,7 @@ const subpathFeatures = {
inputOptions: [
{
variable: "angle",
inputType: "slider",
min: 0,
max: 2,
step: 1 / 50,

View file

@ -1,74 +1,60 @@
import { default as init } from "@/../wasm/pkg";
import BezierDemo from "@/components/BezierDemo";
import BezierDemoPane from "@/components/BezierDemoPane";
import SubpathDemo from "@/components/SubpathDemo";
import SubpathDemoPane from "@/components/SubpathDemoPane";
import type { BezierFeatureKey } from "@/features/bezier-features";
import bezierFeatures from "@/features/bezier-features";
import type { SubpathFeatureKey } from "@/features/subpath-features";
import subpathFeatures from "@/features/subpath-features";
import { default as init, WasmSubpath, WasmBezier } from "@/../wasm/pkg";
import bezierFeatures from "@/features-bezier";
import type { BezierFeatureKey, BezierFeatureOptions } from "@/features-bezier";
import subpathFeatures from "@/features-subpath";
import type { SubpathFeatureKey, SubpathFeatureOptions } from "@/features-subpath";
import type { DemoArgs, BezierCurveType, BezierDemoArgs, SubpathDemoArgs, DemoData, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption, DemoDataBezier, DemoDataSubpath } from "@/types";
import { BEZIER_CURVE_TYPE, getBezierDemoPointDefaults, getSubpathDemoArgs, POINT_INDEX_TO_MANIPULATOR, getConstructorKey, getCurveType, MANIPULATOR_KEYS_FROM_BEZIER_TYPE } from "@/types";
(async () => {
await init();
init().then(renderPage);
window.customElements.define("bezier-demo", BezierDemo);
window.customElements.define("bezier-demo-pane", BezierDemoPane);
window.customElements.define("subpath-demo", SubpathDemo);
window.customElements.define("subpath-demo-pane", SubpathDemoPane);
function renderPage() {
// Determine whether the page needs to recompute which examples to show
window.addEventListener("hashchange", (e: HashChangeEvent) => {
const isUrlSolo = (url: string) => {
const splitHash = url.split("#")?.[1]?.split("/");
return splitHash?.length === 3 && splitHash?.[2] === "solo";
};
window.addEventListener("hashchange", (e: Event) => {
const hashChangeEvent = e as HashChangeEvent;
const isOldHashSolo = isUrlSolo(hashChangeEvent.oldURL);
const isNewHashSolo = isUrlSolo(hashChangeEvent.newURL);
const isOldHashSolo = isUrlSolo(e.oldURL);
const isNewHashSolo = isUrlSolo(e.newURL);
const target = document.getElementById(window.location.hash.substring(1));
// Determine whether the page needs to recompute which examples to show
if (!target || isOldHashSolo !== isNewHashSolo) {
renderExamples();
}
if (!target || isOldHashSolo !== isNewHashSolo) renderPage();
});
renderExamples();
})();
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() {
// Get the hash from the URL
const hash = window.location.hash;
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
if (splitHash[0] === "#bezier" && splitHash[1] in bezierFeatures && splitHash[2] === "solo") {
window.document.body.innerHTML = `<div id="bezier-demos"></div>`;
renderBezierPane(splitHash[1] as BezierFeatureKey, document.getElementById("bezier-demos") || undefined);
} else if (splitHash[0] === "#subpath" && splitHash[1] in subpathFeatures && splitHash[2] === "solo") {
const container = document.getElementById("bezier-demos");
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>`;
renderSubpathPane(splitHash[1] as SubpathFeatureKey, document.getElementById("subpath-demos") || undefined);
} else {
window.document.body.innerHTML = `
const container = document.getElementById("subpath-demos");
if (!container) return;
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>
<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
@ -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.
</p>
<h2 class="class-header">Beziers</h2>
<h2 class="category-header">Beziers</h2>
<div id="bezier-demos"></div>
<h2 class="class-header">Subpaths</h2>
<h2 class="category-header">Subpaths</h2>
<div id="subpath-demos"></div>
`.trim();
const bezierDemos = document.getElementById("bezier-demos") || undefined;
const subpathDemos = document.getElementById("subpath-demos") || undefined;
const bezierDemos = document.getElementById("bezier-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));
(Object.keys(subpathFeatures) as SubpathFeatureKey[]).forEach((feature) => renderSubpathPane(feature, subpathDemos));
const subpathDemos = document.getElementById("subpath-demos") || undefined;
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
if (hash) {
const target = document.getElementById(hash.substring(1));
if (target) {
target.scrollIntoView();
}
const closest = distances.sort((a, b) => a.distance - b.distance)[0];
if (closest) {
if (data.kind === "subpath") data.activeManipulatorIndex = closest.manipulatorIndex;
data.activePointIndex = closest.pointIndex;
}
}
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);
});
}
});
}

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,
};

View file

@ -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,
};

View file

@ -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);
}

View file

@ -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;