mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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:
parent
e6d8c4743d
commit
a2465f40b3
12 changed files with 598 additions and 755 deletions
|
@ -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 */
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import { WasmBezier } from "@/../wasm/pkg";
|
||||
import type { BezierFeatureKey } from "@/features/bezier-features";
|
||||
import bezierFeatures from "@/features/bezier-features";
|
||||
import { renderDemo } from "@/utils/render";
|
||||
import type { BezierCallback, BezierCurveType, InputOption, WasmBezierManipulatorKey, Demo } from "@/utils/types";
|
||||
import { getConstructorKey, getCurveType } from "@/utils/types";
|
||||
|
||||
const SELECTABLE_RANGE = 10;
|
||||
|
||||
// Given the number of points in the curve, map the index of a point to the correct manipulator key
|
||||
const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [key in BezierCurveType]: WasmBezierManipulatorKey[] } = {
|
||||
Linear: ["set_start", "set_end"],
|
||||
Quadratic: ["set_start", "set_handle_start", "set_end"],
|
||||
Cubic: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
|
||||
};
|
||||
|
||||
class BezierDemo extends HTMLElement implements Demo {
|
||||
// Props
|
||||
title!: string;
|
||||
|
||||
points!: number[][];
|
||||
|
||||
key!: BezierFeatureKey;
|
||||
|
||||
inputOptions!: InputOption[];
|
||||
|
||||
triggerOnMouseMove!: boolean;
|
||||
|
||||
// Data
|
||||
bezier!: WasmBezier;
|
||||
|
||||
callback!: BezierCallback;
|
||||
|
||||
manipulatorKeys!: WasmBezierManipulatorKey[];
|
||||
|
||||
activeIndex!: number | undefined;
|
||||
|
||||
sliderData!: Record<string, number>;
|
||||
|
||||
sliderUnits!: Record<string, string | string[]>;
|
||||
|
||||
// Avoids "recursive use of an object detected which would lead to unsafe aliasing in rust" error when moving mouse fast.
|
||||
locked!: boolean;
|
||||
|
||||
async connectedCallback() {
|
||||
this.title = this.getAttribute("title") || "";
|
||||
this.points = JSON.parse(this.getAttribute("points") || "[]");
|
||||
this.key = this.getAttribute("key") as BezierFeatureKey;
|
||||
this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
|
||||
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
|
||||
|
||||
this.callback = bezierFeatures[this.key].callback as BezierCallback;
|
||||
const curveType = getCurveType(this.points.length);
|
||||
|
||||
this.manipulatorKeys = MANIPULATOR_KEYS_FROM_BEZIER_TYPE[curveType];
|
||||
this.activeIndex = undefined as number | undefined;
|
||||
this.sliderData = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.default })));
|
||||
this.sliderUnits = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.unit })));
|
||||
this.render();
|
||||
|
||||
const figure = this.querySelector("figure") as HTMLElement;
|
||||
this.bezier = WasmBezier[getConstructorKey(curveType)](this.points);
|
||||
this.drawDemo(figure);
|
||||
}
|
||||
|
||||
render() {
|
||||
renderDemo(this);
|
||||
}
|
||||
|
||||
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]) {
|
||||
figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation);
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent) {
|
||||
const mx = event.offsetX;
|
||||
const my = event.offsetY;
|
||||
for (let pointIndex = 0; pointIndex < this.points.length; pointIndex += 1) {
|
||||
const point = this.points[pointIndex];
|
||||
if (point && Math.abs(mx - point[0]) < SELECTABLE_RANGE && Math.abs(my - point[1]) < SELECTABLE_RANGE) {
|
||||
this.activeIndex = pointIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
this.activeIndex = undefined;
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
if (this.locked) return;
|
||||
this.locked = true;
|
||||
const mx = event.offsetX;
|
||||
const my = event.offsetY;
|
||||
const figure = event.currentTarget as HTMLElement;
|
||||
|
||||
if (this.activeIndex !== undefined) {
|
||||
this.bezier[this.manipulatorKeys[this.activeIndex]](mx, my);
|
||||
this.points[this.activeIndex] = [mx, my];
|
||||
this.drawDemo(figure);
|
||||
} else if (this.triggerOnMouseMove) {
|
||||
this.drawDemo(figure, [mx, my]);
|
||||
}
|
||||
this.locked = false;
|
||||
}
|
||||
|
||||
getSliderUnit(sliderValue: number, variable: string): string {
|
||||
const _ = sliderValue;
|
||||
const sliderUnit = this.sliderUnits[variable];
|
||||
return (Array.isArray(sliderUnit) ? "" : sliderUnit) || "";
|
||||
}
|
||||
}
|
||||
|
||||
export default BezierDemo;
|
|
@ -1,82 +0,0 @@
|
|||
import type { BezierFeatureKey } from "@/features/bezier-features";
|
||||
import bezierFeatures from "@/features/bezier-features";
|
||||
import { renderDemoPane } from "@/utils/render";
|
||||
import type { BezierCurveType, BezierDemoOptions, InputOption, DemoPane, BezierDemoArgs } from "@/utils/types";
|
||||
import { BEZIER_CURVE_TYPE } from "@/utils/types";
|
||||
|
||||
const demoDefaults = {
|
||||
Linear: {
|
||||
points: [
|
||||
[55, 60],
|
||||
[165, 120],
|
||||
],
|
||||
},
|
||||
Quadratic: {
|
||||
points: [
|
||||
[55, 50],
|
||||
[165, 30],
|
||||
[185, 170],
|
||||
],
|
||||
},
|
||||
Cubic: {
|
||||
points: [
|
||||
[55, 30],
|
||||
[85, 140],
|
||||
[175, 30],
|
||||
[185, 160],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
class BezierDemoPane extends HTMLElement implements DemoPane {
|
||||
// Props
|
||||
key!: BezierFeatureKey;
|
||||
|
||||
name!: string;
|
||||
|
||||
demoOptions!: BezierDemoOptions;
|
||||
|
||||
triggerOnMouseMove!: boolean;
|
||||
|
||||
// Data
|
||||
demos!: BezierDemoArgs[];
|
||||
|
||||
id!: string;
|
||||
|
||||
connectedCallback() {
|
||||
this.key = (this.getAttribute("name") || "") as BezierFeatureKey;
|
||||
this.id = `bezier/${this.key}`;
|
||||
this.name = bezierFeatures[this.key].name;
|
||||
this.demoOptions = JSON.parse(this.getAttribute("demoOptions") || "[]");
|
||||
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
|
||||
// Use quadratic slider options as a default if sliders are not provided for the other curve types.
|
||||
const defaultSliderOptions: InputOption[] = this.demoOptions.Quadratic?.inputOptions || [];
|
||||
this.demos = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => {
|
||||
const givenData = this.demoOptions[curveType];
|
||||
const defaultData = demoDefaults[curveType];
|
||||
return {
|
||||
title: curveType,
|
||||
disabled: givenData?.disabled || false,
|
||||
points: givenData?.customPoints || defaultData.points,
|
||||
inputOptions: givenData?.inputOptions || defaultSliderOptions,
|
||||
};
|
||||
});
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
renderDemoPane(this);
|
||||
}
|
||||
|
||||
buildDemo(demo: BezierDemoArgs): HTMLElement {
|
||||
const bezierDemo = document.createElement("bezier-demo");
|
||||
bezierDemo.setAttribute("title", demo.title);
|
||||
bezierDemo.setAttribute("points", JSON.stringify(demo.points));
|
||||
bezierDemo.setAttribute("key", this.key);
|
||||
bezierDemo.setAttribute("inputOptions", JSON.stringify(demo.inputOptions));
|
||||
bezierDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
|
||||
return bezierDemo;
|
||||
}
|
||||
}
|
||||
|
||||
export default BezierDemoPane;
|
|
@ -1,101 +0,0 @@
|
|||
import { WasmSubpath } from "@/../wasm/pkg";
|
||||
import type { SubpathFeatureKey } from "@/features/subpath-features";
|
||||
import subpathFeatures from "@/features/subpath-features";
|
||||
import { renderDemo } from "@/utils/render";
|
||||
import type { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption } from "@/utils/types";
|
||||
|
||||
const SELECTABLE_RANGE = 10;
|
||||
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
|
||||
|
||||
class SubpathDemo extends HTMLElement {
|
||||
// Props
|
||||
title!: string;
|
||||
|
||||
triples!: (number[] | undefined)[][];
|
||||
|
||||
key!: SubpathFeatureKey;
|
||||
|
||||
closed!: boolean;
|
||||
|
||||
inputOptions!: InputOption[];
|
||||
|
||||
triggerOnMouseMove!: boolean;
|
||||
|
||||
// Data
|
||||
subpath!: WasmSubpath;
|
||||
|
||||
callback!: SubpathCallback;
|
||||
|
||||
manipulatorKeys!: WasmSubpathManipulatorKey[];
|
||||
|
||||
activeIndex!: number[] | undefined;
|
||||
|
||||
sliderData!: Record<string, number>;
|
||||
|
||||
sliderUnits!: Record<string, string | string[]>;
|
||||
|
||||
async connectedCallback() {
|
||||
this.title = this.getAttribute("title") || "";
|
||||
this.triples = JSON.parse(this.getAttribute("triples") || "[]");
|
||||
this.key = this.getAttribute("key") as SubpathFeatureKey;
|
||||
this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
|
||||
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
|
||||
this.closed = this.getAttribute("closed") === "true";
|
||||
|
||||
this.callback = subpathFeatures[this.key].callback as SubpathCallback;
|
||||
this.sliderData = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.default })));
|
||||
this.sliderUnits = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.unit })));
|
||||
this.render();
|
||||
|
||||
const figure = this.querySelector("figure") as HTMLElement;
|
||||
this.subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
|
||||
this.drawDemo(figure);
|
||||
}
|
||||
|
||||
render() {
|
||||
renderDemo(this);
|
||||
}
|
||||
|
||||
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]) {
|
||||
figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation);
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent) {
|
||||
const mx = event.offsetX;
|
||||
const my = event.offsetY;
|
||||
for (let controllerIndex = 0; controllerIndex < this.triples.length; controllerIndex += 1) {
|
||||
for (let pointIndex = 0; pointIndex < 3; pointIndex += 1) {
|
||||
const point = this.triples[controllerIndex][pointIndex];
|
||||
if (point && Math.abs(mx - point[0]) < SELECTABLE_RANGE && Math.abs(my - point[1]) < SELECTABLE_RANGE) {
|
||||
this.activeIndex = [controllerIndex, pointIndex];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
this.activeIndex = undefined;
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
const mx = event.offsetX;
|
||||
const my = event.offsetY;
|
||||
const figure = event.currentTarget as HTMLElement;
|
||||
if (this.activeIndex) {
|
||||
this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my);
|
||||
this.triples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my];
|
||||
this.drawDemo(figure);
|
||||
} else if (this.triggerOnMouseMove) {
|
||||
this.drawDemo(figure, [mx, my]);
|
||||
}
|
||||
}
|
||||
|
||||
getSliderUnit(sliderValue: number, variable: string): string {
|
||||
const _ = sliderValue;
|
||||
const sliderUnit = this.sliderUnits[variable];
|
||||
return (Array.isArray(sliderUnit) ? "" : sliderUnit) || "";
|
||||
}
|
||||
}
|
||||
|
||||
export default SubpathDemo;
|
|
@ -1,77 +0,0 @@
|
|||
import type { SubpathFeatureKey } from "@/features/subpath-features";
|
||||
import subpathFeatures from "@/features/subpath-features";
|
||||
import { renderDemoPane } from "@/utils/render";
|
||||
import type { DemoPane, SubpathDemoArgs, SubpathInputOption } from "@/utils/types";
|
||||
|
||||
class SubpathDemoPane extends HTMLElement implements DemoPane {
|
||||
// Props
|
||||
key!: SubpathFeatureKey;
|
||||
|
||||
name!: string;
|
||||
|
||||
inputOptions!: SubpathInputOption[];
|
||||
|
||||
triggerOnMouseMove!: boolean;
|
||||
|
||||
// Data
|
||||
demos!: SubpathDemoArgs[];
|
||||
|
||||
id!: string;
|
||||
|
||||
connectedCallback() {
|
||||
this.demos = [
|
||||
{
|
||||
title: "Open Subpath",
|
||||
triples: [
|
||||
[[45, 20], undefined, [35, 90]],
|
||||
[[175, 40], [85, 40], undefined],
|
||||
[[200, 175], undefined, undefined],
|
||||
[[125, 100], [65, 120], undefined],
|
||||
],
|
||||
closed: false,
|
||||
},
|
||||
{
|
||||
title: "Closed Subpath",
|
||||
triples: [
|
||||
[[60, 125], undefined, [65, 40]],
|
||||
[[155, 30], [145, 120], undefined],
|
||||
[
|
||||
[170, 150],
|
||||
[200, 90],
|
||||
[95, 185],
|
||||
],
|
||||
],
|
||||
closed: true,
|
||||
},
|
||||
];
|
||||
this.key = (this.getAttribute("name") || "") as SubpathFeatureKey;
|
||||
this.id = `subpath/${this.key}`;
|
||||
this.name = subpathFeatures[this.key].name;
|
||||
this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
|
||||
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
renderDemoPane(this);
|
||||
}
|
||||
|
||||
buildDemo(demo: SubpathDemoArgs): HTMLElement {
|
||||
const subpathDemo = document.createElement("subpath-demo");
|
||||
subpathDemo.setAttribute("title", demo.title);
|
||||
subpathDemo.setAttribute("triples", JSON.stringify(demo.triples));
|
||||
subpathDemo.setAttribute("closed", String(demo.closed));
|
||||
subpathDemo.setAttribute("key", this.key);
|
||||
|
||||
const inputOptions = this.inputOptions.map((option) => ({
|
||||
...option,
|
||||
disabled: option.isDisabledForClosed && demo.closed,
|
||||
}));
|
||||
subpathDemo.setAttribute("inputOptions", JSON.stringify(inputOptions));
|
||||
subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
|
||||
return subpathDemo;
|
||||
}
|
||||
}
|
||||
|
||||
export default SubpathDemoPane;
|
|
@ -1,7 +1,6 @@
|
|||
import { WasmBezier } from "@/../wasm/pkg";
|
||||
import { 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,
|
|
@ -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,
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
241
website/other/bezier-rs-demos/src/types.ts
Normal file
241
website/other/bezier-rs-demos/src/types.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
import type * as WasmPkg from "@/../wasm/pkg";
|
||||
|
||||
type WasmRawInstance = typeof WasmPkg;
|
||||
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
|
||||
|
||||
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
|
||||
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
|
||||
type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
|
||||
type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
|
||||
|
||||
type DemoDataCommon = {
|
||||
title: string;
|
||||
element: HTMLDivElement;
|
||||
inputOptions: InputOption[];
|
||||
locked: boolean;
|
||||
triggerOnMouseMove: boolean;
|
||||
sliderData: Record<string, number>;
|
||||
sliderUnits: Record<string, string | string[]>;
|
||||
activePointIndex: number | undefined;
|
||||
};
|
||||
export type DemoDataBezier = DemoDataCommon & {
|
||||
kind: "bezier";
|
||||
manipulatorKeys: WasmBezierManipulatorKey[];
|
||||
bezier: WasmBezierInstance;
|
||||
points: number[][];
|
||||
callback: BezierCallback;
|
||||
};
|
||||
export type DemoDataSubpath = DemoDataCommon & {
|
||||
kind: "subpath";
|
||||
activeManipulatorIndex: number | undefined;
|
||||
manipulatorKeys: WasmSubpathManipulatorKey[] | undefined;
|
||||
subpath: WasmSubpathInstance;
|
||||
triples: (number[] | undefined)[][];
|
||||
callback: SubpathCallback;
|
||||
};
|
||||
export type DemoData = DemoDataBezier | DemoDataSubpath;
|
||||
|
||||
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
|
||||
export type BezierCurveType = (typeof BEZIER_CURVE_TYPE)[number];
|
||||
|
||||
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
||||
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
||||
|
||||
export type BezierDemoOptions = {
|
||||
[key in BezierCurveType]: {
|
||||
disabled?: boolean;
|
||||
inputOptions?: InputOption[];
|
||||
customPoints?: number[][];
|
||||
};
|
||||
};
|
||||
|
||||
export type InputOption = {
|
||||
variable: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
default?: number;
|
||||
unit?: string | string[];
|
||||
inputType?: "slider" | "dropdown";
|
||||
options?: string[];
|
||||
disabled?: boolean;
|
||||
};
|
||||
export type SubpathInputOption = InputOption & {
|
||||
isDisabledForClosed?: boolean;
|
||||
};
|
||||
|
||||
export function getCurveType(numPoints: number): BezierCurveType {
|
||||
const mapping: Record<number, BezierCurveType> = {
|
||||
2: "Linear",
|
||||
3: "Quadratic",
|
||||
4: "Cubic",
|
||||
};
|
||||
|
||||
if (!(numPoints in mapping)) throw new Error("Invalid number of points for a bezier");
|
||||
|
||||
return mapping[numPoints];
|
||||
}
|
||||
|
||||
export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey {
|
||||
const mapping: Record<BezierCurveType, WasmBezierConstructorKey> = {
|
||||
Linear: "new_linear",
|
||||
Quadratic: "new_quadratic",
|
||||
Cubic: "new_cubic",
|
||||
};
|
||||
return mapping[bezierCurveType];
|
||||
}
|
||||
|
||||
export type DemoArgs = {
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type BezierDemoArgs = {
|
||||
points: number[][];
|
||||
inputOptions: InputOption[];
|
||||
} & DemoArgs;
|
||||
|
||||
export type SubpathDemoArgs = {
|
||||
triples: (number[] | undefined)[][];
|
||||
closed: boolean;
|
||||
} & DemoArgs;
|
||||
|
||||
export const BEZIER_T_VALUE_VARIANTS = ["Parametric", "Euclidean"] as const;
|
||||
export const SUBPATH_T_VALUE_VARIANTS = ["GlobalParametric", "GlobalEuclidean"] as const;
|
||||
|
||||
const CAP_VARIANTS = ["Butt", "Round", "Square"] as const;
|
||||
const JOIN_VARIANTS = ["Bevel", "Miter", "Round"] as const;
|
||||
|
||||
export const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
|
||||
|
||||
// Given the number of points in the curve, map the index of a point to the correct manipulator key
|
||||
export const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [key in BezierCurveType]: WasmBezierManipulatorKey[] } = {
|
||||
Linear: ["set_start", "set_end"],
|
||||
Quadratic: ["set_start", "set_handle_start", "set_end"],
|
||||
Cubic: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
|
||||
};
|
||||
|
||||
export function getBezierDemoPointDefaults() {
|
||||
// We use a function to generate a new object each time it is called
|
||||
// to prevent one instance from being shared and modified across demos
|
||||
return {
|
||||
Linear: [
|
||||
[55, 60],
|
||||
[165, 120],
|
||||
],
|
||||
Quadratic: [
|
||||
[55, 50],
|
||||
[165, 30],
|
||||
[185, 170],
|
||||
],
|
||||
Cubic: [
|
||||
[55, 30],
|
||||
[85, 140],
|
||||
[175, 30],
|
||||
[185, 160],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function getSubpathDemoArgs(): SubpathDemoArgs[] {
|
||||
// We use a function to generate a new object each time it is called
|
||||
// to prevent one instance from being shared and modified across demos
|
||||
return [
|
||||
{
|
||||
title: "Open Subpath",
|
||||
triples: [
|
||||
[[45, 20], undefined, [35, 90]],
|
||||
[[175, 40], [85, 40], undefined],
|
||||
[[200, 175], undefined, undefined],
|
||||
[[125, 100], [65, 120], undefined],
|
||||
],
|
||||
closed: false,
|
||||
},
|
||||
{
|
||||
title: "Closed Subpath",
|
||||
triples: [
|
||||
[[60, 125], undefined, [65, 40]],
|
||||
[[155, 30], [145, 120], undefined],
|
||||
[
|
||||
[170, 150],
|
||||
[200, 90],
|
||||
[95, 185],
|
||||
],
|
||||
],
|
||||
closed: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const tSliderOptions = {
|
||||
variable: "t",
|
||||
inputType: "slider",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
default: 0.5,
|
||||
};
|
||||
|
||||
export const errorOptions = {
|
||||
variable: "error",
|
||||
inputType: "slider",
|
||||
min: 0.1,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
default: 0.5,
|
||||
};
|
||||
|
||||
export const minimumSeparationOptions = {
|
||||
variable: "minimum_separation",
|
||||
inputType: "slider",
|
||||
min: 0.001,
|
||||
max: 0.25,
|
||||
step: 0.001,
|
||||
default: 0.05,
|
||||
};
|
||||
|
||||
export const intersectionErrorOptions = {
|
||||
variable: "error",
|
||||
inputType: "slider",
|
||||
min: 0.001,
|
||||
max: 0.525,
|
||||
step: 0.0025,
|
||||
default: 0.02,
|
||||
};
|
||||
|
||||
export const separationDiskDiameter = {
|
||||
variable: "separation_disk_diameter",
|
||||
inputType: "slider",
|
||||
min: 2.5,
|
||||
max: 25,
|
||||
step: 0.1,
|
||||
default: 5,
|
||||
};
|
||||
|
||||
export const bezierTValueVariantOptions = {
|
||||
variable: "TVariant",
|
||||
inputType: "dropdown",
|
||||
default: 0,
|
||||
options: BEZIER_T_VALUE_VARIANTS,
|
||||
};
|
||||
|
||||
export const subpathTValueVariantOptions = {
|
||||
variable: "TVariant",
|
||||
inputType: "dropdown",
|
||||
default: 0,
|
||||
options: SUBPATH_T_VALUE_VARIANTS,
|
||||
};
|
||||
|
||||
export const joinOptions = {
|
||||
variable: "join",
|
||||
inputType: "dropdown",
|
||||
default: 0,
|
||||
options: JOIN_VARIANTS,
|
||||
};
|
||||
|
||||
export const capOptions = {
|
||||
variable: "cap",
|
||||
inputType: "dropdown",
|
||||
default: 0,
|
||||
options: CAP_VARIANTS,
|
||||
};
|
|
@ -1,69 +0,0 @@
|
|||
import { BEZIER_T_VALUE_VARIANTS, CAP_VARIANTS, JOIN_VARIANTS, SUBPATH_T_VALUE_VARIANTS } from "@/utils/types";
|
||||
|
||||
export const tSliderOptions = {
|
||||
variable: "t",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
default: 0.5,
|
||||
};
|
||||
|
||||
export const errorOptions = {
|
||||
variable: "error",
|
||||
min: 0.1,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
default: 0.5,
|
||||
};
|
||||
|
||||
export const minimumSeparationOptions = {
|
||||
variable: "minimum_separation",
|
||||
min: 0.001,
|
||||
max: 0.25,
|
||||
step: 0.001,
|
||||
default: 0.05,
|
||||
};
|
||||
|
||||
export const intersectionErrorOptions = {
|
||||
variable: "error",
|
||||
min: 0.001,
|
||||
max: 0.525,
|
||||
step: 0.0025,
|
||||
default: 0.02,
|
||||
};
|
||||
|
||||
export const separationDiskDiameter = {
|
||||
variable: "separation_disk_diameter",
|
||||
min: 2.5,
|
||||
max: 25,
|
||||
step: 0.1,
|
||||
default: 5,
|
||||
};
|
||||
|
||||
export const bezierTValueVariantOptions = {
|
||||
variable: "TVariant",
|
||||
default: 0,
|
||||
inputType: "dropdown",
|
||||
options: BEZIER_T_VALUE_VARIANTS,
|
||||
};
|
||||
|
||||
export const subpathTValueVariantOptions = {
|
||||
variable: "TVariant",
|
||||
default: 0,
|
||||
inputType: "dropdown",
|
||||
options: SUBPATH_T_VALUE_VARIANTS,
|
||||
};
|
||||
|
||||
export const joinOptions = {
|
||||
variable: "join",
|
||||
default: 0,
|
||||
inputType: "dropdown",
|
||||
options: JOIN_VARIANTS,
|
||||
};
|
||||
|
||||
export const capOptions = {
|
||||
variable: "cap",
|
||||
default: 0,
|
||||
inputType: "dropdown",
|
||||
options: CAP_VARIANTS,
|
||||
};
|
|
@ -1,119 +0,0 @@
|
|||
import type { Demo, DemoPane, InputOption } from "@/utils/types";
|
||||
|
||||
export function renderDemo(demo: Demo) {
|
||||
const header = document.createElement("h4");
|
||||
header.className = "demo-header";
|
||||
header.innerText = demo.title;
|
||||
|
||||
const figure = document.createElement("figure");
|
||||
figure.className = "demo-figure";
|
||||
figure.addEventListener("mousedown", demo.onMouseDown.bind(demo));
|
||||
figure.addEventListener("mouseup", demo.onMouseUp.bind(demo));
|
||||
figure.addEventListener("mousemove", demo.onMouseMove.bind(demo));
|
||||
|
||||
demo.append(header);
|
||||
demo.append(figure);
|
||||
|
||||
const parentSliderContainer = document.createElement("div");
|
||||
parentSliderContainer.className = "parent-slider-container";
|
||||
|
||||
demo.inputOptions.forEach((inputOption: InputOption) => {
|
||||
const isDropdown = inputOption.inputType === "dropdown";
|
||||
|
||||
const sliderContainer = document.createElement("div");
|
||||
sliderContainer.className = isDropdown ? "select-container" : "slider-container";
|
||||
|
||||
const sliderLabel = document.createElement("div");
|
||||
const sliderData = demo.sliderData[inputOption.variable];
|
||||
const sliderUnit = demo.getSliderUnit(sliderData, inputOption.variable);
|
||||
sliderLabel.className = "slider-label";
|
||||
sliderLabel.innerText = `${inputOption.variable}: ${isDropdown ? "" : sliderData}${sliderUnit}`;
|
||||
sliderContainer.appendChild(sliderLabel);
|
||||
|
||||
if (isDropdown) {
|
||||
const selectInput = document.createElement("select");
|
||||
selectInput.className = "select-input";
|
||||
selectInput.value = String(inputOption.default);
|
||||
inputOption.options?.forEach((value, idx) => {
|
||||
const id = `${idx}-${value}`;
|
||||
const option = document.createElement("option");
|
||||
option.value = String(idx);
|
||||
option.id = id;
|
||||
option.text = value;
|
||||
selectInput.append(option);
|
||||
});
|
||||
|
||||
if (inputOption.disabled) {
|
||||
selectInput.disabled = true;
|
||||
}
|
||||
|
||||
selectInput.addEventListener("change", (event: Event) => {
|
||||
demo.sliderData[inputOption.variable] = Number((event.target as HTMLInputElement).value);
|
||||
demo.drawDemo(figure);
|
||||
});
|
||||
sliderContainer.appendChild(selectInput);
|
||||
} else {
|
||||
const sliderInput = document.createElement("input");
|
||||
sliderInput.className = "slider-input";
|
||||
sliderInput.type = "range";
|
||||
sliderInput.max = String(inputOption.max);
|
||||
sliderInput.min = String(inputOption.min);
|
||||
sliderInput.step = String(inputOption.step);
|
||||
sliderInput.value = String(inputOption.default);
|
||||
const range = Number(inputOption.max) - Number(inputOption.min);
|
||||
|
||||
const ratio = (Number(inputOption.default) - Number(inputOption.min)) / range;
|
||||
sliderInput.style.setProperty("--range-ratio", String(ratio));
|
||||
|
||||
sliderInput.addEventListener("input", (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
demo.sliderData[inputOption.variable] = Number(target.value);
|
||||
const data = demo.sliderData[inputOption.variable];
|
||||
const unit = demo.getSliderUnit(demo.sliderData[inputOption.variable], inputOption.variable);
|
||||
sliderLabel.innerText = `${inputOption.variable}: ${data}${unit}`;
|
||||
|
||||
const ratio = (Number(target.value) - Number(inputOption.min)) / range;
|
||||
sliderInput.style.setProperty("--range-ratio", String(ratio));
|
||||
|
||||
demo.drawDemo(figure);
|
||||
});
|
||||
sliderContainer.appendChild(sliderInput);
|
||||
}
|
||||
|
||||
parentSliderContainer.append(sliderContainer);
|
||||
});
|
||||
|
||||
demo.append(parentSliderContainer);
|
||||
}
|
||||
|
||||
export function renderDemoPane(demoPane: DemoPane) {
|
||||
const container = document.createElement("div");
|
||||
container.className = "demo-pane-container";
|
||||
|
||||
const headerAnchorLink = document.createElement("a");
|
||||
headerAnchorLink.innerText = "#";
|
||||
const currentHash = window.location.hash.split("/");
|
||||
// Add header and href anchor if not on a solo example page
|
||||
if (currentHash.length !== 3 && currentHash[2] !== "solo") {
|
||||
headerAnchorLink.href = `#${demoPane.id}`;
|
||||
const header = document.createElement("h3");
|
||||
header.innerText = demoPane.name;
|
||||
header.className = "demo-pane-header";
|
||||
header.append(headerAnchorLink);
|
||||
container.append(header);
|
||||
}
|
||||
|
||||
const demoRow = document.createElement("div");
|
||||
demoRow.className = "demo-row";
|
||||
|
||||
demoPane.demos.forEach((demo) => {
|
||||
if (demo.disabled) {
|
||||
return;
|
||||
}
|
||||
const demoComponent = demoPane.buildDemo(demo);
|
||||
demoRow.append(demoComponent);
|
||||
});
|
||||
|
||||
container.append(demoRow);
|
||||
demoPane.append(container);
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
import type * as WasmPkg from "@/../wasm/pkg";
|
||||
|
||||
export type WasmRawInstance = typeof WasmPkg;
|
||||
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
|
||||
|
||||
export type WasmBezierKey = keyof WasmBezierInstance;
|
||||
export type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
|
||||
export type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
|
||||
|
||||
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
|
||||
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
|
||||
|
||||
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
|
||||
export type BezierCurveType = (typeof BEZIER_CURVE_TYPE)[number];
|
||||
|
||||
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
||||
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
||||
|
||||
export type BezierDemoOptions = {
|
||||
[key in BezierCurveType]: {
|
||||
disabled?: boolean;
|
||||
inputOptions?: InputOption[];
|
||||
customPoints?: number[][];
|
||||
};
|
||||
};
|
||||
|
||||
export type SubpathInputOption = InputOption & {
|
||||
isDisabledForClosed?: boolean;
|
||||
};
|
||||
|
||||
export type InputOption = {
|
||||
variable: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
default?: number;
|
||||
unit?: string | string[];
|
||||
inputType?: "slider" | "dropdown";
|
||||
options?: string[];
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function getCurveType(numPoints: number): BezierCurveType {
|
||||
const mapping: Record<number, BezierCurveType> = {
|
||||
2: "Linear",
|
||||
3: "Quadratic",
|
||||
4: "Cubic",
|
||||
};
|
||||
|
||||
if (!(numPoints in mapping)) throw new Error("Invalid number of points for a bezier");
|
||||
|
||||
return mapping[numPoints];
|
||||
}
|
||||
|
||||
export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey {
|
||||
const mapping: Record<BezierCurveType, WasmBezierConstructorKey> = {
|
||||
Linear: "new_linear",
|
||||
Quadratic: "new_quadratic",
|
||||
Cubic: "new_cubic",
|
||||
};
|
||||
return mapping[bezierCurveType];
|
||||
}
|
||||
|
||||
export type DemoArgs = {
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type BezierDemoArgs = {
|
||||
points: number[][];
|
||||
inputOptions: InputOption[];
|
||||
} & DemoArgs;
|
||||
|
||||
export type SubpathDemoArgs = {
|
||||
triples: (number[] | undefined)[][];
|
||||
closed: boolean;
|
||||
} & DemoArgs;
|
||||
|
||||
export type Demo = {
|
||||
inputOptions: InputOption[];
|
||||
sliderData: Record<string, number>;
|
||||
sliderUnits: Record<string, string | string[]>;
|
||||
|
||||
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void;
|
||||
onMouseDown(event: MouseEvent): void;
|
||||
onMouseUp(): void;
|
||||
onMouseMove(event: MouseEvent): void;
|
||||
getSliderUnit(sliderValue: number, variable: string): string;
|
||||
} & HTMLElement;
|
||||
|
||||
export type DemoPane = {
|
||||
name: string;
|
||||
demos: DemoArgs[];
|
||||
id: string;
|
||||
buildDemo(demo: DemoArgs): HTMLElement;
|
||||
} & HTMLElement;
|
||||
|
||||
export const BEZIER_T_VALUE_VARIANTS = ["Parametric", "Euclidean"] as const;
|
||||
export const SUBPATH_T_VALUE_VARIANTS = ["GlobalParametric", "GlobalEuclidean"] as const;
|
||||
|
||||
export const CAP_VARIANTS = ["Butt", "Round", "Square"] as const;
|
||||
export const JOIN_VARIANTS = ["Bevel", "Miter", "Round"] as const;
|
Loading…
Add table
Add a link
Reference in a new issue