diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index c6f79efa..1c562541 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -98,6 +98,23 @@ fn static_nodes() -> Vec { }, properties: node_properties::number_properties, }, + DocumentNodeDefinition { + identifier: "Percentage Value", + category: "Value", + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::proto("graphene_core::ops::IdentityNode"), + inputs: vec![NodeInput::value(TaggedValue::F64(0.), false)], + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + input_names: vec!["Percentage".to_string()], + output_names: vec!["Out".to_string()], + ..Default::default() + }, + }, + properties: node_properties::percentage_properties, + }, DocumentNodeDefinition { identifier: "Color Value", category: "Value", @@ -115,6 +132,23 @@ fn static_nodes() -> Vec { }, properties: node_properties::color_properties, }, + DocumentNodeDefinition { + identifier: "Gradient Value", + category: "Value", + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::proto("graphene_core::ops::IdentityNode"), + inputs: vec![NodeInput::value(TaggedValue::GradientStops(vector::style::GradientStops::default()), false)], + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + input_names: vec!["Gradient".to_string()], + output_names: vec!["Out".to_string()], + ..Default::default() + }, + }, + properties: node_properties::gradient_properties, + }, DocumentNodeDefinition { identifier: "Vector2 Value", category: "Value", @@ -934,6 +968,46 @@ fn static_nodes() -> Vec { }, properties: node_properties::node_no_properties, }, + DocumentNodeDefinition { + identifier: "Unwrap", + category: "Debug", + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::proto("graphene_core::ops::UnwrapNode"), + inputs: vec![NodeInput::value(TaggedValue::OptionalColor(None), true)], + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + input_names: vec!["Value".to_string()], + output_names: vec!["Value".to_string()], + ..Default::default() + }, + }, + properties: node_properties::node_no_properties, + }, + // TODO: Consolidate this into the regular Blend node once we can make its generic types all compatible, and not break the brush tool which uses that Blend node + DocumentNodeDefinition { + identifier: "Blend Colors", + category: "Raster", + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::proto("graphene_core::raster::BlendColorsNode<_, _, _>"), + inputs: vec![ + NodeInput::value(TaggedValue::Color(Color::TRANSPARENT), true), + NodeInput::value(TaggedValue::Color(Color::TRANSPARENT), true), + NodeInput::value(TaggedValue::BlendMode(BlendMode::Normal), false), + NodeInput::value(TaggedValue::F64(100.), false), + ], + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + input_names: vec!["Over".to_string(), "Under".to_string(), "Blend Mode".to_string(), "Opacity".to_string()], + output_names: vec!["Combined".to_string()], + ..Default::default() + }, + }, + properties: node_properties::blend_color_properties, + }, // TODO: This needs to work with resolution-aware (raster with footprint, post-Cull node) data. DocumentNodeDefinition { identifier: "Blend", @@ -950,7 +1024,7 @@ fn static_nodes() -> Vec { ..Default::default() }, persistent_node_metadata: DocumentNodePersistentMetadata { - input_names: vec!["Image".to_string(), "Second".to_string(), "BlendMode".to_string(), "Opacity".to_string()], + input_names: vec!["Image".to_string(), "Second".to_string(), "Blend Mode".to_string(), "Opacity".to_string()], output_names: vec!["Image".to_string()], ..Default::default() }, diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 17dbfe67..d3128691 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -969,6 +969,14 @@ pub fn black_and_white_properties(document_node: &DocumentNode, node_id: NodeId, ] } +pub fn blend_color_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let under = color_widget(document_node, node_id, 1, "Under", ColorButton::default(), true); + let blend_mode = blend_mode(document_node, node_id, 2, "Blend Mode", true); + let opacity = number_widget(document_node, node_id, 3, "Opacity", NumberInput::default().mode_range().min(0.).max(100.).unit("%"), true); + + vec![under, blend_mode, LayoutGroup::Row { widgets: opacity }] +} + pub fn blend_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let backdrop = color_widget(document_node, node_id, 1, "Backdrop", ColorButton::default(), true); let blend_mode = blend_mode(document_node, node_id, 2, "Blend Mode", true); @@ -983,6 +991,12 @@ pub fn number_properties(document_node: &DocumentNode, node_id: NodeId, _context vec![LayoutGroup::Row { widgets }] } +pub fn percentage_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let widgets = number_widget(document_node, node_id, 0, "Percentage", NumberInput::default().min(0.).max(100.).mode_range(), true); + + vec![LayoutGroup::Row { widgets }] +} + pub fn vector2_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let x = number_widget(document_node, node_id, 1, "X", NumberInput::default(), true); let y = number_widget(document_node, node_id, 2, "Y", NumberInput::default(), true); @@ -1000,6 +1014,10 @@ pub fn color_properties(document_node: &DocumentNode, node_id: NodeId, _context: vec![color_widget(document_node, node_id, 0, "Color", ColorButton::default(), true)] } +pub fn gradient_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + vec![color_widget(document_node, node_id, 0, "Gradient", ColorButton::default().allow_none(false), true)] +} + pub fn load_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let url = text_widget(document_node, node_id, 1, "URL", true); diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index aef5242f..71de1766 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -218,6 +218,14 @@ fn some(input: T) -> Option { Some(input) } +// Unwrap +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct UnwrapNode; +#[node_macro::node_fn(UnwrapNode)] +fn some(input: Option) -> T { + input.unwrap_or_default() +} + // Clone #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct CloneNode(PhantomData); @@ -337,14 +345,27 @@ impl<'i, N: for<'a> Node<'a, I> + Clone, I: 'i> Clone for TypeNode Node<'a, I> + Copy, I: 'i> Copy for TypeNode>::Output> {} +// Map Option +pub struct MapOptionNode { + node: Mn, + _i: PhantomData, +} +#[node_macro::node_fn(MapOptionNode<_I>)] +fn map_option_node<_I, N>(input: Option<_I>, node: &'input N) -> Option<>::Output> +where + N: for<'a> Node<'a, _I>, +{ + input.map(|x| node.eval(x)) +} + // Map Result pub struct MapResultNode { node: Mn, _i: PhantomData, _e: PhantomData, } -#[node_macro::node_fn(MapResultNode<_I, _E>)] -fn flat_map<_I, _E, N>(input: Result<_I, _E>, node: &'input N) -> Result<>::Output, _E> +#[node_macro::node_fn(MapResultNode<_I, _E>)] +fn map_result_node<_I, _E, N>(input: Result<_I, _E>, node: &'input N) -> Result<>::Output, _E> where N: for<'a> Node<'a, _I>, { @@ -359,7 +380,7 @@ pub struct FlatMapResultNode { _e: PhantomData, } #[node_macro::node_fn(FlatMapResultNode<_I, _O, _E>)] -fn flat_map<_I, _O, _E, N>(input: Result<_I, _E>, node: &'input N) -> Result<_O, _E> +fn flat_map_node<_I, _O, _E, N>(input: Result<_I, _E>, node: &'input N) -> Result<_O, _E> where N: for<'a> Node<'a, _I, Output = Result<_O, _E>>, { diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 1d4d6918..323e228f 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -11,6 +11,7 @@ use crate::GraphicGroup; use dyn_any::{DynAny, StaticType}; +use core::cmp::Ordering; use core::fmt::Debug; #[cfg(feature = "serde")] #[cfg(target_arch = "spirv")] @@ -492,6 +493,37 @@ fn threshold_node(color: Color, min_luminance: f64, max_luminance: f64, luminanc } } +#[derive(Debug, Clone, Copy)] +pub struct BlendColorsNode { + under: Under, + blend_mode: BlendMode, + opacity: Opacity, +} + +#[node_macro::node_fn(BlendColorsNode)] +fn blend_node(over: Color, under: Color, blend_mode: BlendMode, opacity: f64) -> Color { + blend_colors(over, under, blend_mode, opacity / 100.) +} + +#[node_macro::node_impl(BlendColorsNode)] +fn blend_colors(over: GradientStops, under: GradientStops, blend_mode: BlendMode, opacity: f64) -> GradientStops { + let mut combined_stops = over.0.iter().map(|(position, _)| position).chain(under.0.iter().map(|(position, _)| position)).collect::>(); + combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6); + combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + + let stops = combined_stops + .into_iter() + .map(|&position| { + let over_color = over.evalute(position); + let under_color = under.evalute(position); + let color = blend_colors(over_color, under_color, blend_mode, opacity / 100.); + (position, color) + }) + .collect::>(); + + GradientStops(stops) +} + #[derive(Debug, Clone, Copy)] pub struct BlendNode { blend_mode: BlendMode, @@ -500,7 +532,7 @@ pub struct BlendNode { #[node_macro::node_fn(BlendNode)] fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Color { - blend_colors(input.0, input.1, blend_mode, opacity as f32 / 100.) + blend_colors(input.0, input.1, blend_mode, opacity / 100.) } pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color { @@ -543,7 +575,7 @@ pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendM } #[inline(always)] -pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color { +pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f64) -> Color { let target_color = match blend_mode { // Other utility blend modes (hidden from the normal list) - do not have alpha blend BlendMode::Erase => return background.alpha_subtract(foreground), @@ -552,7 +584,7 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, blend_mode => apply_blend_mode(foreground, background, blend_mode), }; - background.alpha_blend(target_color.to_associated_alpha(opacity)) + background.alpha_blend(target_color.to_associated_alpha(opacity as f32)) } #[derive(Debug, Clone, Copy)] diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs index 61cfb98e..cf1390c9 100644 --- a/node-graph/gstd/src/brush.rs +++ b/node-graph/gstd/src/brush.rs @@ -234,7 +234,7 @@ macro_rules! inline_blend_funcs { }; } -pub fn blend_with_mode(background: ImageFrame, foreground: ImageFrame, blend_mode: BlendMode, opacity: f32) -> ImageFrame { +pub fn blend_with_mode(background: ImageFrame, foreground: ImageFrame, blend_mode: BlendMode, opacity: f64) -> ImageFrame { let opacity = opacity / 100.; inline_blend_funcs!( background, @@ -349,7 +349,7 @@ async fn brush(image: ImageFrame, bounds: ImageFrame, strokes: Vec } // TODO: Is this the correct way to do opacity in blending? - actual_image = blend_with_mode(actual_image, stroke_texture, stroke.style.blend_mode, stroke.style.color.a() * 100.0); + actual_image = blend_with_mode(actual_image, stroke_texture, stroke.style.blend_mode, (stroke.style.color.a() * 100.) as f64); } let has_erase_strokes = strokes.iter().any(|s| s.style.blend_mode == BlendMode::Erase); diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 119f4d9d..02618ee1 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -142,6 +142,23 @@ macro_rules! raster_node { NodeIOTypes::new(concrete!(Color), concrete!(Color), params) }, ), + // TODO: Remove this one when we have automatic IntoNode insertion as part of the graph compilation process + ( + ProtoNodeIdentifier::new(stringify!($path)), + |args| { + Box::pin(async move { + let node = construct_node!(args, $path, [$(() => $type),*]).await; + let map_node = graphene_core::ops::MapOptionNode::new(graphene_core::value::ValueNode::new(node)); + let node = graphene_std::any::FutureWrapperNode::new(map_node); + let any: DynAnyNode, _, _> = graphene_std::any::DynAnyNode::new(node); + any.into_type_erased() + }) + }, + { + let params = vec![$(fn_type!($type)),*]; + NodeIOTypes::new(concrete!(Option), concrete!(Option), params) + }, + ), ( ProtoNodeIdentifier::new(stringify!($path)), |args| { @@ -271,6 +288,7 @@ fn node_registry() -> HashMap, input: &f64, params: [&f64]), register_node!(graphene_core::ops::ConstructVector2<_, _>, input: (), params: [f64, f64]), register_node!(graphene_core::ops::SomeNode, input: &WasmEditorApi, params: []), + register_node!(graphene_core::ops::UnwrapNode, input: Option, params: []), register_node!(graphene_core::logic::LogToConsoleNode, input: bool, params: []), register_node!(graphene_core::logic::LogToConsoleNode, input: f64, params: []), register_node!(graphene_core::logic::LogToConsoleNode, input: f64, params: []), @@ -455,6 +473,8 @@ fn node_registry() -> HashMap, params: [RedGreenBlueAlpha]), raster_node!(graphene_core::raster::ExtractOpaqueNode<>, params: []), raster_node!(graphene_core::raster::LevelsNode<_, _, _, _, _>, params: [f64, f64, f64, f64, f64]), + raster_node!(graphene_core::raster::BlendColorsNode<_, _, _>, params: [Color, BlendMode, f64]), + register_node!(graphene_core::raster::BlendColorsNode<_, _, _>, input: GradientStops, params: [GradientStops, BlendMode, f64]), register_node!(graphene_std::image_segmentation::ImageSegmentationNode<_>, input: ImageFrame, params: [ImageFrame]), register_node!(graphene_std::image_color_palette::ImageColorPaletteNode<_>, input: ImageFrame, params: [u32]), register_node!(graphene_core::raster::IndexNode<_>, input: Vec>, params: [u32]), @@ -621,6 +641,7 @@ fn node_registry() -> HashMap, input: (), output: graphene_std::SurfaceFrame, params: [graphene_std::SurfaceFrame]), async_node!(graphene_core::memo::MemoNode<_, _>, input: (), output: RenderOutput, params: [RenderOutput]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: Image, fn_params: [Footprint => Image]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: ImageFrame, fn_params: [Footprint => ImageFrame]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: QuantizationChannels, fn_params: [Footprint => QuantizationChannels]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: Vec, fn_params: [Footprint => Vec]),