mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-08 06:58:11 +08:00
Move bezier-rs into libraries folder and deploy its interactive docs
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
<template>
|
||||
<div class="App">
|
||||
<h1>Bezier-rs Interactive Documentation</h1>
|
||||
<p>This is the interactive documentation for the <b>bezier-rs</b> library. Click and drag on the endpoints of the example curves to visualize the various Bezier utilities and functions.</p>
|
||||
<h2>Beziers</h2>
|
||||
<div v-for="(feature, index) in bezierFeatures" :key="index">
|
||||
<ExamplePane
|
||||
:template="feature.template"
|
||||
:templateOptions="feature.templateOptions"
|
||||
:name="feature.name"
|
||||
:callback="feature.callback"
|
||||
:createThroughPoints="feature.createThroughPoints"
|
||||
:curveDegrees="feature.curveDegrees"
|
||||
:customPoints="feature.customPoints"
|
||||
:customOptions="feature.customOptions"
|
||||
/>
|
||||
</div>
|
||||
<h2>Subpaths</h2>
|
||||
<div v-for="(feature, index) in subpathFeatures" :key="index">
|
||||
<SubpathExamplePane :name="feature.name" :callback="feature.callback" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from "vue";
|
||||
|
||||
import { drawBezier, drawBezierHelper, drawCircle, drawCircleSector, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
|
||||
import { BezierCurveType, CircleSector, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
|
||||
|
||||
import ExamplePane from "@/components/ExamplePane.vue";
|
||||
import SliderExample from "@/components/SliderExample.vue";
|
||||
import SubpathExamplePane from "@/components/SubpathExamplePane.vue";
|
||||
|
||||
const tSliderOptions = {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
default: 0.5,
|
||||
variable: "t",
|
||||
};
|
||||
|
||||
const SCALE_UNIT_VECTOR_FACTOR = 50;
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
bezierFeatures: [
|
||||
{
|
||||
name: "Constructor",
|
||||
// eslint-disable-next-line
|
||||
callback: (): void => {},
|
||||
},
|
||||
{
|
||||
name: "Bezier Through Points",
|
||||
// eslint-disable-next-line
|
||||
callback: (): void => {},
|
||||
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
|
||||
createThroughPoints: true,
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
min: 0.01,
|
||||
max: 0.99,
|
||||
step: 0.01,
|
||||
default: 0.5,
|
||||
variable: "t",
|
||||
},
|
||||
],
|
||||
},
|
||||
customOptions: {
|
||||
[BezierCurveType.Cubic]: {
|
||||
sliders: [
|
||||
{
|
||||
min: 0.01,
|
||||
max: 0.99,
|
||||
step: 0.01,
|
||||
default: 0.5,
|
||||
variable: "t",
|
||||
},
|
||||
{
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 2,
|
||||
default: 30,
|
||||
variable: "midpoint separation",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
customPoints: {
|
||||
[BezierCurveType.Quadratic]: [
|
||||
[30, 50],
|
||||
[120, 70],
|
||||
[160, 170],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Length",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
drawText(getContextFromCanvas(canvas), `Length: ${bezier.length().toFixed(2)}`, 5, canvas.height - 7);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Evaluate",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const point = JSON.parse(bezier.evaluate(options.t));
|
||||
drawPoint(getContextFromCanvas(canvas), point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
name: "Lookup Table",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const lookupPoints: Point[] = JSON.parse(bezier.compute_lookup_table(options.steps));
|
||||
lookupPoints.forEach((point, index) => {
|
||||
if (index !== 0 && index !== lookupPoints.length - 1) {
|
||||
drawPoint(getContextFromCanvas(canvas), point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
}
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
min: 2,
|
||||
max: 15,
|
||||
step: 1,
|
||||
default: 5,
|
||||
variable: "steps",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Derivative",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
|
||||
const derivativeBezier = bezier.derivative();
|
||||
if (derivativeBezier) {
|
||||
const points: Point[] = JSON.parse(derivativeBezier.get_points());
|
||||
if (points.length === 2) {
|
||||
drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
} else {
|
||||
drawBezier(context, points, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
|
||||
}
|
||||
}
|
||||
},
|
||||
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
|
||||
customPoints: {
|
||||
[BezierCurveType.Quadratic]: [
|
||||
[30, 40],
|
||||
[110, 50],
|
||||
[120, 130],
|
||||
],
|
||||
[BezierCurveType.Cubic]: [
|
||||
[50, 50],
|
||||
[60, 100],
|
||||
[100, 140],
|
||||
[140, 150],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Tangent",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
|
||||
const intersection = JSON.parse(bezier.evaluate(options.t));
|
||||
const tangent = JSON.parse(bezier.tangent(options.t));
|
||||
|
||||
const tangentEnd = {
|
||||
x: intersection.x + tangent.x * SCALE_UNIT_VECTOR_FACTOR,
|
||||
y: intersection.y + tangent.y * SCALE_UNIT_VECTOR_FACTOR,
|
||||
};
|
||||
|
||||
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawLine(context, intersection, tangentEnd, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, tangentEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
name: "Normal",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
|
||||
const intersection = JSON.parse(bezier.evaluate(options.t));
|
||||
const normal = JSON.parse(bezier.normal(options.t));
|
||||
|
||||
const normalEnd = {
|
||||
x: intersection.x + normal.x * SCALE_UNIT_VECTOR_FACTOR,
|
||||
y: intersection.y + normal.y * SCALE_UNIT_VECTOR_FACTOR,
|
||||
};
|
||||
|
||||
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawLine(context, intersection, normalEnd, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, normalEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
name: "Curvature",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const point = JSON.parse(bezier.evaluate(options.t));
|
||||
const normal = JSON.parse(bezier.normal(options.t));
|
||||
const curvature = bezier.curvature(options.t);
|
||||
const radius = 1 / curvature;
|
||||
|
||||
const curvatureCenter = { x: point.x + normal.x * radius, y: point.y + normal.y * radius };
|
||||
|
||||
drawCircle(context, curvatureCenter, Math.abs(radius), COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawLine(context, point, curvatureCenter, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, curvatureCenter, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
name: "Split",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const bezierPairPoints = JSON.parse(bezier.split(options.t));
|
||||
|
||||
drawBezier(context, bezierPairPoints[0], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_2, radius: 3.5 });
|
||||
drawBezier(context, bezierPairPoints[1], null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
name: "Trim",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const trimmedBezier = bezier.trim(options.t1, options.t2);
|
||||
drawBezierHelper(context, trimmedBezier, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "t1",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
default: 0.25,
|
||||
},
|
||||
{
|
||||
variable: "t2",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
default: 0.75,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Project",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point): void => {
|
||||
if (mouseLocation != null) {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const t = bezier.project(mouseLocation.x, mouseLocation.y);
|
||||
const closestPoint = JSON.parse(bezier.evaluate(t));
|
||||
drawLine(context, mouseLocation, closestPoint, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Local Extrema",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const dimensionColors = ["red", "green"];
|
||||
const extrema: number[][] = JSON.parse(bezier.local_extrema());
|
||||
extrema.forEach((tValues, index) => {
|
||||
tValues.forEach((t) => {
|
||||
const point: Point = JSON.parse(bezier.evaluate(t));
|
||||
drawPoint(context, point, 4, dimensionColors[index]);
|
||||
});
|
||||
});
|
||||
drawText(getContextFromCanvas(canvas), "X extrema", 5, canvas.height - 20, dimensionColors[0]);
|
||||
drawText(getContextFromCanvas(canvas), "Y extrema", 5, canvas.height - 5, dimensionColors[1]);
|
||||
},
|
||||
customPoints: {
|
||||
[BezierCurveType.Quadratic]: [
|
||||
[40, 40],
|
||||
[160, 30],
|
||||
[110, 150],
|
||||
],
|
||||
[BezierCurveType.Cubic]: [
|
||||
[160, 180],
|
||||
[170, 10],
|
||||
[30, 90],
|
||||
[180, 160],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Bounding Box",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const bboxPoints: Point[] = JSON.parse(bezier.bounding_box());
|
||||
const minPoint = bboxPoints[0];
|
||||
const maxPoint = bboxPoints[1];
|
||||
drawLine(context, minPoint, { x: minPoint.x, y: maxPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawLine(context, minPoint, { x: maxPoint.x, y: minPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawLine(context, maxPoint, { x: minPoint.x, y: maxPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawLine(context, maxPoint, { x: maxPoint.x, y: minPoint.y }, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Inflections",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const inflections: number[] = JSON.parse(bezier.inflections());
|
||||
inflections.forEach((t) => {
|
||||
const point = JSON.parse(bezier.evaluate(t));
|
||||
drawPoint(context, point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
});
|
||||
},
|
||||
curveDegrees: new Set([BezierCurveType.Cubic]),
|
||||
},
|
||||
{
|
||||
name: "De Casteljau Points",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const hullPoints: Point[][] = JSON.parse(bezier.de_casteljau_points(options.t));
|
||||
hullPoints.reverse().forEach((iteration: Point[], iterationIndex) => {
|
||||
const colorLight = `hsl(${90 * iterationIndex}, 100%, 50%)`;
|
||||
|
||||
iteration.forEach((point: Point, index) => {
|
||||
// Skip the anchor and handle points which are already drawn in black
|
||||
if (iterationIndex !== hullPoints.length - 1) {
|
||||
drawPoint(getContextFromCanvas(canvas), point, 4, colorLight);
|
||||
}
|
||||
|
||||
if (index !== 0) {
|
||||
const prevPoint: Point = iteration[index - 1];
|
||||
drawLine(getContextFromCanvas(canvas), point, prevPoint, colorLight);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
name: "Rotate",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const rotatedBezier = JSON.parse(bezier.rotate(options.angle * Math.PI).get_points());
|
||||
drawBezier(context, rotatedBezier, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "angle",
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 1 / 50,
|
||||
default: 0.12,
|
||||
unit: "π",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Intersect (Line Segment)",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const line = [
|
||||
{ x: 150, y: 150 },
|
||||
{ x: 20, y: 20 },
|
||||
];
|
||||
const mappedLine = line.map((p) => [p.x, p.y]);
|
||||
drawLine(context, line[0], line[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
const intersections: Float64Array = bezier.intersect_line_segment(mappedLine);
|
||||
intersections.forEach((t: number) => {
|
||||
const p = JSON.parse(bezier.evaluate(t));
|
||||
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Intersect (Quadratic Segment)",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const points = [
|
||||
{ x: 20, y: 80 },
|
||||
{ x: 180, y: 10 },
|
||||
{ x: 90, y: 120 },
|
||||
];
|
||||
const mappedPoints = points.map((p) => [p.x, p.y]);
|
||||
drawCurve(context, points, COLORS.NON_INTERACTIVE.STROKE_1, 1);
|
||||
const intersections: Float64Array = bezier.intersect_quadratic_segment(mappedPoints, options.error);
|
||||
intersections.forEach((t: number) => {
|
||||
const p = JSON.parse(bezier.evaluate(t));
|
||||
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "error",
|
||||
min: 0.1,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
default: 0.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Intersect (Cubic Segment)",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const points = [
|
||||
{ x: 40, y: 20 },
|
||||
{ x: 100, y: 40 },
|
||||
{ x: 40, y: 120 },
|
||||
{ x: 175, y: 140 },
|
||||
];
|
||||
const mappedPoints = points.map((p) => [p.x, p.y]);
|
||||
drawCurve(context, points, COLORS.NON_INTERACTIVE.STROKE_1, 1);
|
||||
const intersections: Float64Array = bezier.intersect_cubic_segment(mappedPoints, options.error);
|
||||
intersections.forEach((t: number) => {
|
||||
const p = JSON.parse(bezier.evaluate(t));
|
||||
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "error",
|
||||
min: 0.1,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
default: 0.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Intersect (Self)",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const intersections: number[][] = JSON.parse(bezier.intersect_self(options.error));
|
||||
intersections.forEach((tValues: number[]) => {
|
||||
const p = JSON.parse(bezier.evaluate(tValues[0]));
|
||||
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "error",
|
||||
min: 0.01,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
default: 0.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
customPoints: {
|
||||
[BezierCurveType.Cubic]: [
|
||||
[160, 180],
|
||||
[170, 10],
|
||||
[30, 90],
|
||||
[180, 140],
|
||||
],
|
||||
},
|
||||
curveDegrees: new Set([BezierCurveType.Cubic]),
|
||||
},
|
||||
{
|
||||
name: "Reduce",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const curves: Point[][] = JSON.parse(bezier.reduce());
|
||||
curves.forEach((points, index) => {
|
||||
drawBezier(context, points, null, { curveStrokeColor: `hsl(${40 * index}, 100%, 50%)`, radius: 3.5, drawHandles: false });
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Arcs",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const arcs: CircleSector[] = JSON.parse(bezier.arcs(options.error, options.max_iterations, options.strategy));
|
||||
arcs.forEach((circleSector, index) => {
|
||||
drawCircleSector(context, circleSector, `hsl(${40 * index}, 100%, 50%, 75%)`, `hsl(${40 * index}, 100%, 50%, 37.5%)`);
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "strategy",
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 1,
|
||||
default: 0,
|
||||
unit: [": Automatic", ": FavorLargerArcs", ": FavorCorrectness"],
|
||||
},
|
||||
{
|
||||
variable: "error",
|
||||
min: 0.05,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
default: 0.5,
|
||||
},
|
||||
{
|
||||
variable: "max_iterations",
|
||||
min: 50,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
|
||||
customPoints: {
|
||||
[BezierCurveType.Quadratic]: [
|
||||
[50, 50],
|
||||
[85, 65],
|
||||
[100, 100],
|
||||
],
|
||||
[BezierCurveType.Cubic]: [
|
||||
[160, 180],
|
||||
[170, 10],
|
||||
[30, 90],
|
||||
[180, 160],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Offset",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const curves: Point[][] = JSON.parse(bezier.offset(options.distance));
|
||||
curves.forEach((points, index) => {
|
||||
if (points.length === 2) {
|
||||
drawLine(context, points[0], points[1], `hsl(${40 * index}, 100%, 50%)`);
|
||||
} else {
|
||||
drawCurve(context, points, `hsl(${40 * index}, 100%, 50%)`);
|
||||
}
|
||||
});
|
||||
drawPoint(context, curves[0][0], 4, "hsl(0, 100%, 50%)");
|
||||
drawPoint(context, curves[curves.length - 1][curves[0].length - 1], 4, `hsl(${40 * (curves.length - 1)}, 100%, 50%)`);
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: -50,
|
||||
max: 50,
|
||||
step: 1,
|
||||
default: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
subpathFeatures: [
|
||||
{
|
||||
name: "Constructor",
|
||||
callback: (subpath: WasmSubpathInstance): string => subpath.to_svg(),
|
||||
},
|
||||
{
|
||||
name: "Length",
|
||||
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
components: {
|
||||
ExamplePane,
|
||||
SubpathExamplePane,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
import { WasmBezier } from "@/../wasm/pkg";
|
||||
|
||||
import { COLORS, drawBezier, drawPoint, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing";
|
||||
import { BezierCallback, BezierPoint, BezierStyleConfig, Point, WasmBezierManipulatorKey, WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
// Offset to increase selectable range, used to make points easier to grab
|
||||
const FUDGE_FACTOR = 3;
|
||||
|
||||
// Given the number of points in the curve, map the index of a point to the correct manipulator key
|
||||
const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [k: number]: WasmBezierManipulatorKey[] } = {
|
||||
2: ["set_start", "set_end"],
|
||||
3: ["set_start", "set_handle_start", "set_end"],
|
||||
4: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
|
||||
};
|
||||
|
||||
class BezierDrawing {
|
||||
points: BezierPoint[];
|
||||
|
||||
canvas: HTMLCanvasElement;
|
||||
|
||||
ctx: CanvasRenderingContext2D;
|
||||
|
||||
dragIndex: number | null;
|
||||
|
||||
bezier: WasmBezierInstance;
|
||||
|
||||
callback: BezierCallback;
|
||||
|
||||
options: Record<string, number>;
|
||||
|
||||
createThroughPoints: boolean;
|
||||
|
||||
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: Record<string, number>, createThroughPoints = false) {
|
||||
this.bezier = bezier;
|
||||
this.callback = callback;
|
||||
this.options = options;
|
||||
this.createThroughPoints = createThroughPoints;
|
||||
const bezierPoints: Point[] = JSON.parse(bezier.get_points());
|
||||
this.points = bezierPoints.map((p, i, points) => ({
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
r: getPointSizeByIndex(i, points.length),
|
||||
selected: false,
|
||||
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
|
||||
}));
|
||||
|
||||
if (this.createThroughPoints && this.points.length === 4) {
|
||||
// Use the first handler as the middle point
|
||||
this.points = [this.points[0], this.points[1], this.points[3]];
|
||||
}
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
if (canvas === null) {
|
||||
throw Error("Failed to create canvas");
|
||||
}
|
||||
this.canvas = canvas;
|
||||
|
||||
this.canvas.style.border = "solid 1px black";
|
||||
this.canvas.width = 200;
|
||||
this.canvas.height = 200;
|
||||
|
||||
this.ctx = getContextFromCanvas(this.canvas);
|
||||
|
||||
this.dragIndex = null; // Index of the point being moved
|
||||
|
||||
this.canvas.addEventListener("mousedown", (e) => this.mouseDownHandler(e));
|
||||
this.canvas.addEventListener("mousemove", (e) => this.mouseMoveHandler(e));
|
||||
this.canvas.addEventListener("mouseup", () => this.deselectPointHandler());
|
||||
this.updateBezier();
|
||||
}
|
||||
|
||||
clearFigure(): void {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
mouseMoveHandler(evt: MouseEvent): void {
|
||||
if (evt.buttons === 0) this.deselectPointHandler();
|
||||
|
||||
const mx = evt.offsetX;
|
||||
const my = evt.offsetY;
|
||||
|
||||
if (this.dragIndex !== null) {
|
||||
const selectableRange = getPointSizeByIndex(this.dragIndex, this.points.length);
|
||||
if (mx - selectableRange > 0 && my - selectableRange > 0 && mx + selectableRange < this.canvas.width && my + selectableRange < this.canvas.height) {
|
||||
const selectedPoint = this.points[this.dragIndex];
|
||||
selectedPoint.x = mx;
|
||||
selectedPoint.y = my;
|
||||
this.bezier[selectedPoint.manipulator](selectedPoint.x, selectedPoint.y);
|
||||
}
|
||||
}
|
||||
this.updateBezier({ x: mx, y: my });
|
||||
}
|
||||
|
||||
mouseDownHandler(evt: MouseEvent): void {
|
||||
const mx = evt.offsetX;
|
||||
const my = evt.offsetY;
|
||||
for (let i = 0; i < this.points.length; i += 1) {
|
||||
const selectableRange = getPointSizeByIndex(i, this.points.length) + FUDGE_FACTOR;
|
||||
if (Math.abs(mx - this.points[i].x) < selectableRange && Math.abs(my - this.points[i].y) < selectableRange) {
|
||||
this.dragIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.updateBezier();
|
||||
}
|
||||
|
||||
deselectPointHandler(): void {
|
||||
if (this.dragIndex !== undefined) {
|
||||
this.dragIndex = null;
|
||||
this.updateBezier();
|
||||
}
|
||||
}
|
||||
|
||||
updateBezier(mouseLocation?: Point, options: Record<string, number> = {}): void {
|
||||
this.clearFigure();
|
||||
if (Object.values(options).length !== 0) {
|
||||
this.options = options;
|
||||
}
|
||||
this.clearFigure();
|
||||
|
||||
// For the create through points cases, we store a bezier where the handle is actually the point that the curve should pass through
|
||||
// This is so that we can re-use the drag and drop logic, while simply drawing the desired bezier instead
|
||||
const actualBezierPointLength = JSON.parse(this.bezier.get_points()).length;
|
||||
let pointsToDraw = this.points;
|
||||
|
||||
let styleConfig: Partial<BezierStyleConfig> = {
|
||||
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_2,
|
||||
};
|
||||
let dragIndex = this.dragIndex;
|
||||
if (this.createThroughPoints) {
|
||||
let bezierThroughPoints;
|
||||
const pointList = this.points.map((p) => [p.x, p.y]);
|
||||
if (actualBezierPointLength === 3) {
|
||||
bezierThroughPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
|
||||
} else {
|
||||
bezierThroughPoints = WasmBezier.cubic_through_points(pointList, this.options.t, this.options["midpoint separation"]);
|
||||
}
|
||||
pointsToDraw = JSON.parse(bezierThroughPoints.get_points());
|
||||
if (this.dragIndex === 1) {
|
||||
// Do not propagate dragIndex when the the non-endpoint is moved
|
||||
dragIndex = null;
|
||||
} else if (this.dragIndex === 2 && pointsToDraw.length === 4) {
|
||||
// For the cubic case, we want to propagate the drag index when the end point is moved, but need to adjust the index
|
||||
dragIndex = 3;
|
||||
}
|
||||
styleConfig = { handleLineStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, handleStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1 };
|
||||
}
|
||||
drawBezier(this.ctx, pointsToDraw, dragIndex, styleConfig);
|
||||
if (this.createThroughPoints) {
|
||||
// Draw the point that the curve was drawn through
|
||||
drawPoint(this.ctx, this.points[1], getPointSizeByIndex(1, this.points.length), this.dragIndex === 1 ? COLORS.INTERACTIVE.SELECTED : COLORS.INTERACTIVE.STROKE_1);
|
||||
}
|
||||
this.callback(this.canvas, this.bezier, this.options, mouseLocation);
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement {
|
||||
return this.canvas;
|
||||
}
|
||||
}
|
||||
|
||||
export default BezierDrawing;
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4 class="example-header">{{ title }}</h4>
|
||||
<figure class="example-figure" ref="drawing"></figure>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import BezierDrawing from "@/components/BezierDrawing";
|
||||
import { BezierCallback, WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
title: String,
|
||||
bezier: {
|
||||
type: Object as PropType<WasmBezierInstance>,
|
||||
required: true,
|
||||
},
|
||||
callback: {
|
||||
type: Function as PropType<BezierCallback>,
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<Record<string, number>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
createThroughPoints: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const drawing = this.$refs.drawing as HTMLElement;
|
||||
drawing.appendChild(this.bezierDrawing.getCanvas());
|
||||
this.bezierDrawing.updateBezier();
|
||||
},
|
||||
watch: {
|
||||
options: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.bezierDrawing.updateBezier(undefined, this.options);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.example-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.example-figure {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="example-pane-header">{{ name }}</h3>
|
||||
<div class="example-row">
|
||||
<div v-for="(example, index) in exampleData" :key="index">
|
||||
<component :is="template" :templateOptions="example.templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" :createThroughPoints="createThroughPoints" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, Component } from "vue";
|
||||
|
||||
import { BezierCallback, BezierCurveType, TemplateOption, WasmBezierConstructorKey, WasmBezierInstance, WasmRawInstance } from "@/utils/types";
|
||||
|
||||
import Example from "@/components/Example.vue";
|
||||
|
||||
type ExampleData = {
|
||||
title: string;
|
||||
bezier: WasmBezierInstance;
|
||||
templateOptions: TemplateOption;
|
||||
};
|
||||
|
||||
type CustomTemplateOptions = {
|
||||
[key in BezierCurveType]?: TemplateOption;
|
||||
};
|
||||
|
||||
type CustomPoints = {
|
||||
[key in BezierCurveType]?: number[][];
|
||||
};
|
||||
|
||||
const CurveTypeMapping = {
|
||||
[BezierCurveType.Linear]: {
|
||||
points: [
|
||||
[30, 60],
|
||||
[140, 120],
|
||||
],
|
||||
constructor: "new_linear" as WasmBezierConstructorKey,
|
||||
},
|
||||
[BezierCurveType.Quadratic]: {
|
||||
points: [
|
||||
[30, 50],
|
||||
[140, 30],
|
||||
[160, 170],
|
||||
],
|
||||
constructor: "new_quadratic" as WasmBezierConstructorKey,
|
||||
},
|
||||
[BezierCurveType.Cubic]: {
|
||||
points: [
|
||||
[30, 30],
|
||||
[60, 140],
|
||||
[150, 30],
|
||||
[160, 160],
|
||||
],
|
||||
constructor: "new_cubic" as WasmBezierConstructorKey,
|
||||
},
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String as PropType<string>,
|
||||
required: true,
|
||||
},
|
||||
callback: {
|
||||
type: Function as PropType<BezierCallback>,
|
||||
required: true,
|
||||
},
|
||||
template: {
|
||||
type: Object as PropType<Component>,
|
||||
default: Example,
|
||||
},
|
||||
templateOptions: {
|
||||
type: Object as PropType<TemplateOption>,
|
||||
required: false,
|
||||
},
|
||||
customOptions: {
|
||||
type: Object as PropType<CustomTemplateOptions>,
|
||||
default: () => ({}),
|
||||
},
|
||||
createThroughPoints: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
curveDegrees: {
|
||||
type: Set as PropType<Set<BezierCurveType>>,
|
||||
default: () => new Set(Object.values(BezierCurveType)),
|
||||
},
|
||||
customPoints: {
|
||||
type: Object as PropType<CustomPoints>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
exampleData: [] as ExampleData[],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
import("@/../wasm/pkg").then((wasm: WasmRawInstance) => {
|
||||
this.exampleData = [];
|
||||
// Only add example for BezierCurveType that is in the curveDegrees set
|
||||
Object.values(BezierCurveType).forEach((bezierType) => {
|
||||
if (this.curveDegrees.has(bezierType)) {
|
||||
const { points, constructor } = CurveTypeMapping[bezierType];
|
||||
this.exampleData.push({
|
||||
title: bezierType,
|
||||
// Use custom options if they were provided for the current BezierCurveType
|
||||
bezier: wasm.WasmBezier[constructor](this.customPoints[bezierType] || points),
|
||||
templateOptions: (this.customOptions[bezierType] || this.templateOptions) as TemplateOption,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
components: {
|
||||
Example,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.example-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.example-pane-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" :createThroughPoints="createThroughPoints" />
|
||||
<div v-for="(slider, index) in templateOptions.sliders" :key="index">
|
||||
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ getSliderValue(sliderData[slider.variable], sliderUnits[slider.variable]) }}</div>
|
||||
<input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { BezierCallback, TemplateOption, WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
import Example from "@/components/Example.vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
title: String,
|
||||
bezier: {
|
||||
type: Object as PropType<WasmBezierInstance>,
|
||||
required: true,
|
||||
},
|
||||
callback: {
|
||||
type: Function as PropType<BezierCallback>,
|
||||
required: true,
|
||||
},
|
||||
templateOptions: {
|
||||
type: Object as PropType<TemplateOption>,
|
||||
default: () => ({}),
|
||||
},
|
||||
createThroughPoints: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const sliders = this.templateOptions.sliders;
|
||||
return {
|
||||
sliderData: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.default }))),
|
||||
sliderUnits: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.unit }))),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Example,
|
||||
},
|
||||
methods: {
|
||||
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4 class="example-header">{{ title }}</h4>
|
||||
<figure @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove" class="example-figure" v-html="subpathSVG"></figure>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { WasmSubpath } from "@/../wasm/pkg";
|
||||
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey } from "@/utils/types";
|
||||
|
||||
const SELECTABLE_RANGE = 10;
|
||||
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
title: String,
|
||||
triples: {
|
||||
type: Array as PropType<Array<Array<number[] | undefined>>>,
|
||||
required: true,
|
||||
mutable: true,
|
||||
},
|
||||
closed: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
callback: {
|
||||
type: Function as PropType<SubpathCallback>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
|
||||
return {
|
||||
subpath,
|
||||
subpathSVG: this.callback(subpath),
|
||||
activeIndex: undefined as number[] | undefined,
|
||||
mutableTriples: JSON.parse(JSON.stringify(this.triples)),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onMouseDown(event: MouseEvent) {
|
||||
const mx = event.offsetX;
|
||||
const my = event.offsetY;
|
||||
for (let controllerIndex = 0; controllerIndex < this.mutableTriples.length; controllerIndex += 1) {
|
||||
for (let pointIndex = 0; pointIndex < 3; pointIndex += 1) {
|
||||
const point = this.mutableTriples[controllerIndex][pointIndex];
|
||||
if (point && Math.abs(mx - point[0]) < SELECTABLE_RANGE && Math.abs(my - point[1]) < SELECTABLE_RANGE) {
|
||||
this.activeIndex = [controllerIndex, pointIndex];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onMouseUp() {
|
||||
this.activeIndex = undefined;
|
||||
},
|
||||
onMouseMove(event: MouseEvent) {
|
||||
const mx = event.offsetX;
|
||||
const my = event.offsetY;
|
||||
if (this.activeIndex) {
|
||||
this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my);
|
||||
this.mutableTriples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my];
|
||||
this.subpathSVG = this.callback(this.subpath);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.example-figure {
|
||||
border: solid 1px black;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="example-pane-header">{{ name }}</h3>
|
||||
<div class="example-row">
|
||||
<div v-for="(example, index) in examples" :key="index">
|
||||
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { SubpathCallback } from "@/utils/types";
|
||||
|
||||
import SubpathExample from "@/components/SubpathExample.vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: String,
|
||||
callback: {
|
||||
type: Function as PropType<SubpathCallback>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
examples: [
|
||||
{
|
||||
title: "Open Subpath",
|
||||
triples: [
|
||||
[[20, 20], undefined, [10, 90]],
|
||||
[[150, 40], [60, 40], undefined],
|
||||
[[175, 175], undefined, undefined],
|
||||
[[100, 100], [40, 120], undefined],
|
||||
],
|
||||
closed: false,
|
||||
},
|
||||
{
|
||||
title: "Closed Subpath",
|
||||
triples: [
|
||||
[[35, 125], undefined, [40, 40]],
|
||||
[[130, 30], [120, 120], undefined],
|
||||
[
|
||||
[145, 150],
|
||||
[175, 90],
|
||||
[70, 185],
|
||||
],
|
||||
],
|
||||
closed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
components: {
|
||||
SubpathExample,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.example-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.example-pane-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from "vue";
|
||||
|
||||
import App from "@/App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { BezierStyleConfig, CircleSector, Point, WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
const HANDLE_RADIUS_FACTOR = 2 / 3;
|
||||
const DEFAULT_ENDPOINT_RADIUS = 5;
|
||||
|
||||
export const COLORS = {
|
||||
CANVAS: "white",
|
||||
INTERACTIVE: {
|
||||
STROKE_1: "black",
|
||||
STROKE_2: "gray",
|
||||
SELECTED: "blue",
|
||||
},
|
||||
NON_INTERACTIVE: {
|
||||
STROKE_1: "red",
|
||||
STROKE_2: "orange",
|
||||
},
|
||||
};
|
||||
|
||||
export const isIndexFirstOrLast = (index: number, arrayLength: number): boolean => index === 0 || index === arrayLength - 1;
|
||||
|
||||
export const getPointSizeByIndex = (index: number, numPoints: number, radius = DEFAULT_ENDPOINT_RADIUS): number => (isIndexFirstOrLast(index, numPoints) ? radius : radius * HANDLE_RADIUS_FACTOR);
|
||||
|
||||
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx === null) {
|
||||
throw Error("Failed to fetch context");
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = COLORS.INTERACTIVE.STROKE_2, lineWidth = 1): void => {
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(point1.x, point1.y);
|
||||
ctx.lineTo(point2.x, point2.y);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = COLORS.INTERACTIVE.STROKE_1): void => {
|
||||
// Outline the point
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = radius / 3;
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI, false);
|
||||
ctx.stroke();
|
||||
|
||||
// Fill the point (hiding any overlapping lines)
|
||||
ctx.fillStyle = COLORS.CANVAS;
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x, point.y, radius * HANDLE_RADIUS_FACTOR, 0, 2 * Math.PI, false);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number, textColor = COLORS.INTERACTIVE.STROKE_1): void => {
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.font = "16px Arial";
|
||||
ctx.fillText(text, x, y);
|
||||
};
|
||||
|
||||
export const drawCurve = (ctx: CanvasRenderingContext2D, points: Point[], strokeColor = COLORS.INTERACTIVE.STROKE_1, lineWidth = 2): void => {
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0].x, points[0].y);
|
||||
if (points.length === 3) {
|
||||
ctx.quadraticCurveTo(points[1].x, points[1].y, points[2].x, points[2].y);
|
||||
} else {
|
||||
ctx.bezierCurveTo(points[1].x, points[1].y, points[2].x, points[2].y, points[3].x, points[3].y);
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const drawCircle = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = COLORS.INTERACTIVE.STROKE_1): void => {
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI, false);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const drawCircleSector = (ctx: CanvasRenderingContext2D, circleSector: CircleSector, strokeColor = COLORS.INTERACTIVE.STROKE_1, fillColor = COLORS.NON_INTERACTIVE.STROKE_1): void => {
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
const { center, radius, startAngle, endAngle } = circleSector;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(center.x, center.y);
|
||||
ctx.arc(center.x, center.y, radius, startAngle, endAngle);
|
||||
ctx.lineTo(center.x, center.y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
export const drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, bezierStyleConfig: Partial<BezierStyleConfig> = {}): void => {
|
||||
const points = JSON.parse(bezier.get_points());
|
||||
drawBezier(ctx, points, null, bezierStyleConfig);
|
||||
};
|
||||
|
||||
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null, bezierStyleConfig: Partial<BezierStyleConfig> = {}): void => {
|
||||
const styleConfig: BezierStyleConfig = {
|
||||
curveStrokeColor: COLORS.INTERACTIVE.STROKE_1,
|
||||
handleStrokeColor: COLORS.INTERACTIVE.STROKE_1,
|
||||
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_1,
|
||||
radius: DEFAULT_ENDPOINT_RADIUS,
|
||||
drawHandles: true,
|
||||
...bezierStyleConfig,
|
||||
};
|
||||
// If the handle or handle line colors are not specified, use the same color as the rest of the curve
|
||||
if (bezierStyleConfig.curveStrokeColor) {
|
||||
if (!bezierStyleConfig.handleStrokeColor) {
|
||||
styleConfig.handleStrokeColor = bezierStyleConfig.curveStrokeColor;
|
||||
}
|
||||
if (!bezierStyleConfig.handleLineStrokeColor) {
|
||||
styleConfig.handleLineStrokeColor = bezierStyleConfig.curveStrokeColor;
|
||||
}
|
||||
}
|
||||
// Points passed to drawBezier are interpreted as follows
|
||||
// points[0] = start point
|
||||
// points[1] = handle start
|
||||
// points[2] = (optional) handle end
|
||||
// points[3] = end point
|
||||
const start = points[0];
|
||||
let end = null;
|
||||
let handleStart = null;
|
||||
let handleEnd = null;
|
||||
if (points.length === 4) {
|
||||
handleStart = points[1];
|
||||
handleEnd = points[2];
|
||||
end = points[3];
|
||||
} else if (points.length === 3) {
|
||||
handleStart = points[1];
|
||||
handleEnd = handleStart;
|
||||
end = points[2];
|
||||
} else {
|
||||
handleStart = start;
|
||||
handleEnd = points[1];
|
||||
end = handleEnd;
|
||||
}
|
||||
|
||||
if (points.length === 2) {
|
||||
drawLine(ctx, start, end, styleConfig.curveStrokeColor, 2);
|
||||
} else {
|
||||
drawCurve(ctx, points, styleConfig.curveStrokeColor);
|
||||
if (styleConfig.drawHandles) {
|
||||
drawLine(ctx, start, handleStart, styleConfig.handleLineStrokeColor);
|
||||
drawLine(ctx, end, handleEnd, styleConfig.handleLineStrokeColor);
|
||||
}
|
||||
}
|
||||
|
||||
points.forEach((point, index) => {
|
||||
if (styleConfig.drawHandles || isIndexFirstOrLast(index, points.length)) {
|
||||
const strokeColor = isIndexFirstOrLast(index, points.length) ? styleConfig.curveStrokeColor : styleConfig.handleStrokeColor;
|
||||
drawPoint(ctx, point, getPointSizeByIndex(index, points.length, styleConfig.radius), index === dragIndex ? COLORS.INTERACTIVE.SELECTED : strokeColor);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
export type WasmRawInstance = typeof import("@/../wasm/pkg");
|
||||
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
|
||||
|
||||
export type WasmBezierKey = keyof WasmBezierInstance;
|
||||
export type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
|
||||
export type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
|
||||
|
||||
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
|
||||
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
|
||||
|
||||
export enum BezierCurveType {
|
||||
Linear = "Linear",
|
||||
Quadratic = "Quadratic",
|
||||
Cubic = "Cubic",
|
||||
}
|
||||
|
||||
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point) => void;
|
||||
export type SubpathCallback = (subpath: WasmSubpathInstance) => string;
|
||||
|
||||
export type SliderOption = {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
default: number;
|
||||
variable: string;
|
||||
unit?: string | string[];
|
||||
};
|
||||
|
||||
export type TemplateOption = {
|
||||
sliders: SliderOption[];
|
||||
};
|
||||
|
||||
export type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type BezierPoint = Point & {
|
||||
manipulator: WasmBezierManipulatorKey;
|
||||
};
|
||||
|
||||
export type BezierStyleConfig = {
|
||||
curveStrokeColor: string;
|
||||
handleStrokeColor: string;
|
||||
handleLineStrokeColor: string;
|
||||
radius: number;
|
||||
drawHandles: boolean;
|
||||
};
|
||||
|
||||
export type CircleSector = {
|
||||
center: Point;
|
||||
radius: number;
|
||||
startAngle: number;
|
||||
endAngle: number;
|
||||
};
|
||||
Reference in New Issue
Block a user