Bezier-rs: Update interactive demo site UI/UX (#1085)

* wip - change demo iframe styling

* fix typo

* change tVariant select from radio buttons to dropdown

* Added iframe styling

* fix linting errors

* Integrated tvariant picker as input options

* Updated points in demos

* Lint

* Clean up CSS

---------

Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Rob Nadal 2023-03-21 01:23:02 -04:00 committed by Keavon Chambers
parent bfbabbc4dc
commit 834cb1a227
15 changed files with 402 additions and 297 deletions

View File

@ -250,16 +250,16 @@ impl Bezier {
/// The returned `t` values are with respect to the current bezier, not the provided parameter. /// The returned `t` values are with respect to the current bezier, not the provided parameter.
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments. /// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point. /// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
/// - `minimum_seperation` - The minimum difference between adjacent `t` values in sorted order /// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/intersect-cubic/solo" title="Intersections Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/intersect-cubic/solo" title="Intersections Demo"></iframe>
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<f64> { pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
// TODO: Consider using the `intersections_between_vectors_of_curves` helper function here // TODO: Consider using the `intersections_between_vectors_of_curves` helper function here
// Otherwise, use bounding box to determine intersections // Otherwise, use bounding box to determine intersections
let mut intersection_t_values = self.unfiltered_intersections(other, error); let mut intersection_t_values = self.unfiltered_intersections(other, error);
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| { intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPARATION_VALUE) { if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) {
accumulator.pop(); accumulator.pop();
} }
accumulator.push(*t); accumulator.push(*t);

View File

@ -20,7 +20,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Expects the following: /// Expects the following:
/// - `other`: a [Bezier] curve to check intersections against /// - `other`: a [Bezier] curve to check intersections against
/// - `error`: an optional f64 value to provide an error bound /// - `error`: an optional f64 value to provide an error bound
/// - `minimum_seperation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two. /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> { pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
@ -50,12 +50,12 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. /// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point.
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
/// - `minimum_seperation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
/// ///
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
pub fn self_intersections(&self, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<(usize, f64)> { pub fn self_intersections(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
let mut intersections_vec = Vec::new(); let mut intersections_vec = Vec::new();
let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
// TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection
@ -64,7 +64,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
self.iter().enumerate().skip(i + 1).for_each(|(j, curve)| { self.iter().enumerate().skip(i + 1).for_each(|(j, curve)| {
intersections_vec.extend( intersections_vec.extend(
curve curve
.intersections(&other, error, minimum_seperation) .intersections(&other, error, minimum_separation)
.iter() .iter()
.filter(|&value| value > &err && (1. - value) > err) .filter(|&value| value > &err && (1. - value) > err)
.map(|value| (j, *value)), .map(|value| (j, *value)),

View File

@ -1,7 +1,7 @@
import { WasmBezier } from "@/../wasm/pkg"; import { WasmBezier } from "@/../wasm/pkg";
import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features"; import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features";
import { renderDemo } from "@/utils/render"; import { renderDemo } from "@/utils/render";
import { getConstructorKey, getCurveType, BezierCallback, BezierCurveType, SliderOption, WasmBezierManipulatorKey, TVariant, Demo } from "@/utils/types"; import { getConstructorKey, getCurveType, BezierCallback, BezierCurveType, InputOption, WasmBezierManipulatorKey, Demo } from "@/utils/types";
const SELECTABLE_RANGE = 10; const SELECTABLE_RANGE = 10;
@ -20,12 +20,10 @@ class BezierDemo extends HTMLElement implements Demo {
key!: BezierFeatureKey; key!: BezierFeatureKey;
sliderOptions!: SliderOption[]; inputOptions!: InputOption[];
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
tVariant!: TVariant;
// Data // Data
bezier!: WasmBezier; bezier!: WasmBezier;
@ -39,33 +37,20 @@ class BezierDemo extends HTMLElement implements Demo {
sliderUnits!: Record<string, string | string[]>; sliderUnits!: Record<string, string | string[]>;
static get observedAttributes(): string[] {
return ["tvariant"];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
if (name === "tvariant" && oldValue) {
this.tVariant = (newValue || "Parametric") as TVariant;
const figure = this.querySelector("figure") as HTMLElement;
this.drawDemo(figure);
}
}
async connectedCallback(): Promise<void> { async connectedCallback(): Promise<void> {
this.title = this.getAttribute("title") || ""; this.title = this.getAttribute("title") || "";
this.points = JSON.parse(this.getAttribute("points") || "[]"); this.points = JSON.parse(this.getAttribute("points") || "[]");
this.key = this.getAttribute("key") as BezierFeatureKey; this.key = this.getAttribute("key") as BezierFeatureKey;
this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant;
this.callback = bezierFeatures[this.key].callback as BezierCallback; this.callback = bezierFeatures[this.key].callback as BezierCallback;
const curveType = getCurveType(this.points.length); const curveType = getCurveType(this.points.length);
this.manipulatorKeys = MANIPULATOR_KEYS_FROM_BEZIER_TYPE[curveType]; this.manipulatorKeys = MANIPULATOR_KEYS_FROM_BEZIER_TYPE[curveType];
this.activeIndex = undefined as number | undefined; this.activeIndex = undefined as number | undefined;
this.sliderData = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.default }))); this.sliderData = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.default })));
this.sliderUnits = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.unit }))); this.sliderUnits = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.unit })));
this.render(); this.render();
const figure = this.querySelector("figure") as HTMLElement; const figure = this.querySelector("figure") as HTMLElement;
@ -79,7 +64,7 @@ class BezierDemo extends HTMLElement implements Demo {
} }
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void { drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void {
figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation, this.tVariant); figure.innerHTML = this.callback(this.bezier, this.sliderData, mouseLocation);
} }
onMouseDown(event: MouseEvent): void { onMouseDown(event: MouseEvent): void {
@ -114,7 +99,7 @@ class BezierDemo extends HTMLElement implements Demo {
getSliderUnit(sliderValue: number, variable: string): string { getSliderUnit(sliderValue: number, variable: string): string {
const sliderUnit = this.sliderUnits[variable]; const sliderUnit = this.sliderUnits[variable];
return (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit) || ""; return (Array.isArray(sliderUnit) ? "" : sliderUnit) || "";
} }
} }

View File

@ -1,27 +1,27 @@
import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features"; import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features";
import { renderDemoPane } from "@/utils/render"; import { renderDemoPane } from "@/utils/render";
import { BezierCurveType, BEZIER_CURVE_TYPE, TVariant, BezierDemoOptions, SliderOption, Demo, DemoPane, BezierDemoArgs } from "@/utils/types"; import { BezierCurveType, BEZIER_CURVE_TYPE, BezierDemoOptions, InputOption, Demo, DemoPane, BezierDemoArgs } from "@/utils/types";
const demoDefaults = { const demoDefaults = {
Linear: { Linear: {
points: [ points: [
[30, 60], [55, 60],
[140, 120], [165, 120],
], ],
}, },
Quadratic: { Quadratic: {
points: [ points: [
[30, 50], [55, 50],
[140, 30], [165, 30],
[160, 170], [185, 170],
], ],
}, },
Cubic: { Cubic: {
points: [ points: [
[30, 30], [55, 30],
[60, 140], [85, 140],
[150, 30], [175, 30],
[160, 160], [185, 160],
], ],
}, },
}; };
@ -36,25 +36,19 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
chooseTVariant!: boolean;
// Data // Data
demos!: BezierDemoArgs[]; demos!: BezierDemoArgs[];
id!: string; id!: string;
tVariant!: TVariant;
connectedCallback(): void { connectedCallback(): void {
this.tVariant = "Parametric";
this.key = (this.getAttribute("name") || "") as BezierFeatureKey; this.key = (this.getAttribute("name") || "") as BezierFeatureKey;
this.id = `bezier/${this.key}`; this.id = `bezier/${this.key}`;
this.name = bezierFeatures[this.key].name; this.name = bezierFeatures[this.key].name;
this.demoOptions = JSON.parse(this.getAttribute("demoOptions") || "[]"); this.demoOptions = JSON.parse(this.getAttribute("demoOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.chooseTVariant = this.getAttribute("chooseTVariant") === "true";
// Use quadratic slider options as a default if sliders are not provided for the other curve types. // Use quadratic slider options as a default if sliders are not provided for the other curve types.
const defaultSliderOptions: SliderOption[] = this.demoOptions.Quadratic?.sliderOptions || []; const defaultSliderOptions: InputOption[] = this.demoOptions.Quadratic?.inputOptions || [];
this.demos = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => { this.demos = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => {
const givenData = this.demoOptions[curveType]; const givenData = this.demoOptions[curveType];
const defaultData = demoDefaults[curveType]; const defaultData = demoDefaults[curveType];
@ -62,7 +56,7 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
title: curveType, title: curveType,
disabled: givenData?.disabled || false, disabled: givenData?.disabled || false,
points: givenData?.customPoints || defaultData.points, points: givenData?.customPoints || defaultData.points,
sliderOptions: givenData?.sliderOptions || defaultSliderOptions, inputOptions: givenData?.inputOptions || defaultSliderOptions,
}; };
}); });
this.render(); this.render();
@ -77,9 +71,8 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
bezierDemo.setAttribute("title", demo.title); bezierDemo.setAttribute("title", demo.title);
bezierDemo.setAttribute("points", JSON.stringify(demo.points)); bezierDemo.setAttribute("points", JSON.stringify(demo.points));
bezierDemo.setAttribute("key", this.key); bezierDemo.setAttribute("key", this.key);
bezierDemo.setAttribute("sliderOptions", JSON.stringify(demo.sliderOptions)); bezierDemo.setAttribute("inputOptions", JSON.stringify(demo.inputOptions));
bezierDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove)); bezierDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
bezierDemo.setAttribute("tvariant", this.tVariant);
return bezierDemo; return bezierDemo;
} }
} }

View File

@ -2,7 +2,7 @@ import { WasmSubpath } from "@/../wasm/pkg";
import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features"; import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features";
import { renderDemo } from "@/utils/render"; import { renderDemo } from "@/utils/render";
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, SliderOption, TVariant } from "@/utils/types"; import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption } from "@/utils/types";
const SELECTABLE_RANGE = 10; const SELECTABLE_RANGE = 10;
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"]; const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
@ -17,12 +17,10 @@ class SubpathDemo extends HTMLElement {
closed!: boolean; closed!: boolean;
sliderOptions!: SliderOption[]; inputOptions!: InputOption[];
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
tVariant!: TVariant;
// Data // Data
subpath!: WasmSubpath; subpath!: WasmSubpath;
@ -36,30 +34,17 @@ class SubpathDemo extends HTMLElement {
sliderUnits!: Record<string, string | string[]>; sliderUnits!: Record<string, string | string[]>;
static get observedAttributes(): string[] {
return ["tvariant"];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
if (name === "tvariant" && oldValue) {
this.tVariant = (newValue || "Parametric") as TVariant;
const figure = this.querySelector("figure") as HTMLElement;
this.drawDemo(figure);
}
}
async connectedCallback(): Promise<void> { async connectedCallback(): Promise<void> {
this.title = this.getAttribute("title") || ""; this.title = this.getAttribute("title") || "";
this.triples = JSON.parse(this.getAttribute("triples") || "[]"); this.triples = JSON.parse(this.getAttribute("triples") || "[]");
this.key = this.getAttribute("key") as SubpathFeatureKey; this.key = this.getAttribute("key") as SubpathFeatureKey;
this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.closed = this.getAttribute("closed") === "true"; this.closed = this.getAttribute("closed") === "true";
this.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant;
this.callback = subpathFeatures[this.key].callback as SubpathCallback; this.callback = subpathFeatures[this.key].callback as SubpathCallback;
this.sliderData = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.default }))); this.sliderData = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.default })));
this.sliderUnits = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.unit }))); this.sliderUnits = Object.assign({}, ...this.inputOptions.map((s) => ({ [s.variable]: s.unit })));
this.render(); this.render();
const figure = this.querySelector("figure") as HTMLElement; const figure = this.querySelector("figure") as HTMLElement;
@ -73,7 +58,7 @@ class SubpathDemo extends HTMLElement {
} }
drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void { drawDemo(figure: HTMLElement, mouseLocation?: [number, number]): void {
figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation, this.tVariant); figure.innerHTML = this.callback(this.subpath, this.sliderData, mouseLocation);
} }
onMouseDown(event: MouseEvent): void { onMouseDown(event: MouseEvent): void {
@ -109,7 +94,7 @@ class SubpathDemo extends HTMLElement {
getSliderUnit(sliderValue: number, variable: string): string { getSliderUnit(sliderValue: number, variable: string): string {
const sliderUnit = this.sliderUnits[variable]; const sliderUnit = this.sliderUnits[variable];
return (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit) || ""; return (Array.isArray(sliderUnit) ? "" : sliderUnit) || "";
} }
} }

View File

@ -1,6 +1,6 @@
import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features"; import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features";
import { renderDemoPane } from "@/utils/render"; import { renderDemoPane } from "@/utils/render";
import { TVariant, Demo, DemoPane, SliderOption, SubpathDemoArgs } from "@/utils/types"; import { Demo, DemoPane, InputOption, SubpathDemoArgs } from "@/utils/types";
class SubpathDemoPane extends HTMLElement implements DemoPane { class SubpathDemoPane extends HTMLElement implements DemoPane {
// Props // Props
@ -8,52 +8,46 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
name!: string; name!: string;
sliderOptions!: SliderOption[]; inputOptions!: InputOption[];
triggerOnMouseMove!: boolean; triggerOnMouseMove!: boolean;
chooseTVariant!: boolean;
// Data // Data
demos!: SubpathDemoArgs[]; demos!: SubpathDemoArgs[];
id!: string; id!: string;
tVariant!: TVariant;
connectedCallback(): void { connectedCallback(): void {
this.demos = [ this.demos = [
{ {
title: "Open Subpath", title: "Open Subpath",
triples: [ triples: [
[[20, 20], undefined, [10, 90]], [[45, 20], undefined, [35, 90]],
[[150, 40], [60, 40], undefined], [[175, 40], [85, 40], undefined],
[[175, 175], undefined, undefined], [[200, 175], undefined, undefined],
[[100, 100], [40, 120], undefined], [[125, 100], [65, 120], undefined],
], ],
closed: false, closed: false,
}, },
{ {
title: "Closed Subpath", title: "Closed Subpath",
triples: [ triples: [
[[35, 125], undefined, [40, 40]], [[60, 125], undefined, [65, 40]],
[[130, 30], [120, 120], undefined], [[155, 30], [145, 120], undefined],
[ [
[145, 150], [170, 150],
[175, 90], [200, 90],
[70, 185], [95, 185],
], ],
], ],
closed: true, closed: true,
}, },
]; ];
this.tVariant = "Parametric";
this.key = (this.getAttribute("name") || "") as SubpathFeatureKey; this.key = (this.getAttribute("name") || "") as SubpathFeatureKey;
this.id = `subpath/${this.key}`; this.id = `subpath/${this.key}`;
this.name = subpathFeatures[this.key].name; this.name = subpathFeatures[this.key].name;
this.sliderOptions = JSON.parse(this.getAttribute("sliderOptions") || "[]"); this.inputOptions = JSON.parse(this.getAttribute("inputOptions") || "[]");
this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true"; this.triggerOnMouseMove = this.getAttribute("triggerOnMouseMove") === "true";
this.chooseTVariant = this.getAttribute("chooseTVariant") === "true";
this.render(); this.render();
} }
@ -68,9 +62,8 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
subpathDemo.setAttribute("triples", JSON.stringify(demo.triples)); subpathDemo.setAttribute("triples", JSON.stringify(demo.triples));
subpathDemo.setAttribute("closed", String(demo.closed)); subpathDemo.setAttribute("closed", String(demo.closed));
subpathDemo.setAttribute("key", this.key); subpathDemo.setAttribute("key", this.key);
subpathDemo.setAttribute("sliderOptions", JSON.stringify(this.sliderOptions)); subpathDemo.setAttribute("inputOptions", JSON.stringify(this.inputOptions));
subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove)); subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove));
subpathDemo.setAttribute("tvariant", this.tVariant);
return subpathDemo; return subpathDemo;
} }
} }

View File

@ -1,6 +1,6 @@
import { WasmBezier } from "@/../wasm/pkg"; import { WasmBezier } from "@/../wasm/pkg";
import { tSliderOptions, errorOptions, minimumSeparationOptions } from "@/utils/options"; import { tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@/utils/options";
import { TVariant, BezierDemoOptions, WasmBezierInstance, BezierCallback } from "@/utils/types"; import { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption, BEZIER_T_VALUE_VARIANTS } from "@/utils/types";
const bezierFeatures = { const bezierFeatures = {
constructor: { constructor: {
@ -26,7 +26,7 @@ const bezierFeatures = {
[120, 70], [120, 70],
[160, 170], [160, 170],
], ],
sliderOptions: [ inputOptions: [
{ {
min: 0.01, min: 0.01,
max: 0.99, max: 0.99,
@ -42,7 +42,7 @@ const bezierFeatures = {
[120, 70], [120, 70],
[160, 170], [160, 170],
], ],
sliderOptions: [ inputOptions: [
{ {
min: 0.01, min: 0.01,
max: 0.99, max: 0.99,
@ -67,20 +67,19 @@ const bezierFeatures = {
}, },
evaluate: { evaluate: {
name: "Evaluate", name: "Evaluate",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.evaluate(options.t, tVariant), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.evaluate(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], inputOptions: [bezierTValueVariantOptions, tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
"lookup-table": { "lookup-table": {
name: "Lookup Table", name: "Lookup Table",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.compute_lookup_table(options.steps), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.compute_lookup_table(options.steps),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ inputOptions: [
{ {
min: 2, min: 2,
max: 15, max: 15,
@ -118,53 +117,53 @@ const bezierFeatures = {
}, },
tangent: { tangent: {
name: "Tangent", name: "Tangent",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.tangent(options.t, tVariant), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.tangent(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], inputOptions: [bezierTValueVariantOptions, tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
normal: { normal: {
name: "Normal", name: "Normal",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.normal(options.t, tVariant), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.normal(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], inputOptions: [bezierTValueVariantOptions, tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
curvature: { curvature: {
name: "Curvature", name: "Curvature",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.curvature(options.t, tVariant), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.curvature(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: { demoOptions: {
Linear: { Linear: {
disabled: true, disabled: true,
}, },
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], inputOptions: [bezierTValueVariantOptions, tSliderOptions],
},
Cubic: {
inputOptions: [bezierTValueVariantOptions, { ...tSliderOptions, default: 0.7 }],
}, },
}, },
chooseTVariant: true,
}, },
split: { split: {
name: "Split", name: "Split",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.split(options.t, tVariant), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.split(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], inputOptions: [bezierTValueVariantOptions, tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
trim: { trim: {
name: "Trim", name: "Trim",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.trim(options.t1, options.t2, tVariant), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.trim(options.t1, options.t2, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ inputOptions: [
bezierTValueVariantOptions,
{ {
variable: "t1", variable: "t1",
min: 0, min: 0,
@ -182,7 +181,6 @@ const bezierFeatures = {
], ],
}, },
}, },
chooseTVariant: true,
}, },
project: { project: {
name: "Project", name: "Project",
@ -194,6 +192,9 @@ const bezierFeatures = {
name: "Local Extrema", name: "Local Extrema",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.local_extrema(), callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.local_extrema(),
demoOptions: { demoOptions: {
Linear: {
disabled: true,
},
Quadratic: { Quadratic: {
customPoints: [ customPoints: [
[40, 40], [40, 40],
@ -236,7 +237,7 @@ const bezierFeatures = {
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.offset(options.distance), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.offset(options.distance),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
min: -30, min: -30,
@ -253,7 +254,7 @@ const bezierFeatures = {
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.outline(options.distance), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.outline(options.distance),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
min: 0, min: 0,
@ -270,7 +271,7 @@ const bezierFeatures = {
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.graduated_outline(options.start_distance, options.end_distance), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.graduated_outline(options.start_distance, options.end_distance),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ inputOptions: [
{ {
variable: "start_distance", variable: "start_distance",
min: 0, min: 0,
@ -302,7 +303,7 @@ const bezierFeatures = {
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ inputOptions: [
{ {
variable: "distance1", variable: "distance1",
min: 0, min: 0,
@ -338,15 +339,13 @@ const bezierFeatures = {
arcs: { arcs: {
name: "Arcs", name: "Arcs",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.arcs(options.error, options.max_iterations, options.strategy), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.arcs(options.error, options.max_iterations, options.strategy),
demoOptions: ((): Omit<BezierDemoOptions, "Linear"> => { demoOptions: ((): BezierDemoOptions => {
const sliderOptions = [ const inputOptions: InputOption[] = [
{ {
variable: "strategy", variable: "strategy",
min: 0,
max: 2,
step: 1,
default: 0, default: 0,
unit: [": Automatic", ": FavorLargerArcs", ": FavorCorrectness"], inputType: "dropdown",
options: ["Automatic", "FavorLargerArcs", "FavorCorrectness"],
}, },
{ {
variable: "error", variable: "error",
@ -365,13 +364,16 @@ const bezierFeatures = {
]; ];
return { return {
Linear: {
disabled: true,
},
Quadratic: { Quadratic: {
customPoints: [ customPoints: [
[50, 50], [70, 40],
[85, 65], [180, 50],
[100, 100], [160, 150],
], ],
sliderOptions, inputOptions,
disabled: false, disabled: false,
}, },
Cubic: { Cubic: {
@ -381,7 +383,7 @@ const bezierFeatures = {
[30, 90], [30, 90],
[180, 160], [180, 160],
], ],
sliderOptions, inputOptions,
disabled: false, disabled: false,
}, },
}; };
@ -391,8 +393,8 @@ const bezierFeatures = {
name: "Intersect (Line Segment)", name: "Intersect (Line Segment)",
callback: (bezier: WasmBezierInstance): string => { callback: (bezier: WasmBezierInstance): string => {
const line = [ const line = [
[150, 150], [45, 30],
[20, 20], [195, 160],
]; ];
return bezier.intersect_line_segment(line); return bezier.intersect_line_segment(line);
}, },
@ -401,15 +403,15 @@ const bezierFeatures = {
name: "Intersect (Quadratic)", name: "Intersect (Quadratic)",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => { callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => {
const quadratic = [ const quadratic = [
[20, 80], [45, 80],
[180, 10], [205, 10],
[90, 120], [115, 120],
]; ];
return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_separation); return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_separation);
}, },
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [errorOptions, minimumSeparationOptions], inputOptions: [errorOptions, minimumSeparationOptions],
}, },
}, },
}, },
@ -417,16 +419,16 @@ const bezierFeatures = {
name: "Intersect (Cubic)", name: "Intersect (Cubic)",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => { callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => {
const cubic = [ const cubic = [
[40, 20], [65, 20],
[100, 40], [125, 40],
[40, 120], [65, 120],
[175, 140], [200, 140],
]; ];
return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_separation); return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_separation);
}, },
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [errorOptions, minimumSeparationOptions], inputOptions: [errorOptions, minimumSeparationOptions],
}, },
}, },
}, },
@ -434,10 +436,14 @@ const bezierFeatures = {
name: "Intersect (Self)", name: "Intersect (Self)",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.intersect_self(options.error), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.intersect_self(options.error),
demoOptions: { demoOptions: {
Linear: {
disabled: true,
},
Quadratic: { Quadratic: {
sliderOptions: [errorOptions], disabled: true,
}, },
Cubic: { Cubic: {
inputOptions: [errorOptions],
customPoints: [ customPoints: [
[160, 180], [160, 180],
[170, 10], [170, 10],
@ -451,8 +457,8 @@ const bezierFeatures = {
name: "Intersect (Rectangle)", name: "Intersect (Rectangle)",
callback: (bezier: WasmBezierInstance): string => callback: (bezier: WasmBezierInstance): string =>
bezier.intersect_rectangle([ bezier.intersect_rectangle([
[50, 50], [75, 50],
[150, 150], [175, 150],
]), ]),
}, },
rotate: { rotate: {
@ -460,7 +466,7 @@ const bezierFeatures = {
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.rotate(options.angle * Math.PI, 100, 100), callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.rotate(options.angle * Math.PI, 100, 100),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [ inputOptions: [
{ {
variable: "angle", variable: "angle",
min: 0, min: 0,
@ -475,13 +481,12 @@ const bezierFeatures = {
}, },
"de-casteljau-points": { "de-casteljau-points": {
name: "De Casteljau Points", name: "De Casteljau Points",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => bezier.de_casteljau_points(options.t, tVariant), callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.de_casteljau_points(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: { demoOptions: {
Quadratic: { Quadratic: {
sliderOptions: [tSliderOptions], inputOptions: [bezierTValueVariantOptions, tSliderOptions],
}, },
}, },
chooseTVariant: true,
}, },
join: { join: {
name: "Join", name: "Join",
@ -490,21 +495,21 @@ const bezierFeatures = {
let examplePoints = []; let examplePoints = [];
if (points.length === 2) { if (points.length === 2) {
examplePoints = [ examplePoints = [
[120, 155], [145, 155],
[40, 155], [65, 155],
]; ];
} else if (points.length === 3) { } else if (points.length === 3) {
examplePoints = [ examplePoints = [
[40, 150], [65, 150],
[95, 195], [120, 195],
[155, 145], [190, 145],
]; ];
} else { } else {
examplePoints = [ examplePoints = [
[140, 150], [165, 150],
[85, 110], [110, 110],
[65, 180], [90, 180],
[30, 140], [55, 140],
]; ];
} }
return bezier.join(examplePoints); return bezier.join(examplePoints);
@ -512,23 +517,23 @@ const bezierFeatures = {
demoOptions: { demoOptions: {
Linear: { Linear: {
customPoints: [ customPoints: [
[45, 40], [70, 40],
[130, 90], [155, 90],
], ],
}, },
Quadratic: { Quadratic: {
customPoints: [ customPoints: [
[153, 40], [185, 40],
[40, 20], [65, 20],
[75, 85], [100, 85],
], ],
}, },
Cubic: { Cubic: {
customPoints: [ customPoints: [
[20, 80], [45, 80],
[40, 20], [65, 20],
[90, 100], [115, 100],
[130, 55], [155, 55],
], ],
}, },
}, },
@ -541,6 +546,5 @@ export type BezierFeatureOptions = {
callback: BezierCallback; callback: BezierCallback;
demoOptions?: Partial<BezierDemoOptions>; demoOptions?: Partial<BezierDemoOptions>;
triggerOnMouseMove?: boolean; triggerOnMouseMove?: boolean;
chooseTVariant?: boolean;
}; };
export default bezierFeatures as Record<BezierFeatureKey, BezierFeatureOptions>; export default bezierFeatures as Record<BezierFeatureKey, BezierFeatureOptions>;

View File

@ -1,5 +1,5 @@
import { tSliderOptions, intersectionErrorOptions, minimumSeparationOptions } from "@/utils/options"; import { tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@/utils/options";
import { TVariant, SliderOption, SubpathCallback, WasmSubpathInstance } from "@/utils/types"; import { InputOption, SubpathCallback, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@/utils/types";
const subpathFeatures = { const subpathFeatures = {
constructor: { constructor: {
@ -8,9 +8,8 @@ const subpathFeatures = {
}, },
insert: { insert: {
name: "Insert", name: "Insert",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.insert(options.t, tVariant), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.insert(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
sliderOptions: [tSliderOptions], inputOptions: [subpathTValueVariantOptions, tSliderOptions],
chooseTVariant: true,
}, },
length: { length: {
name: "Length", name: "Length",
@ -18,9 +17,8 @@ const subpathFeatures = {
}, },
evaluate: { evaluate: {
name: "Evaluate", name: "Evaluate",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.evaluate(options.t, tVariant), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.evaluate(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
sliderOptions: [tSliderOptions], inputOptions: [subpathTValueVariantOptions, tSliderOptions],
chooseTVariant: true,
}, },
project: { project: {
name: "Project", name: "Project",
@ -30,15 +28,13 @@ const subpathFeatures = {
}, },
tangent: { tangent: {
name: "Tangent", name: "Tangent",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.tangent(options.t, tVariant), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.tangent(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
sliderOptions: [tSliderOptions], inputOptions: [subpathTValueVariantOptions, tSliderOptions],
chooseTVariant: true,
}, },
normal: { normal: {
name: "Normal", name: "Normal",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.normal(options.t, tVariant), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.normal(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
sliderOptions: [tSliderOptions], inputOptions: [subpathTValueVariantOptions, tSliderOptions],
chooseTVariant: true,
}, },
"local-extrema": { "local-extrema": {
name: "Local Extrema", name: "Local Extrema",
@ -57,67 +53,62 @@ const subpathFeatures = {
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.intersect_line_segment( subpath.intersect_line_segment(
[ [
[150, 150], [80, 30],
[20, 20], [210, 150],
], ],
options.error, options.error,
options.minimum_seperation options.minimum_separation
), ),
sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
}, },
"intersect-quadratic": { "intersect-quadratic": {
name: "Intersect (Quadratic Segment)", name: "Intersect (Quadratic Segment)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.intersect_quadratic_segment( subpath.intersect_quadratic_segment(
[ [
[20, 80], [25, 50],
[180, 10], [205, 10],
[90, 120], [135, 180],
], ],
options.error, options.error,
options.minimum_seperation options.minimum_separation
), ),
sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
}, },
"intersect-cubic": { "intersect-cubic": {
name: "Intersect (Cubic Segment)", name: "Intersect (Cubic Segment)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.intersect_cubic_segment( subpath.intersect_cubic_segment(
[ [
[40, 20], [65, 20],
[100, 40], [125, 40],
[40, 120], [65, 120],
[175, 140], [200, 140],
], ],
options.error, options.error,
options.minimum_seperation options.minimum_separation
), ),
sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
}, },
"self-intersect": { "self-intersect": {
name: "Self Intersect", name: "Self Intersect",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.self_intersections(options.error, options.minimum_seperation), callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.self_intersections(options.error, options.minimum_separation),
sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
}, },
split: { split: {
name: "Split", name: "Split",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.split(options.t, tVariant), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.split(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
sliderOptions: [tSliderOptions], inputOptions: [subpathTValueVariantOptions, tSliderOptions],
chooseTVariant: true,
}, },
trim: { trim: {
name: "Trim", name: "Trim",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, tVariant: TVariant): string => subpath.trim(options.tVariant1, options.tVariant2, tVariant), callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.trim(options.t1, options.t2, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
sliderOptions: [ inputOptions: [subpathTValueVariantOptions, { ...tSliderOptions, default: 0.2, variable: "t1" }, { ...tSliderOptions, variable: "t2" }],
{ ...tSliderOptions, default: 0.2, variable: "tVariant1" },
{ ...tSliderOptions, variable: "tVariant2" },
],
chooseTVariant: true,
}, },
offset: { offset: {
name: "Offset", name: "Offset",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance), callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance),
sliderOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
min: -25, min: -25,
@ -130,7 +121,7 @@ const subpathFeatures = {
outline: { outline: {
name: "Outline", name: "Outline",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance), callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance),
sliderOptions: [ inputOptions: [
{ {
variable: "distance", variable: "distance",
min: 0, min: 0,
@ -146,8 +137,7 @@ export type SubpathFeatureKey = keyof typeof subpathFeatures;
export type SubpathFeatureOptions = { export type SubpathFeatureOptions = {
name: string; name: string;
callback: SubpathCallback; callback: SubpathCallback;
sliderOptions?: SliderOption[]; inputOptions?: InputOption[];
triggerOnMouseMove?: boolean; triggerOnMouseMove?: boolean;
chooseTVariant?: boolean;
}; };
export default subpathFeatures as Record<SubpathFeatureKey, SubpathFeatureOptions>; export default subpathFeatures as Record<SubpathFeatureKey, SubpathFeatureOptions>;

View File

@ -18,6 +18,12 @@ declare global {
} }
window.document.title = "Bezier-rs Interactive Documentation"; window.document.title = "Bezier-rs Interactive Documentation";
window.document.head.innerHTML += `
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bona+Nova:wght@700&family=EB+Garamond:ital,wght@0,500;1,500&display=swap" rel="stylesheet">
`.trim();
window.customElements.define("bezier-demo", BezierDemo); window.customElements.define("bezier-demo", BezierDemo);
window.customElements.define("bezier-demo-pane", BezierDemoPane); window.customElements.define("bezier-demo-pane", BezierDemoPane);
@ -31,7 +37,6 @@ function renderBezierPane(featureName: BezierFeatureKey, container: HTMLElement
demo.setAttribute("name", featureName); demo.setAttribute("name", featureName);
demo.setAttribute("demoOptions", JSON.stringify(feature.demoOptions || {})); demo.setAttribute("demoOptions", JSON.stringify(feature.demoOptions || {}));
demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove)); demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove));
demo.setAttribute("chooseTVariant", String(feature.chooseTVariant));
container?.append(demo); container?.append(demo);
} }
@ -40,9 +45,8 @@ function renderSubpathPane(featureName: SubpathFeatureKey, container: HTMLElemen
const demo = document.createElement("subpath-demo-pane"); const demo = document.createElement("subpath-demo-pane");
demo.setAttribute("name", featureName); demo.setAttribute("name", featureName);
demo.setAttribute("sliderOptions", JSON.stringify(feature.sliderOptions || [])); demo.setAttribute("inputOptions", JSON.stringify(feature.inputOptions || []));
demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove)); demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove));
demo.setAttribute("chooseTVariant", String(feature.chooseTVariant));
container?.append(demo); container?.append(demo);
} }
@ -76,16 +80,16 @@ function renderExamples(): void {
renderSubpathPane(splitHash[1] as SubpathFeatureKey, document.getElementById("subpath-demos")); renderSubpathPane(splitHash[1] as SubpathFeatureKey, document.getElementById("subpath-demos"));
} else { } else {
window.document.body.innerHTML = ` window.document.body.innerHTML = `
<h1>Bezier-rs Interactive Documentation</h1> <h1 class="website-header">Bezier-rs Interactive Documentation</h1>
<p> <p class="website-description">
This is the interactive documentation for the <a href="https://crates.io/crates/bezier-rs"><b>Bezier-rs</b></a> library. View the This is the interactive documentation for the <a href="https://crates.io/crates/bezier-rs"><b>Bezier-rs</b></a> library. View the
<a href="https://docs.rs/bezier-rs/latest/bezier_rs">crate documentation</a> <a href="https://docs.rs/bezier-rs/latest/bezier_rs">crate documentation</a>
for detailed function descriptions and API usage. Click and drag on the endpoints of the demo curves to visualize the various Bezier utilities and functions. for detailed function descriptions and API usage. Click and drag on the endpoints of the demo curves to visualize the various Bezier utilities and functions.
</p> </p>
<h2>Beziers</h2> <h2 class="class-header">Beziers</h2>
<div id="bezier-demos"></div> <div id="bezier-demos"></div>
<h2>Subpaths</h2> <h2 class="class-header">Subpaths</h2>
<div id="subpath-demos"></div> <div id="subpath-demos"></div>
`.trim(); `.trim();

View File

@ -1,10 +1,34 @@
:root {
--color-navy: #16323f;
--color-gray: #cccccc;
--range-fill-dark: var(--color-navy);
--range-fill-light: var(--color-gray);
--range-thumb-height: 16px;
}
html, html,
body { body {
font-family: Arial, sans-serif; font-family: "Inter", sans-serif;
text-align: center; text-align: center;
background-color: white; background-color: white;
} }
.website-header {
color: var(--color-navy);
font-family: "Bona Nova", serif;
}
.website-description {
font-weight: 500;
color: var(--color-navy);
}
.class-header {
color: var(--color-navy);
font-family: "Bona Nova", serif;
margin-bottom: 0
}
body > h1 { body > h1 {
margin: 40px 0; margin: 40px 0;
} }
@ -41,6 +65,8 @@ body > h2 {
margin-top: 2em; margin-top: 2em;
margin-bottom: 0; margin-bottom: 0;
padding: 0 1em; padding: 0 1em;
font-family: "Bona Nova", serif;
color: var(--color-navy);
} }
.demo-pane-header a { .demo-pane-header a {
@ -64,17 +90,117 @@ body > h2 {
/* Demo styles */ /* Demo styles */
.demo-header { .demo-header {
margin: 20px 0; font-family: "Bona Nova", serif;
color: var(--color-navy);
margin-top: 10px;
margin-bottom: 5px;
} }
.demo-figure { .demo-figure {
width: 200px; width: 250px;
height: 200px; height: 200px;
margin-bottom: 20px; margin: 10px;
border: solid 1px black; border: solid 1px black;
} }
.parent-slider-container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
svg text { svg text {
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
/* Slider Styles */
.slider-container {
/* width: fit-content; */
width: 250px;
padding-bottom: 5px;
}
.slider-label {
font-family: monospace;
display: flex;
justify-content: left;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
margin-right: 15px;
width: 250px;
height: 7px;
background: rgba(255, 255, 255, 0.6);
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 */
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: var(--range-thumb-height);
width: var(--range-thumb-height);
border-radius: 50%;
background: var(--range-fill-dark);
box-shadow: 0 0 2px 0 #555;
transition: background .3s ease-in-out;
}
input[type="range"]::-moz-range-thumb {
-webkit-appearance: none;
appearance: none;
height: var(--range-thumb-height);
width: var(--range-thumb-height);
border-radius: 50%;
background: var(--range-fill-dark);
box-shadow: 0 0 2px 0 #555;
transition: background .3s ease-in-out;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--range-fill-dark);
}
input[type="range"]::-moz-range-thumb:hover {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--range-fill-dark);
}
/* Input Track */
input[type=range]::-webkit-slider-runnable-track {
-webkit-appearance: none;
box-shadow: none;
border: none;
background: transparent;
background: none;
}
input[type=range]::-moz-range-track {
-webkit-appearance: none;
appearance: none;
box-shadow: none;
border: none;
background: transparent;
background: none;
;
}
/* Select Styles */
select {
font-family: monospace;
}
.select-container {
width: 250px;
padding-bottom: 5px;
display: flex;
}
.select-input {
margin-left: 8px;
}

View File

@ -1,3 +1,5 @@
import { BEZIER_T_VALUE_VARIANTS, SUBPATH_T_VALUE_VARIANTS } from "@/utils/types";
export const tSliderOptions = { export const tSliderOptions = {
min: 0, min: 0,
max: 1, max: 1,
@ -15,7 +17,7 @@ export const errorOptions = {
}; };
export const minimumSeparationOptions = { export const minimumSeparationOptions = {
variable: "minimum_seperation", variable: "minimum_separation",
min: 0.001, min: 0.001,
max: 0.25, max: 0.25,
step: 0.001, step: 0.001,
@ -29,3 +31,17 @@ export const intersectionErrorOptions = {
step: 0.0025, step: 0.0025,
default: 0.02, default: 0.02,
}; };
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,
};

View File

@ -1,4 +1,4 @@
import { TVariant, Demo, DemoPane, SliderOption } from "@/utils/types"; import { Demo, DemoPane, InputOption } from "@/utils/types";
export function renderDemo(demo: Demo): void { export function renderDemo(demo: Demo): void {
const header = document.createElement("h4"); const header = document.createElement("h4");
@ -14,28 +14,72 @@ export function renderDemo(demo: Demo): void {
demo.append(header); demo.append(header);
demo.append(figure); demo.append(figure);
demo.sliderOptions.forEach((sliderOption: SliderOption) => { const parentSliderContainer = document.createElement("div");
const sliderLabel = document.createElement("div"); parentSliderContainer.className = "parent-slider-container";
const sliderData = demo.sliderData[sliderOption.variable];
const sliderUnit = demo.getSliderUnit(sliderData, sliderOption.variable);
sliderLabel.className = "slider-label";
sliderLabel.innerText = `${sliderOption.variable} = ${sliderData}${sliderUnit}`;
demo.append(sliderLabel);
const sliderInput = document.createElement("input"); demo.inputOptions.forEach((inputOption: InputOption) => {
sliderInput.className = "slider-input"; const isDropdown = inputOption.inputType === "dropdown";
sliderInput.type = "range";
sliderInput.max = String(sliderOption.max); const sliderContainer = document.createElement("div");
sliderInput.min = String(sliderOption.min); sliderContainer.className = isDropdown ? "select-container" : "slider-container";
sliderInput.step = String(sliderOption.step);
sliderInput.value = String(sliderOption.default); const sliderLabel = document.createElement("div");
sliderInput.addEventListener("input", (event: Event): void => { const sliderData = demo.sliderData[inputOption.variable];
demo.sliderData[sliderOption.variable] = Number((event.target as HTMLInputElement).value); const sliderUnit = demo.getSliderUnit(sliderData, inputOption.variable);
sliderLabel.innerText = `${sliderOption.variable} = ${demo.sliderData[sliderOption.variable]}${sliderUnit}`; sliderLabel.className = "slider-label";
demo.drawDemo(figure); sliderLabel.innerText = `${inputOption.variable}: ${isDropdown ? "" : sliderData}${sliderUnit}`;
}); sliderContainer.appendChild(sliderLabel);
demo.append(sliderInput);
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);
});
selectInput.addEventListener("change", (event: Event): void => {
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): void => {
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): void { export function renderDemoPane(demoPane: DemoPane): void {
@ -55,30 +99,6 @@ export function renderDemoPane(demoPane: DemoPane): void {
container.append(header); container.append(header);
} }
const tVariantContainer = document.createElement("div");
tVariantContainer.className = "t-variant-choice";
const tVariantLabel = document.createElement("strong");
tVariantLabel.innerText = "TValue Variant:";
tVariantContainer.append(tVariantLabel);
const radioInputs = ["Parametric", "Euclidean"].map((tVariant) => {
const id = `${demoPane.id}-${tVariant}`;
const radioInput = document.createElement("input");
radioInput.type = "radio";
radioInput.id = id;
radioInput.value = tVariant;
radioInput.name = `TVariant - ${demoPane.id}`;
radioInput.checked = tVariant === "Parametric";
tVariantContainer.append(radioInput);
const label = document.createElement("label");
label.htmlFor = id;
label.innerText = tVariant;
tVariantContainer.append(label);
return radioInput;
});
const demoRow = document.createElement("div"); const demoRow = document.createElement("div");
demoRow.className = "demo-row"; demoRow.className = "demo-row";
@ -87,21 +107,9 @@ export function renderDemoPane(demoPane: DemoPane): void {
return; return;
} }
const demoComponent = demoPane.buildDemo(demo); const demoComponent = demoPane.buildDemo(demo);
radioInputs.forEach((radioInput: HTMLElement) => {
radioInput.addEventListener("input", (event: Event): void => {
demoPane.tVariant = (event.target as HTMLInputElement).value as TVariant;
demoComponent.setAttribute("tvariant", demoPane.tVariant);
});
});
demoRow.append(demoComponent); demoRow.append(demoComponent);
}); });
container.append(demoRow); container.append(demoRow);
if (demoPane.chooseTVariant) {
container.append(tVariantContainer);
}
demoPane.append(container); demoPane.append(container);
} }

View File

@ -11,26 +11,26 @@ export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_ou
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const; export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number]; export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number];
export type TVariant = "Euclidean" | "Parametric"; 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 BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number], tVariant?: TVariant) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number], tVariant?: TVariant) => string;
export type BezierDemoOptions = { export type BezierDemoOptions = {
[key in BezierCurveType]: { [key in BezierCurveType]: {
disabled?: boolean; disabled?: boolean;
sliderOptions?: SliderOption[]; inputOptions?: InputOption[];
customPoints?: number[][]; customPoints?: number[][];
}; };
}; };
export type SliderOption = { export type InputOption = {
min: number;
max: number;
step: number;
default: number;
variable: string; variable: string;
min?: number;
max?: number;
step?: number;
default?: number;
unit?: string | string[]; unit?: string | string[];
inputType?: "slider" | "dropdown";
options?: string[];
}; };
export function getCurveType(numPoints: number): BezierCurveType { export function getCurveType(numPoints: number): BezierCurveType {
@ -61,7 +61,7 @@ export interface DemoArgs {
export interface BezierDemoArgs extends DemoArgs { export interface BezierDemoArgs extends DemoArgs {
points: number[][]; points: number[][];
sliderOptions: SliderOption[]; inputOptions: InputOption[];
} }
export interface SubpathDemoArgs extends DemoArgs { export interface SubpathDemoArgs extends DemoArgs {
@ -70,7 +70,7 @@ export interface SubpathDemoArgs extends DemoArgs {
} }
export interface Demo extends HTMLElement { export interface Demo extends HTMLElement {
sliderOptions: SliderOption[]; inputOptions: InputOption[];
sliderData: Record<string, number>; sliderData: Record<string, number>;
sliderUnits: Record<string, string | string[]>; sliderUnits: Record<string, string | string[]>;
@ -85,7 +85,8 @@ export interface DemoPane extends HTMLElement {
name: string; name: string;
demos: DemoArgs[]; demos: DemoArgs[];
id: string; id: string;
chooseTVariant: boolean;
tVariant: TVariant;
buildDemo(demo: DemoArgs): Demo; buildDemo(demo: DemoArgs): Demo;
} }
export const BEZIER_T_VALUE_VARIANTS = ["Parametric", "Euclidean"] as const;
export const SUBPATH_T_VALUE_VARIANTS = ["GlobalParametric", "GlobalEuclidean"] as const;

View File

@ -23,8 +23,8 @@ const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.;
fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue { fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue {
match t_variant.as_str() { match t_variant.as_str() {
"Parametric" => SubpathTValue::GlobalParametric(t), "GlobalParametric" => SubpathTValue::GlobalParametric(t),
"Euclidean" => SubpathTValue::GlobalEuclidean(t), "GlobalEuclidean" => SubpathTValue::GlobalEuclidean(t),
_ => panic!("Unexpected TValue string: '{}'", t_variant), _ => panic!("Unexpected TValue string: '{}'", t_variant),
} }
} }

View File

@ -1,7 +1,7 @@
use glam::DVec2; use glam::DVec2;
// SVG drawing constants // SVG drawing constants
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200px" height="200px">"#; pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="250px" height="200px">"#;
pub const SVG_CLOSE_TAG: &str = "</svg>"; pub const SVG_CLOSE_TAG: &str = "</svg>";
// Stylistic constants // Stylistic constants
@ -30,7 +30,7 @@ pub fn wrap_svg_tag(contents: String) -> String {
/// Helper function to create an SVG text entity. /// Helper function to create an SVG text entity.
pub fn draw_text(text: String, x_pos: f64, y_pos: f64, fill: &str) -> String { pub fn draw_text(text: String, x_pos: f64, y_pos: f64, fill: &str) -> String {
format!(r#"<text x="{x_pos}" y="{y_pos}" fill="{fill}">{text}</text>"#) format!(r#"<text x="{x_pos}" y="{y_pos}" fill="{fill}" font-family="monospace">{text}</text>"#)
} }
/// Helper function to create an SVG circle entity. /// Helper function to create an SVG circle entity.