mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add the Curves adjustment node with a Transfer Curve type and editor widget (#4520)
* Add the Curves adjustment node with a Transfer Curve type and editor widget * Address review feedback on the Transfer Curve widget's edge cases
This commit is contained in:
@@ -274,6 +274,20 @@ impl LayoutMessageHandler {
|
||||
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::TransferCurveInput(curve_input) => {
|
||||
let callback_message = match action {
|
||||
WidgetValueAction::Commit => (curve_input.on_commit.callback)(&()),
|
||||
WidgetValueAction::Update => {
|
||||
let Ok(update) = serde_json::from_value::<TransferCurveInputUpdate>(value) else {
|
||||
warn!("TransferCurveInput update was not able to be parsed as TransferCurveInputUpdate");
|
||||
return;
|
||||
};
|
||||
(curve_input.on_update.callback)(&update)
|
||||
}
|
||||
};
|
||||
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::IconButton(icon_button) => {
|
||||
let callback_message = match action {
|
||||
WidgetValueAction::Commit => (icon_button.on_commit.callback)(&()),
|
||||
@@ -534,6 +548,23 @@ fn populate_computed_display_fields(layout: &mut Layout) {
|
||||
Widget::ColorInput(color_input) => {
|
||||
color_input.chosen_gradient = color_input.value.to_css_background_image();
|
||||
}
|
||||
Widget::TransferCurveInput(curve_input) => {
|
||||
const SAMPLE_COUNT: usize = 128;
|
||||
let curve = graphene_std::transfer_curve::TransferCurve::new(curve_input.points.iter().map(|&(x, y)| glam::DVec2::new(x, y)).collect());
|
||||
let evaluator = curve.evaluator();
|
||||
let [x_min, x_max] = curve_input.domain;
|
||||
let [y_min, y_max] = curve_input.range;
|
||||
let (x_span, y_span) = ((x_max - x_min).max(f64::EPSILON), (y_max - y_min).max(f64::EPSILON));
|
||||
// A spline overshooting the range rides its edge as a flat line, as the clamped adjustment it depicts does
|
||||
let clamp_to_range = curve_input.clamp_to_range;
|
||||
curve_input.samples = (0..=SAMPLE_COUNT)
|
||||
.map(|i| {
|
||||
let t = i as f64 / SAMPLE_COUNT as f64;
|
||||
let y = (evaluator.evaluate(x_min + t * x_span) - y_min) / y_span;
|
||||
(t, if clamp_to_range { y.clamp(0., 1.) } else { y })
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
Widget::SpectrumInput(spectrum_input) => {
|
||||
// The track strip spans exactly 0 to 1, which no spread affects, so the widget carries no spread of its own
|
||||
let settings = graphene_std::vector::style::GradientSettings {
|
||||
|
||||
@@ -471,6 +471,7 @@ impl LayoutGroup {
|
||||
| Widget::ColorComparisonInput(_)
|
||||
| Widget::ColorPresetsInput(_)
|
||||
| Widget::SpectrumInput(_)
|
||||
| Widget::TransferCurveInput(_)
|
||||
| Widget::VisualColorPickersInput(_) => continue,
|
||||
};
|
||||
if val.is_empty() {
|
||||
@@ -808,6 +809,7 @@ pub enum Widget {
|
||||
ColorComparisonInput(ColorComparisonInput),
|
||||
ColorInput(ColorInput),
|
||||
ColorPresetsInput(ColorPresetsInput),
|
||||
TransferCurveInput(TransferCurveInput),
|
||||
DropdownInput(DropdownInput),
|
||||
IconButton(IconButton),
|
||||
IconLabel(IconLabel),
|
||||
@@ -887,6 +889,7 @@ impl DiffUpdate {
|
||||
| Widget::ColorComparisonInput(_)
|
||||
| Widget::ColorPresetsInput(_)
|
||||
| Widget::SpectrumInput(_)
|
||||
| Widget::TransferCurveInput(_)
|
||||
| Widget::VisualColorPickersInput(_) => None,
|
||||
};
|
||||
|
||||
|
||||
@@ -578,6 +578,49 @@ pub enum ColorPresetsInputUpdate {
|
||||
EyedropperColorCode(String),
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct TransferCurveInput {
|
||||
// Content
|
||||
/// The control points in the units of `domain` and `range`, in any x order, since sampling sorts them.
|
||||
#[widget_builder(constructor)]
|
||||
pub points: Vec<(f64, f64)>,
|
||||
/// The x extent the box spans, left to right.
|
||||
pub domain: [f64; 2],
|
||||
/// The y extent the box spans, bottom to top.
|
||||
pub range: [f64; 2],
|
||||
/// Whether the drawn curve and a dragged point's y stay inside `range`. A point's x always stays inside `domain`.
|
||||
#[serde(rename = "clampToRange")]
|
||||
pub clamp_to_range: bool,
|
||||
/// Polyline of the curve in box-normalized 0..1 coordinates with y upward. Auto-populated from `points` at layout-send time.
|
||||
#[widget_builder(skip)]
|
||||
pub samples: Vec<(f64, f64)>,
|
||||
/// Whether clicking empty space inserts a point.
|
||||
#[serde(rename = "allowInsert")]
|
||||
pub allow_insert: bool,
|
||||
/// Whether double-click or right-click removes a point. The handler still has the final say (e.g., enforcing a minimum count).
|
||||
#[serde(rename = "allowDelete")]
|
||||
pub allow_delete: bool,
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TransferCurveInputUpdate>,
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_commit: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransferCurveInputUpdate {
|
||||
MovePoint { index: u32, x: f64, y: f64 },
|
||||
InsertPoint { x: f64, y: f64 },
|
||||
DeletePoint { index: u32 },
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
|
||||
@@ -16,11 +16,13 @@ use graphene_std::list::{Item, List, NodeIdPath};
|
||||
use graphene_std::math::float_noise::round_away_float_noise;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::raster::{
|
||||
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
||||
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
};
|
||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||
use graphene_std::text::TextAlign;
|
||||
use graphene_std::text_nodes::StringCapitalization;
|
||||
use graphene_std::transfer_curve::TransferCurve;
|
||||
use graphene_std::transform::{ReferencePoint, ScaleType};
|
||||
use graphene_std::vector::misc::{
|
||||
ArcType, BezierHandles, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns,
|
||||
@@ -228,6 +230,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
List<GradientInterpolation>,
|
||||
List<DashPattern>,
|
||||
List<BoxCorners>,
|
||||
List<TransferCurve>,
|
||||
List<StrokeJoin>,
|
||||
List<StrokeAlign>,
|
||||
List<StrokeCap>,
|
||||
@@ -240,6 +243,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
List<RedGreenBlueAlpha>,
|
||||
List<RelativeAbsolute>,
|
||||
List<SelectiveColorChoice>,
|
||||
List<AdjustmentChannel>,
|
||||
List<XY>,
|
||||
List<ScaleType>,
|
||||
List<ReferencePoint>,
|
||||
@@ -283,6 +287,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
Item<GradientInterpolation>,
|
||||
Item<DashPattern>,
|
||||
Item<BoxCorners>,
|
||||
Item<TransferCurve>,
|
||||
Item<StrokeJoin>,
|
||||
Item<StrokeAlign>,
|
||||
Item<StrokeCap>,
|
||||
@@ -295,6 +300,7 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
Item<RedGreenBlueAlpha>,
|
||||
Item<RelativeAbsolute>,
|
||||
Item<SelectiveColorChoice>,
|
||||
Item<AdjustmentChannel>,
|
||||
Item<XY>,
|
||||
Item<ScaleType>,
|
||||
Item<ReferencePoint>,
|
||||
@@ -562,6 +568,26 @@ impl TableItemLayout for Coverage {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for TransferCurve {
|
||||
fn type_name() -> &'static str {
|
||||
"Transfer Curve"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
let points = self.points().len();
|
||||
format!("Transfer Curve ({points} {})", if points == 1 { "point" } else { "points" })
|
||||
}
|
||||
// The wrapping `Item` already contributes the breadcrumb; the inner list supplies the next level
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.value_page(data)
|
||||
}
|
||||
fn value_widgets(&self, target: PathStep, data: &LayoutData) -> Vec<WidgetInstance> {
|
||||
self.0.value_widgets(target, data)
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.0.layout_with_breadcrumb(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for BoxCorners {
|
||||
fn type_name() -> &'static str {
|
||||
"BoxCorners"
|
||||
@@ -1044,6 +1070,7 @@ impl_table_item_layout_for_choice_enum!(
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
AdjustmentChannel,
|
||||
XY,
|
||||
ScaleType,
|
||||
CentroidType,
|
||||
@@ -1243,6 +1270,7 @@ macro_rules! known_item_types {
|
||||
Cover,
|
||||
DashPattern,
|
||||
BoxCorners,
|
||||
TransferCurve,
|
||||
BlendMode,
|
||||
GradientForm,
|
||||
GradientSpread,
|
||||
@@ -1261,6 +1289,7 @@ macro_rules! known_item_types {
|
||||
RedGreenBlueAlpha,
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
AdjustmentChannel,
|
||||
XY,
|
||||
ScaleType,
|
||||
ReferencePoint,
|
||||
|
||||
@@ -914,6 +914,7 @@ fn static_node_properties() -> NodeProperties {
|
||||
map.insert("brightness_contrast_properties".to_string(), Box::new(node_properties::brightness_contrast_properties));
|
||||
map.insert("channel_mixer_properties".to_string(), Box::new(node_properties::channel_mixer_properties));
|
||||
map.insert("levels_properties".to_string(), Box::new(node_properties::levels_properties));
|
||||
map.insert("transfer_curves_properties".to_string(), Box::new(node_properties::transfer_curves_properties));
|
||||
map.insert("hue_saturation_properties".to_string(), Box::new(node_properties::hue_saturation_properties));
|
||||
map.insert("black_and_white_properties".to_string(), Box::new(node_properties::black_and_white_properties));
|
||||
map.insert("threshold_properties".to_string(), Box::new(node_properties::threshold_properties));
|
||||
|
||||
@@ -20,12 +20,13 @@ use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::raster::{
|
||||
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
AdjustmentChannel, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
};
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::text::{Font, TextAlign};
|
||||
use graphene_std::text_nodes::StringCapitalization;
|
||||
use graphene_std::transfer_curve::TransferCurve;
|
||||
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
use graphene_std::vector::misc::{
|
||||
@@ -289,6 +290,7 @@ pub(crate) fn property_from_type(
|
||||
// STRUCT TYPES
|
||||
// ============
|
||||
Some(x) if id_is::<Font>(x) => font_widget(default_info),
|
||||
Some(x) if id_is::<TransferCurve>(x) => transfer_curve_widget(default_info),
|
||||
Some(x) if id_is::<Footprint>(x) => footprint_widget(default_info, &mut extra_widgets),
|
||||
Some(x) if id_is::<Box<VectorModification>>(x) => vector_modification_widget(default_info).into(),
|
||||
Some(x) if id_is::<Image<Color>>(x) => image_data_widget(default_info).into(),
|
||||
@@ -316,6 +318,7 @@ pub(crate) fn property_from_type(
|
||||
Some(x) if id_is::<CellularReturnType>(x) => enum_choice::<CellularReturnType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<DomainWarpType>(x) => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<RelativeAbsolute>(x) => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<AdjustmentChannel>(x) => enum_choice::<AdjustmentChannel>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if id_is::<GridType>(x) => enum_choice::<GridType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StrokeCap>(x) => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StrokeJoin>(x) => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
|
||||
@@ -1165,6 +1168,44 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
||||
LayoutGroup::row(widgets)
|
||||
}
|
||||
|
||||
/// A [`TransferCurve`] input's row: the label, then the curve editor spanning the unit square when the input is not exposed.
|
||||
pub fn transfer_curve_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(¶meter_widgets_info);
|
||||
|
||||
let Some(NodeInput::Value { tagged_value, exposed: false }) = parameter_widgets_info.input() else {
|
||||
return LayoutGroup::row(widgets);
|
||||
};
|
||||
let TaggedValue::TransferCurve(points) = &**tagged_value else { return LayoutGroup::row(widgets) };
|
||||
let curve = TransferCurve::from(points.clone());
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets.push(
|
||||
TransferCurveInput::new(curve.points().iter().map(|point| (point.x, point.y)).collect())
|
||||
.domain([0., 1.])
|
||||
.range([0., 1.])
|
||||
.clamp_to_range(true)
|
||||
.allow_insert(true)
|
||||
.allow_delete(true)
|
||||
.on_update(parameter_widgets_info.update_value(move |update: &TransferCurveInputUpdate| {
|
||||
let mut curve = curve.clone();
|
||||
match *update {
|
||||
TransferCurveInputUpdate::MovePoint { index, x, y } => curve.move_point(index as usize, DVec2::new(x, y)),
|
||||
TransferCurveInputUpdate::InsertPoint { x, y } => {
|
||||
curve.insert_point(DVec2::new(x, y));
|
||||
}
|
||||
// A transfer curve keeps at least its two end points
|
||||
TransferCurveInputUpdate::DeletePoint { index } if curve.points().len() > 2 => curve.remove_point(index as usize),
|
||||
TransferCurveInputUpdate::DeletePoint { .. } => {}
|
||||
}
|
||||
TaggedValue::TransferCurve(curve.points().to_vec())
|
||||
}))
|
||||
.on_commit(commit_value)
|
||||
.widget_instance(),
|
||||
);
|
||||
|
||||
LayoutGroup::row(widgets)
|
||||
}
|
||||
|
||||
pub fn font_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
|
||||
let (font_widgets, style_widgets) = font_inputs(parameter_widgets_info);
|
||||
font_widgets.into_iter().chain(style_widgets.unwrap_or_default()).collect::<Vec<_>>().into()
|
||||
@@ -1290,6 +1331,29 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
|
||||
layout
|
||||
}
|
||||
|
||||
pub(crate) fn transfer_curves_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::curves::*;
|
||||
|
||||
let mut channel_info = ParameterWidgetsInfo::new(node_id, ChannelInput, true, context);
|
||||
channel_info.exposable = false;
|
||||
let channel = enum_choice::<AdjustmentChannel>().for_socket(channel_info).property_row();
|
||||
|
||||
let channel_value = match get_document_node(node_id, context).ok().and_then(|document_node| document_node.input_value(ChannelInput).cloned()) {
|
||||
Some(TaggedValue::AdjustmentChannel(channel)) => channel,
|
||||
_ => AdjustmentChannel::Rgb,
|
||||
};
|
||||
let curve_parameter: ParameterRef = match channel_value {
|
||||
AdjustmentChannel::Rgb => CurveInput.into(),
|
||||
AdjustmentChannel::Red => RedCurveInput.into(),
|
||||
AdjustmentChannel::Green => GreenCurveInput.into(),
|
||||
AdjustmentChannel::Blue => BlueCurveInput.into(),
|
||||
AdjustmentChannel::Alpha => AlphaCurveInput.into(),
|
||||
};
|
||||
let transfer_curve = transfer_curve_widget(ParameterWidgetsInfo::new(node_id, curve_parameter, true, context));
|
||||
|
||||
vec![channel, transfer_curve]
|
||||
}
|
||||
|
||||
pub(crate) fn levels_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::levels::*;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user