Implement Bezier curve reduce function (#711)
* fixed extrema bug and added reduce impl Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com> Co-authored-by: Linda Zheng <ll2zheng@uwaterloo.ca> Co-authored-by: Hannah Li <hannahli2010@gmail.com> * Stylistic changes related to reduce * Fixed reduce splitting bug causing panic * Added shortcuts and simplified reduce * Stylistic changes per review * address comments * Removed color gradient function and added consts * Tweaks * Change colors faster * Don't drop on mouseout Co-authored-by: Thomas Cheng <contact.chengthomas@gmail.com> Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com> Co-authored-by: Linda Zheng <ll2zheng@uwaterloo.ca> Co-authored-by: Hannah Li <hannahli2010@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
6decc67571
commit
b01f76f097
|
|
@ -266,6 +266,16 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,9 @@ class BezierDrawing {
|
||||||
|
|
||||||
this.dragIndex = null; // Index of the point being moved
|
this.dragIndex = null; // Index of the point being moved
|
||||||
|
|
||||||
this.canvas.addEventListener("mousedown", this.mouseDownHandler.bind(this));
|
this.canvas.addEventListener("mousedown", (e) => this.mouseDownHandler(e));
|
||||||
this.canvas.addEventListener("mousemove", this.mouseMoveHandler.bind(this));
|
this.canvas.addEventListener("mousemove", (e) => this.mouseMoveHandler(e));
|
||||||
this.canvas.addEventListener("mouseup", this.deselectPointHandler.bind(this));
|
this.canvas.addEventListener("mouseup", () => this.deselectPointHandler());
|
||||||
this.canvas.addEventListener("mouseout", this.deselectPointHandler.bind(this));
|
|
||||||
this.updateBezier();
|
this.updateBezier();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +70,11 @@ class BezierDrawing {
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseMoveHandler(evt: MouseEvent): void {
|
mouseMoveHandler(evt: MouseEvent): void {
|
||||||
|
if (evt.buttons === 0) {
|
||||||
|
this.deselectPointHandler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mx = evt.offsetX;
|
const mx = evt.offsetX;
|
||||||
const my = evt.offsetY;
|
const my = evt.offsetY;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,10 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
|
||||||
handleStrokeColor: COLORS.INTERACTIVE.STROKE_1,
|
handleStrokeColor: COLORS.INTERACTIVE.STROKE_1,
|
||||||
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_1,
|
handleLineStrokeColor: COLORS.INTERACTIVE.STROKE_1,
|
||||||
radius: DEFAULT_ENDPOINT_RADIUS,
|
radius: DEFAULT_ENDPOINT_RADIUS,
|
||||||
|
drawHandles: true,
|
||||||
...bezierStyleConfig,
|
...bezierStyleConfig,
|
||||||
};
|
};
|
||||||
// if the handle or handle line colors are not specified, use the same colour as the rest of the curve
|
// 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.curveStrokeColor) {
|
||||||
if (!bezierStyleConfig.handleStrokeColor) {
|
if (!bezierStyleConfig.handleStrokeColor) {
|
||||||
styleConfig.handleStrokeColor = bezierStyleConfig.curveStrokeColor;
|
styleConfig.handleStrokeColor = bezierStyleConfig.curveStrokeColor;
|
||||||
|
|
@ -112,11 +113,15 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
drawLine(ctx, start, handleStart, styleConfig.handleLineStrokeColor);
|
if (styleConfig.drawHandles) {
|
||||||
drawLine(ctx, end, handleEnd, styleConfig.handleLineStrokeColor);
|
drawLine(ctx, start, handleStart, styleConfig.handleLineStrokeColor);
|
||||||
|
drawLine(ctx, end, handleEnd, styleConfig.handleLineStrokeColor);
|
||||||
|
}
|
||||||
|
|
||||||
points.forEach((point, index) => {
|
points.forEach((point, index) => {
|
||||||
const strokeColor = isIndexFirstOrLast(index, points.length) ? styleConfig.curveStrokeColor : styleConfig.handleStrokeColor;
|
if (styleConfig.drawHandles || isIndexFirstOrLast(index, points.length)) {
|
||||||
drawPoint(ctx, point, getPointSizeByIndex(index, points.length, styleConfig.radius), index === dragIndex ? COLORS.INTERACTIVE.SELECTED : strokeColor);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,5 @@ export type BezierStyleConfig = {
|
||||||
handleStrokeColor: string;
|
handleStrokeColor: string;
|
||||||
handleLineStrokeColor: string;
|
handleLineStrokeColor: string;
|
||||||
radius: number;
|
radius: number;
|
||||||
|
drawHandles: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,20 @@ struct Point {
|
||||||
pub struct WasmBezier(Bezier);
|
pub struct WasmBezier(Bezier);
|
||||||
|
|
||||||
/// Convert a `DVec2` into a `JsValue`.
|
/// Convert a `DVec2` into a `JsValue`.
|
||||||
pub fn vec_to_point(p: &DVec2) -> JsValue {
|
fn vec_to_point(p: &DVec2) -> JsValue {
|
||||||
JsValue::from_serde(&serde_json::to_string(&Point { x: p.x, y: p.y }).unwrap()).unwrap()
|
JsValue::from_serde(&serde_json::to_string(&Point { x: p.x, y: p.y }).unwrap()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a bezier to a list of points.
|
||||||
|
fn bezier_to_points(bezier: Bezier) -> Vec<Point> {
|
||||||
|
bezier.get_points().iter().flatten().map(|point| Point { x: point.x, y: point.y }).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize some data and then convert it to a JsValue.
|
||||||
|
fn to_js_value<T: Serialize>(data: T) -> JsValue {
|
||||||
|
JsValue::from_serde(&serde_json::to_string(&data).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl WasmBezier {
|
impl WasmBezier {
|
||||||
/// Expect js_points to be a list of 3 pairs.
|
/// Expect js_points to be a list of 3 pairs.
|
||||||
|
|
@ -88,11 +98,8 @@ impl WasmBezier {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn split(&self, t: f64) -> JsValue {
|
pub fn split(&self, t: f64) -> JsValue {
|
||||||
let bezier_points: [Vec<Point>; 2] = self
|
let bezier_points: [Vec<Point>; 2] = self.0.split(t).map(bezier_to_points);
|
||||||
.0
|
to_js_value(bezier_points)
|
||||||
.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 {
|
pub fn trim(&self, t1: f64, t2: f64) -> WasmBezier {
|
||||||
|
|
@ -105,7 +112,7 @@ impl WasmBezier {
|
||||||
|
|
||||||
pub fn local_extrema(&self) -> JsValue {
|
pub fn local_extrema(&self) -> JsValue {
|
||||||
let local_extrema = self.0.local_extrema();
|
let local_extrema = self.0.local_extrema();
|
||||||
JsValue::from_serde(&serde_json::to_string(&local_extrema).unwrap()).unwrap()
|
to_js_value(local_extrema)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rotate(&self, angle: f64) -> WasmBezier {
|
pub fn rotate(&self, angle: f64) -> WasmBezier {
|
||||||
|
|
@ -116,4 +123,9 @@ impl WasmBezier {
|
||||||
let line: [DVec2; 2] = js_points.into_serde().unwrap();
|
let line: [DVec2; 2] = js_points.into_serde().unwrap();
|
||||||
self.0.intersect_line_segment(line).iter().map(|&p| vec_to_point(&p)).collect::<Vec<JsValue>>()
|
self.0.intersect_line_segment(line).iter().map(|&p| vec_to_point(&p)).collect::<Vec<JsValue>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reduce(&self) -> JsValue {
|
||||||
|
let bezier_points: Vec<Vec<Point>> = self.0.reduce(None).into_iter().map(bezier_to_points).collect();
|
||||||
|
to_js_value(bezier_points)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/// Implementation constants
|
||||||
|
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.;
|
||||||
|
|
||||||
|
/// Method argument defaults
|
||||||
|
pub const REDUCE_STEP_SIZE_DEFAULT: f64 = 0.01;
|
||||||
|
|
||||||
/// Default `t` value used for the `curve_through_points` functions
|
/// Default `t` value used for the `curve_through_points` functions
|
||||||
pub const DEFAULT_T_VALUE: f64 = 0.5;
|
pub const DEFAULT_T_VALUE: f64 = 0.5;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
//! Bezier-rs: A Bezier Math Library for Rust
|
//! Bezier-rs: A Bezier Math Library for Rust
|
||||||
|
|
||||||
mod consts;
|
mod consts;
|
||||||
use consts::*;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
use consts::*;
|
||||||
use glam::{DMat2, DVec2};
|
use glam::{DMat2, DVec2};
|
||||||
|
|
||||||
/// Representation of the handle point(s) in a bezier curve.
|
/// Representation of the handle point(s) in a bezier segment.
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
enum BezierHandles {
|
enum BezierHandles {
|
||||||
/// Handles for a quadratic curve.
|
/// Handles for a quadratic curve.
|
||||||
|
|
@ -568,6 +568,92 @@ impl Bezier {
|
||||||
.filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all())
|
.filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all())
|
||||||
.collect::<Vec<DVec2>>()
|
.collect::<Vec<DVec2>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine if it is possible to scale the given curve, using the following conditions:
|
||||||
|
/// 1. All the control points are located on a single side of the curve.
|
||||||
|
/// 2. The on-curve point for `t = 0.5` must occur roughly in the center of the polygon defined by the curve's endpoint normals.
|
||||||
|
/// See [the offset section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer for more details.
|
||||||
|
fn is_scalable(&self) -> bool {
|
||||||
|
// Verify all the control points are located on a single side of the curve.
|
||||||
|
if let BezierHandles::Cubic { handle_start, handle_end } = self.handles {
|
||||||
|
let angle_1 = (self.end - self.start).angle_between(handle_start - self.start);
|
||||||
|
let angle_2 = (self.end - self.start).angle_between(handle_end - self.start);
|
||||||
|
if (angle_1 > 0. && angle_2 < 0.) || (angle_1 < 0. && angle_2 > 0.) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verify the angle formed by the endpoint normals is sufficiently small, ensuring the on-curve point for `t = 0.5` occurs roughly in the center of the polygon.
|
||||||
|
let normal_0 = self.normal(0.);
|
||||||
|
let normal_1 = self.normal(1.);
|
||||||
|
let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).acos();
|
||||||
|
endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split the curve into a number of scalable subcurves. This function may introduce gaps if subsections of the curve are not reducible.
|
||||||
|
/// The function takes the following parameter:
|
||||||
|
/// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`.
|
||||||
|
/// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time.
|
||||||
|
pub fn reduce(&self, step_size: Option<f64>) -> Vec<Bezier> {
|
||||||
|
let step_size = step_size.unwrap_or(REDUCE_STEP_SIZE_DEFAULT);
|
||||||
|
|
||||||
|
let mut extrema: Vec<f64> = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
|
||||||
|
extrema.append(&mut vec![0., 1.]);
|
||||||
|
extrema.dedup();
|
||||||
|
extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap());
|
||||||
|
|
||||||
|
// Split the curve on the extremas. Simplifies procedure for ensuring each curve can be scaled.
|
||||||
|
let mut subcurves = Vec::new();
|
||||||
|
let mut t1: f64 = extrema[0];
|
||||||
|
for t2 in extrema.iter().skip(1) {
|
||||||
|
subcurves.push(self.trim(t1, *t2));
|
||||||
|
t1 = *t2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split each subcurve such that each resulting segment is scalable.
|
||||||
|
let mut result: Vec<Bezier> = Vec::new();
|
||||||
|
subcurves.iter().for_each(|&subcurve| {
|
||||||
|
// Perform no processing on the subcurve if it's already scalable.
|
||||||
|
if subcurve.is_scalable() {
|
||||||
|
result.push(subcurve);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// According to <https://pomax.github.io/bezierinfo/#offsetting>, it is generally sufficient to split subcurves with no local extrema at `t = 0.5` to generate two scalable segments.
|
||||||
|
let [first_half, second_half] = subcurve.split(0.5);
|
||||||
|
if first_half.is_scalable() && second_half.is_scalable() {
|
||||||
|
result.push(first_half);
|
||||||
|
result.push(second_half);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greedily iterate across the subcurve at intervals of size `step_size` to break up the curve into maximally large segments
|
||||||
|
let mut segment: Bezier;
|
||||||
|
let mut t1 = 0.;
|
||||||
|
let mut t2 = step_size;
|
||||||
|
while t2 <= 1. + step_size {
|
||||||
|
segment = subcurve.trim(t1, f64::min(t2, 1.));
|
||||||
|
if !segment.is_scalable() {
|
||||||
|
t2 -= step_size;
|
||||||
|
|
||||||
|
// If the previous step does not exist, the start of the subcurve is irreducible.
|
||||||
|
// Otherwise, add the valid segment from the previous step to the result.
|
||||||
|
if f64::abs(t1 - t2) >= step_size {
|
||||||
|
segment = subcurve.trim(t1, t2);
|
||||||
|
result.push(segment);
|
||||||
|
}
|
||||||
|
t1 = t2;
|
||||||
|
}
|
||||||
|
t2 += step_size;
|
||||||
|
}
|
||||||
|
// Collect final remainder of the curve.
|
||||||
|
if t1 < 1. {
|
||||||
|
segment = subcurve.trim(t1, 1.);
|
||||||
|
if segment.is_scalable() {
|
||||||
|
result.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue