mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
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:
@@ -123,6 +123,12 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
|
||||
let callback_message = (color_input.on_update.callback)(color_input);
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::CurveInput(curve_input) => {
|
||||
let curve = serde_json::from_value(value).expect("CurveInput event data could not be deserialized");
|
||||
curve_input.value = curve;
|
||||
let callback_message = (curve_input.on_update.callback)(curve_input);
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::DropdownInput(dropdown_input) => {
|
||||
let update_value = value.as_u64().expect("DropdownInput update was not of type: u64");
|
||||
dropdown_input.selected_index = Some(update_value as u32);
|
||||
|
||||
@@ -281,6 +281,7 @@ impl LayoutGroup {
|
||||
let val = match &mut widget.widget {
|
||||
Widget::CheckboxInput(x) => &mut x.tooltip,
|
||||
Widget::ColorInput(x) => &mut x.tooltip,
|
||||
Widget::CurveInput(x) => &mut x.tooltip,
|
||||
Widget::DropdownInput(x) => &mut x.tooltip,
|
||||
Widget::FontInput(x) => &mut x.tooltip,
|
||||
Widget::IconButton(x) => &mut x.tooltip,
|
||||
@@ -430,6 +431,7 @@ pub enum Widget {
|
||||
BreadcrumbTrailButtons(BreadcrumbTrailButtons),
|
||||
CheckboxInput(CheckboxInput),
|
||||
ColorInput(ColorInput),
|
||||
CurveInput(CurveInput),
|
||||
DropdownInput(DropdownInput),
|
||||
FontInput(FontInput),
|
||||
IconButton(IconButton),
|
||||
@@ -512,6 +514,7 @@ impl DiffUpdate {
|
||||
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::IconLabel(_)
|
||||
| Widget::CurveInput(_)
|
||||
| Widget::InvisibleStandinInput(_)
|
||||
| Widget::PivotAssist(_)
|
||||
| Widget::RadioInput(_)
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
||||
use document_legacy::LayerId;
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::raster::curve::Curve;
|
||||
use graphite_proc_macros::WidgetBuilder;
|
||||
|
||||
use derivative::*;
|
||||
@@ -414,3 +415,19 @@ pub struct TextInput {
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder, specta::Type)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct CurveInput {
|
||||
#[widget_builder(constructor)]
|
||||
pub value: Curve,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<CurveInput>,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
||||
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use document_legacy::document::Document;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::{value::TaggedValue, NodeInput};
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
|
||||
pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64, DVec2, DVec2) {
|
||||
let x_axis = transform.matrix2.x_axis;
|
||||
|
||||
@@ -1524,6 +1524,18 @@ fn static_nodes() -> Vec<DocumentNodeType> {
|
||||
properties: node_properties::brightness_contrast_properties,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeType {
|
||||
name: "Curves",
|
||||
category: "Image Adjustments",
|
||||
identifier: NodeImplementation::proto("graphene_core::raster::CurvesNode<_>"),
|
||||
inputs: vec![
|
||||
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
|
||||
DocumentInputType::value("Curve", TaggedValue::Curve(Default::default()), false),
|
||||
],
|
||||
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||
properties: node_properties::curves_properties,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeType {
|
||||
name: "Threshold",
|
||||
category: "Image Adjustments",
|
||||
|
||||
@@ -589,6 +589,25 @@ fn color_widget(document_node: &DocumentNode, node_id: u64, index: usize, name:
|
||||
}
|
||||
LayoutGroup::Row { widgets }
|
||||
}
|
||||
|
||||
fn curves_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
|
||||
if let NodeInput::Value {
|
||||
tagged_value: TaggedValue::Curve(curve),
|
||||
exposed: false,
|
||||
} = &document_node.inputs[index]
|
||||
{
|
||||
widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
CurveInput::new(curve.clone())
|
||||
.on_update(update_value(|x: &CurveInput| TaggedValue::Curve(x.value.clone()), node_id, index))
|
||||
.widget_holder(),
|
||||
])
|
||||
}
|
||||
LayoutGroup::Row { widgets }
|
||||
}
|
||||
|
||||
/// Properties for the input node, with information describing how frames work and a refresh button
|
||||
pub fn input_properties(_document_node: &DocumentNode, _node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let information = TextLabel::new("The graph's input frame is the rasterized artwork under the layer").widget_holder();
|
||||
@@ -777,6 +796,12 @@ pub fn brightness_contrast_properties(document_node: &DocumentNode, node_id: Nod
|
||||
]
|
||||
}
|
||||
|
||||
pub fn curves_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let curves = curves_widget(document_node, node_id, 1, "Curve", true);
|
||||
|
||||
vec![curves]
|
||||
}
|
||||
|
||||
pub fn _blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let radius = number_widget(document_node, node_id, 1, "Radius", NumberInput::default().min(0.).max(20.).int(), true);
|
||||
let sigma = number_widget(document_node, node_id, 2, "Sigma", NumberInput::default().min(0.).max(10000.), true);
|
||||
|
||||
@@ -3,11 +3,11 @@ use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use bezier_rs::{Bezier, TValue};
|
||||
use document_legacy::document::Document;
|
||||
use document_legacy::LayerId;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::{ManipulatorPointId, SelectedType, VectorData};
|
||||
|
||||
use document_legacy::document::Document;
|
||||
use glam::DVec2;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
Reference in New Issue
Block a user