mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 22:18:12 +08:00
Add Levels adjustment node (#1028)
* Add Levels Node * Fix naming and algorithm --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
+39
@@ -192,6 +192,45 @@ fn static_nodes() -> Vec<DocumentNodeType> {
|
|||||||
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||||
properties: |_document_node, _node_id, _context| node_properties::string_properties("Creates an embedded image with the given transform"),
|
properties: |_document_node, _node_id, _context| node_properties::string_properties("Creates an embedded image with the given transform"),
|
||||||
},
|
},
|
||||||
|
DocumentNodeType {
|
||||||
|
name: "Levels",
|
||||||
|
category: "Image Adjustments",
|
||||||
|
identifier: NodeImplementation::proto("graphene_core::raster::LevelsNode<_, _, _, _, _>"),
|
||||||
|
inputs: vec![
|
||||||
|
DocumentInputType {
|
||||||
|
name: "Image",
|
||||||
|
data_type: FrontendGraphDataType::Raster,
|
||||||
|
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
|
||||||
|
},
|
||||||
|
DocumentInputType {
|
||||||
|
name: "Shadows",
|
||||||
|
data_type: FrontendGraphDataType::Number,
|
||||||
|
default: NodeInput::value(TaggedValue::F64(0.), false),
|
||||||
|
},
|
||||||
|
DocumentInputType {
|
||||||
|
name: "Midtones",
|
||||||
|
data_type: FrontendGraphDataType::Number,
|
||||||
|
default: NodeInput::value(TaggedValue::F64(50.), false),
|
||||||
|
},
|
||||||
|
DocumentInputType {
|
||||||
|
name: "Highlights",
|
||||||
|
data_type: FrontendGraphDataType::Number,
|
||||||
|
default: NodeInput::value(TaggedValue::F64(100.), false),
|
||||||
|
},
|
||||||
|
DocumentInputType {
|
||||||
|
name: "Output Minimums",
|
||||||
|
data_type: FrontendGraphDataType::Number,
|
||||||
|
default: NodeInput::value(TaggedValue::F64(0.), false),
|
||||||
|
},
|
||||||
|
DocumentInputType {
|
||||||
|
name: "Output Maximums",
|
||||||
|
data_type: FrontendGraphDataType::Number,
|
||||||
|
default: NodeInput::value(TaggedValue::F64(100.), false),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||||
|
properties: node_properties::levels_properties,
|
||||||
|
},
|
||||||
DocumentNodeType {
|
DocumentNodeType {
|
||||||
name: "Grayscale",
|
name: "Grayscale",
|
||||||
category: "Image Adjustments",
|
category: "Image Adjustments",
|
||||||
|
|||||||
+16
@@ -199,6 +199,22 @@ pub fn input_properties(_document_node: &DocumentNode, _node_id: NodeId, _contex
|
|||||||
vec![LayoutGroup::Row { widgets: vec![information] }, LayoutGroup::Row { widgets: vec![refresh_button] }]
|
vec![LayoutGroup::Row { widgets: vec![information] }, LayoutGroup::Row { widgets: vec![refresh_button] }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn levels_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
|
let input_shadows = number_widget(document_node, node_id, 1, "Shadows", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||||
|
let input_midtones = number_widget(document_node, node_id, 2, "Midtones", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||||
|
let input_highlights = number_widget(document_node, node_id, 3, "Highlights", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||||
|
let output_minimums = number_widget(document_node, node_id, 4, "Output Minimums", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||||
|
let output_maximums = number_widget(document_node, node_id, 5, "Output Maximums", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||||
|
|
||||||
|
vec![
|
||||||
|
LayoutGroup::Row { widgets: input_shadows },
|
||||||
|
LayoutGroup::Row { widgets: input_midtones },
|
||||||
|
LayoutGroup::Row { widgets: input_highlights },
|
||||||
|
LayoutGroup::Row { widgets: output_minimums },
|
||||||
|
LayoutGroup::Row { widgets: output_maximums },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn grayscale_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub fn grayscale_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
const MIN: f64 = -200.;
|
const MIN: f64 = -200.;
|
||||||
const MAX: f64 = 300.;
|
const MAX: f64 = 300.;
|
||||||
|
|||||||
@@ -51,6 +51,47 @@ fn luminance_color_node(color: Color, luma_calculation: LuminanceCalculation) ->
|
|||||||
color.map_rgb(|_| luminance)
|
color.map_rgb(|_| luminance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct LevelsNode<InputStart, InputMid, InputEnd, OutputStart, OutputEnd> {
|
||||||
|
input_start: InputStart,
|
||||||
|
input_mid: InputMid,
|
||||||
|
input_end: InputEnd,
|
||||||
|
output_start: OutputStart,
|
||||||
|
output_end: OutputEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://stackoverflow.com/questions/39510072/algorithm-for-adjustment-of-image-levels
|
||||||
|
#[node_macro::node_fn(LevelsNode)]
|
||||||
|
fn levels_node(color: Color, input_start: f64, input_mid: f64, input_end: f64, output_start: f64, output_end: f64) -> Color {
|
||||||
|
// Input Range
|
||||||
|
let input_shadows = (input_start / 100.) as f32;
|
||||||
|
let input_midtones = (input_mid / 100.) as f32;
|
||||||
|
let input_highlights = (input_end / 100.) as f32;
|
||||||
|
|
||||||
|
// Output Range
|
||||||
|
let output_minimums = (output_start / 100.) as f32;
|
||||||
|
let output_maximums = (output_end / 100.) as f32;
|
||||||
|
|
||||||
|
// Midtones interpolation factor between minimums and maximums
|
||||||
|
let midtones = output_minimums + (output_maximums - output_minimums) * input_midtones;
|
||||||
|
|
||||||
|
// Gamma correction
|
||||||
|
let gamma = if midtones < 0.5 {
|
||||||
|
(1. + (9. * (1. - midtones * 2.))).min(9.99)
|
||||||
|
} else {
|
||||||
|
((1. - midtones) * 2.).max(0.01)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Input levels
|
||||||
|
let color = color.map_rgb(|channel| (channel - input_shadows) / (input_highlights - input_shadows));
|
||||||
|
|
||||||
|
// Midtones
|
||||||
|
let color = color.map_rgb(|channel| channel.powf(gamma));
|
||||||
|
|
||||||
|
// Output levels
|
||||||
|
color.map_rgb(|channel| channel * (output_maximums - output_minimums) + output_minimums)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub struct GrayscaleNode<Tint, Reds, Yellows, Greens, Cyans, Blues, Magentas> {
|
pub struct GrayscaleNode<Tint, Reds, Yellows, Greens, Cyans, Blues, Magentas> {
|
||||||
tint: Tint,
|
tint: Tint,
|
||||||
|
|||||||
@@ -30,19 +30,26 @@ macro_rules! register_node {
|
|||||||
|args| {
|
|args| {
|
||||||
let mut args = args.clone();
|
let mut args = args.clone();
|
||||||
args.reverse();
|
args.reverse();
|
||||||
let node = <$path>::new($(graphene_std::any::input_node::<$type>(args.pop().expect("not enough arguments provided to construct node"))),*);
|
let node = <$path>::new($(
|
||||||
|
graphene_std::any::input_node::<$type>(
|
||||||
|
args.pop().expect("not enough arguments provided to construct node")
|
||||||
|
)
|
||||||
|
),*);
|
||||||
let any: DynAnyNode<$input, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node));
|
let any: DynAnyNode<$input, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(node));
|
||||||
Box::pin(any)
|
Box::pin(any)
|
||||||
},{
|
},
|
||||||
|
{
|
||||||
let node = IdNode::new().into_type_erased();
|
let node = IdNode::new().into_type_erased();
|
||||||
let node = NodeContainer::new(node, vec![]);
|
let node = NodeContainer::new(node, vec![]);
|
||||||
let _node = unsafe { node.erase_lifetime().static_ref() };
|
let _node = unsafe { node.erase_lifetime().static_ref() };
|
||||||
let node = <$path>::new($(graphene_std::any::input_node::<$type>(_node)),*);
|
let node = <$path>::new($(
|
||||||
|
graphene_std::any::input_node::<$type>(_node)
|
||||||
|
),*);
|
||||||
let params = vec![$(concrete!($type)),*];
|
let params = vec![$(concrete!($type)),*];
|
||||||
let mut node_io = <$path as NodeIO<'_, $input>>::to_node_io(&node, params);
|
let mut node_io = <$path as NodeIO<'_, $input>>::to_node_io(&node, params);
|
||||||
node_io.input = concrete!(<$input as StaticType>::Static);
|
node_io.input = concrete!(<$input as StaticType>::Static);
|
||||||
node_io
|
node_io
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -50,21 +57,24 @@ macro_rules! raster_node {
|
|||||||
($path:ty, params: [$($type:ty),*]) => {
|
($path:ty, params: [$($type:ty),*]) => {
|
||||||
(
|
(
|
||||||
NodeIdentifier::new(stringify!($path)),
|
NodeIdentifier::new(stringify!($path)),
|
||||||
|args| {
|
|args| {
|
||||||
let mut args = args.clone();
|
let mut args = args.clone();
|
||||||
args.reverse();
|
args.reverse();
|
||||||
let node = <$path>::new($(
|
let node = <$path>::new($(
|
||||||
graphene_core::value::ClonedNode::new(
|
graphene_core::value::ClonedNode::new(
|
||||||
graphene_std::any::input_node::<$type>(args.pop().expect("Not enough arguments provided to construct node"))
|
graphene_std::any::input_node::<$type>(
|
||||||
.eval(()))
|
args.pop().expect("Not enough arguments provided to construct node")
|
||||||
),*);
|
).eval(())
|
||||||
let map_node = graphene_std::raster::MapImageNode::new(graphene_core::value::ValueNode::new(node));
|
)
|
||||||
let any: DynAnyNode<Image, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(map_node));
|
),*);
|
||||||
Box::pin(any)
|
let map_node = graphene_std::raster::MapImageNode::new(graphene_core::value::ValueNode::new(node));
|
||||||
}, {
|
let any: DynAnyNode<Image, _, _> = graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(map_node));
|
||||||
let params = vec![$(concrete!($type)),*];
|
Box::pin(any)
|
||||||
NodeIOTypes::new(concrete!(Image), concrete!(Image), params)
|
},
|
||||||
}
|
{
|
||||||
|
let params = vec![$(concrete!($type)),*];
|
||||||
|
NodeIOTypes::new(concrete!(Image), concrete!(Image), params)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -103,6 +113,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
|||||||
),
|
),
|
||||||
// Filters
|
// Filters
|
||||||
raster_node!(graphene_core::raster::LuminanceNode<_>, params: [LuminanceCalculation]),
|
raster_node!(graphene_core::raster::LuminanceNode<_>, params: [LuminanceCalculation]),
|
||||||
|
raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f64, f64, f64, f64, f64]),
|
||||||
raster_node!(graphene_core::raster::GrayscaleNode<_, _, _, _, _, _, _>, params: [Color, f64, f64, f64, f64, f64, f64]),
|
raster_node!(graphene_core::raster::GrayscaleNode<_, _, _, _, _, _, _>, params: [Color, f64, f64, f64, f64, f64, f64]),
|
||||||
raster_node!(graphene_core::raster::HueSaturationNode<_, _, _>, params: [f64, f64, f64]),
|
raster_node!(graphene_core::raster::HueSaturationNode<_, _, _>, params: [f64, f64, f64]),
|
||||||
raster_node!(graphene_core::raster::InvertRGBNode, params: []),
|
raster_node!(graphene_core::raster::InvertRGBNode, params: []),
|
||||||
|
|||||||
Reference in New Issue
Block a user