import { default as init, WasmSubpath, WasmBezier } from "@/../wasm/pkg"; import bezierFeatures from "@/features-bezier"; import type { BezierFeatureKey, BezierFeatureOptions } from "@/features-bezier"; import subpathFeatures from "@/features-subpath"; import type { SubpathFeatureKey, SubpathFeatureOptions } from "@/features-subpath"; import type { DemoArgs, BezierCurveType, BezierDemoArgs, SubpathDemoArgs, DemoData, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption, DemoDataBezier, DemoDataSubpath } from "@/types"; import { BEZIER_CURVE_TYPE, getBezierDemoPointDefaults, getSubpathDemoArgs, POINT_INDEX_TO_MANIPULATOR, getConstructorKey, getCurveType, MANIPULATOR_KEYS_FROM_BEZIER_TYPE } from "@/types"; init().then(renderPage); function renderPage() { // Determine whether the page needs to recompute which examples to show window.addEventListener("hashchange", (e: HashChangeEvent) => { const isUrlSolo = (url: string) => { const splitHash = url.split("#")?.[1]?.split("/"); return splitHash?.length === 3 && splitHash?.[2] === "solo"; }; const isOldHashSolo = isUrlSolo(e.oldURL); const isNewHashSolo = isUrlSolo(e.newURL); const target = document.getElementById(window.location.hash.substring(1)); if (!target || isOldHashSolo !== isNewHashSolo) renderPage(); }); // Get the hash from the URL const hash = window.location.hash; const splitHash = hash.split("/"); // Scroll to specified hash if it exists if (hash) document.getElementById(hash.substring(1))?.scrollIntoView(); // Determine which examples to render based on hash if (splitHash[0] === "#bezier" && splitHash[1] in bezierFeatures && splitHash[2] === "solo") { window.document.body.innerHTML = `
`; const container = document.getElementById("bezier-demos"); if (!container) return; const key = splitHash[1]; const value = (bezierFeatures as Record)[key]; if (value) container.append(bezierDemoGroup(key as BezierFeatureKey, value)); return; } if (splitHash[0] === "#subpath" && splitHash[1] in subpathFeatures && splitHash[2] === "solo") { window.document.body.innerHTML = `
`; const container = document.getElementById("subpath-demos"); if (!container) return; const key = splitHash[1]; const value = (subpathFeatures as Record)[key]; if (value) container.append(subpathDemoGroup(key as SubpathFeatureKey, value)); return; } window.document.body.innerHTML = `

Bezier-rs Interactive Documentation

This is the interactive documentation for the Bezier-rs library. View the crate documentation for detailed function descriptions and API usage. Click and drag on the endpoints of the demo curves to visualize the various Bezier utilities and functions.

Beziers

Subpaths

`.trim(); const bezierDemos = document.getElementById("bezier-demos") || undefined; if (bezierDemos) Object.entries(bezierFeatures).forEach(([key, options]) => bezierDemos.appendChild(bezierDemoGroup(key as BezierFeatureKey, options))); const subpathDemos = document.getElementById("subpath-demos") || undefined; if (subpathDemos) Object.entries(subpathFeatures).forEach(([key, options]) => subpathDemos.appendChild(subpathDemoGroup(key as SubpathFeatureKey, options))); } function bezierDemoGroup(key: BezierFeatureKey, options: BezierFeatureOptions): HTMLDivElement { const demoOptions = options.demoOptions || {}; const demos: BezierDemoArgs[] = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => ({ title: curveType, disabled: demoOptions[curveType]?.disabled || false, points: demoOptions[curveType]?.customPoints || getBezierDemoPointDefaults()[curveType], inputOptions: demoOptions[curveType]?.inputOptions || demoOptions.Quadratic?.inputOptions || [], })); return renderDemoGroup(`bezier/${key}`, bezierFeatures[key].name, demos, (demo: BezierDemoArgs) => demoBezier(demo.title, demo.points, key, demo.inputOptions, options.triggerOnMouseMove || false), ); } function subpathDemoGroup(key: SubpathFeatureKey, options: SubpathFeatureOptions): HTMLDivElement { const buildDemo = (demo: SubpathDemoArgs) => { const newInputOptions = (options.inputOptions || []).map((option) => ({ ...option, disabled: option.isDisabledForClosed && demo.closed, })); return demoSubpath(demo.title, demo.triples, key, demo.closed, newInputOptions, options.triggerOnMouseMove || false); }; return renderDemoGroup(`subpath/${key}`, subpathFeatures[key].name, getSubpathDemoArgs(), buildDemo); } function demoBezier(title: string, points: number[][], key: BezierFeatureKey, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataBezier { return { kind: "bezier", title, element: document.createElement("div"), inputOptions, locked: false, triggerOnMouseMove, sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))), sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))), activePointIndex: undefined as number | undefined, manipulatorKeys: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[getCurveType(points.length)], bezier: WasmBezier[getConstructorKey(getCurveType(points.length))](points), points, callback: bezierFeatures[key].callback, }; } function demoSubpath(title: string, triples: (number[] | undefined)[][], key: SubpathFeatureKey, closed: boolean, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataSubpath { return { kind: "subpath", title, element: document.createElement("div"), inputOptions, locked: false, triggerOnMouseMove, sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))), sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))), activePointIndex: undefined as number | undefined, activeManipulatorIndex: undefined as number | undefined, manipulatorKeys: undefined as undefined | WasmSubpathManipulatorKey[], subpath: WasmSubpath.from_triples(triples, closed) as WasmSubpathInstance, triples, callback: subpathFeatures[key].callback, }; } function updateDemoSVG(data: DemoData, figure: HTMLElement, mouseLocation?: [number, number]) { if (data.kind === "subpath") figure.innerHTML = data.callback(data.subpath, data.sliderData, mouseLocation); if (data.kind === "bezier") figure.innerHTML = data.callback(data.bezier, data.sliderData, mouseLocation); } function onMouseDown(data: DemoData, e: MouseEvent) { const SELECTABLE_RANGE = 10; let distances; if (data.kind === "bezier") { distances = data.points.flatMap((point, pointIndex) => { if (!point) return []; const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2)); return distance < SELECTABLE_RANGE ? [{ manipulatorIndex: undefined, pointIndex, distance }] : []; }); } else if (data.kind === "subpath") { distances = data.triples.flatMap((triple, manipulatorIndex) => triple.flatMap((point, pointIndex) => { if (!point) return []; const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2)); return distance < SELECTABLE_RANGE ? [{ manipulatorIndex, pointIndex, distance }] : []; }), ); } else { return; } const closest = distances.sort((a, b) => a.distance - b.distance)[0]; if (closest) { if (data.kind === "subpath") data.activeManipulatorIndex = closest.manipulatorIndex; data.activePointIndex = closest.pointIndex; } } function onMouseMove(data: DemoData, e: MouseEvent) { if (data.locked || !(e.currentTarget instanceof HTMLElement)) return; data.locked = true; if (data.kind === "bezier" && data.activePointIndex !== undefined) { data.bezier[data.manipulatorKeys[data.activePointIndex]](e.offsetX, e.offsetY); data.points[data.activePointIndex] = [e.offsetX, e.offsetY]; updateDemoSVG(data, e.currentTarget); } else if (data.kind === "subpath" && data.activePointIndex !== undefined && data.activeManipulatorIndex !== undefined) { data.subpath[POINT_INDEX_TO_MANIPULATOR[data.activePointIndex]](data.activeManipulatorIndex, e.offsetX, e.offsetY); data.triples[data.activeManipulatorIndex][data.activePointIndex] = [e.offsetX, e.offsetY]; updateDemoSVG(data, e.currentTarget); } else if (data.triggerOnMouseMove) { updateDemoSVG(data, e.currentTarget, [e.offsetX, e.offsetY]); } data.locked = false; } function onMouseUp(data: DemoData) { data.activePointIndex = undefined; if (data.kind === "subpath") data.activeManipulatorIndex = undefined; } function renderDemoGroup(id: string, name: string, demos: T[], buildDemo: (demo: T) => DemoData): HTMLDivElement { const demoGroup = document.createElement("div"); demoGroup.className = "demo-group-container"; demoGroup.insertAdjacentHTML( "beforeend", ` ${(() => { // Add header and href anchor if not on a solo example page const currentHash = window.location.hash.split("/"); if (currentHash.length === 3 || currentHash[2] === "solo") return ""; return `

# ${name}

`.trim(); })()}
`.trim(), ); const demoRow = demoGroup.querySelector("[data-demo-row]"); if (!demoRow) return demoGroup; demos.forEach((demo) => { if (demo.disabled) return; const data = buildDemo(demo); renderDemo(data); const figure = data.element.querySelector("[data-demo-figure]"); if (figure instanceof HTMLElement) updateDemoSVG(data, figure); demoRow.append(data.element); }); return demoGroup; } function renderDemo(demo: DemoData) { const getSliderUnit = (data: DemoData, variable: string): string => { return (Array.isArray(data.sliderUnits[variable]) ? "" : data.sliderUnits[variable]) || ""; }; demo.element.insertAdjacentHTML( "beforeend", `

${demo.title}

${(() => demo.inputOptions .map((inputOption) => `
${inputOption.variable}: ${inputOption.inputType === "dropdown" ? "" : demo.sliderData[inputOption.variable]}${getSliderUnit(demo, inputOption.variable)}
${(() => { if (inputOption.inputType !== "dropdown") return ""; return ` `.trim(); })()} ${(() => { if (inputOption.inputType !== "slider") return ""; const ratio = (Number(inputOption.default) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0)); return ` `.trim(); })()}
`.trim(), ) .join("\n"))()}
`.trim(), ); const figure = demo.element.querySelector(`[data-demo-figure]`); if (!(figure instanceof HTMLElement)) return; figure.addEventListener("mousedown", (e) => onMouseDown(demo, e)); figure.addEventListener("mouseup", () => onMouseUp(demo)); figure.addEventListener("mousemove", (e) => onMouseMove(demo, e)); demo.inputOptions.forEach((inputOption, index) => { const inputContainer = demo.element.querySelectorAll(`[data-parent-input-container] [data-input-container]`)[index]; if (!(inputContainer instanceof HTMLDivElement)) return; if (inputOption.inputType === "dropdown") { const selectElement = inputContainer.querySelector("[data-select]"); if (!(selectElement instanceof HTMLSelectElement)) return; selectElement.addEventListener("change", (e: Event) => { if (!(e.target instanceof HTMLSelectElement)) return; demo.sliderData[inputOption.variable] = Number(e.target.value); updateDemoSVG(demo, figure); }); } if (inputOption.inputType === "slider") { const sliderInput = inputContainer.querySelector("[data-slider-input]"); if (!(sliderInput instanceof HTMLInputElement)) return; sliderInput.addEventListener("input", (e: Event) => { const target = e.target; if (!(target instanceof HTMLInputElement)) return; // Set the slider label text const variable = inputOption.variable; const data = demo.sliderData[variable]; const unit = getSliderUnit(demo, variable); const label = inputContainer.querySelector("[data-input-label]"); if (!(label instanceof HTMLDivElement)) return; label.innerText = `${variable}: ${data}${unit}`; // Set the slider input range percentage sliderInput.style.setProperty("--range-ratio", String((Number(target.value) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0)))); // Update the slider data and redraw the demo demo.sliderData[variable] = Number(target.value); updateDemoSVG(demo, figure); }); } }); }