From a2465f40b35ba1b904393195e0297bb405e49652 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 3 Oct 2024 17:20:50 -0700 Subject: [PATCH] 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 --- .../other/bezier-rs-demos/public/style.css | 17 +- .../src/components/BezierDemo.ts | 114 ----- .../src/components/BezierDemoPane.ts | 82 ---- .../src/components/SubpathDemo.ts | 101 ----- .../src/components/SubpathDemoPane.ts | 77 ---- .../bezier-features.ts => features-bezier.ts} | 32 +- ...ubpath-features.ts => features-subpath.ts} | 13 +- website/other/bezier-rs-demos/src/main.ts | 390 ++++++++++++++---- website/other/bezier-rs-demos/src/types.ts | 241 +++++++++++ .../bezier-rs-demos/src/utils/options.ts | 69 ---- .../other/bezier-rs-demos/src/utils/render.ts | 119 ------ .../other/bezier-rs-demos/src/utils/types.ts | 102 ----- 12 files changed, 600 insertions(+), 757 deletions(-) delete mode 100644 website/other/bezier-rs-demos/src/components/BezierDemo.ts delete mode 100644 website/other/bezier-rs-demos/src/components/BezierDemoPane.ts delete mode 100644 website/other/bezier-rs-demos/src/components/SubpathDemo.ts delete mode 100644 website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts rename website/other/bezier-rs-demos/src/{features/bezier-features.ts => features-bezier.ts} (95%) rename website/other/bezier-rs-demos/src/{features/subpath-features.ts => features-subpath.ts} (96%) create mode 100644 website/other/bezier-rs-demos/src/types.ts delete mode 100644 website/other/bezier-rs-demos/src/utils/options.ts delete mode 100644 website/other/bezier-rs-demos/src/utils/render.ts delete mode 100644 website/other/bezier-rs-demos/src/utils/types.ts diff --git a/website/other/bezier-rs-demos/public/style.css b/website/other/bezier-rs-demos/public/style.css index 7355538dd9..d277d04c69 100644 --- a/website/other/bezier-rs-demos/public/style.css +++ b/website/other/bezier-rs-demos/public/style.css @@ -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 */ diff --git a/website/other/bezier-rs-demos/src/components/BezierDemo.ts b/website/other/bezier-rs-demos/src/components/BezierDemo.ts deleted file mode 100644 index 29b7972044..0000000000 --- a/website/other/bezier-rs-demos/src/components/BezierDemo.ts +++ /dev/null @@ -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; - - sliderUnits!: Record; - - // 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; diff --git a/website/other/bezier-rs-demos/src/components/BezierDemoPane.ts b/website/other/bezier-rs-demos/src/components/BezierDemoPane.ts deleted file mode 100644 index b74ba2813d..0000000000 --- a/website/other/bezier-rs-demos/src/components/BezierDemoPane.ts +++ /dev/null @@ -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; diff --git a/website/other/bezier-rs-demos/src/components/SubpathDemo.ts b/website/other/bezier-rs-demos/src/components/SubpathDemo.ts deleted file mode 100644 index a780323868..0000000000 --- a/website/other/bezier-rs-demos/src/components/SubpathDemo.ts +++ /dev/null @@ -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; - - sliderUnits!: Record; - - 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; diff --git a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts b/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts deleted file mode 100644 index f37a495347..0000000000 --- a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts +++ /dev/null @@ -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; diff --git a/website/other/bezier-rs-demos/src/features/bezier-features.ts b/website/other/bezier-rs-demos/src/features-bezier.ts similarity index 95% rename from website/other/bezier-rs-demos/src/features/bezier-features.ts rename to website/other/bezier-rs-demos/src/features-bezier.ts index 82d0ae52c4..f9c6f5d83e 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features-bezier.ts @@ -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, diff --git a/website/other/bezier-rs-demos/src/features/subpath-features.ts b/website/other/bezier-rs-demos/src/features-subpath.ts similarity index 96% rename from website/other/bezier-rs-demos/src/features/subpath-features.ts rename to website/other/bezier-rs-demos/src/features-subpath.ts index b67d62f1cc..7c5371f727 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features-subpath.ts @@ -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, diff --git a/website/other/bezier-rs-demos/src/main.ts b/website/other/bezier-rs-demos/src/main.ts index d841010c72..d01826a0f4 100644 --- a/website/other/bezier-rs-demos/src/main.ts +++ b/website/other/bezier-rs-demos/src/main.ts @@ -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"; - -(async () => { - await init(); - - 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); - - window.addEventListener("hashchange", (e: Event) => { - const hashChangeEvent = e as HashChangeEvent; - const isOldHashSolo = isUrlSolo(hashChangeEvent.oldURL); - const isNewHashSolo = isUrlSolo(hashChangeEvent.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(); - } - }); - - renderExamples(); -})(); - -function renderBezierPane(featureName: BezierFeatureKey, container?: HTMLElement) { - const feature = bezierFeatures[featureName]; - const demo = document.createElement("bezier-demo-pane"); +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"; - 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"); +init().then(renderPage); - demo.setAttribute("name", featureName); - demo.setAttribute("inputOptions", JSON.stringify(feature.inputOptions || [])); - demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove)); - container?.append(demo); -} +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"; + }; -function isUrlSolo(url: string): boolean { - const hash = url.split("#")?.[1]; - const splitHash = hash?.split("/"); - return splitHash?.length === 3 && splitHash?.[2] === "solo"; -} + const isOldHashSolo = isUrlSolo(e.oldURL); + const isNewHashSolo = isUrlSolo(e.newURL); + const target = document.getElementById(window.location.hash.substring(1)); + if (!target || isOldHashSolo !== isNewHashSolo) renderPage(); + }); -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 = `
`; - 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)[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 = `
`; - 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)[key]; + if (value) container.append(subpathDemoGroup(key as SubpathFeatureKey, value)); + + return; + } + + window.document.body.innerHTML = `

Bezier-rs Interactive Documentation

This is the interactive documentation for the Bezier-rs 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.

-

Beziers

+

Beziers

-

Subpaths

+ +

Subpaths

`.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))); + + 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; + } - (Object.keys(bezierFeatures) as BezierFeatureKey[]).forEach((feature) => renderBezierPane(feature, bezierDemos)); - (Object.keys(subpathFeatures) as SubpathFeatureKey[]).forEach((feature) => renderSubpathPane(feature, subpathDemos)); + 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; } +} - // Scroll to specified hash if it exists - if (hash) { - const target = document.getElementById(hash.substring(1)); - if (target) { - 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(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 ` +

+ # + ${name} +

+ `.trim(); + })()} +
+ `.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", + ` +

${demo.title}

+
+
+ ${(() => + demo.inputOptions + .map((inputOption) => + ` +
+
+ ${inputOption.variable}: ${inputOption.inputType === "dropdown" ? "" : demo.sliderData[inputOption.variable]}${getSliderUnit(demo, inputOption.variable)} +
+ ${(() => { + if (inputOption.inputType !== "dropdown") return ""; + return ` + + `.trim(); + })()} + ${(() => { + if (inputOption.inputType !== "slider") return ""; + const ratio = (Number(inputOption.default) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0)); + return ` + + `.trim(); + })()} +
+ `.trim(), + ) + .join("\n"))()} +
+ `.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); + }); + } + }); } diff --git a/website/other/bezier-rs-demos/src/types.ts b/website/other/bezier-rs-demos/src/types.ts new file mode 100644 index 0000000000..dc96de89dc --- /dev/null +++ b/website/other/bezier-rs-demos/src/types.ts @@ -0,0 +1,241 @@ +import type * as WasmPkg from "@/../wasm/pkg"; + +type WasmRawInstance = typeof WasmPkg; +export type WasmBezierInstance = InstanceType; + +export type WasmSubpathInstance = InstanceType; +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; + sliderUnits: Record; + 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, mouseLocation?: [number, number]) => string; +export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record, 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 = { + 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 = { + 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, +}; diff --git a/website/other/bezier-rs-demos/src/utils/options.ts b/website/other/bezier-rs-demos/src/utils/options.ts deleted file mode 100644 index 36bc31d982..0000000000 --- a/website/other/bezier-rs-demos/src/utils/options.ts +++ /dev/null @@ -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, -}; diff --git a/website/other/bezier-rs-demos/src/utils/render.ts b/website/other/bezier-rs-demos/src/utils/render.ts deleted file mode 100644 index 680377a5ba..0000000000 --- a/website/other/bezier-rs-demos/src/utils/render.ts +++ /dev/null @@ -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); -} diff --git a/website/other/bezier-rs-demos/src/utils/types.ts b/website/other/bezier-rs-demos/src/utils/types.ts deleted file mode 100644 index ff2483dbc2..0000000000 --- a/website/other/bezier-rs-demos/src/utils/types.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type * as WasmPkg from "@/../wasm/pkg"; - -export type WasmRawInstance = typeof WasmPkg; -export type WasmBezierInstance = InstanceType; - -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; -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, mouseLocation?: [number, number]) => string; -export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record, 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 = { - 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 = { - 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; - sliderUnits: Record; - - 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;