mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 15:28:12 +08:00
Bezier derivative and normal implementation (#679)
* added ui for derivative impl * Add derivative computation for bezier-rs library * integrate devivative ui with library * Add implementation for the normal function * Update rustdoc comments * Rename handles and getters, add tangent function * Rename variables, address nits Co-authored-by: Rob Nadal <robnadal44@gmail.com> Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com> Co-authored-by: ll2zheng <Linda Zheng>
This commit is contained in:
co-authored by
Rob Nadal
Thomas Cheng
ll2zheng
parent
2052c00010
commit
a4a174db04
@@ -13,7 +13,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from "vue";
|
import { defineComponent, markRaw } from "vue";
|
||||||
|
|
||||||
import { drawText, drawPoint, getContextFromCanvas } from "@/utils/drawing";
|
import { drawText, drawPoint, drawLine, getContextFromCanvas } from "@/utils/drawing";
|
||||||
import { WasmBezierInstance } from "@/utils/types";
|
import { WasmBezierInstance } from "@/utils/types";
|
||||||
|
|
||||||
import ExamplePane from "@/components/ExamplePane.vue";
|
import ExamplePane from "@/components/ExamplePane.vue";
|
||||||
@@ -34,6 +34,14 @@ const testBezierLib = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tSliderOptions = {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.01,
|
||||||
|
default: 0.5,
|
||||||
|
variable: "t",
|
||||||
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "App",
|
name: "App",
|
||||||
components: {
|
components: {
|
||||||
@@ -60,30 +68,19 @@ export default defineComponent({
|
|||||||
name: "Compute",
|
name: "Compute",
|
||||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||||
const point = JSON.parse(bezier.compute(parseFloat(options)));
|
const point = JSON.parse(bezier.compute(parseFloat(options)));
|
||||||
point.r = 4;
|
drawPoint(getContextFromCanvas(canvas), point, 4, "Red");
|
||||||
point.selected = false;
|
|
||||||
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
|
|
||||||
},
|
},
|
||||||
template: markRaw(SliderExample),
|
template: markRaw(SliderExample),
|
||||||
templateOptions: {
|
templateOptions: tSliderOptions,
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.01,
|
|
||||||
default: 0.5,
|
|
||||||
variable: "t",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
name: "Lookup Table",
|
name: "Lookup Table",
|
||||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||||
const lookupPoints = bezier.compute_lookup_table(Number(options));
|
const lookupPoints = bezier.compute_lookup_table(Number(options));
|
||||||
lookupPoints.forEach((serPoint, index) => {
|
lookupPoints.forEach((serialisedPoint, index) => {
|
||||||
if (index !== 0 && index !== lookupPoints.length - 1) {
|
if (index !== 0 && index !== lookupPoints.length - 1) {
|
||||||
const point = JSON.parse(serPoint);
|
drawPoint(getContextFromCanvas(canvas), JSON.parse(serialisedPoint), 3, "Red");
|
||||||
point.r = 3;
|
|
||||||
point.selected = false;
|
|
||||||
drawPoint(getContextFromCanvas(canvas), point, "DarkBlue");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -96,6 +93,61 @@ export default defineComponent({
|
|||||||
variable: "Steps",
|
variable: "Steps",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Derivative",
|
||||||
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||||
|
const t = parseFloat(options);
|
||||||
|
const context = getContextFromCanvas(canvas);
|
||||||
|
|
||||||
|
const intersection = JSON.parse(bezier.compute(t));
|
||||||
|
const derivative = JSON.parse(bezier.derivative(t));
|
||||||
|
const curveFactor = bezier.get_points().length - 1;
|
||||||
|
|
||||||
|
const tangentStart = {
|
||||||
|
x: intersection.x - derivative.x / curveFactor,
|
||||||
|
y: intersection.y - derivative.y / curveFactor,
|
||||||
|
};
|
||||||
|
const tangentEnd = {
|
||||||
|
x: intersection.x + derivative.x / curveFactor,
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
template: markRaw(SliderExample),
|
||||||
|
templateOptions: tSliderOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: "Normal",
|
||||||
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: string): void => {
|
||||||
|
const t = parseFloat(options);
|
||||||
|
const context = getContextFromCanvas(canvas);
|
||||||
|
|
||||||
|
const intersection = JSON.parse(bezier.compute(t));
|
||||||
|
const normal = JSON.parse(bezier.normal(t));
|
||||||
|
|
||||||
|
const normalStart = {
|
||||||
|
x: intersection.x - normal.x * 20,
|
||||||
|
y: intersection.y - normal.y * 20,
|
||||||
|
};
|
||||||
|
const normalEnd = {
|
||||||
|
x: intersection.x + normal.x * 20,
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
template: markRaw(SliderExample),
|
||||||
|
templateOptions: tSliderOptions,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { drawBezier, getContextFromCanvas } from "@/utils/drawing";
|
import { drawBezier, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing";
|
||||||
import { BezierCallback, Point, WasmBezierMutatorKey } from "@/utils/types";
|
import { BezierCallback, BezierPoint, WasmBezierMutatorKey } from "@/utils/types";
|
||||||
import { WasmBezierInstance } from "@/utils/wasm-comm";
|
import { WasmBezierInstance } from "@/utils/wasm-comm";
|
||||||
|
|
||||||
class BezierDrawing {
|
// Offset to increase selectable range, used to make points easier to grab
|
||||||
static indexToMutator: WasmBezierMutatorKey[] = ["set_start", "set_handle1", "set_handle2", "set_end"];
|
const FUDGE_FACTOR = 3;
|
||||||
|
|
||||||
points: Point[];
|
class BezierDrawing {
|
||||||
|
static indexToMutator: WasmBezierMutatorKey[] = ["set_start", "set_handle_start", "set_handle_end", "set_end"];
|
||||||
|
|
||||||
|
points: BezierPoint[];
|
||||||
|
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
@@ -29,7 +32,7 @@ class BezierDrawing {
|
|||||||
.map((p, i, points) => ({
|
.map((p, i, points) => ({
|
||||||
x: p.x,
|
x: p.x,
|
||||||
y: p.y,
|
y: p.y,
|
||||||
r: i === 0 || i === points.length - 1 ? 5 : 3,
|
r: getPointSizeByIndex(i, points.length),
|
||||||
selected: false,
|
selected: false,
|
||||||
mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i],
|
mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i],
|
||||||
}));
|
}));
|
||||||
@@ -40,6 +43,7 @@ class BezierDrawing {
|
|||||||
}
|
}
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.canvas.style.border = "solid 1px black";
|
||||||
this.canvas.width = 200;
|
this.canvas.width = 200;
|
||||||
this.canvas.height = 200;
|
this.canvas.height = 200;
|
||||||
|
|
||||||
@@ -51,52 +55,48 @@ class BezierDrawing {
|
|||||||
this.canvas.addEventListener("mousemove", this.mouseMoveHandler.bind(this));
|
this.canvas.addEventListener("mousemove", this.mouseMoveHandler.bind(this));
|
||||||
this.canvas.addEventListener("mouseup", this.deselectPointHandler.bind(this));
|
this.canvas.addEventListener("mouseup", this.deselectPointHandler.bind(this));
|
||||||
this.canvas.addEventListener("mouseout", this.deselectPointHandler.bind(this));
|
this.canvas.addEventListener("mouseout", this.deselectPointHandler.bind(this));
|
||||||
this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.updateBezier();
|
this.updateBezier();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearFigure(): void {
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
mouseMoveHandler(evt: MouseEvent): void {
|
mouseMoveHandler(evt: MouseEvent): void {
|
||||||
const mx = evt.offsetX;
|
const mx = evt.offsetX;
|
||||||
const my = evt.offsetY;
|
const my = evt.offsetY;
|
||||||
|
|
||||||
if (
|
if (this.dragIndex !== null) {
|
||||||
this.dragIndex != null &&
|
const selectableRange = getPointSizeByIndex(this.dragIndex, this.points.length);
|
||||||
mx - this.points[this.dragIndex].r > 0 &&
|
if (mx - selectableRange > 0 && my - selectableRange > 0 && mx + selectableRange < this.canvas.width && my + selectableRange < this.canvas.height) {
|
||||||
my - this.points[this.dragIndex].r > 0 &&
|
const selectedPoint = this.points[this.dragIndex];
|
||||||
mx + this.points[this.dragIndex].r < this.canvas.width &&
|
selectedPoint.x = mx;
|
||||||
my + this.points[this.dragIndex].r < this.canvas.height
|
selectedPoint.y = my;
|
||||||
) {
|
this.bezier[selectedPoint.mutator](selectedPoint.x, selectedPoint.y);
|
||||||
const selectedPoint = this.points[this.dragIndex];
|
this.clearFigure();
|
||||||
selectedPoint.x = mx;
|
}
|
||||||
selectedPoint.y = my;
|
|
||||||
this.bezier[selectedPoint.mutator](selectedPoint.x, selectedPoint.y);
|
|
||||||
|
|
||||||
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
|
|
||||||
this.updateBezier();
|
|
||||||
}
|
}
|
||||||
|
this.updateBezier();
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseDownHandler(evt: MouseEvent): void {
|
mouseDownHandler(evt: MouseEvent): void {
|
||||||
const mx = evt.offsetX;
|
const mx = evt.offsetX;
|
||||||
const my = evt.offsetY;
|
const my = evt.offsetY;
|
||||||
for (let i = 0; i < this.points.length; i += 1) {
|
for (let i = 0; i < this.points.length; i += 1) {
|
||||||
if (
|
const selectableRange = getPointSizeByIndex(i, this.points.length) + FUDGE_FACTOR;
|
||||||
Math.abs(mx - this.points[i].x) < this.points[i].r + 3 &&
|
if (Math.abs(mx - this.points[i].x) < selectableRange && Math.abs(my - this.points[i].y) < selectableRange) {
|
||||||
Math.abs(my - this.points[i].y) < this.points[i].r + 3 // Fudge factor makes the points easier to grab
|
|
||||||
) {
|
|
||||||
this.dragIndex = i;
|
this.dragIndex = i;
|
||||||
this.points[this.dragIndex].selected = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.updateBezier();
|
||||||
}
|
}
|
||||||
|
|
||||||
deselectPointHandler(): void {
|
deselectPointHandler(): void {
|
||||||
if (this.dragIndex != null) {
|
if (this.dragIndex !== undefined) {
|
||||||
this.points[this.dragIndex].selected = false;
|
this.clearFigure();
|
||||||
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
|
|
||||||
this.updateBezier();
|
|
||||||
this.dragIndex = null;
|
this.dragIndex = null;
|
||||||
|
this.updateBezier();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +104,8 @@ class BezierDrawing {
|
|||||||
if (options !== "") {
|
if (options !== "") {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
this.ctx.clearRect(1, 1, this.canvas.width - 2, this.canvas.height - 2);
|
this.clearFigure();
|
||||||
drawBezier(this.ctx, this.points);
|
drawBezier(this.ctx, this.points, this.dragIndex);
|
||||||
this.callback(this.canvas, this.bezier, this.options);
|
this.callback(this.canvas, this.bezier, this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export default defineComponent({
|
|||||||
id: 0,
|
id: 0,
|
||||||
title: "Quadratic",
|
title: "Quadratic",
|
||||||
bezier: wasm.WasmBezier.new_quad([
|
bezier: wasm.WasmBezier.new_quad([
|
||||||
[30, 30],
|
[30, 50],
|
||||||
[140, 20],
|
[140, 30],
|
||||||
[160, 170],
|
[160, 170],
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { Point } from "@/utils/types";
|
import { Point } from "@/utils/types";
|
||||||
|
|
||||||
|
const RADIUS_SIZE = {
|
||||||
|
large: 5,
|
||||||
|
small: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPointSizeByIndex = (index: number, numPoints: number): number => RADIUS_SIZE[index === 0 || index === numPoints - 1 ? "large" : "small"];
|
||||||
|
|
||||||
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
|
export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (ctx === null) {
|
if (ctx === null) {
|
||||||
@@ -8,28 +15,28 @@ export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRendering
|
|||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawLine = (ctx: CanvasRenderingContext2D, p1: Point, p2: Point): void => {
|
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = "gray"): void => {
|
||||||
ctx.strokeStyle = "grey";
|
ctx.strokeStyle = strokeColor;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(p1.x, p1.y);
|
ctx.moveTo(point1.x, point1.y);
|
||||||
ctx.lineTo(p2.x, p2.y);
|
ctx.lineTo(point2.x, point2.y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawPoint = (ctx: CanvasRenderingContext2D, p: Point, stroke = "black"): void => {
|
export const drawPoint = (ctx: CanvasRenderingContext2D, point: Point, radius: number, strokeColor = "black"): void => {
|
||||||
// Outline the point
|
// Outline the point
|
||||||
ctx.strokeStyle = p.selected ? "blue" : stroke;
|
ctx.strokeStyle = strokeColor;
|
||||||
ctx.lineWidth = p.r / 3;
|
ctx.lineWidth = radius / 3;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, p.r, 0, 2 * Math.PI, false);
|
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI, false);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Fill the point (hiding any overlapping lines)
|
// Fill the point (hiding any overlapping lines)
|
||||||
ctx.fillStyle = "white";
|
ctx.fillStyle = "white";
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(p.x, p.y, p.r * (2 / 3), 0, 2 * Math.PI, false);
|
ctx.arc(point.x, point.y, radius * (2 / 3), 0, 2 * Math.PI, false);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,24 +46,24 @@ export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number,
|
|||||||
ctx.fillText(text, x, y);
|
ctx.fillText(text, x, y);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[]): void => {
|
export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragIndex: number | null = null): void => {
|
||||||
/* Until a bezier representation is finalized, treat the points as follows
|
/* Until a bezier representation is finalized, treat the points as follows
|
||||||
points[0] = start point
|
points[0] = start point
|
||||||
points[1] = handle 1
|
points[1] = handle start
|
||||||
points[2] = (optional) handle 2
|
points[2] = (optional) handle end
|
||||||
points[3] = end point
|
points[3] = end point
|
||||||
*/
|
*/
|
||||||
const start = points[0];
|
const start = points[0];
|
||||||
let end = null;
|
let end = null;
|
||||||
let handle1 = null;
|
let handleStart = null;
|
||||||
let handle2 = null;
|
let handleEnd = null;
|
||||||
if (points.length === 4) {
|
if (points.length === 4) {
|
||||||
handle1 = points[1];
|
handleStart = points[1];
|
||||||
handle2 = points[2];
|
handleEnd = points[2];
|
||||||
end = points[3];
|
end = points[3];
|
||||||
} else {
|
} else {
|
||||||
handle1 = points[1];
|
handleStart = points[1];
|
||||||
handle2 = handle1;
|
handleEnd = handleStart;
|
||||||
end = points[2];
|
end = points[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,16 +73,16 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[]): void
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(points[0].x, points[0].y);
|
ctx.moveTo(points[0].x, points[0].y);
|
||||||
if (points.length === 3) {
|
if (points.length === 3) {
|
||||||
ctx.quadraticCurveTo(handle1.x, handle1.y, end.x, end.y);
|
ctx.quadraticCurveTo(handleStart.x, handleStart.y, end.x, end.y);
|
||||||
} else {
|
} else {
|
||||||
ctx.bezierCurveTo(handle1.x, handle1.y, handle2.x, handle2.y, end.x, end.y);
|
ctx.bezierCurveTo(handleStart.x, handleStart.y, handleEnd.x, handleEnd.y, end.x, end.y);
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
drawLine(ctx, start, handle1);
|
drawLine(ctx, start, handleStart);
|
||||||
drawLine(ctx, end, handle2);
|
drawLine(ctx, end, handleEnd);
|
||||||
|
|
||||||
points.forEach((point) => {
|
points.forEach((point, index) => {
|
||||||
drawPoint(ctx, point);
|
drawPoint(ctx, point, getPointSizeByIndex(index, points.length), index === dragIndex ? "Blue" : "Black");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ export type WasmRawInstance = typeof import("../../wasm/pkg");
|
|||||||
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
|
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
|
||||||
|
|
||||||
export type WasmBezierKey = keyof WasmBezierInstance;
|
export type WasmBezierKey = keyof WasmBezierInstance;
|
||||||
export type WasmBezierMutatorKey = "set_start" | "set_handle1" | "set_handle2" | "set_end";
|
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: string) => void;
|
||||||
|
|
||||||
export type Point = {
|
export type Point = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
r: number;
|
};
|
||||||
|
|
||||||
|
export type BezierPoint = Point & {
|
||||||
mutator: WasmBezierMutatorKey;
|
mutator: WasmBezierMutatorKey;
|
||||||
selected: boolean;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ struct Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
/// Wrapper of the `Bezier` struct to be used in JS
|
||||||
pub struct WasmBezier {
|
pub struct WasmBezier {
|
||||||
internal: Bezier,
|
internal: Bezier,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a `DVec2` into a `JsValue`
|
||||||
pub fn vec_to_point(p: &DVec2) -> JsValue {
|
pub fn vec_to_point(p: &DVec2) -> JsValue {
|
||||||
JsValue::from_serde(&serde_json::to_string(&Point { x: p[0], y: p[1] }).unwrap()).unwrap()
|
JsValue::from_serde(&serde_json::to_string(&Point { x: p.x, y: p.y }).unwrap()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -44,12 +46,12 @@ impl WasmBezier {
|
|||||||
self.internal.set_end(DVec2::from((x, y)));
|
self.internal.set_end(DVec2::from((x, y)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_handle1(&mut self, x: f64, y: f64) {
|
pub fn set_handle_start(&mut self, x: f64, y: f64) {
|
||||||
self.internal.set_handle1(DVec2::from((x, y)));
|
self.internal.set_handle_start(DVec2::from((x, y)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_handle2(&mut self, x: f64, y: f64) {
|
pub fn set_handle_end(&mut self, x: f64, y: f64) {
|
||||||
self.internal.set_handle2(DVec2::from((x, y)));
|
self.internal.set_handle_end(DVec2::from((x, y)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_points(&self) -> Vec<JsValue> {
|
pub fn get_points(&self) -> Vec<JsValue> {
|
||||||
@@ -71,4 +73,12 @@ impl WasmBezier {
|
|||||||
pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> {
|
pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> {
|
||||||
self.internal.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
|
self.internal.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normal(&self, t: f64) -> JsValue {
|
||||||
|
vec_to_point(&self.internal.normal(t))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-26
@@ -2,8 +2,18 @@ use glam::DVec2;
|
|||||||
|
|
||||||
/// Representation of the handle point(s) in a bezier segment
|
/// Representation of the handle point(s) in a bezier segment
|
||||||
pub enum BezierHandles {
|
pub enum BezierHandles {
|
||||||
Quadratic { handle: DVec2 },
|
/// Handles for a quadratic segment
|
||||||
Cubic { handle1: DVec2, handle2: DVec2 },
|
Quadratic {
|
||||||
|
/// Point representing the location of the single handle
|
||||||
|
handle: DVec2,
|
||||||
|
},
|
||||||
|
/// Handles for a cubic segment
|
||||||
|
Cubic {
|
||||||
|
/// Point representing the location of the handle associated to the start point
|
||||||
|
handle_start: DVec2,
|
||||||
|
/// Point representing the location of the handle associated to the end point
|
||||||
|
handle_end: DVec2,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Representation of a bezier segment with 2D points
|
/// Representation of a bezier segment with 2D points
|
||||||
@@ -42,8 +52,8 @@ impl Bezier {
|
|||||||
Bezier {
|
Bezier {
|
||||||
start: DVec2::from((x1, y1)),
|
start: DVec2::from((x1, y1)),
|
||||||
handles: BezierHandles::Cubic {
|
handles: BezierHandles::Cubic {
|
||||||
handle1: DVec2::from((x2, y2)),
|
handle_start: DVec2::from((x2, y2)),
|
||||||
handle2: DVec2::from((x3, y3)),
|
handle_end: DVec2::from((x3, y3)),
|
||||||
},
|
},
|
||||||
end: DVec2::from((x4, y4)),
|
end: DVec2::from((x4, y4)),
|
||||||
}
|
}
|
||||||
@@ -53,7 +63,7 @@ impl Bezier {
|
|||||||
pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self {
|
pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self {
|
||||||
Bezier {
|
Bezier {
|
||||||
start: p1,
|
start: p1,
|
||||||
handles: BezierHandles::Cubic { handle1: p2, handle2: p3 },
|
handles: BezierHandles::Cubic { handle_start: p2, handle_end: p3 },
|
||||||
end: p4,
|
end: p4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,8 +90,8 @@ impl Bezier {
|
|||||||
BezierHandles::Quadratic { handle } => {
|
BezierHandles::Quadratic { handle } => {
|
||||||
format!("Q {} {}", handle.x, handle.y)
|
format!("Q {} {}", handle.x, handle.y)
|
||||||
}
|
}
|
||||||
BezierHandles::Cubic { handle1, handle2 } => {
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
format!("C {} {}, {} {}", handle1.x, handle1.y, handle2.x, handle2.y)
|
format!("C {} {}, {} {}", handle_start.x, handle_start.y, handle_end.x, handle_end.y)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let curve_path = format!("{}, {} {}", handles_path, self.end.x, self.end.y);
|
let curve_path = format!("{}, {} {}", handles_path, self.end.x, self.end.y);
|
||||||
@@ -102,60 +112,67 @@ impl Bezier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment.
|
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment.
|
||||||
pub fn set_handle1(&mut self, h1: DVec2) {
|
pub fn set_handle_start(&mut self, h1: DVec2) {
|
||||||
match self.handles {
|
match self.handles {
|
||||||
BezierHandles::Quadratic { ref mut handle } => {
|
BezierHandles::Quadratic { ref mut handle } => {
|
||||||
*handle = h1;
|
*handle = h1;
|
||||||
}
|
}
|
||||||
BezierHandles::Cubic { ref mut handle1, .. } => {
|
BezierHandles::Cubic { ref mut handle_start, .. } => {
|
||||||
*handle1 = h1;
|
*handle_start = h1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the coordinates of the second handle point. This will convert a quadratic segment into a cubic one.
|
/// Set the coordinates of the second handle point. This will convert a quadratic segment into a cubic one.
|
||||||
pub fn set_handle2(&mut self, h2: DVec2) {
|
pub fn set_handle_end(&mut self, h2: DVec2) {
|
||||||
match self.handles {
|
match self.handles {
|
||||||
BezierHandles::Quadratic { handle } => {
|
BezierHandles::Quadratic { handle } => {
|
||||||
self.handles = BezierHandles::Cubic { handle1: handle, handle2: h2 };
|
self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 };
|
||||||
}
|
}
|
||||||
BezierHandles::Cubic { ref mut handle2, .. } => {
|
BezierHandles::Cubic { ref mut handle_end, .. } => {
|
||||||
*handle2 = h2;
|
*handle_end = h2;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_start(&self) -> DVec2 {
|
/// Get the coordinates of the bezier segment's start point.
|
||||||
|
pub fn start(&self) -> DVec2 {
|
||||||
self.start
|
self.start
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_end(&self) -> DVec2 {
|
/// Get the coordinates of the bezier segment's end point.
|
||||||
|
pub fn end(&self) -> DVec2 {
|
||||||
self.end
|
self.end
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_handle1(&self) -> DVec2 {
|
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
|
||||||
|
pub fn handle_start(&self) -> DVec2 {
|
||||||
match self.handles {
|
match self.handles {
|
||||||
BezierHandles::Quadratic { handle } => handle,
|
BezierHandles::Quadratic { handle } => handle,
|
||||||
BezierHandles::Cubic { handle1, .. } => handle1,
|
BezierHandles::Cubic { handle_start, .. } => handle_start,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_handle2(&self) -> Option<DVec2> {
|
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
|
||||||
|
pub fn handle_end(&self) -> Option<DVec2> {
|
||||||
match self.handles {
|
match self.handles {
|
||||||
BezierHandles::Quadratic { .. } => None,
|
BezierHandles::Quadratic { .. } => None,
|
||||||
BezierHandles::Cubic { handle2, .. } => Some(handle2),
|
BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the coordinates of all points in an array of 4 optional points.
|
||||||
|
/// For a quadratic segment, the order of the points will be: `start`, `handle`, `end`. The fourth element will be `None`.
|
||||||
|
/// For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`.
|
||||||
pub fn get_points(&self) -> [Option<DVec2>; 4] {
|
pub fn get_points(&self) -> [Option<DVec2>; 4] {
|
||||||
match self.handles {
|
match self.handles {
|
||||||
BezierHandles::Quadratic { handle } => [Some(self.start), Some(handle), Some(self.end), None],
|
BezierHandles::Quadratic { handle } => [Some(self.start), Some(handle), Some(self.end), None],
|
||||||
BezierHandles::Cubic { handle1, handle2 } => [Some(self.start), Some(handle1), Some(handle2), Some(self.end)],
|
BezierHandles::Cubic { handle_start, handle_end } => [Some(self.start), Some(handle_start), Some(handle_end), Some(self.end)],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the point on the curve based on the t-value provided
|
/// Calculate the point on the curve based on the `t`-value provided.
|
||||||
/// basis code based off of pseudocode found here: https://pomax.github.io/bezierinfo/#explanation
|
/// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>
|
||||||
pub fn compute(&self, t: f64) -> DVec2 {
|
pub fn compute(&self, t: f64) -> DVec2 {
|
||||||
assert!((0.0..=1.0).contains(&t));
|
assert!((0.0..=1.0).contains(&t));
|
||||||
|
|
||||||
@@ -165,10 +182,10 @@ impl Bezier {
|
|||||||
|
|
||||||
match self.handles {
|
match self.handles {
|
||||||
BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2.0 * one_minus_t * t * handle + t_squared * self.end,
|
BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2.0 * one_minus_t * t * handle + t_squared * self.end,
|
||||||
BezierHandles::Cubic { handle1, handle2 } => {
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
let t_cubed = t_squared * t;
|
let t_cubed = t_squared * t;
|
||||||
let cubed_one_minus_t = squared_one_minus_t * one_minus_t;
|
let cubed_one_minus_t = squared_one_minus_t * one_minus_t;
|
||||||
cubed_one_minus_t * self.start + 3.0 * squared_one_minus_t * t * handle1 + 3.0 * one_minus_t * t_squared * handle2 + t_cubed * self.end
|
cubed_one_minus_t * self.start + 3.0 * squared_one_minus_t * t * handle_start + 3.0 * one_minus_t * t_squared * handle_end + t_cubed * self.end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +205,7 @@ impl Bezier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return an approximation of the length of the bezier curve
|
/// Return an approximation of the length of the bezier curve
|
||||||
/// code example taken from: https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427
|
/// code example taken from: <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>
|
||||||
pub fn length(&self) -> f64 {
|
pub fn length(&self) -> f64 {
|
||||||
// We will use an approximate approach where
|
// We will use an approximate approach where
|
||||||
// we split the curve into many subdivisions
|
// we split the curve into many subdivisions
|
||||||
@@ -208,4 +225,33 @@ impl Bezier {
|
|||||||
|
|
||||||
approx_curve_length
|
approx_curve_length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a vector representing the derivative at the point designated by `t` on the curve
|
||||||
|
pub fn derivative(&self, t: f64) -> DVec2 {
|
||||||
|
let one_minus_t = 1. - t;
|
||||||
|
match self.handles {
|
||||||
|
BezierHandles::Quadratic { handle } => {
|
||||||
|
let p1_minus_p0 = handle - self.start;
|
||||||
|
let p2_minus_p1 = self.end - handle;
|
||||||
|
2. * one_minus_t * p1_minus_p0 + 2. * t * p2_minus_p1
|
||||||
|
}
|
||||||
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
|
let p1_minus_p0 = handle_start - self.start;
|
||||||
|
let p2_minus_p1 = handle_end - handle_start;
|
||||||
|
let p3_minus_p2 = self.end - handle_end;
|
||||||
|
3. * one_minus_t * one_minus_t * p1_minus_p0 + 6. * t * one_minus_t * p2_minus_p1 + 3. * t * t * p3_minus_p2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve
|
||||||
|
pub fn tangent(&self, t: f64) -> DVec2 {
|
||||||
|
self.derivative(t).normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve
|
||||||
|
pub fn normal(&self, t: f64) -> DVec2 {
|
||||||
|
let derivative = self.derivative(t);
|
||||||
|
derivative.normalize().perp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user