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.
/// 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.
/// - `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>
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
// Otherwise, use bounding box to determine intersections
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.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.push(*t);

View File

@ -20,7 +20,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Expects the following:
/// - `other`: a [Bezier] curve to check intersections against
/// - `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.
/// <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)> {
@ -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.
/// - `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
///
/// **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>
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 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
@ -64,7 +64,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
self.iter().enumerate().skip(i + 1).for_each(|(j, curve)| {
intersections_vec.extend(
curve
.intersections(&other, error, minimum_seperation)
.intersections(&other, error, minimum_separation)
.iter()
.filter(|&value| value > &err && (1. - value) > err)
.map(|value| (j, *value)),

View File

@ -1,7 +1,7 @@
import { WasmBezier } from "@/../wasm/pkg";
import bezierFeatures, { BezierFeatureKey } from "@/features/bezier-features";
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;
@ -20,12 +20,10 @@ class BezierDemo extends HTMLElement implements Demo {
key!: BezierFeatureKey;
sliderOptions!: SliderOption[];
inputOptions!: InputOption[];
triggerOnMouseMove!: boolean;
tVariant!: TVariant;
// Data
bezier!: WasmBezier;
@ -39,33 +37,20 @@ class BezierDemo extends HTMLElement implements Demo {
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> {
this.title = this.getAttribute("title") || "";
this.points = JSON.parse(this.getAttribute("points") || "[]");
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.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant;
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.sliderOptions.map((s) => ({ [s.variable]: s.default })));
this.sliderUnits = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.unit })));
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;
@ -79,7 +64,7 @@ class BezierDemo extends HTMLElement implements Demo {
}
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 {
@ -114,7 +99,7 @@ class BezierDemo extends HTMLElement implements Demo {
getSliderUnit(sliderValue: number, variable: string): string {
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 { 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 = {
Linear: {
points: [
[30, 60],
[140, 120],
[55, 60],
[165, 120],
],
},
Quadratic: {
points: [
[30, 50],
[140, 30],
[160, 170],
[55, 50],
[165, 30],
[185, 170],
],
},
Cubic: {
points: [
[30, 30],
[60, 140],
[150, 30],
[160, 160],
[55, 30],
[85, 140],
[175, 30],
[185, 160],
],
},
};
@ -36,25 +36,19 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
triggerOnMouseMove!: boolean;
chooseTVariant!: boolean;
// Data
demos!: BezierDemoArgs[];
id!: string;
tVariant!: TVariant;
connectedCallback(): void {
this.tVariant = "Parametric";
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";
this.chooseTVariant = this.getAttribute("chooseTVariant") === "true";
// 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) => {
const givenData = this.demoOptions[curveType];
const defaultData = demoDefaults[curveType];
@ -62,7 +56,7 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
title: curveType,
disabled: givenData?.disabled || false,
points: givenData?.customPoints || defaultData.points,
sliderOptions: givenData?.sliderOptions || defaultSliderOptions,
inputOptions: givenData?.inputOptions || defaultSliderOptions,
};
});
this.render();
@ -77,9 +71,8 @@ class BezierDemoPane extends HTMLElement implements DemoPane {
bezierDemo.setAttribute("title", demo.title);
bezierDemo.setAttribute("points", JSON.stringify(demo.points));
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("tvariant", this.tVariant);
return bezierDemo;
}
}

View File

@ -2,7 +2,7 @@ import { WasmSubpath } from "@/../wasm/pkg";
import subpathFeatures, { SubpathFeatureKey } from "@/features/subpath-features";
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 POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
@ -17,12 +17,10 @@ class SubpathDemo extends HTMLElement {
closed!: boolean;
sliderOptions!: SliderOption[];
inputOptions!: InputOption[];
triggerOnMouseMove!: boolean;
tVariant!: TVariant;
// Data
subpath!: WasmSubpath;
@ -36,30 +34,17 @@ class SubpathDemo extends HTMLElement {
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> {
this.title = this.getAttribute("title") || "";
this.triples = JSON.parse(this.getAttribute("triples") || "[]");
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.closed = this.getAttribute("closed") === "true";
this.tVariant = (this.getAttribute("tvariant") || "Parametric") as TVariant;
this.callback = subpathFeatures[this.key].callback as SubpathCallback;
this.sliderData = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.default })));
this.sliderUnits = Object.assign({}, ...this.sliderOptions.map((s) => ({ [s.variable]: s.unit })));
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;
@ -73,7 +58,7 @@ class SubpathDemo extends HTMLElement {
}
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 {
@ -109,7 +94,7 @@ class SubpathDemo extends HTMLElement {
getSliderUnit(sliderValue: number, variable: string): string {
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 { 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 {
// Props
@ -8,52 +8,46 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
name!: string;
sliderOptions!: SliderOption[];
inputOptions!: InputOption[];
triggerOnMouseMove!: boolean;
chooseTVariant!: boolean;
// Data
demos!: SubpathDemoArgs[];
id!: string;
tVariant!: TVariant;
connectedCallback(): void {
this.demos = [
{
title: "Open Subpath",
triples: [
[[20, 20], undefined, [10, 90]],
[[150, 40], [60, 40], undefined],
[[175, 175], undefined, undefined],
[[100, 100], [40, 120], undefined],
[[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: [
[[35, 125], undefined, [40, 40]],
[[130, 30], [120, 120], undefined],
[[60, 125], undefined, [65, 40]],
[[155, 30], [145, 120], undefined],
[
[145, 150],
[175, 90],
[70, 185],
[170, 150],
[200, 90],
[95, 185],
],
],
closed: true,
},
];
this.tVariant = "Parametric";
this.key = (this.getAttribute("name") || "") as SubpathFeatureKey;
this.id = `subpath/${this.key}`;
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.chooseTVariant = this.getAttribute("chooseTVariant") === "true";
this.render();
}
@ -68,9 +62,8 @@ class SubpathDemoPane extends HTMLElement implements DemoPane {
subpathDemo.setAttribute("triples", JSON.stringify(demo.triples));
subpathDemo.setAttribute("closed", String(demo.closed));
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("tvariant", this.tVariant);
return subpathDemo;
}
}

View File

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

View File

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

View File

@ -18,6 +18,12 @@ declare global {
}
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-pane", BezierDemoPane);
@ -31,7 +37,6 @@ function renderBezierPane(featureName: BezierFeatureKey, container: HTMLElement
demo.setAttribute("name", featureName);
demo.setAttribute("demoOptions", JSON.stringify(feature.demoOptions || {}));
demo.setAttribute("triggerOnMouseMove", String(feature.triggerOnMouseMove));
demo.setAttribute("chooseTVariant", String(feature.chooseTVariant));
container?.append(demo);
}
@ -40,9 +45,8 @@ function renderSubpathPane(featureName: SubpathFeatureKey, container: HTMLElemen
const demo = document.createElement("subpath-demo-pane");
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("chooseTVariant", String(feature.chooseTVariant));
container?.append(demo);
}
@ -76,16 +80,16 @@ function renderExamples(): void {
renderSubpathPane(splitHash[1] as SubpathFeatureKey, document.getElementById("subpath-demos"));
} else {
window.document.body.innerHTML = `
<h1>Bezier-rs Interactive Documentation</h1>
<p>
<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"><b>Bezier-rs</b></a> library. View the
<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.
</p>
<h2>Beziers</h2>
<h2 class="class-header">Beziers</h2>
<div id="bezier-demos"></div>
<h2>Subpaths</h2>
<h2 class="class-header">Subpaths</h2>
<div id="subpath-demos"></div>
`.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,
body {
font-family: Arial, sans-serif;
font-family: "Inter", sans-serif;
text-align: center;
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 {
margin: 40px 0;
}
@ -41,6 +65,8 @@ body > h2 {
margin-top: 2em;
margin-bottom: 0;
padding: 0 1em;
font-family: "Bona Nova", serif;
color: var(--color-navy);
}
.demo-pane-header a {
@ -64,17 +90,117 @@ body > h2 {
/* Demo styles */
.demo-header {
margin: 20px 0;
font-family: "Bona Nova", serif;
color: var(--color-navy);
margin-top: 10px;
margin-bottom: 5px;
}
.demo-figure {
width: 200px;
width: 250px;
height: 200px;
margin-bottom: 20px;
margin: 10px;
border: solid 1px black;
}
.parent-slider-container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
svg text {
pointer-events: 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 = {
min: 0,
max: 1,
@ -15,7 +17,7 @@ export const errorOptions = {
};
export const minimumSeparationOptions = {
variable: "minimum_seperation",
variable: "minimum_separation",
min: 0.001,
max: 0.25,
step: 0.001,
@ -29,3 +31,17 @@ export const intersectionErrorOptions = {
step: 0.0025,
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 {
const header = document.createElement("h4");
@ -14,28 +14,72 @@ export function renderDemo(demo: Demo): void {
demo.append(header);
demo.append(figure);
demo.sliderOptions.forEach((sliderOption: SliderOption) => {
const sliderLabel = document.createElement("div");
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 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);
});
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(sliderOption.max);
sliderInput.min = String(sliderOption.min);
sliderInput.step = String(sliderOption.step);
sliderInput.value = String(sliderOption.default);
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 => {
demo.sliderData[sliderOption.variable] = Number((event.target as HTMLInputElement).value);
sliderLabel.innerText = `${sliderOption.variable} = ${demo.sliderData[sliderOption.variable]}${sliderUnit}`;
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);
});
demo.append(sliderInput);
sliderContainer.appendChild(sliderInput);
}
parentSliderContainer.append(sliderContainer);
});
demo.append(parentSliderContainer);
}
export function renderDemoPane(demoPane: DemoPane): void {
@ -55,30 +99,6 @@ export function renderDemoPane(demoPane: DemoPane): void {
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");
demoRow.className = "demo-row";
@ -87,21 +107,9 @@ export function renderDemoPane(demoPane: DemoPane): void {
return;
}
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);
});
container.append(demoRow);
if (demoPane.chooseTVariant) {
container.append(tVariantContainer);
}
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 type BezierCurveType = typeof BEZIER_CURVE_TYPE[number];
export type TVariant = "Euclidean" | "Parametric";
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 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;
sliderOptions?: SliderOption[];
inputOptions?: InputOption[];
customPoints?: number[][];
};
};
export type SliderOption = {
min: number;
max: number;
step: number;
default: number;
export type InputOption = {
variable: string;
min?: number;
max?: number;
step?: number;
default?: number;
unit?: string | string[];
inputType?: "slider" | "dropdown";
options?: string[];
};
export function getCurveType(numPoints: number): BezierCurveType {
@ -61,7 +61,7 @@ export interface DemoArgs {
export interface BezierDemoArgs extends DemoArgs {
points: number[][];
sliderOptions: SliderOption[];
inputOptions: InputOption[];
}
export interface SubpathDemoArgs extends DemoArgs {
@ -70,7 +70,7 @@ export interface SubpathDemoArgs extends DemoArgs {
}
export interface Demo extends HTMLElement {
sliderOptions: SliderOption[];
inputOptions: InputOption[];
sliderData: Record<string, number>;
sliderUnits: Record<string, string | string[]>;
@ -85,7 +85,8 @@ export interface DemoPane extends HTMLElement {
name: string;
demos: DemoArgs[];
id: string;
chooseTVariant: boolean;
tVariant: TVariant;
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 {
match t_variant.as_str() {
"Parametric" => SubpathTValue::GlobalParametric(t),
"Euclidean" => SubpathTValue::GlobalEuclidean(t),
"GlobalParametric" => SubpathTValue::GlobalParametric(t),
"GlobalEuclidean" => SubpathTValue::GlobalEuclidean(t),
_ => panic!("Unexpected TValue string: '{}'", t_variant),
}
}

View File

@ -1,7 +1,7 @@
use glam::DVec2;
// 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>";
// Stylistic constants
@ -30,7 +30,7 @@ pub fn wrap_svg_tag(contents: String) -> String {
/// Helper function to create an SVG text entity.
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.