Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions editor/src/messages/layout/layout_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)(&()),
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions editor/src/messages/layout/utility_types/layout_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ impl LayoutGroup {
| Widget::ColorComparisonInput(_)
| Widget::ColorPresetsInput(_)
| Widget::SpectrumInput(_)
| Widget::TransferCurveInput(_)
| Widget::VisualColorPickersInput(_) => continue,
};
if val.is_empty() {
Expand Down Expand Up @@ -808,6 +809,7 @@ pub enum Widget {
ColorComparisonInput(ColorComparisonInput),
ColorInput(ColorInput),
ColorPresetsInput(ColorPresetsInput),
TransferCurveInput(TransferCurveInput),
DropdownInput(DropdownInput),
IconButton(IconButton),
IconLabel(IconLabel),
Expand Down Expand Up @@ -887,6 +889,7 @@ impl DiffUpdate {
| Widget::ColorComparisonInput(_)
| Widget::ColorPresetsInput(_)
| Widget::SpectrumInput(_)
| Widget::TransferCurveInput(_)
| Widget::VisualColorPickersInput(_) => None,
};

Expand Down
43 changes: 43 additions & 0 deletions editor/src/messages/layout/utility_types/widgets/input_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>,
Expand All @@ -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>,
Expand Down Expand Up @@ -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>,
Expand All @@ -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>,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -1044,6 +1070,7 @@ impl_table_item_layout_for_choice_enum!(
RedGreenBlueAlpha,
RelativeAbsolute,
SelectiveColorChoice,
AdjustmentChannel,
XY,
ScaleType,
CentroidType,
Expand Down Expand Up @@ -1243,6 +1270,7 @@ macro_rules! known_item_types {
Cover,
DashPattern,
BoxCorners,
TransferCurve,
BlendMode,
GradientForm,
GradientSpread,
Expand All @@ -1261,6 +1289,7 @@ macro_rules! known_item_types {
RedGreenBlueAlpha,
RelativeAbsolute,
SelectiveColorChoice,
AdjustmentChannel,
XY,
ScaleType,
ReferencePoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(&parameter_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)
Comment thread
Keavon marked this conversation as resolved.
.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()
Expand Down Expand Up @@ -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::*;

Expand Down
11 changes: 11 additions & 0 deletions frontend/src/components/widgets/WidgetSpan.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import SpectrumInput from "/src/components/widgets/inputs/SpectrumInput.svelte";
import TextAreaInput from "/src/components/widgets/inputs/TextAreaInput.svelte";
import TextInput from "/src/components/widgets/inputs/TextInput.svelte";
import TransferCurveInput from "/src/components/widgets/inputs/TransferCurveInput.svelte";
import VisualColorPickersInput from "/src/components/widgets/inputs/VisualColorPickersInput.svelte";
import WorkingColorsInput from "/src/components/widgets/inputs/WorkingColorsInput.svelte";
import IconLabel from "/src/components/widgets/labels/IconLabel.svelte";
Expand Down Expand Up @@ -232,6 +233,16 @@
$$events: { value: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
}),
},
TransferCurveInput: {
component: TransferCurveInput,
getProps: (props, index) => ({
...props,
$$events: {
update: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
commit: () => widgetValueCommit(index, undefined),
},
}),
},
SpectrumInput: {
component: SpectrumInput,
getProps: (props, index) => ({
Expand Down
Loading
Loading