From 2ddae98bcfe339e9121e0c282aceb7b2baa01ead Mon Sep 17 00:00:00 2001 From: Firestar99 Date: Fri, 27 Jun 2025 11:54:34 +0200 Subject: [PATCH] Prep `gcore` splitup: move various symbols into their own modules (#2746) * move `trait AsU32` from `gcore::vector::misc` to `gcore` * move blending and gradient to their own modules * fix unused warnings * move `Quad`, `Rect` and `BBox` to `gcore::math` * extract `ReferencePoint` and transform nodes from `transform` * move color-related code to `mod color` * fix unused warning in test code * move blending-related nodes and code to `mod blending_nodes` * move ClickTarget code to `mod vector::click_target` --- .../portfolio/document/document_message.rs | 2 +- .../document/document_message_handler.rs | 3 +- .../node_graph/document_node_definitions.rs | 18 +- .../node_graph/node_graph_message_handler.rs | 1 + .../document/overlays/utility_types.rs | 4 +- .../utility_types/document_metadata.rs | 3 +- .../utility_types/network_interface.rs | 3 +- .../messages/portfolio/document_migration.rs | 13 +- .../shapes/shape_utility.rs | 2 +- .../snapping/layer_snapper.rs | 1 + node-graph/README.md | 2 +- node-graph/gcore/src/blending.rs | 271 +++++++++++++++ node-graph/gcore/src/blending_nodes.rs | 175 ++++++++++ .../gcore/src/{raster => color}/color.rs | 34 +- node-graph/gcore/src/color/color_traits.rs | 205 +++++++++++ .../src/{raster => color}/discrete_srgb.rs | 0 node-graph/gcore/src/color/mod.rs | 7 + node-graph/gcore/src/gradient.rs | 248 ++++++++++++++ node-graph/gcore/src/graphic_element.rs | 59 +--- .../gcore/src/graphic_element/renderer.rs | 169 +-------- node-graph/gcore/src/lib.rs | 39 ++- node-graph/gcore/src/{raster => math}/bbox.rs | 0 node-graph/gcore/src/math/math_ext.rs | 25 ++ node-graph/gcore/src/math/mod.rs | 4 + .../renderer => math}/quad.rs | 5 - .../renderer => math}/rect.rs | 7 +- node-graph/gcore/src/raster.rs | 322 +----------------- node-graph/gcore/src/raster/adjustments.rs | 295 +--------------- node-graph/gcore/src/raster/image.rs | 2 +- node-graph/gcore/src/registry.rs | 2 +- node-graph/gcore/src/transform.rs | 191 +---------- node-graph/gcore/src/transform_nodes.rs | 89 +++++ node-graph/gcore/src/vector/brush_stroke.rs | 2 +- node-graph/gcore/src/vector/click_target.rs | 162 +++++++++ node-graph/gcore/src/vector/misc.rs | 9 - node-graph/gcore/src/vector/mod.rs | 3 + .../gcore/src/vector/reference_point.rs | 103 ++++++ node-graph/gcore/src/vector/style.rs | 244 +------------ node-graph/gcore/src/vector/vector_data.rs | 2 +- node-graph/gstd/src/brush.rs | 2 +- node-graph/gstd/src/raster.rs | 4 +- node-graph/gstd/src/wasm_application_io.rs | 2 +- .../interpreted-executor/src/node_registry.rs | 12 +- .../node-macro/src/derive_choice_type.rs | 2 +- 44 files changed, 1407 insertions(+), 1341 deletions(-) create mode 100644 node-graph/gcore/src/blending.rs create mode 100644 node-graph/gcore/src/blending_nodes.rs rename node-graph/gcore/src/{raster => color}/color.rs (97%) create mode 100644 node-graph/gcore/src/color/color_traits.rs rename node-graph/gcore/src/{raster => color}/discrete_srgb.rs (100%) create mode 100644 node-graph/gcore/src/color/mod.rs create mode 100644 node-graph/gcore/src/gradient.rs rename node-graph/gcore/src/{raster => math}/bbox.rs (100%) create mode 100644 node-graph/gcore/src/math/math_ext.rs create mode 100644 node-graph/gcore/src/math/mod.rs rename node-graph/gcore/src/{graphic_element/renderer => math}/quad.rs (96%) rename node-graph/gcore/src/{graphic_element/renderer => math}/rect.rs (92%) create mode 100644 node-graph/gcore/src/transform_nodes.rs create mode 100644 node-graph/gcore/src/vector/click_target.rs create mode 100644 node-graph/gcore/src/vector/reference_point.rs diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 2a201f58..18ede64b 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -11,8 +11,8 @@ use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::raster::BlendMode; use graphene_std::raster::Image; -use graphene_std::renderer::ClickTarget; use graphene_std::transform::Footprint; +use graphene_std::vector::click_target::ClickTarget; use graphene_std::vector::style::ViewMode; #[impl_message(Message, PortfolioMessage, Document)] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index a4550d4c..9b316e07 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -29,9 +29,10 @@ use bezier_rs::Subpath; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; +use graphene_std::math::quad::Quad; use graphene_std::raster::BlendMode; use graphene_std::raster_types::{Raster, RasterDataTable}; -use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; +use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::style::ViewMode; use graphene_std::vector::{PointId, path_bool_lib}; use std::time::Duration; 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 f0432906..98ae6c41 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 @@ -166,13 +166,13 @@ fn static_nodes() -> Vec { }, DocumentNode { inputs: vec![NodeInput::node(NodeId(0), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")), manual_composition: Some(generic!(T)), ..Default::default() }, DocumentNode { inputs: vec![NodeInput::node(NodeId(1), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")), manual_composition: Some(generic!(T)), ..Default::default() }, @@ -1650,7 +1650,7 @@ fn static_nodes() -> Vec { NodeInput::network(concrete!(DVec2), 5), ], manual_composition: Some(concrete!(Context)), - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::TransformNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::TransformNode")), ..Default::default() }, ] @@ -1746,13 +1746,13 @@ fn static_nodes() -> Vec { }, DocumentNode { inputs: vec![NodeInput::node(NodeId(1), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")), manual_composition: Some(generic!(T)), ..Default::default() }, DocumentNode { inputs: vec![NodeInput::node(NodeId(2), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")), manual_composition: Some(generic!(T)), ..Default::default() }, @@ -1859,13 +1859,13 @@ fn static_nodes() -> Vec { }, DocumentNode { inputs: vec![NodeInput::node(NodeId(2), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")), manual_composition: Some(generic!(T)), ..Default::default() }, DocumentNode { inputs: vec![NodeInput::node(NodeId(3), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")), manual_composition: Some(generic!(T)), ..Default::default() }, @@ -2014,13 +2014,13 @@ fn static_nodes() -> Vec { }, DocumentNode { inputs: vec![NodeInput::node(NodeId(1), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")), manual_composition: Some(generic!(T)), ..Default::default() }, DocumentNode { inputs: vec![NodeInput::node(NodeId(2), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")), manual_composition: Some(generic!(T)), ..Default::default() }, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index bd7f62dd..49256220 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -21,6 +21,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::proto::GraphErrors; +use graphene_std::math::math_ext::QuadExt; use graphene_std::*; use renderer::Quad; use std::cmp::Ordering; diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index c1b1649b..9587e1d6 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -9,8 +9,8 @@ use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, TAU}; use glam::{DAffine2, DVec2}; use graphene_std::Color; -use graphene_std::renderer::ClickTargetType; -use graphene_std::renderer::Quad; +use graphene_std::math::quad::Quad; +use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 8eb3defc..23887db3 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -3,8 +3,9 @@ use crate::messages::portfolio::document::graph_operation::transform_utils; use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; -use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; +use graphene_std::math::quad::Quad; use graphene_std::transform::Footprint; +use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::{PointId, VectorData}; use std::collections::{HashMap, HashSet}; use std::num::NonZeroU64; diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 640c7e64..04e2f080 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -12,8 +12,9 @@ use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::{Type, concrete}; -use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad}; +use graphene_std::math::quad::Quad; use graphene_std::transform::Footprint; +use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::{PointId, VectorData, VectorModificationType}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes; use interpreted_executor::node_registry::NODE_REGISTRY; diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 4b8f8a85..3d9c7d24 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -14,12 +14,12 @@ use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAl use graphene_std::vector::{VectorData, VectorDataTable}; use std::collections::HashMap; -const TEXT_REPLACEMENTS: [(&str, &str); 2] = [ +const TEXT_REPLACEMENTS: &[(&str, &str)] = &[ ("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"), ("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"), ]; -const REPLACEMENTS: [(&str, &str); 40] = [ +const REPLACEMENTS: &[(&str, &str)] = &[ ("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"), ("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"), ("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"), @@ -31,6 +31,9 @@ const REPLACEMENTS: [(&str, &str); 40] = [ ("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"), ("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"), ("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"), + ("graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"), + ("graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"), + ("graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"), ("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"), ("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"), ("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"), @@ -48,7 +51,11 @@ const REPLACEMENTS: [(&str, &str); 40] = [ ("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"), ("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"), ("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"), - ("graphene_core::transform::SetTransformNode", "graphene_core::transform::ReplaceTransformNode"), + ("graphene_core::transform::SetTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"), + ("graphene_core::transform::ReplaceTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"), + ("graphene_core::transform::TransformNode", "graphene_core::transform_nodes::TransformNode"), + ("graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform_nodes::BoundlessFootprintNode"), + ("graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::FreezeRealTimeNode"), ("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"), ("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"), ("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"), diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index be61c976..95598415 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -13,7 +13,7 @@ use bezier_rs::Subpath; use glam::{DAffine2, DMat2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; -use graphene_std::renderer::ClickTargetType; +use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::dvec2_to_point; use kurbo::{BezPath, PathEl, Shape}; use std::collections::VecDeque; diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs index 5e41f5df..3a306713 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::misc::*; use crate::messages::prelude::*; use bezier_rs::{Bezier, Identifier, Subpath, TValue}; use glam::{DAffine2, DVec2}; +use graphene_std::math::math_ext::QuadExt; use graphene_std::renderer::Quad; use graphene_std::vector::PointId; diff --git a/node-graph/README.md b/node-graph/README.md index c93fcc79..24ff12c7 100644 --- a/node-graph/README.md +++ b/node-graph/README.md @@ -157,7 +157,7 @@ raster_node!(graphene_core::raster::OpacityNode<_>, params: [f64]), There is also the more general `register_node!` for nodes that do not need to run per pixel. ```rs -register_node!(graphene_core::transform::SetTransformNode<_>, input: VectorData, params: [DAffine2]), +register_node!(graphene_core::transform_nodes::SetTransformNode<_>, input: VectorData, params: [DAffine2]), ``` ## Debugging diff --git a/node-graph/gcore/src/blending.rs b/node-graph/gcore/src/blending.rs new file mode 100644 index 00000000..18cc4fc6 --- /dev/null +++ b/node-graph/gcore/src/blending.rs @@ -0,0 +1,271 @@ +use dyn_any::DynAny; +use std::hash::Hash; + +#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)] +#[serde(default)] +pub struct AlphaBlending { + pub blend_mode: BlendMode, + pub opacity: f32, + pub fill: f32, + pub clip: bool, +} +impl Default for AlphaBlending { + fn default() -> Self { + Self::new() + } +} +impl Hash for AlphaBlending { + fn hash(&self, state: &mut H) { + self.opacity.to_bits().hash(state); + self.fill.to_bits().hash(state); + self.blend_mode.hash(state); + self.clip.hash(state); + } +} +impl std::fmt::Display for AlphaBlending { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let round = |x: f32| (x * 1e3).round() / 1e3; + write!( + f, + "Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}", + self.blend_mode, + round(self.opacity * 100.), + round(self.fill * 100.), + if self.clip { "Yes" } else { "No" } + ) + } +} + +impl AlphaBlending { + pub const fn new() -> Self { + Self { + opacity: 1., + fill: 1., + blend_mode: BlendMode::Normal, + clip: false, + } + } + + pub fn lerp(&self, other: &Self, t: f32) -> Self { + let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t; + + AlphaBlending { + opacity: lerp(self.opacity, other.opacity, t), + fill: lerp(self.fill, other.fill, t), + blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode }, + clip: if t < 0.5 { self.clip } else { other.clip }, + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)] +#[repr(i32)] +pub enum BlendMode { + // Basic group + #[default] + Normal, + + // Darken group + Darken, + Multiply, + ColorBurn, + LinearBurn, + DarkerColor, + + // Lighten group + Lighten, + Screen, + ColorDodge, + LinearDodge, + LighterColor, + + // Contrast group + Overlay, + SoftLight, + HardLight, + VividLight, + LinearLight, + PinLight, + HardMix, + + // Inversion group + Difference, + Exclusion, + Subtract, + Divide, + + // Component group + Hue, + Saturation, + Color, + Luminosity, + + // Other stuff + Erase, + Restore, + MultiplyAlpha, +} + +impl BlendMode { + /// All standard blend modes ordered by group. + pub fn list() -> [&'static [BlendMode]; 6] { + use BlendMode::*; + [ + // Normal group + &[Normal], + // Darken group + &[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor], + // Lighten group + &[Lighten, Screen, ColorDodge, LinearDodge, LighterColor], + // Contrast group + &[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix], + // Inversion group + &[Difference, Exclusion, Subtract, Divide], + // Component group + &[Hue, Saturation, Color, Luminosity], + ] + } + + /// The subset of [`BlendMode::list()`] that is supported by SVG. + pub fn list_svg_subset() -> [&'static [BlendMode]; 6] { + use BlendMode::*; + [ + // Normal group + &[Normal], + // Darken group + &[Darken, Multiply, ColorBurn], + // Lighten group + &[Lighten, Screen, ColorDodge], + // Contrast group + &[Overlay, SoftLight, HardLight], + // Inversion group + &[Difference, Exclusion], + // Component group + &[Hue, Saturation, Color, Luminosity], + ] + } + + pub fn index_in_list(&self) -> Option { + Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self) + } + + pub fn index_in_list_svg_subset(&self) -> Option { + Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self) + } + + /// Convert the enum to the CSS string for the blend mode. + /// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values) + pub fn to_svg_style_name(&self) -> Option<&'static str> { + match self { + // Normal group + BlendMode::Normal => Some("normal"), + // Darken group + BlendMode::Darken => Some("darken"), + BlendMode::Multiply => Some("multiply"), + BlendMode::ColorBurn => Some("color-burn"), + // Lighten group + BlendMode::Lighten => Some("lighten"), + BlendMode::Screen => Some("screen"), + BlendMode::ColorDodge => Some("color-dodge"), + // Contrast group + BlendMode::Overlay => Some("overlay"), + BlendMode::SoftLight => Some("soft-light"), + BlendMode::HardLight => Some("hard-light"), + // Inversion group + BlendMode::Difference => Some("difference"), + BlendMode::Exclusion => Some("exclusion"), + // Component group + BlendMode::Hue => Some("hue"), + BlendMode::Saturation => Some("saturation"), + BlendMode::Color => Some("color"), + BlendMode::Luminosity => Some("luminosity"), + _ => None, + } + } + + /// Renders the blend mode CSS style declaration. + pub fn render(&self) -> String { + format!( + r#" mix-blend-mode: {};"#, + self.to_svg_style_name().unwrap_or_else(|| { + warn!("Unsupported blend mode {self:?}"); + "normal" + }) + ) + } +} + +impl std::fmt::Display for BlendMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + // Normal group + BlendMode::Normal => write!(f, "Normal"), + // Darken group + BlendMode::Darken => write!(f, "Darken"), + BlendMode::Multiply => write!(f, "Multiply"), + BlendMode::ColorBurn => write!(f, "Color Burn"), + BlendMode::LinearBurn => write!(f, "Linear Burn"), + BlendMode::DarkerColor => write!(f, "Darker Color"), + // Lighten group + BlendMode::Lighten => write!(f, "Lighten"), + BlendMode::Screen => write!(f, "Screen"), + BlendMode::ColorDodge => write!(f, "Color Dodge"), + BlendMode::LinearDodge => write!(f, "Linear Dodge"), + BlendMode::LighterColor => write!(f, "Lighter Color"), + // Contrast group + BlendMode::Overlay => write!(f, "Overlay"), + BlendMode::SoftLight => write!(f, "Soft Light"), + BlendMode::HardLight => write!(f, "Hard Light"), + BlendMode::VividLight => write!(f, "Vivid Light"), + BlendMode::LinearLight => write!(f, "Linear Light"), + BlendMode::PinLight => write!(f, "Pin Light"), + BlendMode::HardMix => write!(f, "Hard Mix"), + // Inversion group + BlendMode::Difference => write!(f, "Difference"), + BlendMode::Exclusion => write!(f, "Exclusion"), + BlendMode::Subtract => write!(f, "Subtract"), + BlendMode::Divide => write!(f, "Divide"), + // Component group + BlendMode::Hue => write!(f, "Hue"), + BlendMode::Saturation => write!(f, "Saturation"), + BlendMode::Color => write!(f, "Color"), + BlendMode::Luminosity => write!(f, "Luminosity"), + // Other utility blend modes (hidden from the normal list) + BlendMode::Erase => write!(f, "Erase"), + BlendMode::Restore => write!(f, "Restore"), + BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"), + } + } +} + +#[cfg(feature = "vello")] +impl From for vello::peniko::Mix { + fn from(val: BlendMode) -> Self { + match val { + // Normal group + BlendMode::Normal => vello::peniko::Mix::Normal, + // Darken group + BlendMode::Darken => vello::peniko::Mix::Darken, + BlendMode::Multiply => vello::peniko::Mix::Multiply, + BlendMode::ColorBurn => vello::peniko::Mix::ColorBurn, + // Lighten group + BlendMode::Lighten => vello::peniko::Mix::Lighten, + BlendMode::Screen => vello::peniko::Mix::Screen, + BlendMode::ColorDodge => vello::peniko::Mix::ColorDodge, + // Contrast group + BlendMode::Overlay => vello::peniko::Mix::Overlay, + BlendMode::SoftLight => vello::peniko::Mix::SoftLight, + BlendMode::HardLight => vello::peniko::Mix::HardLight, + // Inversion group + BlendMode::Difference => vello::peniko::Mix::Difference, + BlendMode::Exclusion => vello::peniko::Mix::Exclusion, + // Component group + BlendMode::Hue => vello::peniko::Mix::Hue, + BlendMode::Saturation => vello::peniko::Mix::Saturation, + BlendMode::Color => vello::peniko::Mix::Color, + BlendMode::Luminosity => vello::peniko::Mix::Luminosity, + _ => todo!(), + } + } +} diff --git a/node-graph/gcore/src/blending_nodes.rs b/node-graph/gcore/src/blending_nodes.rs new file mode 100644 index 00000000..fcf2bd0c --- /dev/null +++ b/node-graph/gcore/src/blending_nodes.rs @@ -0,0 +1,175 @@ +use crate::raster::Image; +use crate::raster_types::{CPU, RasterDataTable}; +use crate::registry::types::Percentage; +use crate::vector::VectorDataTable; +use crate::{BlendMode, Color, Ctx, GraphicElement, GraphicGroupTable}; + +pub(super) trait MultiplyAlpha { + fn multiply_alpha(&mut self, factor: f64); +} + +impl MultiplyAlpha for Color { + fn multiply_alpha(&mut self, factor: f64) { + *self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.)) + } +} +impl MultiplyAlpha for VectorDataTable { + fn multiply_alpha(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.opacity *= factor as f32; + } + } +} +impl MultiplyAlpha for GraphicGroupTable { + fn multiply_alpha(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.opacity *= factor as f32; + } + } +} +impl MultiplyAlpha for RasterDataTable +where + GraphicElement: From>, +{ + fn multiply_alpha(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.opacity *= factor as f32; + } + } +} + +pub(super) trait MultiplyFill { + fn multiply_fill(&mut self, factor: f64); +} +impl MultiplyFill for Color { + fn multiply_fill(&mut self, factor: f64) { + *self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.)) + } +} +impl MultiplyFill for VectorDataTable { + fn multiply_fill(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.fill *= factor as f32; + } + } +} +impl MultiplyFill for GraphicGroupTable { + fn multiply_fill(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.fill *= factor as f32; + } + } +} +impl MultiplyFill for RasterDataTable { + fn multiply_fill(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.fill *= factor as f32; + } + } +} + +trait SetBlendMode { + fn set_blend_mode(&mut self, blend_mode: BlendMode); +} + +impl SetBlendMode for VectorDataTable { + fn set_blend_mode(&mut self, blend_mode: BlendMode) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.blend_mode = blend_mode; + } + } +} +impl SetBlendMode for GraphicGroupTable { + fn set_blend_mode(&mut self, blend_mode: BlendMode) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.blend_mode = blend_mode; + } + } +} +impl SetBlendMode for RasterDataTable { + fn set_blend_mode(&mut self, blend_mode: BlendMode) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.blend_mode = blend_mode; + } + } +} + +trait SetClip { + fn set_clip(&mut self, clip: bool); +} + +impl SetClip for VectorDataTable { + fn set_clip(&mut self, clip: bool) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.clip = clip; + } + } +} +impl SetClip for GraphicGroupTable { + fn set_clip(&mut self, clip: bool) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.clip = clip; + } + } +} +impl SetClip for RasterDataTable { + fn set_clip(&mut self, clip: bool) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.clip = clip; + } + } +} + +#[node_macro::node(category("Style"))] +fn blend_mode( + _: impl Ctx, + #[implementations( + GraphicGroupTable, + VectorDataTable, + RasterDataTable, + )] + mut value: T, + blend_mode: BlendMode, +) -> T { + // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result + value.set_blend_mode(blend_mode); + value +} + +#[node_macro::node(category("Style"))] +fn opacity( + _: impl Ctx, + #[implementations( + GraphicGroupTable, + VectorDataTable, + RasterDataTable, + )] + mut value: T, + #[default(100.)] opacity: Percentage, +) -> T { + // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result + value.multiply_alpha(opacity / 100.); + value +} + +#[node_macro::node(category("Style"))] +fn blending( + _: impl Ctx, + #[implementations( + GraphicGroupTable, + VectorDataTable, + RasterDataTable, + )] + mut value: T, + blend_mode: BlendMode, + #[default(100.)] opacity: Percentage, + #[default(100.)] fill: Percentage, + #[default(false)] clip: bool, +) -> T { + // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result + value.set_blend_mode(blend_mode); + value.multiply_alpha(opacity / 100.); + value.multiply_fill(fill / 100.); + value.set_clip(clip); + value +} diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/color/color.rs similarity index 97% rename from node-graph/gcore/src/raster/color.rs rename to node-graph/gcore/src/color/color.rs index e1e0cd2f..69edc39e 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/color/color.rs @@ -1,5 +1,5 @@ +use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB}; use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float}; -use super::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB}; use bytemuck::{Pod, Zeroable}; use dyn_any::DynAny; use half::f16; @@ -345,7 +345,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap(); /// assert!(color.components() == (0.3, 0.14, 0.15, 0.92)); /// @@ -383,7 +383,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62); /// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF); /// assert_eq!(color, color2) @@ -398,7 +398,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61); /// ``` #[inline(always)] @@ -416,7 +416,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.); /// ``` pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color { @@ -458,7 +458,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// assert!(color.r() == 0.114); /// ``` @@ -471,7 +471,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// assert!(color.g() == 0.103); /// ``` @@ -484,7 +484,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// assert!(color.b() == 0.98); /// ``` @@ -497,7 +497,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// assert!(color.a() == 0.97); /// ``` @@ -773,7 +773,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// assert_eq!(color.components(), (0.114, 0.103, 0.98, 0.97)); /// ``` @@ -786,7 +786,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha /// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// ``` @@ -803,7 +803,7 @@ impl Color { /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space. /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// ``` @@ -813,7 +813,7 @@ impl Color { /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space. /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// ``` @@ -825,7 +825,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap(); /// // TODO: Add test /// ``` @@ -840,7 +840,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla(); /// ``` pub fn to_hsla(&self) -> [f32; 4] { @@ -876,7 +876,7 @@ impl Color { /// /// # Examples /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgba_str("7C67FA61").unwrap(); /// ``` pub fn from_rgba_str(color_str: &str) -> Option { @@ -894,7 +894,7 @@ impl Color { /// Creates a color from a 6-character RGB hex string (without a # prefix). /// /// ``` - /// use graphene_core::raster::color::Color; + /// use graphene_core::color::Color; /// let color = Color::from_rgb_str("7C67FA").unwrap(); /// ``` pub fn from_rgb_str(color_str: &str) -> Option { diff --git a/node-graph/gcore/src/color/color_traits.rs b/node-graph/gcore/src/color/color_traits.rs new file mode 100644 index 00000000..b8ffa41f --- /dev/null +++ b/node-graph/gcore/src/color/color_traits.rs @@ -0,0 +1,205 @@ +use bytemuck::{Pod, Zeroable}; +use glam::DVec2; +use std::fmt::Debug; + +#[cfg(target_arch = "spirv")] +use spirv_std::num_traits::float::Float; + +pub use crate::blending::*; + +pub trait Linear { + fn from_f32(x: f32) -> Self; + fn to_f32(self) -> f32; + fn from_f64(x: f64) -> Self; + fn to_f64(self) -> f64; + fn lerp(self, other: Self, value: Self) -> Self + where + Self: Sized + Copy, + Self: std::ops::Sub, + Self: std::ops::Mul, + Self: std::ops::Add, + { + self + (other - self) * value + } +} + +#[rustfmt::skip] +impl Linear for f32 { + #[inline(always)] fn from_f32(x: f32) -> Self { x } + #[inline(always)] fn to_f32(self) -> f32 { self } + #[inline(always)] fn from_f64(x: f64) -> Self { x as f32 } + #[inline(always)] fn to_f64(self) -> f64 { self as f64 } +} + +#[rustfmt::skip] +impl Linear for f64 { + #[inline(always)] fn from_f32(x: f32) -> Self { x as f64 } + #[inline(always)] fn to_f32(self) -> f32 { self as f32 } + #[inline(always)] fn from_f64(x: f64) -> Self { x } + #[inline(always)] fn to_f64(self) -> f64 { self } +} + +pub trait Channel: Copy + Debug { + fn to_linear(self) -> Out; + fn from_linear(linear: In) -> Self; +} + +pub trait LinearChannel: Channel { + fn cast_linear_channel(self) -> Out { + Out::from_linear(self.to_linear::()) + } +} + +impl Channel for T { + #[inline(always)] + fn to_linear(self) -> Out { + Out::from_f64(self.to_f64()) + } + + #[inline(always)] + fn from_linear(linear: In) -> Self { + Self::from_f64(linear.to_f64()) + } +} + +impl LinearChannel for T {} + +use num_derive::*; +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)] +pub struct SRGBGammaFloat(f32); + +impl Channel for SRGBGammaFloat { + #[inline(always)] + fn to_linear(self) -> Out { + let x = self.0; + Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) }) + } + + #[inline(always)] + fn from_linear(linear: In) -> Self { + let x = linear.to_f32(); + if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) } + } +} +pub trait RGBPrimaries { + const RED: DVec2; + const GREEN: DVec2; + const BLUE: DVec2; + const WHITE: DVec2; +} +pub trait Rec709Primaries {} +impl RGBPrimaries for T { + const RED: DVec2 = DVec2::new(0.64, 0.33); + const GREEN: DVec2 = DVec2::new(0.3, 0.6); + const BLUE: DVec2 = DVec2::new(0.15, 0.06); + const WHITE: DVec2 = DVec2::new(0.3127, 0.329); +} + +pub trait SRGB: Rec709Primaries {} + +pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {} +#[cfg(not(feature = "serde"))] +pub trait Serde {} + +impl serde::Deserialize<'a>> Serde for T {} +#[cfg(not(feature = "serde"))] +impl Serde for T {} + +// TODO: Come up with a better name for this trait +pub trait Pixel: Clone + Pod + Zeroable + Default { + #[cfg(not(target_arch = "spirv"))] + fn to_bytes(&self) -> Vec { + bytemuck::bytes_of(self).to_vec() + } + // TODO: use u8 for Color + fn from_bytes(bytes: &[u8]) -> Self { + *bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel") + } + + fn byte_size() -> usize { + size_of::() + } +} +pub trait RGB: Pixel { + type ColorChannel: Channel; + + fn red(&self) -> Self::ColorChannel; + fn r(&self) -> Self::ColorChannel { + self.red() + } + fn green(&self) -> Self::ColorChannel; + fn g(&self) -> Self::ColorChannel { + self.green() + } + fn blue(&self) -> Self::ColorChannel; + fn b(&self) -> Self::ColorChannel { + self.blue() + } +} +pub trait RGBMut: RGB { + fn set_red(&mut self, red: Self::ColorChannel); + fn set_green(&mut self, green: Self::ColorChannel); + fn set_blue(&mut self, blue: Self::ColorChannel); +} + +pub trait AssociatedAlpha: RGB + Alpha { + fn to_unassociated(&self) -> Out; +} + +pub trait UnassociatedAlpha: RGB + Alpha { + fn to_associated(&self) -> Out; +} + +pub trait Alpha { + type AlphaChannel: LinearChannel; + const TRANSPARENT: Self; + fn alpha(&self) -> Self::AlphaChannel; + fn a(&self) -> Self::AlphaChannel { + self.alpha() + } + fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self; +} +pub trait AlphaMut: Alpha { + fn set_alpha(&mut self, value: Self::AlphaChannel); +} + +pub trait Depth { + type DepthChannel: Channel; + fn depth(&self) -> Self::DepthChannel; + fn d(&self) -> Self::DepthChannel { + self.depth() + } +} + +pub trait ExtraChannels { + type ChannelType: Channel; + fn extra_channels(&self) -> [Self::ChannelType; NUM]; +} + +pub trait Luminance { + type LuminanceChannel: LinearChannel; + fn luminance(&self) -> Self::LuminanceChannel; + fn l(&self) -> Self::LuminanceChannel { + self.luminance() + } +} + +pub trait LuminanceMut: Luminance { + fn set_luminance(&mut self, luminance: Self::LuminanceChannel); +} + +// TODO: We might rename this to Raster at some point +pub trait Sample { + type Pixel: Pixel; + // TODO: Add an area parameter + fn sample(&self, pos: DVec2, area: DVec2) -> Option; +} + +impl Sample for &T { + type Pixel = T::Pixel; + + #[inline(always)] + fn sample(&self, pos: DVec2, area: DVec2) -> Option { + (**self).sample(pos, area) + } +} diff --git a/node-graph/gcore/src/raster/discrete_srgb.rs b/node-graph/gcore/src/color/discrete_srgb.rs similarity index 100% rename from node-graph/gcore/src/raster/discrete_srgb.rs rename to node-graph/gcore/src/color/discrete_srgb.rs diff --git a/node-graph/gcore/src/color/mod.rs b/node-graph/gcore/src/color/mod.rs new file mode 100644 index 00000000..3b286c51 --- /dev/null +++ b/node-graph/gcore/src/color/mod.rs @@ -0,0 +1,7 @@ +mod color; +mod color_traits; +mod discrete_srgb; + +pub use color::*; +pub use color_traits::*; +pub use discrete_srgb::*; diff --git a/node-graph/gcore/src/gradient.rs b/node-graph/gcore/src/gradient.rs new file mode 100644 index 00000000..8034cc98 --- /dev/null +++ b/node-graph/gcore/src/gradient.rs @@ -0,0 +1,248 @@ +use crate::Color; +use dyn_any::DynAny; +use glam::{DAffine2, DVec2}; + +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum GradientType { + #[default] + Linear, + Radial, +} + +// TODO: Someday we could switch this to a Box[T] to avoid over-allocation +// TODO: Use linear not gamma colors +/// A list of colors associated with positions (in the range 0 to 1) along a gradient. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] +pub struct GradientStops(pub Vec<(f64, Color)>); + +impl std::hash::Hash for GradientStops { + fn hash(&self, state: &mut H) { + self.0.len().hash(state); + self.0.iter().for_each(|(position, color)| { + position.to_bits().hash(state); + color.hash(state); + }); + } +} + +impl Default for GradientStops { + fn default() -> Self { + Self(vec![(0., Color::BLACK), (1., Color::WHITE)]) + } +} + +impl IntoIterator for GradientStops { + type Item = (f64, Color); + type IntoIter = std::vec::IntoIter<(f64, Color)>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a GradientStops { + type Item = &'a (f64, Color); + type IntoIter = std::slice::Iter<'a, (f64, Color)>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl std::ops::Index for GradientStops { + type Output = (f64, Color); + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +impl std::ops::Deref for GradientStops { + type Target = Vec<(f64, Color)>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for GradientStops { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl GradientStops { + pub fn new(stops: Vec<(f64, Color)>) -> Self { + let mut stops = Self(stops); + stops.sort(); + stops + } + + pub fn evaluate(&self, t: f64) -> Color { + if self.0.is_empty() { + return Color::BLACK; + } + + if t <= self.0[0].0 { + return self.0[0].1; + } + if t >= self.0[self.0.len() - 1].0 { + return self.0[self.0.len() - 1].1; + } + + for i in 0..self.0.len() - 1 { + let (t1, c1) = self.0[i]; + let (t2, c2) = self.0[i + 1]; + if t >= t1 && t <= t2 { + let normalized_t = (t - t1) / (t2 - t1); + return c1.lerp(&c2, normalized_t as f32); + } + } + + Color::BLACK + } + + pub fn sort(&mut self) { + self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + } + + pub fn reversed(&self) -> Self { + Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect()) + } + + pub fn map_colors Color>(&self, f: F) -> Self { + Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect()) + } +} + +/// A gradient fill. +/// +/// Contains the start and end points, along with the colors at varying points along the length. +#[repr(C)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] +pub struct Gradient { + pub stops: GradientStops, + pub gradient_type: GradientType, + pub start: DVec2, + pub end: DVec2, + pub transform: DAffine2, +} + +impl Default for Gradient { + fn default() -> Self { + Self { + stops: GradientStops::default(), + gradient_type: GradientType::Linear, + start: DVec2::new(0., 0.5), + end: DVec2::new(1., 0.5), + transform: DAffine2::IDENTITY, + } + } +} + +impl std::hash::Hash for Gradient { + fn hash(&self, state: &mut H) { + self.stops.0.len().hash(state); + [].iter() + .chain(self.start.to_array().iter()) + .chain(self.end.to_array().iter()) + .chain(self.transform.to_cols_array().iter()) + .chain(self.stops.0.iter().map(|(position, _)| position)) + .for_each(|x| x.to_bits().hash(state)); + self.stops.0.iter().for_each(|(_, color)| color.hash(state)); + self.gradient_type.hash(state); + } +} + +impl std::fmt::Display for Gradient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let round = |x: f64| (x * 1e3).round() / 1e3; + let stops = self + .stops + .0 + .iter() + .map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb())) + .collect::>() + .join(", "); + write!(f, "{} Gradient: {stops}", self.gradient_type) + } +} + +impl Gradient { + /// Constructs a new gradient with the colors at 0 and 1 specified. + pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self { + Gradient { + start, + end, + stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]), + transform, + gradient_type, + } + } + + pub fn lerp(&self, other: &Self, time: f64) -> Self { + let start = self.start + (other.start - self.start) * time; + let end = self.end + (other.end - self.end) * time; + let transform = self.transform; + let stops = self + .stops + .0 + .iter() + .zip(other.stops.0.iter()) + .map(|((a_pos, a_color), (b_pos, b_color))| { + let position = a_pos + (b_pos - a_pos) * time; + let color = a_color.lerp(b_color, time as f32); + (position, color) + }) + .collect::>(); + let stops = GradientStops::new(stops); + let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type }; + + Self { + start, + end, + transform, + stops, + gradient_type, + } + } + + /// Insert a stop into the gradient, the index if successful + pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option { + // Transform the start and end positions to the same coordinate space as the mouse. + let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end)); + + // Calculate the new position by finding the closest point on the line + let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); + + // Don't insert point past end of line + if !(0. ..=1.).contains(&new_position) { + return None; + } + + // Compute the color of the inserted stop + let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) { + // Lerp between the nearest colors if applicable + (a, Some(b)) => a.lerp( + &b, + ((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32, + ), + // Use the start or the end color if applicable + (v, _) => v, + }; + + // Compute the correct index to keep the positions in order + let mut index = 0; + while self.stops.0.len() > index && self.stops.0[index].0 <= new_position { + index += 1; + } + + let new_color = get_color(index - 1, new_position); + + // Insert the new stop + self.stops.0.insert(index, (new_position, new_color)); + + Some(index) + } +} diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 3ca4f43e..744172b2 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -1,5 +1,5 @@ +use crate::blending::AlphaBlending; use crate::instances::{Instance, Instances}; -use crate::raster::BlendMode; use crate::raster::image::Image; use crate::raster_types::{CPU, GPU, Raster, RasterDataTable}; use crate::transform::TransformMut; @@ -12,63 +12,6 @@ use std::hash::Hash; pub mod renderer; -#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)] -#[serde(default)] -pub struct AlphaBlending { - pub blend_mode: BlendMode, - pub opacity: f32, - pub fill: f32, - pub clip: bool, -} -impl Default for AlphaBlending { - fn default() -> Self { - Self::new() - } -} -impl Hash for AlphaBlending { - fn hash(&self, state: &mut H) { - self.opacity.to_bits().hash(state); - self.fill.to_bits().hash(state); - self.blend_mode.hash(state); - self.clip.hash(state); - } -} -impl std::fmt::Display for AlphaBlending { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let round = |x: f32| (x * 1e3).round() / 1e3; - write!( - f, - "Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}", - self.blend_mode, - round(self.opacity * 100.), - round(self.fill * 100.), - if self.clip { "Yes" } else { "No" } - ) - } -} - -impl AlphaBlending { - pub const fn new() -> Self { - Self { - opacity: 1., - fill: 1., - blend_mode: BlendMode::Normal, - clip: false, - } - } - - pub fn lerp(&self, other: &Self, t: f32) -> Self { - let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t; - - AlphaBlending { - opacity: lerp(self.opacity, other.opacity, t), - fill: lerp(self.fill, other.fill, t), - blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode }, - clip: if t < 0.5 { self.clip } else { other.clip }, - } - } -} - // TODO: Eventually remove this migration document upgrade code pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { use serde::Deserialize; diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index f05ef86b..c8e12a4f 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -1,55 +1,23 @@ -mod quad; -mod rect; - use crate::instances::Instance; +pub use crate::math::quad::Quad; +pub use crate::math::rect::Rect; use crate::raster::{BlendMode, Image}; use crate::raster_types::{CPU, GPU, RasterDataTable}; use crate::transform::{Footprint, Transform}; use crate::uuid::{NodeId, generate_uuid}; +use crate::vector::VectorDataTable; +use crate::vector::click_target::{ClickTarget, FreePoint}; use crate::vector::style::{Fill, Stroke, StrokeAlign, ViewMode}; -use crate::vector::{PointId, VectorDataTable}; use crate::{Artboard, ArtboardGroupTable, Color, GraphicElement, GraphicGroupTable}; use bezier_rs::Subpath; use dyn_any::DynAny; -use glam::{DAffine2, DMat2, DVec2}; +use glam::{DAffine2, DVec2}; use num_traits::Zero; -pub use quad::Quad; -pub use rect::Rect; use std::collections::{HashMap, HashSet}; use std::fmt::Write; #[cfg(feature = "vello")] use vello::*; -#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct FreePoint { - pub id: PointId, - pub position: DVec2, -} - -impl FreePoint { - pub fn new(id: PointId, position: DVec2) -> Self { - Self { id, position } - } - - pub fn apply_transform(&mut self, transform: DAffine2) { - self.position = transform.transform_point2(self.position); - } -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum ClickTargetType { - Subpath(Subpath), - FreePoint(FreePoint), -} - -/// Represents a clickable target for the layer -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct ClickTarget { - target_type: ClickTargetType, - stroke_width: f64, - bounding_box: Option<[DVec2; 2]>, -} - #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] enum MaskType { Clip, @@ -73,133 +41,6 @@ impl MaskType { } } -impl ClickTarget { - pub fn new_with_subpath(subpath: Subpath, stroke_width: f64) -> Self { - let bounding_box = subpath.loose_bounding_box(); - Self { - target_type: ClickTargetType::Subpath(subpath), - stroke_width, - bounding_box, - } - } - - pub fn new_with_free_point(point: FreePoint) -> Self { - const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.; - let stroke_width = 10.; - let bounding_box = Some([ - point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT), - point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT), - ]); - - Self { - target_type: ClickTargetType::FreePoint(point), - stroke_width, - bounding_box, - } - } - - pub fn target_type(&self) -> &ClickTargetType { - &self.target_type - } - - pub fn bounding_box(&self) -> Option<[DVec2; 2]> { - self.bounding_box - } - - pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> { - self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)]) - } - - pub fn apply_transform(&mut self, affine_transform: DAffine2) { - match self.target_type { - ClickTargetType::Subpath(ref mut subpath) => { - subpath.apply_transform(affine_transform); - } - ClickTargetType::FreePoint(ref mut point) => { - point.apply_transform(affine_transform); - } - } - self.update_bbox(); - } - - fn update_bbox(&mut self) { - match self.target_type { - ClickTargetType::Subpath(ref subpath) => { - self.bounding_box = subpath.bounding_box(); - } - ClickTargetType::FreePoint(ref point) => { - self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]); - } - } - } - - /// Does the click target intersect the path - pub fn intersect_path>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool { - // Check if the matrix is not invertible - let mut layer_transform = layer_transform; - if layer_transform.matrix2.determinant().abs() <= f64::EPSILON { - layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this? - } - - let inverse = layer_transform.inverse(); - let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point))); - - match self.target_type() { - ClickTargetType::Subpath(subpath) => { - // Check if outlines intersect - let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty()); - if subpath.iter().any(outline_intersects) { - return true; - } - // Check if selection is entirely within the shape - if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) { - return true; - } - - // Check if shape is entirely within selection - let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor); - any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0) - } - ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::() != 0, - } - } - - /// Does the click target intersect the point (accounting for stroke size) - pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool { - let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)]; - let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y; - // This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast. - if !self - .bounding_box - .is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds)) - { - return false; - } - - // Allows for selecting lines - // TODO: actual intersection of stroke - let inflated_quad = Quad::from_box(target_bounds); - self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform) - } - - /// Does the click target intersect the point (not accounting for stroke size) - pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool { - // Check if the point is within the bounding box - if self - .bounding_box - .is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y) - { - // Check if the point is within the shape - match self.target_type() { - ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), - ClickTargetType::FreePoint(free_point) => free_point.position == point, - } - } else { - false - } - } -} - /// Mutable state used whilst rendering to an SVG pub struct SvgRender { pub svg: Vec, diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index 68cf13e4..1a66b38f 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -1,38 +1,40 @@ #[macro_use] extern crate log; -pub use crate as graphene_core; -pub use ctor; -pub use num_traits; - pub mod animation; +pub mod blending; +pub mod blending_nodes; +pub mod color; pub mod consts; pub mod context; pub mod generic; +pub mod gradient; +mod graphic_element; pub mod instances; pub mod logic; +pub mod math; +pub mod memo; pub mod misc; pub mod ops; +pub mod raster; pub mod raster_types; +pub mod registry; pub mod structural; pub mod text; +pub mod transform; +pub mod transform_nodes; pub mod uuid; pub mod value; - -pub mod memo; - -pub mod raster; -pub mod transform; - -mod graphic_element; -pub use graphic_element::*; pub mod vector; -pub mod registry; - +pub use crate as graphene_core; +pub use blending::*; pub use context::*; +pub use ctor; pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync}; +pub use graphic_element::*; pub use memo::MemoHash; +pub use num_traits; pub use raster::Color; use std::any::TypeId; use std::future::Future; @@ -159,3 +161,12 @@ pub trait NodeInputDecleration { fn identifier() -> &'static str; type Result; } + +pub trait AsU32 { + fn as_u32(&self) -> u32; +} +impl AsU32 for u32 { + fn as_u32(&self) -> u32 { + *self + } +} diff --git a/node-graph/gcore/src/raster/bbox.rs b/node-graph/gcore/src/math/bbox.rs similarity index 100% rename from node-graph/gcore/src/raster/bbox.rs rename to node-graph/gcore/src/math/bbox.rs diff --git a/node-graph/gcore/src/math/math_ext.rs b/node-graph/gcore/src/math/math_ext.rs new file mode 100644 index 00000000..bab9c19b --- /dev/null +++ b/node-graph/gcore/src/math/math_ext.rs @@ -0,0 +1,25 @@ +use crate::math::quad::Quad; +use crate::math::rect::Rect; +use bezier_rs::Bezier; + +pub trait QuadExt { + /// Get all the edges in the rect as linear bezier curves + fn bezier_lines(&self) -> impl Iterator + '_; +} + +impl QuadExt for Quad { + fn bezier_lines(&self) -> impl Iterator + '_ { + self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end)) + } +} + +pub trait RectExt { + /// Get all the edges in the quad as linear bezier curves + fn bezier_lines(&self) -> impl Iterator + '_; +} + +impl RectExt for Rect { + fn bezier_lines(&self) -> impl Iterator + '_ { + self.edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end)) + } +} diff --git a/node-graph/gcore/src/math/mod.rs b/node-graph/gcore/src/math/mod.rs new file mode 100644 index 00000000..06a1c21b --- /dev/null +++ b/node-graph/gcore/src/math/mod.rs @@ -0,0 +1,4 @@ +pub mod bbox; +pub mod math_ext; +pub mod quad; +pub mod rect; diff --git a/node-graph/gcore/src/graphic_element/renderer/quad.rs b/node-graph/gcore/src/math/quad.rs similarity index 96% rename from node-graph/gcore/src/graphic_element/renderer/quad.rs rename to node-graph/gcore/src/math/quad.rs index 569f3e3d..8586bba1 100644 --- a/node-graph/gcore/src/graphic_element/renderer/quad.rs +++ b/node-graph/gcore/src/math/quad.rs @@ -58,11 +58,6 @@ impl Quad { self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2)) } - /// Get all the edges in the quad as linear bezier curves - pub fn bezier_lines(&self) -> impl Iterator + '_ { - self.all_edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end)) - } - /// Generates the axis aligned bounding box of the quad pub fn bounding_box(&self) -> [DVec2; 2] { [ diff --git a/node-graph/gcore/src/graphic_element/renderer/rect.rs b/node-graph/gcore/src/math/rect.rs similarity index 92% rename from node-graph/gcore/src/graphic_element/renderer/rect.rs rename to node-graph/gcore/src/math/rect.rs index b1de523b..4998eddb 100644 --- a/node-graph/gcore/src/graphic_element/renderer/rect.rs +++ b/node-graph/gcore/src/math/rect.rs @@ -1,4 +1,4 @@ -use super::Quad; +use crate::math::quad::Quad; use glam::{DAffine2, DVec2}; #[derive(Debug, Clone, Default, Copy, PartialEq)] @@ -43,11 +43,6 @@ impl Rect { [[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]] } - /// Get all the edges in the rect as linear bezier curves - pub fn bezier_lines(&self) -> impl Iterator + '_ { - self.edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end)) - } - /// Gets the center of a rect #[must_use] pub fn center(&self) -> DVec2 { diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index 3d8a1ccf..3bc6d4ab 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -1,222 +1,25 @@ -pub use self::color::{Color, Luma, SRGBA8}; -use crate::Ctx; use crate::GraphicGroupTable; +pub use crate::color::*; use crate::raster_types::{CPU, RasterDataTable}; -use crate::registry::types::Percentage; use crate::vector::VectorDataTable; -use bytemuck::{Pod, Zeroable}; -use glam::DVec2; use std::fmt::Debug; #[cfg(target_arch = "spirv")] use spirv_std::num_traits::float::Float; +/// as to not yet rename all references +pub mod color { + pub use super::*; +} + pub mod adjustments; -pub mod bbox; pub mod brush_cache; -pub mod color; pub mod curve; -pub mod discrete_srgb; +pub mod image; +pub use self::image::Image; pub use adjustments::*; -pub trait Linear { - fn from_f32(x: f32) -> Self; - fn to_f32(self) -> f32; - fn from_f64(x: f64) -> Self; - fn to_f64(self) -> f64; - fn lerp(self, other: Self, value: Self) -> Self - where - Self: Sized + Copy, - Self: std::ops::Sub, - Self: std::ops::Mul, - Self: std::ops::Add, - { - self + (other - self) * value - } -} - -#[rustfmt::skip] -impl Linear for f32 { - #[inline(always)] fn from_f32(x: f32) -> Self { x } - #[inline(always)] fn to_f32(self) -> f32 { self } - #[inline(always)] fn from_f64(x: f64) -> Self { x as f32 } - #[inline(always)] fn to_f64(self) -> f64 { self as f64 } -} - -#[rustfmt::skip] -impl Linear for f64 { - #[inline(always)] fn from_f32(x: f32) -> Self { x as f64 } - #[inline(always)] fn to_f32(self) -> f32 { self as f32 } - #[inline(always)] fn from_f64(x: f64) -> Self { x } - #[inline(always)] fn to_f64(self) -> f64 { self } -} - -pub trait Channel: Copy + Debug { - fn to_linear(self) -> Out; - fn from_linear(linear: In) -> Self; -} - -pub trait LinearChannel: Channel { - fn cast_linear_channel(self) -> Out { - Out::from_linear(self.to_linear::()) - } -} - -impl Channel for T { - #[inline(always)] - fn to_linear(self) -> Out { - Out::from_f64(self.to_f64()) - } - - #[inline(always)] - fn from_linear(linear: In) -> Self { - Self::from_f64(linear.to_f64()) - } -} - -impl LinearChannel for T {} - -use num_derive::*; -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)] -pub struct SRGBGammaFloat(f32); - -impl Channel for SRGBGammaFloat { - #[inline(always)] - fn to_linear(self) -> Out { - let x = self.0; - Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) }) - } - - #[inline(always)] - fn from_linear(linear: In) -> Self { - let x = linear.to_f32(); - if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) } - } -} -pub trait RGBPrimaries { - const RED: DVec2; - const GREEN: DVec2; - const BLUE: DVec2; - const WHITE: DVec2; -} -pub trait Rec709Primaries {} -impl RGBPrimaries for T { - const RED: DVec2 = DVec2::new(0.64, 0.33); - const GREEN: DVec2 = DVec2::new(0.3, 0.6); - const BLUE: DVec2 = DVec2::new(0.15, 0.06); - const WHITE: DVec2 = DVec2::new(0.3127, 0.329); -} - -pub trait SRGB: Rec709Primaries {} - -pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {} -#[cfg(not(feature = "serde"))] -pub trait Serde {} - -impl serde::Deserialize<'a>> Serde for T {} -#[cfg(not(feature = "serde"))] -impl Serde for T {} - -// TODO: Come up with a better name for this trait -pub trait Pixel: Clone + Pod + Zeroable + Default { - #[cfg(not(target_arch = "spirv"))] - fn to_bytes(&self) -> Vec { - bytemuck::bytes_of(self).to_vec() - } - // TODO: use u8 for Color - fn from_bytes(bytes: &[u8]) -> Self { - *bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel") - } - - fn byte_size() -> usize { - size_of::() - } -} -pub trait RGB: Pixel { - type ColorChannel: Channel; - - fn red(&self) -> Self::ColorChannel; - fn r(&self) -> Self::ColorChannel { - self.red() - } - fn green(&self) -> Self::ColorChannel; - fn g(&self) -> Self::ColorChannel { - self.green() - } - fn blue(&self) -> Self::ColorChannel; - fn b(&self) -> Self::ColorChannel { - self.blue() - } -} -pub trait RGBMut: RGB { - fn set_red(&mut self, red: Self::ColorChannel); - fn set_green(&mut self, green: Self::ColorChannel); - fn set_blue(&mut self, blue: Self::ColorChannel); -} - -pub trait AssociatedAlpha: RGB + Alpha { - fn to_unassociated(&self) -> Out; -} - -pub trait UnassociatedAlpha: RGB + Alpha { - fn to_associated(&self) -> Out; -} - -pub trait Alpha { - type AlphaChannel: LinearChannel; - const TRANSPARENT: Self; - fn alpha(&self) -> Self::AlphaChannel; - fn a(&self) -> Self::AlphaChannel { - self.alpha() - } - fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self; -} -pub trait AlphaMut: Alpha { - fn set_alpha(&mut self, value: Self::AlphaChannel); -} - -pub trait Depth { - type DepthChannel: Channel; - fn depth(&self) -> Self::DepthChannel; - fn d(&self) -> Self::DepthChannel { - self.depth() - } -} - -pub trait ExtraChannels { - type ChannelType: Channel; - fn extra_channels(&self) -> [Self::ChannelType; NUM]; -} - -pub trait Luminance { - type LuminanceChannel: LinearChannel; - fn luminance(&self) -> Self::LuminanceChannel; - fn l(&self) -> Self::LuminanceChannel { - self.luminance() - } -} - -pub trait LuminanceMut: Luminance { - fn set_luminance(&mut self, luminance: Self::LuminanceChannel); -} - -// TODO: We might rename this to Raster at some point -pub trait Sample { - type Pixel: Pixel; - // TODO: Add an area parameter - fn sample(&self, pos: DVec2, area: DVec2) -> Option; -} - -impl Sample for &T { - type Pixel = T::Pixel; - - #[inline(always)] - fn sample(&self, pos: DVec2, area: DVec2) -> Option { - (**self).sample(pos, area) - } -} - pub trait Bitmap { type Pixel: Pixel; fn width(&self) -> u32; @@ -282,112 +85,3 @@ impl BitmapMut for &mut T { (*self).get_pixel_mut(x, y) } } - -pub use self::image::Image; -pub mod image; - -trait SetBlendMode { - fn set_blend_mode(&mut self, blend_mode: BlendMode); -} - -impl SetBlendMode for VectorDataTable { - fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.blend_mode = blend_mode; - } - } -} -impl SetBlendMode for GraphicGroupTable { - fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.blend_mode = blend_mode; - } - } -} -impl SetBlendMode for RasterDataTable { - fn set_blend_mode(&mut self, blend_mode: BlendMode) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.blend_mode = blend_mode; - } - } -} - -trait SetClip { - fn set_clip(&mut self, clip: bool); -} - -impl SetClip for VectorDataTable { - fn set_clip(&mut self, clip: bool) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.clip = clip; - } - } -} -impl SetClip for GraphicGroupTable { - fn set_clip(&mut self, clip: bool) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.clip = clip; - } - } -} -impl SetClip for RasterDataTable { - fn set_clip(&mut self, clip: bool) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.clip = clip; - } - } -} - -#[node_macro::node(category("Style"))] -fn blend_mode( - _: impl Ctx, - #[implementations( - GraphicGroupTable, - VectorDataTable, - RasterDataTable, - )] - mut value: T, - blend_mode: BlendMode, -) -> T { - // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result - value.set_blend_mode(blend_mode); - value -} - -#[node_macro::node(category("Style"))] -fn opacity( - _: impl Ctx, - #[implementations( - GraphicGroupTable, - VectorDataTable, - RasterDataTable, - )] - mut value: T, - #[default(100.)] opacity: Percentage, -) -> T { - // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result - value.multiply_alpha(opacity / 100.); - value -} - -#[node_macro::node(category("Style"))] -fn blending( - _: impl Ctx, - #[implementations( - GraphicGroupTable, - VectorDataTable, - RasterDataTable, - )] - mut value: T, - blend_mode: BlendMode, - #[default(100.)] opacity: Percentage, - #[default(100.)] fill: Percentage, - #[default(false)] clip: bool, -) -> T { - // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result - value.set_blend_mode(blend_mode); - value.multiply_alpha(opacity / 100.); - value.multiply_fill(fill / 100.); - value.set_clip(clip); - value -} diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index a39caa81..c2c52eb1 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -1,15 +1,15 @@ #![allow(clippy::too_many_arguments)] +use crate::GraphicElement; +use crate::blending::BlendMode; use crate::raster::curve::{CubicSplines, CurveManipulatorGroup}; use crate::raster::curve::{Curve, ValueMapperNode}; use crate::raster::image::Image; use crate::raster::{Channel, Color, Pixel}; use crate::raster_types::{CPU, Raster, RasterDataTable}; use crate::registry::types::{Angle, Percentage, SignedPercentage}; -use crate::vector::VectorDataTable; use crate::vector::style::GradientStops; use crate::{Ctx, Node}; -use crate::{GraphicElement, GraphicGroupTable}; use dyn_any::DynAny; use std::cmp::Ordering; use std::fmt::Debug; @@ -41,217 +41,6 @@ pub enum LuminanceCalculation { MaximumChannels, } -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type, serde::Serialize, serde::Deserialize)] -#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this? -pub enum BlendMode { - // Basic group - #[default] - Normal, - - // Darken group - Darken, - Multiply, - ColorBurn, - LinearBurn, - DarkerColor, - - // Lighten group - Lighten, - Screen, - ColorDodge, - LinearDodge, - LighterColor, - - // Contrast group - Overlay, - SoftLight, - HardLight, - VividLight, - LinearLight, - PinLight, - HardMix, - - // Inversion group - Difference, - Exclusion, - Subtract, - Divide, - - // Component group - Hue, - Saturation, - Color, - Luminosity, - - // Other stuff - Erase, - Restore, - MultiplyAlpha, -} - -impl BlendMode { - /// All standard blend modes ordered by group. - pub fn list() -> [&'static [BlendMode]; 6] { - use BlendMode::*; - [ - // Normal group - &[Normal], - // Darken group - &[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor], - // Lighten group - &[Lighten, Screen, ColorDodge, LinearDodge, LighterColor], - // Contrast group - &[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix], - // Inversion group - &[Difference, Exclusion, Subtract, Divide], - // Component group - &[Hue, Saturation, Color, Luminosity], - ] - } - - /// The subset of [`BlendMode::list()`] that is supported by SVG. - pub fn list_svg_subset() -> [&'static [BlendMode]; 6] { - use BlendMode::*; - [ - // Normal group - &[Normal], - // Darken group - &[Darken, Multiply, ColorBurn], - // Lighten group - &[Lighten, Screen, ColorDodge], - // Contrast group - &[Overlay, SoftLight, HardLight], - // Inversion group - &[Difference, Exclusion], - // Component group - &[Hue, Saturation, Color, Luminosity], - ] - } - - pub fn index_in_list(&self) -> Option { - Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self) - } - - pub fn index_in_list_svg_subset(&self) -> Option { - Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self) - } - - /// Convert the enum to the CSS string for the blend mode. - /// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values) - pub fn to_svg_style_name(&self) -> Option<&'static str> { - match self { - // Normal group - BlendMode::Normal => Some("normal"), - // Darken group - BlendMode::Darken => Some("darken"), - BlendMode::Multiply => Some("multiply"), - BlendMode::ColorBurn => Some("color-burn"), - // Lighten group - BlendMode::Lighten => Some("lighten"), - BlendMode::Screen => Some("screen"), - BlendMode::ColorDodge => Some("color-dodge"), - // Contrast group - BlendMode::Overlay => Some("overlay"), - BlendMode::SoftLight => Some("soft-light"), - BlendMode::HardLight => Some("hard-light"), - // Inversion group - BlendMode::Difference => Some("difference"), - BlendMode::Exclusion => Some("exclusion"), - // Component group - BlendMode::Hue => Some("hue"), - BlendMode::Saturation => Some("saturation"), - BlendMode::Color => Some("color"), - BlendMode::Luminosity => Some("luminosity"), - _ => None, - } - } - - /// Renders the blend mode CSS style declaration. - pub fn render(&self) -> String { - format!( - r#" mix-blend-mode: {};"#, - self.to_svg_style_name().unwrap_or_else(|| { - warn!("Unsupported blend mode {self:?}"); - "normal" - }) - ) - } -} - -impl std::fmt::Display for BlendMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - // Normal group - BlendMode::Normal => write!(f, "Normal"), - // Darken group - BlendMode::Darken => write!(f, "Darken"), - BlendMode::Multiply => write!(f, "Multiply"), - BlendMode::ColorBurn => write!(f, "Color Burn"), - BlendMode::LinearBurn => write!(f, "Linear Burn"), - BlendMode::DarkerColor => write!(f, "Darker Color"), - // Lighten group - BlendMode::Lighten => write!(f, "Lighten"), - BlendMode::Screen => write!(f, "Screen"), - BlendMode::ColorDodge => write!(f, "Color Dodge"), - BlendMode::LinearDodge => write!(f, "Linear Dodge"), - BlendMode::LighterColor => write!(f, "Lighter Color"), - // Contrast group - BlendMode::Overlay => write!(f, "Overlay"), - BlendMode::SoftLight => write!(f, "Soft Light"), - BlendMode::HardLight => write!(f, "Hard Light"), - BlendMode::VividLight => write!(f, "Vivid Light"), - BlendMode::LinearLight => write!(f, "Linear Light"), - BlendMode::PinLight => write!(f, "Pin Light"), - BlendMode::HardMix => write!(f, "Hard Mix"), - // Inversion group - BlendMode::Difference => write!(f, "Difference"), - BlendMode::Exclusion => write!(f, "Exclusion"), - BlendMode::Subtract => write!(f, "Subtract"), - BlendMode::Divide => write!(f, "Divide"), - // Component group - BlendMode::Hue => write!(f, "Hue"), - BlendMode::Saturation => write!(f, "Saturation"), - BlendMode::Color => write!(f, "Color"), - BlendMode::Luminosity => write!(f, "Luminosity"), - // Other utility blend modes (hidden from the normal list) - BlendMode::Erase => write!(f, "Erase"), - BlendMode::Restore => write!(f, "Restore"), - BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"), - } - } -} - -#[cfg(feature = "vello")] -impl From for vello::peniko::Mix { - fn from(val: BlendMode) -> Self { - match val { - // Normal group - BlendMode::Normal => vello::peniko::Mix::Normal, - // Darken group - BlendMode::Darken => vello::peniko::Mix::Darken, - BlendMode::Multiply => vello::peniko::Mix::Multiply, - BlendMode::ColorBurn => vello::peniko::Mix::ColorBurn, - // Lighten group - BlendMode::Lighten => vello::peniko::Mix::Lighten, - BlendMode::Screen => vello::peniko::Mix::Screen, - BlendMode::ColorDodge => vello::peniko::Mix::ColorDodge, - // Contrast group - BlendMode::Overlay => vello::peniko::Mix::Overlay, - BlendMode::SoftLight => vello::peniko::Mix::SoftLight, - BlendMode::HardLight => vello::peniko::Mix::HardLight, - // Inversion group - BlendMode::Difference => vello::peniko::Mix::Difference, - BlendMode::Exclusion => vello::peniko::Mix::Exclusion, - // Component group - BlendMode::Hue => vello::peniko::Mix::Hue, - BlendMode::Saturation => vello::peniko::Mix::Saturation, - BlendMode::Color => vello::peniko::Mix::Color, - BlendMode::Luminosity => vello::peniko::Mix::Luminosity, - _ => todo!(), - } - } -} - #[node_macro::node(category("Raster: Adjustment"))] fn luminance>( _: impl Ctx, @@ -1272,70 +1061,6 @@ async fn selective_color>( image } -pub(super) trait MultiplyAlpha { - fn multiply_alpha(&mut self, factor: f64); -} - -impl MultiplyAlpha for Color { - fn multiply_alpha(&mut self, factor: f64) { - *self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.)) - } -} -impl MultiplyAlpha for VectorDataTable { - fn multiply_alpha(&mut self, factor: f64) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.opacity *= factor as f32; - } - } -} -impl MultiplyAlpha for GraphicGroupTable { - fn multiply_alpha(&mut self, factor: f64) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.opacity *= factor as f32; - } - } -} -impl MultiplyAlpha for RasterDataTable -where - GraphicElement: From>, -{ - fn multiply_alpha(&mut self, factor: f64) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.opacity *= factor as f32; - } - } -} - -pub(super) trait MultiplyFill { - fn multiply_fill(&mut self, factor: f64); -} -impl MultiplyFill for Color { - fn multiply_fill(&mut self, factor: f64) { - *self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.)) - } -} -impl MultiplyFill for VectorDataTable { - fn multiply_fill(&mut self, factor: f64) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.fill *= factor as f32; - } - } -} -impl MultiplyFill for GraphicGroupTable { - fn multiply_fill(&mut self, factor: f64) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.fill *= factor as f32; - } - } -} -impl MultiplyFill for RasterDataTable { - fn multiply_fill(&mut self, factor: f64) { - for instance in self.instance_mut_iter() { - instance.alpha_blending.fill *= factor as f32; - } - } -} - // Aims for interoperable compatibility with: // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold // @@ -1499,22 +1224,10 @@ fn color_overlay>( #[cfg(test)] mod test { - use crate::raster::adjustments::BlendMode; + use crate::Color; + use crate::blending::BlendMode; use crate::raster::image::Image; use crate::raster_types::{Raster, RasterDataTable}; - use crate::{Color, Node}; - use std::pin::Pin; - - #[derive(Clone)] - pub struct FutureWrapperNode(T); - - impl<'i, T: 'i + Clone + Send> Node<'i, ()> for FutureWrapperNode { - type Output = Pin + 'i + Send>>; - fn eval(&'i self, _input: ()) -> Self::Output { - let value = self.0.clone(); - Box::pin(async move { value }) - } - } #[tokio::test] async fn color_overlay_multiply() { diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs index 1c920a16..b3b70849 100644 --- a/node-graph/gcore/src/raster/image.rs +++ b/node-graph/gcore/src/raster/image.rs @@ -1,6 +1,6 @@ use super::Color; -use super::discrete_srgb::float_to_srgb_u8; use crate::AlphaBlending; +use crate::color::float_to_srgb_u8; use crate::instances::{Instance, Instances}; use crate::raster_types::Raster; use core::hash::{Hash, Hasher}; diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index 4a7cbdbf..2727a957 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -59,7 +59,7 @@ pub struct FieldMetadata { pub unit: Option<&'static str>, } -pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync { +pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync { const WIDGET_HINT: ChoiceWidgetHint; const DESCRIPTION: Option<&'static str>; fn list() -> &'static [&'static [(Self, VariantMetadata)]]; diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs index e1587ec7..dafd3791 100644 --- a/node-graph/gcore/src/transform.rs +++ b/node-graph/gcore/src/transform.rs @@ -1,8 +1,6 @@ -use crate::instances::Instances; -use crate::raster::bbox::AxisAlignedBbox; -use crate::raster_types::{CPU, GPU, RasterDataTable}; -use crate::vector::VectorDataTable; -use crate::{Artboard, CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl}; +use crate::Artboard; +use crate::math::bbox::AxisAlignedBbox; +pub use crate::vector::ReferencePoint; use core::f64; use glam::{DAffine2, DMat2, DVec2}; @@ -152,186 +150,3 @@ impl ApplyTransform for T { impl ApplyTransform for () { fn apply_transform(&mut self, &_modification: &DAffine2) {} } - -#[node_macro::node(category(""))] -async fn transform( - ctx: impl Ctx + CloneVarArgs + ExtractAll, - #[implementations( - Context -> VectorDataTable, - Context -> GraphicGroupTable, - Context -> RasterDataTable, - Context -> RasterDataTable, - )] - transform_target: impl Node, Output = Instances>, - translate: DVec2, - rotate: f64, - scale: DVec2, - shear: DVec2, - _pivot: DVec2, -) -> Instances { - let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]); - - let footprint = ctx.try_footprint().copied(); - - let mut ctx = OwnedContextImpl::from(ctx); - if let Some(mut footprint) = footprint { - footprint.apply_transform(&matrix); - ctx = ctx.with_footprint(footprint); - } - - let mut transform_target = transform_target.eval(ctx.into_context()).await; - - for data_transform in transform_target.instance_mut_iter() { - *data_transform.transform = matrix * *data_transform.transform; - } - - transform_target -} - -#[node_macro::node(category(""))] -fn replace_transform( - _: impl Ctx, - #[implementations(VectorDataTable, RasterDataTable, GraphicGroupTable)] mut data: Instances, - #[implementations(DAffine2)] transform: TransformInput, -) -> Instances { - for data_transform in data.instance_mut_iter() { - *data_transform.transform = transform.transform(); - } - data -} - -#[node_macro::node(category("Debug"))] -async fn boundless_footprint( - ctx: impl Ctx + CloneVarArgs + ExtractAll, - #[implementations( - Context -> VectorDataTable, - Context -> GraphicGroupTable, - Context -> RasterDataTable, - Context -> RasterDataTable, - Context -> String, - Context -> f64, - )] - transform_target: impl Node, Output = T>, -) -> T { - let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS); - - transform_target.eval(ctx.into_context()).await -} -#[node_macro::node(category("Debug"))] -async fn freeze_real_time( - ctx: impl Ctx + CloneVarArgs + ExtractAll, - #[implementations( - Context -> VectorDataTable, - Context -> GraphicGroupTable, - Context -> RasterDataTable, - Context -> RasterDataTable, - Context -> String, - Context -> f64, - )] - transform_target: impl Node, Output = T>, -) -> T { - let ctx = OwnedContextImpl::from(ctx).with_real_time(0.); - - transform_target.eval(ctx.into_context()).await -} - -#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum ReferencePoint { - #[default] - None, - TopLeft, - TopCenter, - TopRight, - CenterLeft, - Center, - CenterRight, - BottomLeft, - BottomCenter, - BottomRight, -} - -impl ReferencePoint { - pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option { - let size = bounding_box.size(); - let offset = match self { - ReferencePoint::None => return None, - ReferencePoint::TopLeft => DVec2::ZERO, - ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.), - ReferencePoint::TopRight => DVec2::new(size.x, 0.), - ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.), - ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.), - ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.), - ReferencePoint::BottomLeft => DVec2::new(0., size.y), - ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y), - ReferencePoint::BottomRight => DVec2::new(size.x, size.y), - }; - Some(bounding_box.start + offset) - } -} - -impl From<&str> for ReferencePoint { - fn from(input: &str) -> Self { - match input { - "None" => ReferencePoint::None, - "TopLeft" => ReferencePoint::TopLeft, - "TopCenter" => ReferencePoint::TopCenter, - "TopRight" => ReferencePoint::TopRight, - "CenterLeft" => ReferencePoint::CenterLeft, - "Center" => ReferencePoint::Center, - "CenterRight" => ReferencePoint::CenterRight, - "BottomLeft" => ReferencePoint::BottomLeft, - "BottomCenter" => ReferencePoint::BottomCenter, - "BottomRight" => ReferencePoint::BottomRight, - _ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"), - } - } -} - -impl From for Option { - fn from(input: ReferencePoint) -> Self { - match input { - ReferencePoint::None => None, - ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)), - ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)), - ReferencePoint::TopRight => Some(DVec2::new(1., 0.)), - ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)), - ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)), - ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)), - ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)), - ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)), - ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)), - } - } -} - -impl From for ReferencePoint { - fn from(input: DVec2) -> Self { - const TOLERANCE: f64 = 1e-5_f64; - if input.y.abs() < TOLERANCE { - if input.x.abs() < TOLERANCE { - return ReferencePoint::TopLeft; - } else if (input.x - 0.5).abs() < TOLERANCE { - return ReferencePoint::TopCenter; - } else if (input.x - 1.).abs() < TOLERANCE { - return ReferencePoint::TopRight; - } - } else if (input.y - 0.5).abs() < TOLERANCE { - if input.x.abs() < TOLERANCE { - return ReferencePoint::CenterLeft; - } else if (input.x - 0.5).abs() < TOLERANCE { - return ReferencePoint::Center; - } else if (input.x - 1.).abs() < TOLERANCE { - return ReferencePoint::CenterRight; - } - } else if (input.y - 1.).abs() < TOLERANCE { - if input.x.abs() < TOLERANCE { - return ReferencePoint::BottomLeft; - } else if (input.x - 0.5).abs() < TOLERANCE { - return ReferencePoint::BottomCenter; - } else if (input.x - 1.).abs() < TOLERANCE { - return ReferencePoint::BottomRight; - } - } - ReferencePoint::None - } -} diff --git a/node-graph/gcore/src/transform_nodes.rs b/node-graph/gcore/src/transform_nodes.rs new file mode 100644 index 00000000..39024681 --- /dev/null +++ b/node-graph/gcore/src/transform_nodes.rs @@ -0,0 +1,89 @@ +use crate::instances::Instances; +use crate::raster_types::{CPU, GPU, RasterDataTable}; +use crate::transform::{ApplyTransform, Footprint, Transform}; +use crate::vector::VectorDataTable; +use crate::{CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl}; +use core::f64; +use glam::{DAffine2, DVec2}; + +#[node_macro::node(category(""))] +async fn transform( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[implementations( + Context -> VectorDataTable, + Context -> GraphicGroupTable, + Context -> RasterDataTable, + Context -> RasterDataTable, + )] + transform_target: impl Node, Output = Instances>, + translate: DVec2, + rotate: f64, + scale: DVec2, + shear: DVec2, + _pivot: DVec2, +) -> Instances { + let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]); + + let footprint = ctx.try_footprint().copied(); + + let mut ctx = OwnedContextImpl::from(ctx); + if let Some(mut footprint) = footprint { + footprint.apply_transform(&matrix); + ctx = ctx.with_footprint(footprint); + } + + let mut transform_target = transform_target.eval(ctx.into_context()).await; + + for data_transform in transform_target.instance_mut_iter() { + *data_transform.transform = matrix * *data_transform.transform; + } + + transform_target +} + +#[node_macro::node(category(""))] +fn replace_transform( + _: impl Ctx, + #[implementations(VectorDataTable, RasterDataTable, GraphicGroupTable)] mut data: Instances, + #[implementations(DAffine2)] transform: TransformInput, +) -> Instances { + for data_transform in data.instance_mut_iter() { + *data_transform.transform = transform.transform(); + } + data +} + +#[node_macro::node(category("Debug"))] +async fn boundless_footprint( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[implementations( + Context -> VectorDataTable, + Context -> GraphicGroupTable, + Context -> RasterDataTable, + Context -> RasterDataTable, + Context -> String, + Context -> f64, + )] + transform_target: impl Node, Output = T>, +) -> T { + let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS); + + transform_target.eval(ctx.into_context()).await +} +#[node_macro::node(category("Debug"))] +async fn freeze_real_time( + ctx: impl Ctx + CloneVarArgs + ExtractAll, + #[implementations( + Context -> VectorDataTable, + Context -> GraphicGroupTable, + Context -> RasterDataTable, + Context -> RasterDataTable, + Context -> String, + Context -> f64, + )] + transform_target: impl Node, Output = T>, +) -> T { + let ctx = OwnedContextImpl::from(ctx).with_real_time(0.); + + transform_target.eval(ctx.into_context()).await +} diff --git a/node-graph/gcore/src/vector/brush_stroke.rs b/node-graph/gcore/src/vector/brush_stroke.rs index e1c2ae63..17ec7fe4 100644 --- a/node-graph/gcore/src/vector/brush_stroke.rs +++ b/node-graph/gcore/src/vector/brush_stroke.rs @@ -1,6 +1,6 @@ use crate::Color; +use crate::math::bbox::AxisAlignedBbox; use crate::raster::BlendMode; -use crate::raster::bbox::AxisAlignedBbox; use dyn_any::DynAny; use glam::DVec2; use std::hash::{Hash, Hasher}; diff --git a/node-graph/gcore/src/vector/click_target.rs b/node-graph/gcore/src/vector/click_target.rs new file mode 100644 index 00000000..a1d06868 --- /dev/null +++ b/node-graph/gcore/src/vector/click_target.rs @@ -0,0 +1,162 @@ +use crate::math::math_ext::QuadExt; +use crate::renderer::Quad; +use crate::vector::PointId; +use bezier_rs::Subpath; +use glam::{DAffine2, DMat2, DVec2}; + +#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FreePoint { + pub id: PointId, + pub position: DVec2, +} + +impl FreePoint { + pub fn new(id: PointId, position: DVec2) -> Self { + Self { id, position } + } + + pub fn apply_transform(&mut self, transform: DAffine2) { + self.position = transform.transform_point2(self.position); + } +} + +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum ClickTargetType { + Subpath(Subpath), + FreePoint(FreePoint), +} + +/// Represents a clickable target for the layer +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ClickTarget { + target_type: ClickTargetType, + stroke_width: f64, + bounding_box: Option<[DVec2; 2]>, +} + +impl ClickTarget { + pub fn new_with_subpath(subpath: Subpath, stroke_width: f64) -> Self { + let bounding_box = subpath.loose_bounding_box(); + Self { + target_type: ClickTargetType::Subpath(subpath), + stroke_width, + bounding_box, + } + } + + pub fn new_with_free_point(point: FreePoint) -> Self { + const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.; + let stroke_width = 10.; + let bounding_box = Some([ + point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT), + point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT), + ]); + + Self { + target_type: ClickTargetType::FreePoint(point), + stroke_width, + bounding_box, + } + } + + pub fn target_type(&self) -> &ClickTargetType { + &self.target_type + } + + pub fn bounding_box(&self) -> Option<[DVec2; 2]> { + self.bounding_box + } + + pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)]) + } + + pub fn apply_transform(&mut self, affine_transform: DAffine2) { + match self.target_type { + ClickTargetType::Subpath(ref mut subpath) => { + subpath.apply_transform(affine_transform); + } + ClickTargetType::FreePoint(ref mut point) => { + point.apply_transform(affine_transform); + } + } + self.update_bbox(); + } + + fn update_bbox(&mut self) { + match self.target_type { + ClickTargetType::Subpath(ref subpath) => { + self.bounding_box = subpath.bounding_box(); + } + ClickTargetType::FreePoint(ref point) => { + self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]); + } + } + } + + /// Does the click target intersect the path + pub fn intersect_path>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool { + // Check if the matrix is not invertible + let mut layer_transform = layer_transform; + if layer_transform.matrix2.determinant().abs() <= f64::EPSILON { + layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this? + } + + let inverse = layer_transform.inverse(); + let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point))); + + match self.target_type() { + ClickTargetType::Subpath(subpath) => { + // Check if outlines intersect + let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty()); + if subpath.iter().any(outline_intersects) { + return true; + } + // Check if selection is entirely within the shape + if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) { + return true; + } + + // Check if shape is entirely within selection + let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor); + any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0) + } + ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::() != 0, + } + } + + /// Does the click target intersect the point (accounting for stroke size) + pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool { + let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)]; + let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y; + // This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast. + if !self + .bounding_box + .is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds)) + { + return false; + } + + // Allows for selecting lines + // TODO: actual intersection of stroke + let inflated_quad = Quad::from_box(target_bounds); + self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform) + } + + /// Does the click target intersect the point (not accounting for stroke size) + pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool { + // Check if the point is within the bounding box + if self + .bounding_box + .is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y) + { + // Check if the point is within the shape + match self.target_type() { + ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), + ClickTargetType::FreePoint(free_point) => free_point.position == point, + } + } else { + false + } + } +} diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index f29286b6..e8b2bef9 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -29,15 +29,6 @@ pub enum BooleanOperation { Difference, } -pub trait AsU32 { - fn as_u32(&self) -> u32; -} -impl AsU32 for u32 { - fn as_u32(&self) -> u32 { - *self - } -} - pub trait AsU64 { fn as_u64(&self) -> u64; } diff --git a/node-graph/gcore/src/vector/mod.rs b/node-graph/gcore/src/vector/mod.rs index 12869434..5293e88f 100644 --- a/node-graph/gcore/src/vector/mod.rs +++ b/node-graph/gcore/src/vector/mod.rs @@ -1,12 +1,15 @@ mod algorithms; pub mod brush_stroke; +pub mod click_target; pub mod generator_nodes; pub mod misc; +mod reference_point; pub mod style; mod vector_data; mod vector_nodes; pub use bezier_rs; +pub use reference_point::*; pub use style::PathStyle; pub use vector_data::*; pub use vector_nodes::*; diff --git a/node-graph/gcore/src/vector/reference_point.rs b/node-graph/gcore/src/vector/reference_point.rs new file mode 100644 index 00000000..7c618032 --- /dev/null +++ b/node-graph/gcore/src/vector/reference_point.rs @@ -0,0 +1,103 @@ +use crate::math::bbox::AxisAlignedBbox; +use glam::DVec2; + +#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum ReferencePoint { + #[default] + None, + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +} + +impl ReferencePoint { + pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option { + let size = bounding_box.size(); + let offset = match self { + ReferencePoint::None => return None, + ReferencePoint::TopLeft => DVec2::ZERO, + ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.), + ReferencePoint::TopRight => DVec2::new(size.x, 0.), + ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.), + ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.), + ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.), + ReferencePoint::BottomLeft => DVec2::new(0., size.y), + ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y), + ReferencePoint::BottomRight => DVec2::new(size.x, size.y), + }; + Some(bounding_box.start + offset) + } +} + +impl From<&str> for ReferencePoint { + fn from(input: &str) -> Self { + match input { + "None" => ReferencePoint::None, + "TopLeft" => ReferencePoint::TopLeft, + "TopCenter" => ReferencePoint::TopCenter, + "TopRight" => ReferencePoint::TopRight, + "CenterLeft" => ReferencePoint::CenterLeft, + "Center" => ReferencePoint::Center, + "CenterRight" => ReferencePoint::CenterRight, + "BottomLeft" => ReferencePoint::BottomLeft, + "BottomCenter" => ReferencePoint::BottomCenter, + "BottomRight" => ReferencePoint::BottomRight, + _ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"), + } + } +} + +impl From for Option { + fn from(input: ReferencePoint) -> Self { + match input { + ReferencePoint::None => None, + ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)), + ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)), + ReferencePoint::TopRight => Some(DVec2::new(1., 0.)), + ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)), + ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)), + ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)), + ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)), + ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)), + ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)), + } + } +} + +impl From for ReferencePoint { + fn from(input: DVec2) -> Self { + const TOLERANCE: f64 = 1e-5_f64; + if input.y.abs() < TOLERANCE { + if input.x.abs() < TOLERANCE { + return ReferencePoint::TopLeft; + } else if (input.x - 0.5).abs() < TOLERANCE { + return ReferencePoint::TopCenter; + } else if (input.x - 1.).abs() < TOLERANCE { + return ReferencePoint::TopRight; + } + } else if (input.y - 0.5).abs() < TOLERANCE { + if input.x.abs() < TOLERANCE { + return ReferencePoint::CenterLeft; + } else if (input.x - 0.5).abs() < TOLERANCE { + return ReferencePoint::Center; + } else if (input.x - 1.).abs() < TOLERANCE { + return ReferencePoint::CenterRight; + } + } else if (input.y - 1.).abs() < TOLERANCE { + if input.x.abs() < TOLERANCE { + return ReferencePoint::BottomLeft; + } else if (input.x - 0.5).abs() < TOLERANCE { + return ReferencePoint::BottomCenter; + } else if (input.x - 1.).abs() < TOLERANCE { + return ReferencePoint::BottomRight; + } + } + ReferencePoint::None + } +} diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 7a5783ac..3a916514 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -2,217 +2,13 @@ use crate::Color; use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; +pub use crate::gradient::*; use crate::renderer::{RenderParams, format_transform_matrix}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use std::fmt::Write; -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)] -#[widget(Radio)] -pub enum GradientType { - #[default] - Linear, - Radial, -} - -// TODO: Someday we could switch this to a Box[T] to avoid over-allocation -// TODO: Use linear not gamma colors -/// A list of colors associated with positions (in the range 0 to 1) along a gradient. -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] -pub struct GradientStops(Vec<(f64, Color)>); - -impl std::hash::Hash for GradientStops { - fn hash(&self, state: &mut H) { - self.0.len().hash(state); - self.0.iter().for_each(|(position, color)| { - position.to_bits().hash(state); - color.hash(state); - }); - } -} - -impl Default for GradientStops { - fn default() -> Self { - Self(vec![(0., Color::BLACK), (1., Color::WHITE)]) - } -} - -impl IntoIterator for GradientStops { - type Item = (f64, Color); - type IntoIter = std::vec::IntoIter<(f64, Color)>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a> IntoIterator for &'a GradientStops { - type Item = &'a (f64, Color); - type IntoIter = std::slice::Iter<'a, (f64, Color)>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -impl std::ops::Index for GradientStops { - type Output = (f64, Color); - - fn index(&self, index: usize) -> &Self::Output { - &self.0[index] - } -} - -impl std::ops::Deref for GradientStops { - type Target = Vec<(f64, Color)>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for GradientStops { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl GradientStops { - pub fn new(stops: Vec<(f64, Color)>) -> Self { - let mut stops = Self(stops); - stops.sort(); - stops - } - - pub fn evaluate(&self, t: f64) -> Color { - if self.0.is_empty() { - return Color::BLACK; - } - - if t <= self.0[0].0 { - return self.0[0].1; - } - if t >= self.0[self.0.len() - 1].0 { - return self.0[self.0.len() - 1].1; - } - - for i in 0..self.0.len() - 1 { - let (t1, c1) = self.0[i]; - let (t2, c2) = self.0[i + 1]; - if t >= t1 && t <= t2 { - let normalized_t = (t - t1) / (t2 - t1); - return c1.lerp(&c2, normalized_t as f32); - } - } - - Color::BLACK - } - - pub fn sort(&mut self) { - self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - } - - pub fn reversed(&self) -> Self { - Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect()) - } - - pub fn map_colors Color>(&self, f: F) -> Self { - Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect()) - } -} - -/// A gradient fill. -/// -/// Contains the start and end points, along with the colors at varying points along the length. -#[repr(C)] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] -pub struct Gradient { - pub stops: GradientStops, - pub gradient_type: GradientType, - pub start: DVec2, - pub end: DVec2, - pub transform: DAffine2, -} - -impl Default for Gradient { - fn default() -> Self { - Self { - stops: GradientStops::default(), - gradient_type: GradientType::Linear, - start: DVec2::new(0., 0.5), - end: DVec2::new(1., 0.5), - transform: DAffine2::IDENTITY, - } - } -} - -impl std::hash::Hash for Gradient { - fn hash(&self, state: &mut H) { - self.stops.0.len().hash(state); - [].iter() - .chain(self.start.to_array().iter()) - .chain(self.end.to_array().iter()) - .chain(self.transform.to_cols_array().iter()) - .chain(self.stops.0.iter().map(|(position, _)| position)) - .for_each(|x| x.to_bits().hash(state)); - self.stops.0.iter().for_each(|(_, color)| color.hash(state)); - self.gradient_type.hash(state); - } -} - -impl std::fmt::Display for Gradient { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let round = |x: f64| (x * 1e3).round() / 1e3; - let stops = self - .stops - .0 - .iter() - .map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb())) - .collect::>() - .join(", "); - write!(f, "{} Gradient: {stops}", self.gradient_type) - } -} - impl Gradient { - /// Constructs a new gradient with the colors at 0 and 1 specified. - pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self { - Gradient { - start, - end, - stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]), - transform, - gradient_type, - } - } - - pub fn lerp(&self, other: &Self, time: f64) -> Self { - let start = self.start + (other.start - self.start) * time; - let end = self.end + (other.end - self.end) * time; - let transform = self.transform; - let stops = self - .stops - .0 - .iter() - .zip(other.stops.0.iter()) - .map(|((a_pos, a_color), (b_pos, b_color))| { - let position = a_pos + (b_pos - a_pos) * time; - let color = a_color.lerp(b_color, time as f32); - (position, color) - }) - .collect::>(); - let stops = GradientStops::new(stops); - let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type }; - - Self { - start, - end, - transform, - stops, - gradient_type, - } - } - /// Adds the gradient def through mutating the first argument, returning the gradient ID. fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], _render_params: &RenderParams) -> u64 { // TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to. @@ -268,44 +64,6 @@ impl Gradient { gradient_id } - - /// Insert a stop into the gradient, the index if successful - pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option { - // Transform the start and end positions to the same coordinate space as the mouse. - let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end)); - - // Calculate the new position by finding the closest point on the line - let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); - - // Don't insert point past end of line - if !(0. ..=1.).contains(&new_position) { - return None; - } - - // Compute the color of the inserted stop - let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) { - // Lerp between the nearest colors if applicable - (a, Some(b)) => a.lerp( - &b, - ((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32, - ), - // Use the start or the end color if applicable - (v, _) => v, - }; - - // Compute the correct index to keep the positions in order - let mut index = 0; - while self.stops.0.len() > index && self.stops.0[index].0 <= new_position { - index += 1; - } - - let new_color = get_color(index - 1, new_position); - - // Insert the new stop - self.stops.0.insert(index, (new_position, new_color)); - - Some(index) - } } /// Describes the fill of a layer. diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 66b2d22f..6b442926 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -5,7 +5,7 @@ mod modification; use super::misc::{dvec2_to_point, point_to_dvec2}; use super::style::{PathStyle, Stroke}; use crate::instances::Instances; -use crate::renderer::{ClickTargetType, FreePoint}; +use crate::vector::click_target::{ClickTargetType, FreePoint}; use crate::{AlphaBlending, Color, GraphicGroupTable}; pub use attributes::*; use bezier_rs::ManipulatorGroup; diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs index ed4cbb16..58a215a0 100644 --- a/node-graph/gstd/src/brush.rs +++ b/node-graph/gstd/src/brush.rs @@ -3,8 +3,8 @@ use glam::{DAffine2, DVec2}; use graph_craft::generic::FnNode; use graph_craft::proto::FutureWrapperNode; use graphene_core::instances::Instance; +use graphene_core::math::bbox::{AxisAlignedBbox, Bbox}; use graphene_core::raster::adjustments::blend_colors; -use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox}; use graphene_core::raster::brush_cache::BrushCache; use graphene_core::raster::image::Image; use graphene_core::raster::{Alpha, BitmapMut, BlendMode, Color, Pixel, Sample}; diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index a3dfc770..146d4d1d 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -2,11 +2,11 @@ use dyn_any::DynAny; use fastnoise_lite; use glam::{DAffine2, DVec2, Vec2}; use graphene_core::instances::Instance; -use graphene_core::raster::bbox::Bbox; +use graphene_core::math::bbox::Bbox; pub use graphene_core::raster::*; use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; use graphene_core::transform::Transform; -use graphene_core::{AlphaBlending, Ctx, ExtractFootprint}; +use graphene_core::{Ctx, ExtractFootprint}; use rand::prelude::*; use rand_chacha::ChaCha8Rng; use std::fmt::Debug; diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 6c8f52cc..5cd7b4f9 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -5,7 +5,7 @@ use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; #[cfg(target_arch = "wasm32")] use graphene_core::instances::Instances; #[cfg(target_arch = "wasm32")] -use graphene_core::raster::bbox::Bbox; +use graphene_core::math::bbox::Bbox; use graphene_core::raster::image::Image; use graphene_core::raster_types::{CPU, Raster, RasterDataTable}; use graphene_core::renderer::RenderMetadata; diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 4f45992c..838f9985 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -12,14 +12,20 @@ use graphene_core::{NodeIO, NodeIOTypes}; use graphene_core::{fn_type_fut, future}; use graphene_std::Context; use graphene_std::GraphicElement; -use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, IntoTypeErasedNode}; +#[cfg(feature = "gpu")] +use graphene_std::any::DowncastBothNode; +use graphene_std::any::{ComposeTypeErased, DynAnyNode, IntoTypeErasedNode}; use graphene_std::application_io::{ImageTexture, SurfaceFrame}; -use graphene_std::wasm_application_io::*; +#[cfg(feature = "gpu")] +use graphene_std::wasm_application_io::{WasmEditorApi, WasmSurfaceHandle}; use node_registry_macros::{async_node, convert_node, into_node}; use once_cell::sync::Lazy; use std::collections::HashMap; +#[cfg(feature = "gpu")] use std::sync::Arc; -use wgpu_executor::{WgpuExecutor, WgpuSurface, WindowHandle}; +#[cfg(feature = "gpu")] +use wgpu_executor::WgpuExecutor; +use wgpu_executor::{WgpuSurface, WindowHandle}; // TODO: turn into hashmap fn node_registry() -> HashMap> { diff --git a/node-graph/node-macro/src/derive_choice_type.rs b/node-graph/node-macro/src/derive_choice_type.rs index 14df0f24..380b0409 100644 --- a/node-graph/node-macro/src/derive_choice_type.rs +++ b/node-graph/node-macro/src/derive_choice_type.rs @@ -168,7 +168,7 @@ fn derive_enum(enum_attributes: &[Attribute], name: Ident, input: syn::DataEnum) WidgetHint::Dropdown => quote! { Dropdown }, }; Ok(quote! { - impl #crate_name::vector::misc::AsU32 for #name { + impl #crate_name::AsU32 for #name { fn as_u32(&self) -> u32 { *self as u32 }