Curves image adjustment node (#1214)

* Create ValueMapperNode and use it for brightness/contrast

* move spline code into seperate module

* Add GenerateCurvesNode

* add a `LuminanceMut`-trait
* add `lerp` to `Channel`

* Add frontend code to handle the curves widget's inputs

* Rename spline module to curve

* Make messages in CurveInput pass

* Improve curves widget design and fix sizing issue

* Implement proper bezier handling

* Use bezier_rs's intersections function instead of own cubic root solver

* Debounce CurveInput events and change how debouncer works

the first event issued to the debouncer was unneccessarily delayed.
Instead now the debouncer fires it instantaneously but blocks events
that come in until a timeout was reached.

* Make curve editing more user friendly

* Change code to use project terminology

* sample -> manipulator group or manipulator
* marker -> handle

* Fix small documentation mistake in bezier-rs

* Add find_tvalues_for_x function to bezier-rs

also integrate the function into curves node

* Add tests for find_tvalues_for_x in bezier-rs

* Fix formatting

* Revert BrightnessContrastNode changes

* Frontend cleanup

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
nat-rix
2023-08-13 10:07:11 +02:00
committed by GitHub
parent cfe38c6413
commit dc4b16aead
29 changed files with 822 additions and 75 deletions

View File

@@ -4,7 +4,7 @@ use std::fmt::Write;
/// Functionality relating to core `Bezier` operations, such as constructors and `abs_diff_eq`.
impl Bezier {
// TODO: Consider removing this function
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
/// Create a linear bezier using the provided coordinates as the start and end points.
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
@@ -13,7 +13,7 @@ impl Bezier {
}
}
/// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points.
/// Create a linear bezier using the provided DVec2s as the start and end points.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/constructor/solo" title="Constructor Demo"></iframe>
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
Bezier {

View File

@@ -164,6 +164,40 @@ impl Bezier {
min_corner.x <= bounding_box_min.x && min_corner.y <= bounding_box_min.y && bounding_box_max.x <= max_corner.x && bounding_box_max.y <= max_corner.y
}
/// Returns an `Iterator` containing all possible parametric `t`-values at the given `x`-coordinate.
pub fn find_tvalues_for_x(&self, x: f64) -> impl Iterator<Item = f64> {
// Compute the roots of the resulting bezier curve
match self.handles {
BezierHandles::Linear => {
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
let a = self.end.x - self.start.x;
let b = self.start.x - x;
utils::solve_linear(a, b)
}
BezierHandles::Quadratic { handle } => {
let a = self.start.x - 2. * handle.x + self.end.x;
let b = 2. * (handle.x - self.start.x);
let c = self.start.x - x;
let discriminant = b * b - 4. * a * c;
let two_times_a = 2. * a;
utils::solve_quadratic(discriminant, two_times_a, b, c)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let start_x = self.start.x;
let a = -start_x + 3. * handle_start.x - 3. * handle_end.x + self.end.x;
let b = 3. * start_x - 6. * handle_start.x + 3. * handle_end.x;
let c = -3. * start_x + 3. * handle_start.x;
let d = start_x - x;
utils::solve_cubic(a, b, c, d)
}
}
.into_iter()
.filter(|&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns list of `t`-values representing the inflection points of the curve.
/// The inflection points are defined to be points at which the second derivative of the curve is equal to zero.
@@ -281,54 +315,24 @@ impl Bezier {
if other.handles == BezierHandles::Linear {
// Rotate the bezier and the line by the angle that the line makes with the x axis
let line_directional_vector = other.end - other.start;
let angle = line_directional_vector.angle_between(DVec2::new(1., 0.));
let angle = line_directional_vector.angle_between(DVec2::new(0., 1.));
let rotation_matrix = DMat2::from_angle(angle);
let rotated_bezier = self.apply_transformation(|point| rotation_matrix.mul_vec2(point));
let rotated_line = [rotation_matrix.mul_vec2(other.start), rotation_matrix.mul_vec2(other.end)];
// Translate the bezier such that the line becomes aligned on top of the x-axis
let vertical_distance = rotated_line[0].y;
let translated_bezier = rotated_bezier.translate(DVec2::new(0., -vertical_distance));
let vertical_distance = rotated_line[0].x;
let translated_bezier = rotated_bezier.translate(DVec2::new(-vertical_distance, 0.));
// Compute the roots of the resulting bezier curve
let list_intersection_t = match translated_bezier.handles {
BezierHandles::Linear => {
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
let a = translated_bezier.end.y - translated_bezier.start.y;
let b = translated_bezier.start.y;
utils::solve_linear(a, b)
}
BezierHandles::Quadratic { handle } => {
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
let b = 2. * (handle.y - translated_bezier.start.y);
let c = translated_bezier.start.y;
let discriminant = b * b - 4. * a * c;
let two_times_a = 2. * a;
utils::solve_quadratic(discriminant, two_times_a, b, c)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let start_y = translated_bezier.start.y;
let a = -start_y + 3. * handle_start.y - 3. * handle_end.y + translated_bezier.end.y;
let b = 3. * start_y - 6. * handle_start.y + 3. * handle_end.y;
let c = -3. * start_y + 3. * handle_start.y;
let d = start_y;
utils::solve_cubic(a, b, c, d)
}
};
let list_intersection_t = translated_bezier.find_tvalues_for_x(0.);
let min = other.start.min(other.end);
let max = other.start.max(other.end);
return list_intersection_t
.into_iter()
// Accept the t value if it is approximately in [0, 1] and if the corresponding coordinates are within the range of the linear line
.filter(|&t| {
utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE)
&& utils::dvec2_approximately_in_range(self.unrestricted_parametric_evaluate(t), min, max, MAX_ABSOLUTE_DIFFERENCE).all()
})
.filter(|&t| utils::dvec2_approximately_in_range(self.unrestricted_parametric_evaluate(t), min, max, MAX_ABSOLUTE_DIFFERENCE).all())
// Ensure the returned value is within the correct range
.map(|t| t.clamp(0., 1.))
.collect::<Vec<f64>>();
@@ -724,6 +728,62 @@ mod tests {
));
}
#[test]
fn test_find_tvalues_for_x() {
struct Assertion {
bezier: Bezier,
x: f64,
ys: &'static [f64],
}
let assertions = [
Assertion {
bezier: Bezier::from_linear_coordinates(0., 0., 20., 10.),
x: 5.,
ys: &[2.5],
},
Assertion {
bezier: Bezier::from_quadratic_coordinates(0., 0., 10., 5., 20., 10.),
x: 5.,
ys: &[2.5],
},
Assertion {
bezier: Bezier::from_cubic_coordinates(0., 0., 10., 5., 10., 5., 20., 10.),
x: 5.,
ys: &[2.5],
},
Assertion {
bezier: Bezier::from_cubic_coordinates(90., 70., 25., 25., 175., 175., 110., 130.),
x: 100.,
ys: &[100.],
},
Assertion {
bezier: Bezier::from_cubic_coordinates(90., 70., 25., 25., 175., 175., 110., 130.),
x: 80.,
ys: &[63.62683, 74.53867],
},
Assertion {
bezier: Bezier::from_cubic_coordinates(110., 70., 25., 25., 175., 175., 90., 130.),
x: 100.,
ys: &[65.11345, 100., 134.88655],
},
];
for Assertion { bezier, x, ys } in assertions {
let mut got: Vec<f64> = bezier
.find_tvalues_for_x(x)
.map(|t| bezier.evaluate(TValue::Parametric(t)))
.inspect(|p| assert!((p.x - x).abs() < 1e-4, "wrong x-coordinate, got {} expected {x}", p.x))
.map(|p| p.y)
.collect();
assert_eq!(got.len(), ys.len());
got.sort_by(f64::total_cmp);
got.into_iter()
.zip(ys)
.for_each(|(got, &expected)| assert!((got - expected).abs() < 1e-4, "wrong y-coordinate, got {got} expected {expected}"));
}
}
#[test]
fn test_inflections() {
let bezier = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.);