From 9f0ea35d9b13ca08637c3c2babb9976852e5584f Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 17 Dec 2023 02:06:25 -0800 Subject: [PATCH] New node: Noise Pattern (#1518) Add the Noise Pattern node Closes #1517 --- Cargo.lock | 10 + Cargo.toml | 1 + .../document_node_types.rs | 33 +- .../node_properties.rs | 343 +++++++++++++++--- .../widgets/inputs/NumberInput.svelte | 21 +- node-graph/gcore/src/raster/adjustments.rs | 162 ++++++++- node-graph/graph-craft/src/document/value.rs | 139 ++++--- node-graph/gstd/Cargo.toml | 1 + node-graph/gstd/src/raster.rs | 169 ++++++++- .../interpreted-executor/src/node_registry.rs | 4 +- 10 files changed, 749 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fdaab1c..83696e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,6 +1461,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastnoise-lite" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2b3d9ffd3c9e98a5fc12f58ac3f7c48943dac14c6638e96db09ac77880fa9a" +dependencies = [ + "num-traits", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2218,6 +2227,7 @@ dependencies = [ "bytemuck", "compilation-client", "dyn-any", + "fastnoise-lite", "futures", "glam", "gpu-compiler-bin-wrapper", diff --git a/Cargo.toml b/Cargo.toml index 9329dfe6..7cbed1b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ quote = "1.0" axum = "0.6" chrono = "^0.4.23" ron = "0.8" +fastnoise-lite = "1.1.0" wgpu-types = "0.17" wgpu = "0.17" wasm-bindgen-futures = { version = "0.4.36" } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 5e49f40e..2505b1cb 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -11,7 +11,10 @@ use graph_craft::ProtoNodeIdentifier; #[cfg(feature = "gpu")] use graphene_core::application_io::SurfaceHandle; use graphene_core::raster::brush_cache::BrushCache; -use graphene_core::raster::{BlendMode, Color, Image, ImageFrame, LuminanceCalculation, NoiseType, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice}; +use graphene_core::raster::{ + BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, Image, ImageFrame, LuminanceCalculation, NoiseType, RedGreenBlue, RelativeAbsolute, + SelectiveColorChoice, +}; use graphene_core::text::Font; use graphene_core::transform::Footprint; use graphene_core::vector::VectorData; @@ -567,17 +570,33 @@ fn static_nodes() -> Vec { ..Default::default() }, DocumentNodeDefinition { - name: "Pixel Noise", + name: "Noise Pattern", category: "General", - implementation: NodeImplementation::proto("graphene_std::raster::PixelNoiseNode<_, _, _>"), + implementation: NodeImplementation::proto("graphene_std::raster::NoisePatternNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _>"), inputs: vec![ - DocumentInputType::value("Width", TaggedValue::U32(100), false), - DocumentInputType::value("Height", TaggedValue::U32(100), false), + DocumentInputType::value("None", TaggedValue::None, false), + // All + DocumentInputType::value("Dimensions", TaggedValue::UVec2((512, 512).into()), false), DocumentInputType::value("Seed", TaggedValue::U32(0), false), - DocumentInputType::value("Noise Type", TaggedValue::NoiseType(NoiseType::WhiteNoise), false), + DocumentInputType::value("Scale", TaggedValue::F32(10.), false), + DocumentInputType::value("Noise Type", TaggedValue::NoiseType(NoiseType::Perlin), false), + // Domain Warp + DocumentInputType::value("Domain Warp Type", TaggedValue::DomainWarpType(DomainWarpType::None), false), + DocumentInputType::value("Domain Warp Amplitude", TaggedValue::F32(100.), false), + // Fractal + DocumentInputType::value("Fractal Type", TaggedValue::FractalType(FractalType::None), false), + DocumentInputType::value("Fractal Octaves", TaggedValue::U32(3), false), + DocumentInputType::value("Fractal Lacunarity", TaggedValue::F32(2.), false), + DocumentInputType::value("Fractal Gain", TaggedValue::F32(0.5), false), + DocumentInputType::value("Fractal Weighted Strength", TaggedValue::F32(0.), false), // 0-1 range + DocumentInputType::value("Fractal Ping Pong Strength", TaggedValue::F32(2.), false), + // Cellular + DocumentInputType::value("Cellular Distance Function", TaggedValue::CellularDistanceFunction(CellularDistanceFunction::Euclidean), false), + DocumentInputType::value("Cellular Return Type", TaggedValue::CellularReturnType(CellularReturnType::Nearest), false), + DocumentInputType::value("Cellular Jitter", TaggedValue::F32(1.), false), ], outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)], - properties: node_properties::pixel_noise_properties, + properties: node_properties::noise_pattern_properties, ..Default::default() }, DocumentNodeDefinition { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index f047500c..6d5d278a 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -11,11 +11,13 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus}; use graphene_core::memo::IORecord; -use graphene_core::raster::{BlendMode, Color, ImageFrame, LuminanceCalculation, NoiseType, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice}; +use graphene_core::raster::{ + BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, ImageFrame, LuminanceCalculation, NoiseType, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice, +}; use graphene_core::text::Font; use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin}; -use glam::{DVec2, IVec2}; +use glam::{DVec2, IVec2, UVec2}; pub fn string_properties(text: impl Into) -> Vec { let widget = TextLabel::new(text).widget_holder(); @@ -129,57 +131,84 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name widgets } -fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, x: &str, y: &str, unit: &str, mut assist: impl FnMut(&mut Vec)) -> LayoutGroup { +fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, x: &str, y: &str, unit: &str, min: Option, mut assist: impl FnMut(&mut Vec)) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, false); assist(&mut widgets); if let NodeInput::Value { - tagged_value: TaggedValue::DVec2(vec2), + tagged_value: TaggedValue::DVec2(dvec2), exposed: false, } = document_node.inputs[index] { widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(vec2.x)) + NumberInput::new(Some(dvec2.x)) .label(x) .unit(unit) - .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .min(min.unwrap_or(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))) .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) - .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index)) + .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), dvec2.y)), node_id, index)) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(vec2.y)) + NumberInput::new(Some(dvec2.y)) .label(y) .unit(unit) - .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .min(min.unwrap_or(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))) .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) - .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index)) + .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(dvec2.x, input.value.unwrap())), node_id, index)) .widget_holder(), ]); } else if let NodeInput::Value { - tagged_value: TaggedValue::IVec2(vec2), + tagged_value: TaggedValue::IVec2(ivec2), exposed: false, } = document_node.inputs[index] { - let update_x = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(input.value.unwrap() as i32, vec2.y)); - let update_y = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(vec2.x, input.value.unwrap() as i32)); + let update_x = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(input.value.unwrap() as i32, ivec2.y)); + let update_y = move |input: &NumberInput| TaggedValue::IVec2(IVec2::new(ivec2.x, input.value.unwrap() as i32)); widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(vec2.x as f64)) + NumberInput::new(Some(ivec2.x as f64)) .int() .label(x) .unit(unit) - .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .min(min.unwrap_or(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))) .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) .on_update(update_value(update_x, node_id, index)) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(vec2.y as f64)) + NumberInput::new(Some(ivec2.y as f64)) .int() .label(y) .unit(unit) - .min(-((1u64 << std::f64::MANTISSA_DIGITS) as f64)) + .min(min.unwrap_or(-((1u64 << std::f64::MANTISSA_DIGITS) as f64))) + .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) + .on_update(update_value(update_y, node_id, index)) + .widget_holder(), + ]); + } else if let NodeInput::Value { + tagged_value: TaggedValue::UVec2(uvec2), + exposed: false, + } = document_node.inputs[index] + { + let update_x = move |input: &NumberInput| TaggedValue::UVec2(UVec2::new(input.value.unwrap() as u32, uvec2.y)); + let update_y = move |input: &NumberInput| TaggedValue::UVec2(UVec2::new(uvec2.x, input.value.unwrap() as u32)); + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + NumberInput::new(Some(uvec2.x as f64)) + .int() + .label(x) + .unit(unit) + .min(min.unwrap_or(0.)) + .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) + .on_update(update_value(update_x, node_id, index)) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(Some(uvec2.y as f64)) + .int() + .label(y) + .unit(unit) + .min(min.unwrap_or(0.)) .max((1u64 << std::f64::MANTISSA_DIGITS) as f64) .on_update(update_value(update_y, node_id, index)) .widget_holder(), @@ -323,7 +352,7 @@ fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, na widgets } -//TODO Use generalized Version of this as soon as it's available +//TODO Generalize this instead of using a separate function per dropdown menu enum fn color_channel(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 { @@ -346,30 +375,117 @@ fn color_channel(document_node: &DocumentNode, node_id: u64, index: usize, name: LayoutGroup::Row { widgets }.with_tooltip("Color Channel") } -//TODO Use generalized Version of this as soon as it's available +// TODO Generalize this instead of using a separate function per dropdown menu enum fn noise_type(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::NoiseType(calculation), + tagged_value: TaggedValue::NoiseType(noise_type), exposed: false, } = &document_node.inputs[index] { - let calculation_modes = NoiseType::list(); - let mut entries = Vec::with_capacity(calculation_modes.len()); - for method in calculation_modes { - entries.push(MenuListEntry::new(method.to_string()).on_update(update_value(move |_| TaggedValue::NoiseType(method), node_id, index))); - } - let entries = vec![entries]; + let entries = NoiseType::list() + .iter() + .map(|noise_type| MenuListEntry::new(noise_type.to_string()).on_update(update_value(move |_| TaggedValue::NoiseType(*noise_type), node_id, index))) + .collect(); widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(entries).selected_index(Some(calculation as u32)).widget_holder(), + DropdownInput::new(vec![entries]).selected_index(Some(noise_type as u32)).widget_holder(), ]); } - LayoutGroup::Row { widgets }.with_tooltip("Type of Noise") + LayoutGroup::Row { widgets }.with_tooltip("Style of noise pattern") } -// TODO: Use generalized version of this as soon as it's available +// TODO Generalize this instead of using a separate function per dropdown menu enum +fn fractal_type(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::FractalType(fractal_type), + exposed: false, + } = &document_node.inputs[index] + { + let entries = FractalType::list() + .iter() + .map(|fractal_type| MenuListEntry::new(fractal_type.to_string()).on_update(update_value(move |_| TaggedValue::FractalType(*fractal_type), node_id, index))) + .collect(); + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + DropdownInput::new(vec![entries]).selected_index(Some(fractal_type as u32)).disabled(disabled).widget_holder(), + ]); + } + LayoutGroup::Row { widgets }.with_tooltip("Style of layered levels of the noise pattern") +} + +// TODO Generalize this instead of using a separate function per dropdown menu enum +fn cellular_distance_function(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::CellularDistanceFunction(cellular_distance_function), + exposed: false, + } = &document_node.inputs[index] + { + let entries = CellularDistanceFunction::list() + .iter() + .map(|cellular_distance_function| { + MenuListEntry::new(cellular_distance_function.to_string()).on_update(update_value(move |_| TaggedValue::CellularDistanceFunction(*cellular_distance_function), node_id, index)) + }) + .collect(); + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + DropdownInput::new(vec![entries]) + .selected_index(Some(cellular_distance_function as u32)) + .disabled(disabled) + .widget_holder(), + ]); + } + LayoutGroup::Row { widgets }.with_tooltip("Distance function used by the cellular noise") +} + +// TODO Generalize this instead of using a separate function per dropdown menu enum +fn cellular_return_type(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::CellularReturnType(cellular_return_type), + exposed: false, + } = &document_node.inputs[index] + { + let entries = CellularReturnType::list() + .iter() + .map(|cellular_return_type| MenuListEntry::new(cellular_return_type.to_string()).on_update(update_value(move |_| TaggedValue::CellularReturnType(*cellular_return_type), node_id, index))) + .collect(); + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + DropdownInput::new(vec![entries]).selected_index(Some(cellular_return_type as u32)).disabled(disabled).widget_holder(), + ]); + } + LayoutGroup::Row { widgets }.with_tooltip("Return type of the cellular noise") +} + +// TODO Generalize this instead of using a separate function per dropdown menu enum +fn domain_warp_type(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool, disabled: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + if let &NodeInput::Value { + tagged_value: TaggedValue::DomainWarpType(domain_warp_type), + exposed: false, + } = &document_node.inputs[index] + { + let entries = DomainWarpType::list() + .iter() + .map(|domain_warp_type| MenuListEntry::new(domain_warp_type.to_string()).on_update(update_value(move |_| TaggedValue::DomainWarpType(*domain_warp_type), node_id, index))) + .collect(); + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + DropdownInput::new(vec![entries]).selected_index(Some(domain_warp_type as u32)).disabled(disabled).widget_holder(), + ]); + } + LayoutGroup::Row { widgets }.with_tooltip("Type of domain warp") +} + +// TODO: Generalize this instead of using a separate function per dropdown menu enum fn blend_mode(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 { @@ -803,17 +919,158 @@ pub fn extract_channel_properties(document_node: &DocumentNode, node_id: NodeId, // Noise Type is commented out for now as there is only one type of noise (White Noise). // As soon as there are more types of noise, this should be uncommented. -pub fn pixel_noise_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let width = number_widget(document_node, node_id, 0, "Width", NumberInput::default().unit("px").min(1.), true); - let height = number_widget(document_node, node_id, 1, "Height", NumberInput::default().unit("px").min(1.), true); - let seed = number_widget(document_node, node_id, 2, "Seed", NumberInput::default().min(0.), true); - let _noise_type = noise_type(document_node, node_id, 3, "Noise Type", true); +pub fn noise_pattern_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + // Get the current values of the inputs of interest so they can set whether certain inputs are disabled based on various conditions. + let current_noise_type = match &document_node.inputs[4] { + NodeInput::Value { + tagged_value: TaggedValue::NoiseType(noise_type), + .. + } => Some(*noise_type), + _ => None, + }; + let current_domain_warp_type = match &document_node.inputs[5] { + NodeInput::Value { + tagged_value: TaggedValue::DomainWarpType(domain_warp_type), + .. + } => Some(*domain_warp_type), + _ => None, + }; + let current_fractal_type = match &document_node.inputs[7] { + NodeInput::Value { + tagged_value: TaggedValue::FractalType(fractal_type), + .. + } => Some(*fractal_type), + _ => None, + }; + let fractal_active = current_fractal_type != Some(FractalType::None); + let coherent_noise_active = current_noise_type != Some(NoiseType::WhiteNoise); + let cellular_noise_active = current_noise_type == Some(NoiseType::Cellular); + let ping_pong_active = current_fractal_type == Some(FractalType::PingPong); + let domain_warp_active = current_domain_warp_type != Some(DomainWarpType::None); + let domain_warp_only_fractal_type_wrongly_active = + !domain_warp_active && (current_fractal_type == Some(FractalType::DomainWarpIndependent) || current_fractal_type == Some(FractalType::DomainWarpProgressive)); + + // All + let dimensions = vec2_widget(document_node, node_id, 1, "Dimensions", "W", "H", "px", Some(1.), add_blank_assist); + let seed = number_widget(document_node, node_id, 2, "Seed", NumberInput::default().min(0.).is_integer(true), true); + let scale = number_widget(document_node, node_id, 3, "Scale", NumberInput::default().min(0.).disabled(!coherent_noise_active), true); + let noise_type_row = noise_type(document_node, node_id, 4, "Noise Type", true); + + // Domain Warp + let domain_warp_type_row = domain_warp_type(document_node, node_id, 5, "Domain Warp Type", true, !coherent_noise_active); + let domain_warp_amplitude = number_widget( + document_node, + node_id, + 6, + "Domain Warp Amplitude", + NumberInput::default().min(0.).disabled(!coherent_noise_active || !domain_warp_active), + true, + ); + + // Fractal + let fractal_type_row = fractal_type(document_node, node_id, 7, "Fractal Type", true, !coherent_noise_active); + let fractal_octaves = number_widget( + document_node, + node_id, + 8, + "Fractal Octaves", + NumberInput::default() + .mode_range() + .min(1.) + .max(10.) + .range_max(Some(4.)) + .is_integer(true) + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + let fractal_lacunarity = number_widget( + document_node, + node_id, + 9, + "Fractal Lacunarity", + NumberInput::default() + .mode_range() + .min(0.) + .range_max(Some(10.)) + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + let fractal_gain = number_widget( + document_node, + node_id, + 10, + "Fractal Gain", + NumberInput::default() + .mode_range() + .min(0.) + .range_max(Some(10.)) + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + let fractal_weighted_strength = number_widget( + document_node, + node_id, + 11, + "Fractal Weighted Strength", + NumberInput::default() + .mode_range() + .min(0.) + .max(1.) // Defined for the 0-1 range + .disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + let fractal_ping_pong_strength = number_widget( + document_node, + node_id, + 12, + "Fractal Ping Pong Strength", + NumberInput::default() + .mode_range() + .min(0.) + .range_max(Some(10.)) + .disabled(!ping_pong_active || !coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), + true, + ); + + // Cellular + let cellular_distance_function_row = cellular_distance_function(document_node, node_id, 13, "Cellular Distance Function", true, !coherent_noise_active || !cellular_noise_active); + let cellular_return_type = cellular_return_type(document_node, node_id, 14, "Cellular Return Type", true, !coherent_noise_active || !cellular_noise_active); + let cellular_jitter = number_widget( + document_node, + node_id, + 15, + "Cellular Jitter", + NumberInput::default() + .mode_range() + .range_min(Some(0.)) + .range_max(Some(1.)) + .disabled(!coherent_noise_active || !cellular_noise_active), + true, + ); vec![ - LayoutGroup::Row { widgets: width }, - LayoutGroup::Row { widgets: height }, + // All + dimensions, LayoutGroup::Row { widgets: seed }, - //_noise_type + LayoutGroup::Row { widgets: scale }, + noise_type_row, + LayoutGroup::Row { widgets: Vec::new() }, + // Domain Warp + domain_warp_type_row, + LayoutGroup::Row { widgets: domain_warp_amplitude }, + LayoutGroup::Row { widgets: Vec::new() }, + // Fractal + fractal_type_row, + LayoutGroup::Row { widgets: fractal_octaves }, + LayoutGroup::Row { widgets: fractal_lacunarity }, + LayoutGroup::Row { widgets: fractal_gain }, + LayoutGroup::Row { widgets: fractal_weighted_strength }, + LayoutGroup::Row { widgets: fractal_ping_pong_strength }, + LayoutGroup::Row { widgets: Vec::new() }, + // Cellular + cellular_distance_function_row, + cellular_return_type, + LayoutGroup::Row { widgets: cellular_jitter }, ] } @@ -1163,7 +1420,7 @@ pub fn star_properties(document_node: &DocumentNode, node_id: NodeId, _context: } pub fn line_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let operand = |name: &str, index| vec2_widget(document_node, node_id, index, name, "X", "Y", "px", add_blank_assist); + let operand = |name: &str, index| vec2_widget(document_node, node_id, index, name, "X", "Y", "px", None, add_blank_assist); vec![operand("Start", 1), operand("End", 2)] } pub fn spline_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { @@ -1195,7 +1452,7 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont add_blank_assist(widgets); } }; - let translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", " px", translation_assist); + let translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", " px", None, translation_assist); let rotation = { let index = 2; @@ -1226,7 +1483,7 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont LayoutGroup::Row { widgets } }; - let scale = vec2_widget(document_node, node_id, 3, "Scale", "W", "H", "x", add_blank_assist); + let scale = vec2_widget(document_node, node_id, 3, "Scale", "W", "H", "x", None, add_blank_assist); let vector_data = start_widgets(document_node, node_id, 0, "Data", FrontendGraphDataType::Vector, false); let vector_data = LayoutGroup::Row { widgets: vector_data }; @@ -1836,7 +2093,7 @@ pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context } pub fn repeat_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let direction = vec2_widget(document_node, node_id, 1, "Direction", "X", "Y", " px", add_blank_assist); + let direction = vec2_widget(document_node, node_id, 1, "Direction", "X", "Y", " px", None, add_blank_assist); let count = number_widget(document_node, node_id, 2, "Count", NumberInput::default().min(1.), true); vec![direction, LayoutGroup::Row { widgets: count }] @@ -1895,8 +2152,8 @@ pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: } pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", " px", add_blank_assist); - let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", " px", add_blank_assist); + let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", " px", None, add_blank_assist); + let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", " px", None, add_blank_assist); let background = color_widget(document_node, node_id, 3, "Background", ColorButton::default().allow_none(false), true); let clip = LayoutGroup::Row { widgets: bool_widget(document_node, node_id, 4, "Clip", true), diff --git a/frontend/src/components/widgets/inputs/NumberInput.svelte b/frontend/src/components/widgets/inputs/NumberInput.svelte index 32f68e50..c6f62645 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.svelte +++ b/frontend/src/components/widgets/inputs/NumberInput.svelte @@ -84,7 +84,7 @@ $: sliderStepValue = isInteger ? (step === undefined ? 1 : step) : "any"; $: styles = { ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}), - ...(mode === "Range" ? { "--progress-factor": (rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin) } : {}), + ...(mode === "Range" ? { "--progress-factor": Math.min(Math.max((rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin), 0), 1) } : {}), }; // Keep track of the Ctrl key being held down. @@ -129,7 +129,7 @@ // Called internally to update the value indirectly by informing the parent component of the new value, // so it can update the prop for this component, finally yielding the value change. - function updateValue(newValue: number | undefined) { + function updateValue(newValue: number | undefined): number | undefined { // Check if the new value is valid, otherwise we use the old value (rounded if it's an integer) const oldValue = value !== undefined && isInteger ? Math.round(value) : value; let newValueValidated = newValue !== undefined ? newValue : oldValue; @@ -138,6 +138,8 @@ if (typeof min === "number" && !Number.isNaN(min)) newValueValidated = Math.max(newValueValidated, min); if (typeof max === "number" && !Number.isNaN(max)) newValueValidated = Math.min(newValueValidated, max); + if (isInteger) newValueValidated = Math.round(newValueValidated); + rangeSliderValue = newValueValidated; rangeSliderValueAsRendered = newValueValidated; } @@ -145,6 +147,9 @@ text = displayText(newValueValidated); if (newValue !== undefined) dispatch("value", newValueValidated); + + // For any caller that needs to know what the value was changed to, we return it here + return newValueValidated; } // ================ @@ -185,7 +190,10 @@ // The `unFocus()` call at the bottom of this function and in `onTextChangeCanceled()` causes this function to be run again, so this check skips a second run. if (!editing) return; - let newValue = evaluateMathExpression(text); + // Insert a leading zero before all decimal points lacking a preceding digit, since the library doesn't realize that "point" means "zero point". + const textWithLeadingZeroes = text.replaceAll(/(?<=^|[^0-9])\./g, "0."); // Match any "." that is preceded by the start of the string (^) or a non-digit character ([^0-9]) + + let newValue = evaluateMathExpression(textWithLeadingZeroes); if (newValue !== undefined && isNaN(newValue)) newValue = undefined; // Rejects `sqrt(-1)` updateValue(newValue); @@ -321,9 +329,12 @@ cumulativeDragDelta += dragDelta; const combined = initialValueBeforeDragging + cumulativeDragDelta; - const combineSnapped = e.ctrlKey || isInteger ? Math.round(combined) : combined; + const combineSnapped = e.ctrlKey ? Math.round(combined) : combined; - updateValue(combineSnapped); + const newValue = updateValue(combineSnapped); + + // If the value was altered within the `updateValue()` call, we need to rectify the cumulative drag delta to account for the change. + if (newValue !== undefined) cumulativeDragDelta -= combineSnapped - newValue; } ignoredFirstMovement = true; }; diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 2e56fa80..c3c55380 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -611,20 +611,178 @@ impl core::fmt::Display for RedGreenBlue { #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] pub enum NoiseType { + Perlin, + OpenSimplex2, + OpenSimplex2S, + Cellular, + ValueCubic, + Value, WhiteNoise, } impl core::fmt::Display for NoiseType { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { + NoiseType::Perlin => write!(f, "Perlin"), + NoiseType::OpenSimplex2 => write!(f, "OpenSimplex2"), + NoiseType::OpenSimplex2S => write!(f, "OpenSimplex2S"), + NoiseType::Cellular => write!(f, "Cellular"), + NoiseType::ValueCubic => write!(f, "Value Cubic"), + NoiseType::Value => write!(f, "Value"), NoiseType::WhiteNoise => write!(f, "White Noise"), } } } impl NoiseType { - pub fn list() -> [NoiseType; 1] { - [NoiseType::WhiteNoise] + pub fn list() -> &'static [NoiseType; 7] { + &[ + NoiseType::Perlin, + NoiseType::OpenSimplex2, + NoiseType::OpenSimplex2S, + NoiseType::Cellular, + NoiseType::ValueCubic, + NoiseType::Value, + NoiseType::WhiteNoise, + ] + } +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", derive(specta::Type))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] +pub enum FractalType { + None, + FBm, + Ridged, + PingPong, + DomainWarpProgressive, + DomainWarpIndependent, +} + +impl core::fmt::Display for FractalType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FractalType::None => write!(f, "None"), + FractalType::FBm => write!(f, "Fractional Brownian Motion"), + FractalType::Ridged => write!(f, "Ridged"), + FractalType::PingPong => write!(f, "Ping Pong"), + FractalType::DomainWarpProgressive => write!(f, "Progressive (Domain Warp Only)"), + FractalType::DomainWarpIndependent => write!(f, "Independent (Domain Warp Only)"), + } + } +} + +impl FractalType { + pub fn list() -> &'static [FractalType; 6] { + &[ + FractalType::None, + FractalType::FBm, + FractalType::Ridged, + FractalType::PingPong, + FractalType::DomainWarpProgressive, + FractalType::DomainWarpIndependent, + ] + } +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", derive(specta::Type))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] +pub enum CellularDistanceFunction { + Euclidean, + EuclideanSq, + Manhattan, + Hybrid, +} + +impl core::fmt::Display for CellularDistanceFunction { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CellularDistanceFunction::Euclidean => write!(f, "Euclidean"), + CellularDistanceFunction::EuclideanSq => write!(f, "Euclidean Squared (Faster)"), + CellularDistanceFunction::Manhattan => write!(f, "Manhattan"), + CellularDistanceFunction::Hybrid => write!(f, "Hybrid"), + } + } +} + +impl CellularDistanceFunction { + pub fn list() -> &'static [CellularDistanceFunction; 4] { + &[ + CellularDistanceFunction::Euclidean, + CellularDistanceFunction::EuclideanSq, + CellularDistanceFunction::Manhattan, + CellularDistanceFunction::Hybrid, + ] + } +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", derive(specta::Type))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] +pub enum CellularReturnType { + CellValue, + Nearest, + NextNearest, + Average, + Difference, + Product, + Division, +} + +impl core::fmt::Display for CellularReturnType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CellularReturnType::CellValue => write!(f, "Cell Value"), + CellularReturnType::Nearest => write!(f, "Nearest (F1)"), + CellularReturnType::NextNearest => write!(f, "Next Nearest (F2)"), + CellularReturnType::Average => write!(f, "Average (F1 / 2 + F2 / 2)"), + CellularReturnType::Difference => write!(f, "Difference (F2 - F1)"), + CellularReturnType::Product => write!(f, "Product (F2 * F1 / 2)"), + CellularReturnType::Division => write!(f, "Division (F1 / F2)"), + } + } +} + +impl CellularReturnType { + pub fn list() -> &'static [CellularReturnType; 7] { + &[ + CellularReturnType::CellValue, + CellularReturnType::Nearest, + CellularReturnType::NextNearest, + CellularReturnType::Average, + CellularReturnType::Difference, + CellularReturnType::Product, + CellularReturnType::Division, + ] + } +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", derive(specta::Type))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DynAny)] +pub enum DomainWarpType { + None, + OpenSimplex2, + OpenSimplex2Reduced, + BasicGrid, +} + +impl core::fmt::Display for DomainWarpType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + DomainWarpType::None => write!(f, "None"), + DomainWarpType::OpenSimplex2 => write!(f, "OpenSimplex2"), + DomainWarpType::OpenSimplex2Reduced => write!(f, "OpenSimplex2 Reduced"), + DomainWarpType::BasicGrid => write!(f, "Basic Grid"), + } + } +} + +impl DomainWarpType { + pub fn list() -> &'static [DomainWarpType; 4] { + &[DomainWarpType::None, DomainWarpType::OpenSimplex2, DomainWarpType::OpenSimplex2Reduced, DomainWarpType::BasicGrid] } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0d711f88..9c528bb6 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -9,7 +9,7 @@ use graphene_core::{Color, Node, Type}; use dyn_any::DynAny; pub use dyn_any::StaticType; -pub use glam::{DAffine2, DVec2}; +pub use glam::{DAffine2, DVec2, UVec2}; use std::hash::Hash; pub use std::sync::Arc; @@ -23,6 +23,7 @@ pub enum TaggedValue { F32(f32), F64(f64), Bool(bool), + UVec2(UVec2), DVec2(DVec2), OptionalDVec2(Option), DAffine2(DAffine2), @@ -45,6 +46,10 @@ pub enum TaggedValue { VecDVec2(Vec), RedGreenBlue(graphene_core::raster::RedGreenBlue), NoiseType(graphene_core::raster::NoiseType), + FractalType(graphene_core::raster::FractalType), + CellularDistanceFunction(graphene_core::raster::CellularDistanceFunction), + CellularReturnType(graphene_core::raster::CellularReturnType), + DomainWarpType(graphene_core::raster::DomainWarpType), RelativeAbsolute(graphene_core::raster::RelativeAbsolute), SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice), LineCap(graphene_core::vector::style::LineCap), @@ -76,70 +81,75 @@ impl Hash for TaggedValue { core::mem::discriminant(self).hash(state); match self { Self::None => {} - Self::String(s) => s.hash(state), - Self::U32(u) => u.hash(state), - Self::F32(f) => f.to_bits().hash(state), - Self::F64(f) => f.to_bits().hash(state), - Self::Bool(b) => b.hash(state), - Self::DVec2(v) => v.to_array().iter().for_each(|x| x.to_bits().hash(state)), + Self::String(x) => x.hash(state), + Self::U32(x) => x.hash(state), + Self::F32(x) => x.to_bits().hash(state), + Self::F64(x) => x.to_bits().hash(state), + Self::Bool(x) => x.hash(state), + Self::UVec2(x) => x.to_array().iter().for_each(|x| x.hash(state)), + Self::DVec2(x) => x.to_array().iter().for_each(|x| x.to_bits().hash(state)), Self::OptionalDVec2(None) => 0.hash(state), - Self::OptionalDVec2(Some(v)) => { + Self::OptionalDVec2(Some(x)) => { 1.hash(state); - Self::DVec2(*v).hash(state) + Self::DVec2(*x).hash(state) } - Self::DAffine2(m) => m.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)), - Self::Image(i) => i.hash(state), - Self::ImaginateCache(i) => i.hash(state), - Self::Color(c) => c.hash(state), - Self::Subpaths(s) => s.iter().for_each(|subpath| subpath.hash(state)), - Self::RcSubpath(s) => s.hash(state), - Self::BlendMode(b) => b.hash(state), - Self::LuminanceCalculation(l) => l.hash(state), - Self::ImaginateSamplingMethod(m) => m.hash(state), - Self::ImaginateMaskStartingFill(f) => f.hash(state), - Self::ImaginateController(s) => s.hash(state), - Self::LayerPath(p) => p.hash(state), - Self::ImageFrame(i) => i.hash(state), - Self::VectorData(vector_data) => vector_data.hash(state), - Self::Fill(fill) => fill.hash(state), - Self::Stroke(stroke) => stroke.hash(state), - Self::VecF32(vec_f32) => vec_f32.iter().for_each(|val| val.to_bits().hash(state)), - Self::VecDVec2(vec_dvec2) => vec_dvec2.iter().for_each(|val| val.to_array().iter().for_each(|x| x.to_bits().hash(state))), - Self::RedGreenBlue(red_green_blue) => red_green_blue.hash(state), - Self::NoiseType(noise_type) => noise_type.hash(state), - Self::RelativeAbsolute(relative_absolute) => relative_absolute.hash(state), - Self::SelectiveColorChoice(selective_color_choice) => selective_color_choice.hash(state), - Self::LineCap(line_cap) => line_cap.hash(state), - Self::LineJoin(line_join) => line_join.hash(state), - Self::FillType(fill_type) => fill_type.hash(state), - Self::GradientType(gradient_type) => gradient_type.hash(state), - Self::GradientPositions(gradient_positions) => { - gradient_positions.len().hash(state); - for (position, color) in gradient_positions { + Self::DAffine2(x) => x.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)), + Self::Image(x) => x.hash(state), + Self::ImaginateCache(x) => x.hash(state), + Self::Color(x) => x.hash(state), + Self::Subpaths(x) => x.iter().for_each(|subpath| subpath.hash(state)), + Self::RcSubpath(x) => x.hash(state), + Self::BlendMode(x) => x.hash(state), + Self::LuminanceCalculation(x) => x.hash(state), + Self::ImaginateSamplingMethod(x) => x.hash(state), + Self::ImaginateMaskStartingFill(x) => x.hash(state), + Self::ImaginateController(x) => x.hash(state), + Self::LayerPath(x) => x.hash(state), + Self::ImageFrame(x) => x.hash(state), + Self::VectorData(x) => x.hash(state), + Self::Fill(x) => x.hash(state), + Self::Stroke(x) => x.hash(state), + Self::VecF32(x) => x.iter().for_each(|val| val.to_bits().hash(state)), + Self::VecDVec2(x) => x.iter().for_each(|val| val.to_array().iter().for_each(|x| x.to_bits().hash(state))), + Self::RedGreenBlue(x) => x.hash(state), + Self::NoiseType(x) => x.hash(state), + Self::FractalType(x) => x.hash(state), + Self::CellularDistanceFunction(x) => x.hash(state), + Self::CellularReturnType(x) => x.hash(state), + Self::DomainWarpType(x) => x.hash(state), + Self::RelativeAbsolute(x) => x.hash(state), + Self::SelectiveColorChoice(x) => x.hash(state), + Self::LineCap(x) => x.hash(state), + Self::LineJoin(x) => x.hash(state), + Self::FillType(x) => x.hash(state), + Self::GradientType(x) => x.hash(state), + Self::GradientPositions(x) => { + x.len().hash(state); + for (position, color) in x { position.to_bits().hash(state); color.hash(state); } } - Self::Quantization(quantized_image) => quantized_image.hash(state), - Self::OptionalColor(color) => color.hash(state), - Self::ManipulatorGroupIds(mirror) => mirror.hash(state), - Self::Font(font) => font.hash(state), - Self::BrushStrokes(brush_strokes) => brush_strokes.hash(state), - Self::BrushCache(brush_cache) => brush_cache.hash(state), - Self::Segments(segments) => { - for segment in segments { + Self::Quantization(x) => x.hash(state), + Self::OptionalColor(x) => x.hash(state), + Self::ManipulatorGroupIds(x) => x.hash(state), + Self::Font(x) => x.hash(state), + Self::BrushStrokes(x) => x.hash(state), + Self::BrushCache(x) => x.hash(state), + Self::Segments(x) => { + for segment in x { segment.hash(state) } } - Self::DocumentNode(document_node) => document_node.hash(state), - Self::GraphicGroup(graphic_group) => graphic_group.hash(state), - Self::Artboard(artboard) => artboard.hash(state), - Self::Curve(curve) => curve.hash(state), - Self::IVec2(v) => v.hash(state), - Self::SurfaceFrame(surface_id) => surface_id.hash(state), - Self::Footprint(footprint) => footprint.hash(state), - Self::RenderOutput(render_output) => render_output.hash(state), - Self::Palette(palette) => palette.hash(state), + Self::DocumentNode(x) => x.hash(state), + Self::GraphicGroup(x) => x.hash(state), + Self::Artboard(x) => x.hash(state), + Self::Curve(x) => x.hash(state), + Self::IVec2(x) => x.hash(state), + Self::SurfaceFrame(x) => x.hash(state), + Self::Footprint(x) => x.hash(state), + Self::RenderOutput(x) => x.hash(state), + Self::Palette(x) => x.hash(state), } } } @@ -154,6 +164,7 @@ impl<'a> TaggedValue { TaggedValue::F32(x) => Box::new(x), TaggedValue::F64(x) => Box::new(x), TaggedValue::Bool(x) => Box::new(x), + TaggedValue::UVec2(x) => Box::new(x), TaggedValue::DVec2(x) => Box::new(x), TaggedValue::OptionalDVec2(x) => Box::new(x), TaggedValue::DAffine2(x) => Box::new(x), @@ -176,6 +187,10 @@ impl<'a> TaggedValue { TaggedValue::VecDVec2(x) => Box::new(x), TaggedValue::RedGreenBlue(x) => Box::new(x), TaggedValue::NoiseType(x) => Box::new(x), + TaggedValue::FractalType(x) => Box::new(x), + TaggedValue::CellularDistanceFunction(x) => Box::new(x), + TaggedValue::CellularReturnType(x) => Box::new(x), + TaggedValue::DomainWarpType(x) => Box::new(x), TaggedValue::RelativeAbsolute(x) => Box::new(x), TaggedValue::SelectiveColorChoice(x) => Box::new(x), TaggedValue::LineCap(x) => Box::new(x), @@ -210,8 +225,8 @@ impl<'a> TaggedValue { TaggedValue::F32(x) => x.to_string() + "_f32", TaggedValue::F64(x) => x.to_string() + "_f64", TaggedValue::Bool(x) => x.to_string(), - TaggedValue::BlendMode(blend_mode) => "BlendMode::".to_string() + &blend_mode.to_string(), - TaggedValue::Color(color) => format!("Color {color:?}"), + TaggedValue::BlendMode(x) => "BlendMode::".to_string() + &x.to_string(), + TaggedValue::Color(x) => format!("Color {x:?}"), _ => panic!("Cannot convert to primitive string"), } } @@ -224,6 +239,7 @@ impl<'a> TaggedValue { TaggedValue::F32(_) => concrete!(f32), TaggedValue::F64(_) => concrete!(f64), TaggedValue::Bool(_) => concrete!(bool), + TaggedValue::UVec2(_) => concrete!(UVec2), TaggedValue::DVec2(_) => concrete!(DVec2), TaggedValue::OptionalDVec2(_) => concrete!(Option), TaggedValue::Image(_) => concrete!(graphene_core::raster::Image), @@ -246,6 +262,10 @@ impl<'a> TaggedValue { TaggedValue::VecDVec2(_) => concrete!(Vec), TaggedValue::RedGreenBlue(_) => concrete!(graphene_core::raster::RedGreenBlue), TaggedValue::NoiseType(_) => concrete!(graphene_core::raster::NoiseType), + TaggedValue::FractalType(_) => concrete!(graphene_core::raster::FractalType), + TaggedValue::CellularDistanceFunction(_) => concrete!(graphene_core::raster::CellularDistanceFunction), + TaggedValue::CellularReturnType(_) => concrete!(graphene_core::raster::CellularReturnType), + TaggedValue::DomainWarpType(_) => concrete!(graphene_core::raster::DomainWarpType), TaggedValue::RelativeAbsolute(_) => concrete!(graphene_core::raster::RelativeAbsolute), TaggedValue::SelectiveColorChoice(_) => concrete!(graphene_core::raster::SelectiveColorChoice), TaggedValue::LineCap(_) => concrete!(graphene_core::vector::style::LineCap), @@ -283,6 +303,7 @@ impl<'a> TaggedValue { x if x == TypeId::of::() => Ok(TaggedValue::F32(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::F64(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::Bool(*downcast(input).unwrap())), + x if x == TypeId::of::() => Ok(TaggedValue::UVec2(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::DVec2(*downcast(input).unwrap())), x if x == TypeId::of::>() => Ok(TaggedValue::OptionalDVec2(*downcast(input).unwrap())), x if x == TypeId::of::>() => Ok(TaggedValue::Image(*downcast(input).unwrap())), @@ -305,6 +326,10 @@ impl<'a> TaggedValue { x if x == TypeId::of::>() => Ok(TaggedValue::VecDVec2(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::RedGreenBlue(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::NoiseType(*downcast(input).unwrap())), + x if x == TypeId::of::() => Ok(TaggedValue::FractalType(*downcast(input).unwrap())), + x if x == TypeId::of::() => Ok(TaggedValue::CellularDistanceFunction(*downcast(input).unwrap())), + x if x == TypeId::of::() => Ok(TaggedValue::CellularReturnType(*downcast(input).unwrap())), + x if x == TypeId::of::() => Ok(TaggedValue::DomainWarpType(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::RelativeAbsolute(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::SelectiveColorChoice(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::LineCap(*downcast(input).unwrap())), diff --git a/node-graph/gstd/Cargo.toml b/node-graph/gstd/Cargo.toml index a0a85477..b5878203 100644 --- a/node-graph/gstd/Cargo.toml +++ b/node-graph/gstd/Cargo.toml @@ -27,6 +27,7 @@ resvg = ["dep:resvg"] wayland = [] [dependencies] +fastnoise-lite = { workspace = true } rand = { workspace = true, features = [ "alloc", "small_rng", diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 983161c7..8a154136 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -1,24 +1,27 @@ +use crate::wasm_application_io::WasmEditorApi; + use dyn_any::{DynAny, StaticType}; -use glam::{DAffine2, DVec2, Vec2}; use graph_craft::imaginate_input::{ImaginateController, ImaginateMaskStartingFill, ImaginateSamplingMethod}; use graph_craft::proto::DynFuture; -use graphene_core::raster::{Alpha, Bitmap, BitmapMut, BlendMode, BlendNode, Image, ImageFrame, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue, Sample}; -use graphene_core::transform::{Footprint, Transform}; - -use crate::wasm_application_io::WasmEditorApi; use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox}; +use graphene_core::raster::{ + Alpha, Bitmap, BitmapMut, BlendMode, BlendNode, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Image, ImageFrame, Linear, LinearChannel, Luminance, NoiseType, Pixel, + RGBMut, RedGreenBlue, Sample, +}; +use graphene_core::transform::{Footprint, Transform}; use graphene_core::value::CopiedNode; use graphene_core::{AlphaBlending, Color, Node}; +use fastnoise_lite; +use glam::{DAffine2, DVec2, UVec2, Vec2}; +use rand::prelude::*; +use rand_chacha::ChaCha8Rng; use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; use std::marker::PhantomData; use std::path::Path; -use rand::prelude::*; -use rand_chacha::ChaCha8Rng; - #[derive(Debug, DynAny)] pub enum Error { IO(std::io::Error), @@ -192,7 +195,7 @@ pub struct MaskImageNode { } #[node_macro::node_fn(MaskImageNode<_P, _S>)] -fn mask_imge< +fn mask_image< // _P is the color of the input image. It must have an alpha channel because that is going to // be modified by the mask _P: Copy + Alpha, @@ -405,7 +408,7 @@ fn extend_image_to_bounds_node(image: ImageFrame, bounds: DAffine2) -> Im let new_end = bounds_in_image_space.end.ceil().max(orig_image_scale); let new_scale = new_end - new_start; - // Copy over original image into embiggened image. + // Copy over original image into enlarged image. let mut new_img = Image::new(new_scale.x as u32, new_scale.y as u32, Color::TRANSPARENT); let offset_in_new_image = (-new_start).as_uvec2(); for y in 0..image.image.height { @@ -553,25 +556,155 @@ fn image_frame<_P: Pixel>(image: Image<_P>, transform: DAffine2) -> graphene_cor } #[derive(Debug, Clone, Copy)] -pub struct PixelNoiseNode { - height: Height, +pub struct NoisePatternNode< + Dimensions, + Seed, + Scale, + NoiseType, + DomainWarpType, + DomainWarpAmplitude, + FractalType, + FractalOctaves, + FractalLacunarity, + FractalGain, + FractalWeightedStrength, + FractalPingPongStrength, + CellularDistanceFunction, + CellularReturnType, + CellularJitter, +> { + dimensions: Dimensions, seed: Seed, + scale: Scale, noise_type: NoiseType, + domain_warp_type: DomainWarpType, + domain_warp_amplitude: DomainWarpAmplitude, + fractal_type: FractalType, + fractal_octaves: FractalOctaves, + fractal_lacunarity: FractalLacunarity, + fractal_gain: FractalGain, + fractal_weighted_strength: FractalWeightedStrength, + fractal_ping_pong_strength: FractalPingPongStrength, + cellular_distance_function: CellularDistanceFunction, + cellular_return_type: CellularReturnType, + cellular_jitter: CellularJitter, } -#[node_macro::node_fn(PixelNoiseNode)] -fn pixel_noise(width: u32, height: u32, seed: u32, noise_type: NoiseType) -> graphene_core::raster::ImageFrame { - let mut rng = ChaCha8Rng::seed_from_u64(seed as u64); +#[allow(clippy::too_many_arguments)] +#[node_macro::node_fn(NoisePatternNode)] +fn noise_pattern( + _no_primary_input: (), + dimensions: UVec2, + seed: u32, + scale: f32, + noise_type: NoiseType, + domain_warp_type: DomainWarpType, + domain_warp_amplitude: f32, + fractal_type: FractalType, + fractal_octaves: u32, + fractal_lacunarity: f32, + fractal_gain: f32, + fractal_weighted_strength: f32, + fractal_ping_pong_strength: f32, + cellular_distance_function: CellularDistanceFunction, + cellular_return_type: CellularReturnType, + cellular_jitter: f32, +) -> graphene_core::raster::ImageFrame { + // All + let [width, height] = dimensions.to_array(); let mut image = Image::new(width, height, Color::from_luminance(0.5)); + let mut noise = fastnoise_lite::FastNoiseLite::with_seed(seed as i32); + noise.set_frequency(Some(scale / 1000.)); + + // Domain Warp + let domain_warp_type = match domain_warp_type { + DomainWarpType::None => None, + DomainWarpType::OpenSimplex2 => Some(fastnoise_lite::DomainWarpType::OpenSimplex2), + DomainWarpType::OpenSimplex2Reduced => Some(fastnoise_lite::DomainWarpType::OpenSimplex2Reduced), + DomainWarpType::BasicGrid => Some(fastnoise_lite::DomainWarpType::BasicGrid), + }; + let domain_warp_active = domain_warp_type.is_some(); + noise.set_domain_warp_type(domain_warp_type); + noise.set_domain_warp_amp(Some(domain_warp_amplitude)); + + // Fractal + let noise_type = match noise_type { + NoiseType::Perlin => fastnoise_lite::NoiseType::Perlin, + NoiseType::OpenSimplex2 => fastnoise_lite::NoiseType::OpenSimplex2, + NoiseType::OpenSimplex2S => fastnoise_lite::NoiseType::OpenSimplex2S, + NoiseType::Cellular => fastnoise_lite::NoiseType::Cellular, + NoiseType::ValueCubic => fastnoise_lite::NoiseType::ValueCubic, + NoiseType::Value => fastnoise_lite::NoiseType::Value, + NoiseType::WhiteNoise => { + let mut rng = ChaCha8Rng::seed_from_u64(seed as u64); + + for y in 0..height { + for x in 0..width { + let pixel = image.get_pixel_mut(x, y).unwrap(); + let luminance = rng.gen_range(0.0..1.) as f32; + *pixel = Color::from_luminance(luminance); + } + } + + return ImageFrame:: { + image, + transform: DAffine2::from_scale(DVec2::new(width as f64, height as f64)), + alpha_blending: AlphaBlending::default(), + }; + } + }; + noise.set_noise_type(Some(noise_type)); + let fractal_type = match fractal_type { + FractalType::None => fastnoise_lite::FractalType::None, + FractalType::FBm => fastnoise_lite::FractalType::FBm, + FractalType::Ridged => fastnoise_lite::FractalType::Ridged, + FractalType::PingPong => fastnoise_lite::FractalType::PingPong, + FractalType::DomainWarpProgressive => fastnoise_lite::FractalType::DomainWarpProgressive, + FractalType::DomainWarpIndependent => fastnoise_lite::FractalType::DomainWarpIndependent, + }; + noise.set_fractal_type(Some(fractal_type)); + noise.set_fractal_octaves(Some(fractal_octaves as i32)); + noise.set_fractal_lacunarity(Some(fractal_lacunarity)); + noise.set_fractal_gain(Some(fractal_gain)); + noise.set_fractal_weighted_strength(Some(fractal_weighted_strength)); + noise.set_fractal_ping_pong_strength(Some(fractal_ping_pong_strength)); + + // Cellular + let cellular_distance_function = match cellular_distance_function { + CellularDistanceFunction::Euclidean => fastnoise_lite::CellularDistanceFunction::Euclidean, + CellularDistanceFunction::EuclideanSq => fastnoise_lite::CellularDistanceFunction::EuclideanSq, + CellularDistanceFunction::Manhattan => fastnoise_lite::CellularDistanceFunction::Manhattan, + CellularDistanceFunction::Hybrid => fastnoise_lite::CellularDistanceFunction::Hybrid, + }; + let cellular_return_type = match cellular_return_type { + CellularReturnType::CellValue => fastnoise_lite::CellularReturnType::CellValue, + CellularReturnType::Nearest => fastnoise_lite::CellularReturnType::Distance, + CellularReturnType::NextNearest => fastnoise_lite::CellularReturnType::Distance2, + CellularReturnType::Average => fastnoise_lite::CellularReturnType::Distance2Add, + CellularReturnType::Difference => fastnoise_lite::CellularReturnType::Distance2Sub, + CellularReturnType::Product => fastnoise_lite::CellularReturnType::Distance2Mul, + CellularReturnType::Division => fastnoise_lite::CellularReturnType::Distance2Div, + }; + noise.set_cellular_distance_function(Some(cellular_distance_function)); + noise.set_cellular_return_type(Some(cellular_return_type)); + noise.set_cellular_jitter(Some(cellular_jitter)); + + // Calculate the noise for every pixel for y in 0..height { for x in 0..width { let pixel = image.get_pixel_mut(x, y).unwrap(); - let luminance = match noise_type { - NoiseType::WhiteNoise => rng.gen_range(0.0..1.0) as f32, - }; + + let (mut x, mut y) = (x as f32, y as f32); + if domain_warp_active && domain_warp_amplitude > 0. { + (x, y) = noise.domain_warp_2d(x, y); + } + + let luminance = (noise.get_noise_2d(x, y) + 1.) * 0.5; *pixel = Color::from_luminance(luminance); } } + + // Return the coherent noise image ImageFrame:: { image, transform: DAffine2::from_scale(DVec2::new(width as f64, height as f64)), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index e186ff20..5fb55d15 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -26,7 +26,7 @@ use graphene_std::wasm_application_io::WasmEditorApi; use wgpu_executor::WgpuExecutor; use dyn_any::StaticType; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, UVec2}; use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Arc; @@ -655,7 +655,7 @@ fn node_registry() -> HashMap, input: (), output: RenderOutput, params: [RenderOutput]), register_node!(graphene_core::structural::ConsNode<_, _>, input: Image, params: [&str]), register_node!(graphene_std::raster::ImageFrameNode<_, _>, input: Image, params: [DAffine2]), - register_node!(graphene_std::raster::PixelNoiseNode<_, _, _>, input: u32, params: [u32, u32, NoiseType]), + register_node!(graphene_std::raster::NoisePatternNode<_, _, _, _, _, _, _, _, _, _, _, _, _, _, _>, input: (), params: [UVec2, u32, f32, NoiseType, DomainWarpType, f32, FractalType, u32, f32, f32, f32, f32, CellularDistanceFunction, CellularReturnType, f32]), #[cfg(feature = "quantization")] register_node!(graphene_std::quantization::GenerateQuantizationNode<_, _>, input: ImageFrame, params: [u32, u32]), register_node!(graphene_core::quantization::QuantizeNode<_>, input: Color, params: [QuantizationChannels]),