mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Bezier split and trim implementation (#680)
* Added split implementation and UI Co-authored-by: Thomas Cheng <Androxium@users.noreply.github.com> * Added bezier split impl * Adjust struct traits * Implement trim and adjust FE code to handle multiple sliders per feature Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com> Co-authored-by: Rob Nadal <robnadal44@gmail.com> * Fix edge case in trim and add rust doc comments * Stylistic changes per review * More stylistic changes per review * replaced last explicit color usages Co-authored-by: Robert Nadal <Robnadal44@gmail.com> Co-authored-by: Thomas Cheng <Androxium@users.noreply.github.com> Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com>
This commit is contained in:
parent
7f15cac5e2
commit
4eaffd0e5a
8 changed files with 224 additions and 102 deletions
|
|
@ -2,7 +2,7 @@
|
|||
<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>
|
||||
<div v-for="feature in features" :key="feature.id">
|
||||
<div v-for="(feature, index) in features" :key="index">
|
||||
<ExamplePane :template="feature.template" :templateOptions="feature.templateOptions" :name="feature.name" :callback="feature.callback" />
|
||||
</div>
|
||||
<br />
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from "vue";
|
||||
|
||||
import { drawText, drawPoint, drawLine, getContextFromCanvas } from "@/utils/drawing";
|
||||
import { drawText, drawPoint, drawBezier, drawLine, getContextFromCanvas, drawBezierHelper, COLORS } from "@/utils/drawing";
|
||||
import { WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
import ExamplePane from "@/components/ExamplePane.vue";
|
||||
|
|
@ -51,57 +51,55 @@ export default defineComponent({
|
|||
return {
|
||||
features: [
|
||||
{
|
||||
id: 0,
|
||||
name: "Constructor",
|
||||
// eslint-disable-next-line
|
||||
callback: (): void => {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Length",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||
drawText(getContextFromCanvas(canvas), `Length: ${bezier.length().toFixed(2)}`, 5, canvas.height - 7);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Compute",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||
const point = JSON.parse(bezier.compute(parseFloat(options)));
|
||||
drawPoint(getContextFromCanvas(canvas), point, 4, "Red");
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const point = JSON.parse(bezier.compute(options.t));
|
||||
drawPoint(getContextFromCanvas(canvas), point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: tSliderOptions,
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Lookup Table",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||
const lookupPoints = bezier.compute_lookup_table(Number(options));
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const lookupPoints = bezier.compute_lookup_table(options.steps);
|
||||
lookupPoints.forEach((serialisedPoint, index) => {
|
||||
if (index !== 0 && index !== lookupPoints.length - 1) {
|
||||
drawPoint(getContextFromCanvas(canvas), JSON.parse(serialisedPoint), 3, "Red");
|
||||
drawPoint(getContextFromCanvas(canvas), JSON.parse(serialisedPoint), 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
}
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
min: 2,
|
||||
max: 15,
|
||||
step: 1,
|
||||
default: 5,
|
||||
variable: "Steps",
|
||||
sliders: [
|
||||
{
|
||||
min: 2,
|
||||
max: 15,
|
||||
step: 1,
|
||||
default: 5,
|
||||
variable: "steps",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Derivative",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||
const t = parseFloat(options);
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
|
||||
const intersection = JSON.parse(bezier.compute(t));
|
||||
const derivative = JSON.parse(bezier.derivative(t));
|
||||
const intersection = JSON.parse(bezier.compute(options.t));
|
||||
const derivative = JSON.parse(bezier.derivative(options.t));
|
||||
const curveFactor = bezier.get_points().length - 1;
|
||||
|
||||
const tangentStart = {
|
||||
|
|
@ -113,23 +111,21 @@ export default defineComponent({
|
|||
y: intersection.y + derivative.y / curveFactor,
|
||||
};
|
||||
|
||||
drawLine(context, tangentStart, tangentEnd, "Red");
|
||||
drawPoint(context, tangentStart, 3, "Red");
|
||||
drawPoint(context, intersection, 3, "Red");
|
||||
drawPoint(context, tangentEnd, 3, "Red");
|
||||
drawLine(context, tangentStart, tangentEnd, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, tangentStart, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, tangentEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: tSliderOptions,
|
||||
templateOptions: { sliders: [tSliderOptions] },
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Normal",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||
const t = parseFloat(options);
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
|
||||
const intersection = JSON.parse(bezier.compute(t));
|
||||
const normal = JSON.parse(bezier.normal(t));
|
||||
const intersection = JSON.parse(bezier.compute(options.t));
|
||||
const normal = JSON.parse(bezier.normal(options.t));
|
||||
|
||||
const normalStart = {
|
||||
x: intersection.x - normal.x * 20,
|
||||
|
|
@ -140,13 +136,52 @@ export default defineComponent({
|
|||
y: intersection.y + normal.y * 20,
|
||||
};
|
||||
|
||||
drawLine(context, normalStart, normalEnd, "Red");
|
||||
drawPoint(context, normalStart, 3, "Red");
|
||||
drawPoint(context, intersection, 3, "Red");
|
||||
drawPoint(context, normalEnd, 3, "Red");
|
||||
drawLine(context, normalStart, normalEnd, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, normalStart, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(context, normalEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: tSliderOptions,
|
||||
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, COLORS.NON_INTERACTIVE.STROKE_2, 3.5);
|
||||
drawBezier(context, bezierPairPoints[1], null, COLORS.NON_INTERACTIVE.STROKE_1, 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, COLORS.NON_INTERACTIVE.STROKE_1, 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ class BezierDrawing {
|
|||
|
||||
callback: BezierCallback;
|
||||
|
||||
options: string;
|
||||
options: Record<string, number>;
|
||||
|
||||
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: string) {
|
||||
constructor(bezier: WasmBezierInstance, callback: BezierCallback, options: Record<string, number>) {
|
||||
this.bezier = bezier;
|
||||
this.callback = callback;
|
||||
this.options = options;
|
||||
|
|
@ -100,8 +100,8 @@ class BezierDrawing {
|
|||
}
|
||||
}
|
||||
|
||||
updateBezier(options = ""): void {
|
||||
if (options !== "") {
|
||||
updateBezier(options: Record<string, number> = {}): void {
|
||||
if (Object.values(options).length !== 0) {
|
||||
this.options = options;
|
||||
}
|
||||
this.clearFigure();
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: Object as PropType<Record<string, number>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -40,8 +40,11 @@ export default defineComponent({
|
|||
this.bezierDrawing.updateBezier();
|
||||
},
|
||||
watch: {
|
||||
options() {
|
||||
this.bezierDrawing.updateBezier(this.options);
|
||||
options: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.bezierDrawing.updateBezier(this.options);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<Example :title="title" :bezier="bezier" :callback="callback" :options="value.toString()" />
|
||||
<div class="slider_label">{{ templateOptions.variable }} = {{ value }}</div>
|
||||
<input class="slider" v-model="value" type="range" :step="templateOptions.step" :min="templateOptions.min" :max="templateOptions.max" />
|
||||
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" />
|
||||
<div v-for="(slider, index) in templateOptions.sliders" :key="index">
|
||||
<div class="slider_label">{{ slider.variable }} = {{ sliderData[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 } from "@/utils/types";
|
||||
import { BezierCallback, SliderOption } from "@/utils/types";
|
||||
import { WasmBezierInstance } from "@/utils/wasm-comm";
|
||||
|
||||
import Example from "@/components/Example.vue";
|
||||
|
|
@ -35,8 +37,9 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
data() {
|
||||
const sliders: SliderOption[] = this.templateOptions.sliders;
|
||||
return {
|
||||
value: this.templateOptions.default,
|
||||
sliderData: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.default }))),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
import { Point } from "@/utils/types";
|
||||
import { Point, WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
const RADIUS_SIZE = {
|
||||
large: 5,
|
||||
small: 3,
|
||||
const HANDLE_RADIUS_FACTOR = 2 / 3;
|
||||
const DEFAULT_ENDPOINT_RADIUS = 5;
|
||||
|
||||
export const COLORS = {
|
||||
CANVAS: "white",
|
||||
INTERACTIVE: {
|
||||
STROKE_1: "black",
|
||||
STROKE_2: "grey",
|
||||
SELECTED: "blue",
|
||||
},
|
||||
NON_INTERACTIVE: {
|
||||
STROKE_1: "red",
|
||||
STROKE_2: "orange",
|
||||
},
|
||||
};
|
||||
|
||||
export const getPointSizeByIndex = (index: number, numPoints: number): number => RADIUS_SIZE[index === 0 || index === numPoints - 1 ? "large" : "small"];
|
||||
export const getPointSizeByIndex = (index: number, numPoints: number, radius = DEFAULT_ENDPOINT_RADIUS): number => (index === 0 || index === numPoints - 1 ? radius : radius * HANDLE_RADIUS_FACTOR);
|
||||
|
||||
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
|
@ -15,7 +26,7 @@ export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRendering
|
|||
return ctx;
|
||||
};
|
||||
|
||||
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = "gray"): void => {
|
||||
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = COLORS.INTERACTIVE.STROKE_2): void => {
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
|
|
@ -25,7 +36,7 @@ export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: P
|
|||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = "black"): void => {
|
||||
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;
|
||||
|
|
@ -34,25 +45,29 @@ export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: n
|
|||
ctx.stroke();
|
||||
|
||||
// Fill the point (hiding any overlapping lines)
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillStyle = COLORS.CANVAS;
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x, point.y, radius * (2 / 3), 0, 2 * Math.PI, false);
|
||||
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): void => {
|
||||
ctx.fillStyle = "black";
|
||||
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 drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null): void => {
|
||||
/* Until a bezier representation is finalized, treat the points as follows
|
||||
points[0] = start point
|
||||
points[1] = handle start
|
||||
points[2] = (optional) handle end
|
||||
points[3] = end point
|
||||
*/
|
||||
export const drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, strokeColor = COLORS.INTERACTIVE.STROKE_1, radius = DEFAULT_ENDPOINT_RADIUS): void => {
|
||||
const points = bezier.get_points().map((p: string) => JSON.parse(p));
|
||||
drawBezier(ctx, points, null, strokeColor, radius);
|
||||
};
|
||||
|
||||
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null, strokeColor = COLORS.INTERACTIVE.STROKE_1, radius = DEFAULT_ENDPOINT_RADIUS): void => {
|
||||
// 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;
|
||||
|
|
@ -67,7 +82,7 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
|
|||
end = points[2];
|
||||
}
|
||||
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
ctx.beginPath();
|
||||
|
|
@ -79,10 +94,10 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
|
|||
}
|
||||
ctx.stroke();
|
||||
|
||||
drawLine(ctx, start, handleStart);
|
||||
drawLine(ctx, end, handleEnd);
|
||||
drawLine(ctx, start, handleStart, strokeColor);
|
||||
drawLine(ctx, end, handleEnd, strokeColor);
|
||||
|
||||
points.forEach((point, index) => {
|
||||
drawPoint(ctx, point, getPointSizeByIndex(index, points.length), index === dragIndex ? "Blue" : "Black");
|
||||
drawPoint(ctx, point, getPointSizeByIndex(index, points.length, radius), index === dragIndex ? COLORS.INTERACTIVE.SELECTED : strokeColor);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
|
|||
export type WasmBezierKey = keyof WasmBezierInstance;
|
||||
export type WasmBezierMutatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
|
||||
|
||||
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string) => void;
|
||||
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>) => void;
|
||||
|
||||
export type SliderOption = {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
default: number;
|
||||
variable: string;
|
||||
};
|
||||
|
||||
export type Point = {
|
||||
x: number;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@ struct Point {
|
|||
y: f64,
|
||||
}
|
||||
|
||||
/// Wrapper of the `Bezier` struct to be used in JS.
|
||||
#[wasm_bindgen]
|
||||
/// Wrapper of the `Bezier` struct to be used in JS
|
||||
pub struct WasmBezier {
|
||||
internal: Bezier,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct WasmBezier(Bezier);
|
||||
|
||||
/// Convert a `DVec2` into a `JsValue`
|
||||
pub fn vec_to_point(p: &DVec2) -> JsValue {
|
||||
|
|
@ -25,60 +24,68 @@ impl WasmBezier {
|
|||
/// Expect js_points to be a list of 3 pairs
|
||||
pub fn new_quad(js_points: &JsValue) -> WasmBezier {
|
||||
let points: [DVec2; 3] = js_points.into_serde().unwrap();
|
||||
WasmBezier {
|
||||
internal: Bezier::from_quadratic_dvec2(points[0], points[1], points[2]),
|
||||
}
|
||||
WasmBezier(Bezier::from_quadratic_dvec2(points[0], points[1], points[2]))
|
||||
}
|
||||
|
||||
/// Expect js_points to be a list of 4 pairs
|
||||
pub fn new_cubic(js_points: &JsValue) -> WasmBezier {
|
||||
let points: [DVec2; 4] = js_points.into_serde().unwrap();
|
||||
WasmBezier {
|
||||
internal: Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]),
|
||||
}
|
||||
WasmBezier(Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]))
|
||||
}
|
||||
|
||||
pub fn set_start(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_start(DVec2::from((x, y)));
|
||||
self.0.set_start(DVec2::new(x, y));
|
||||
}
|
||||
|
||||
pub fn set_end(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_end(DVec2::from((x, y)));
|
||||
self.0.set_end(DVec2::new(x, y));
|
||||
}
|
||||
|
||||
pub fn set_handle_start(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_handle_start(DVec2::from((x, y)));
|
||||
self.0.set_handle_start(DVec2::new(x, y));
|
||||
}
|
||||
|
||||
pub fn set_handle_end(&mut self, x: f64, y: f64) {
|
||||
self.internal.set_handle_end(DVec2::from((x, y)));
|
||||
self.0.set_handle_end(DVec2::new(x, y));
|
||||
}
|
||||
|
||||
pub fn get_points(&self) -> Vec<JsValue> {
|
||||
self.internal.get_points().iter().flatten().map(vec_to_point).collect()
|
||||
self.0.get_points().iter().flatten().map(vec_to_point).collect()
|
||||
}
|
||||
|
||||
pub fn to_svg(&self) -> String {
|
||||
self.internal.to_svg()
|
||||
self.0.to_svg()
|
||||
}
|
||||
|
||||
pub fn length(&self) -> f64 {
|
||||
self.internal.length()
|
||||
self.0.length()
|
||||
}
|
||||
|
||||
pub fn compute(&self, t: f64) -> JsValue {
|
||||
vec_to_point(&self.internal.compute(t))
|
||||
vec_to_point(&self.0.compute(t))
|
||||
}
|
||||
|
||||
pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> {
|
||||
self.internal.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
|
||||
self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
|
||||
}
|
||||
|
||||
pub fn derivative(&self, t: f64) -> JsValue {
|
||||
vec_to_point(&self.internal.derivative(t))
|
||||
vec_to_point(&self.0.derivative(t))
|
||||
}
|
||||
|
||||
pub fn normal(&self, t: f64) -> JsValue {
|
||||
vec_to_point(&self.internal.normal(t))
|
||||
vec_to_point(&self.0.normal(t))
|
||||
}
|
||||
|
||||
pub fn split(&self, t: f64) -> JsValue {
|
||||
let bezier_points: [Vec<Point>; 2] = self
|
||||
.0
|
||||
.split(t)
|
||||
.map(|bezier| bezier.get_points().iter().flatten().map(|point| Point { x: point.x, y: point.y }).collect());
|
||||
JsValue::from_serde(&serde_json::to_string(&bezier_points).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
pub fn trim(&self, t1: f64, t2: f64) -> WasmBezier {
|
||||
WasmBezier(self.0.trim(t1, t2))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use glam::DVec2;
|
||||
|
||||
/// Representation of the handle point(s) in a bezier segment
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum BezierHandles {
|
||||
/// Handles for a quadratic segment
|
||||
Quadratic {
|
||||
|
|
@ -17,6 +18,7 @@ pub enum BezierHandles {
|
|||
}
|
||||
|
||||
/// Representation of a bezier segment with 2D points
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Bezier {
|
||||
/// Start point of the bezier segment
|
||||
start: DVec2,
|
||||
|
|
@ -31,9 +33,9 @@ impl Bezier {
|
|||
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points
|
||||
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::from((x1, y1)),
|
||||
handles: BezierHandles::Quadratic { handle: DVec2::from((x2, y2)) },
|
||||
end: DVec2::from((x3, y3)),
|
||||
start: DVec2::new(x1, y1),
|
||||
handles: BezierHandles::Quadratic { handle: DVec2::new(x2, y2) },
|
||||
end: DVec2::new(x3, y3),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,12 +52,12 @@ impl Bezier {
|
|||
/// Create a cubic bezier using the provided coordinates as the start, handles, and end points
|
||||
pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::from((x1, y1)),
|
||||
start: DVec2::new(x1, y1),
|
||||
handles: BezierHandles::Cubic {
|
||||
handle_start: DVec2::from((x2, y2)),
|
||||
handle_end: DVec2::from((x3, y3)),
|
||||
handle_start: DVec2::new(x2, y2),
|
||||
handle_end: DVec2::new(x3, y3),
|
||||
},
|
||||
end: DVec2::from((x4, y4)),
|
||||
end: DVec2::new(x4, y4),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,4 +256,53 @@ impl Bezier {
|
|||
let derivative = self.derivative(t);
|
||||
derivative.normalize().perp()
|
||||
}
|
||||
|
||||
/// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t`
|
||||
pub fn split(&self, t: f64) -> [Bezier; 2] {
|
||||
let split_point = self.compute(t);
|
||||
|
||||
let t_squared = t * t;
|
||||
let t_minus_one = t - 1.;
|
||||
let squared_t_minus_one = t_minus_one * t_minus_one;
|
||||
|
||||
match self.handles {
|
||||
// TODO: Actually calculate the correct handle locations
|
||||
BezierHandles::Quadratic { handle } => [
|
||||
Bezier::from_quadratic_dvec2(self.start, t * handle - t_minus_one * self.start, split_point),
|
||||
Bezier::from_quadratic_dvec2(split_point, t * self.end - t_minus_one * handle, self.end),
|
||||
],
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [
|
||||
Bezier::from_cubic_dvec2(
|
||||
self.start,
|
||||
t * handle_start - t_minus_one * self.start,
|
||||
t_squared * handle_end - 2. * t * t_minus_one * handle_start + squared_t_minus_one * self.start,
|
||||
split_point,
|
||||
),
|
||||
Bezier::from_cubic_dvec2(
|
||||
split_point,
|
||||
t_squared * self.end - 2. * t * t_minus_one * handle_end + squared_t_minus_one * handle_start,
|
||||
t * self.end - t_minus_one * handle_end,
|
||||
self.end,
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Bezier curve representing the sub-curve starting at the point corresponding to `t1` and ending at the point corresponding to `t2`
|
||||
pub fn trim(&self, t1: f64, t2: f64) -> Bezier {
|
||||
// Depending on the order of `t1` and `t2`, determine which half of the split we need to keep
|
||||
let t1_split_side = if t1 <= t2 { 1 } else { 0 };
|
||||
let t2_split_side = if t1 <= t2 { 0 } else { 1 };
|
||||
let bezier_starting_at_t1 = self.split(t1)[t1_split_side];
|
||||
// Adjust the ratio `t2` to its corresponding value on the new curve that was split on `t1`
|
||||
let adjusted_t2 = if t1 < t2 || (t1 == t2 && t1 == 0.) {
|
||||
// Case where we took the split from t1 to the end
|
||||
// Also cover the `t1` == t2 case where there would otherwise be a divide by 0
|
||||
(t2 - t1) / (1. - t1)
|
||||
} else {
|
||||
// Case where we took the split from the beginning to `t1`
|
||||
t2 / t1
|
||||
};
|
||||
bezier_starting_at_t1.split(adjusted_t2)[t2_split_side]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue