Add a basic API and rudimentary frontend for node graph layers (#846)

* Node graph API stub

* Rename and fix SetInputValue

* Get list of links from network

* Test populating node graph UI

* Node properties

* Fix viewport bounds

* Slightly change promise usage

* A tiny bit of cleanup I did while reading code

* Cleanup and work towards hooking up node links in Vue template

* Add the brighten colour node

* Run cargo fmt

* Add to and from hsla

* GrayscaleImage node with small perf improvement

* Fix gutter panel resizing

* Display node links from backend

* Add support for connecting node links

* Use existing message

* Fix formatting error

* Add a (currently crashing) brighten node

* Replace brighten node with proto node implementation

* Add support for connecting node links

* Update watch dirs

* Add hue shift node

* Add create_node function to editor api

* Basic insert node UI

* Fix broken names

* Add log

* Fix positioning

* Set connector index to 0

* Add properties for Heu shift / brighten

* Allow deselecting nodes

* Redesign Properties panel collapsible sections

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
0HyperCube 2022-11-12 21:23:28 +00:00 committed by Keavon Chambers
parent e8256dd350
commit 504136b61b
37 changed files with 1213 additions and 294 deletions

View file

@ -22,6 +22,58 @@ impl<'n> Node<Color> for &'n GrayscaleNode {
}
}
#[derive(Debug, Clone, Copy)]
pub struct BrightenColorNode<N: Node<(), Output = f32>>(N);
impl<N: Node<(), Output = f32>> Node<Color> for BrightenColorNode<N> {
type Output = Color;
fn eval(self, color: Color) -> Color {
let brightness = self.0.eval(());
let per_channel = |col: f32| (col + brightness / 255.).clamp(0., 1.);
Color::from_rgbaf32_unchecked(per_channel(color.r()), per_channel(color.g()), per_channel(color.b()), color.a())
}
}
impl<N: Node<(), Output = f32> + Copy> Node<Color> for &BrightenColorNode<N> {
type Output = Color;
fn eval(self, color: Color) -> Color {
let brightness = self.0.eval(());
let per_channel = |col: f32| (col + brightness / 255.).clamp(0., 1.);
Color::from_rgbaf32_unchecked(per_channel(color.r()), per_channel(color.g()), per_channel(color.b()), color.a())
}
}
impl<N: Node<(), Output = f32> + Copy> BrightenColorNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[derive(Debug, Clone, Copy)]
pub struct HueShiftNode<N: Node<(), Output = f32>>(N);
impl<N: Node<(), Output = f32>> Node<Color> for HueShiftNode<N> {
type Output = Color;
fn eval(self, color: Color) -> Color {
let hue_shift = self.0.eval(());
let [hue, saturation, luminance, alpha] = color.to_hsla();
Color::from_hsla(hue + hue_shift / 360., saturation, luminance, alpha)
}
}
impl<N: Node<(), Output = f32> + Copy> Node<Color> for &HueShiftNode<N> {
type Output = Color;
fn eval(self, color: Color) -> Color {
let hue_shift = self.0.eval(());
let [hue, saturation, luminance, alpha] = color.to_hsla();
Color::from_hsla(hue + hue_shift / 360., saturation, luminance, alpha)
}
}
impl<N: Node<(), Output = f32> + Copy> HueShiftNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
pub struct ForEachNode<MN>(pub MN);
impl<'n, I: Iterator<Item = S>, MN: 'n, S> Node<I> for &'n ForEachNode<MN>

View file

@ -82,6 +82,41 @@ impl Color {
}
}
/// Create a [Color] from a hue, saturation, luminance and alpha (all between 0 and 1)
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.);
/// ```
pub fn from_hsla(hue: f32, saturation: f32, luminance: f32, alpha: f32) -> Color {
let temp1 = if luminance < 0.5 {
luminance * (saturation + 1.)
} else {
luminance + saturation - luminance * saturation
};
let temp2 = 2. * luminance - temp1;
let mut red = (hue + 1. / 3.).rem_euclid(1.);
let mut green = hue.rem_euclid(1.);
let mut blue = (hue - 1. / 3.).rem_euclid(1.);
for channel in [&mut red, &mut green, &mut blue] {
*channel = if *channel * 6. < 1. {
temp2 + (temp1 - temp2) * 6. * *channel
} else if *channel * 2. < 1. {
temp1
} else if *channel * 3. < 2. {
temp2 + (temp1 - temp2) * (2. / 3. - *channel) * 6.
} else {
temp2
}
.clamp(0., 1.);
}
Color { red, green, blue, alpha }
}
/// Return the `red` component.
///
/// # Examples
@ -154,6 +189,38 @@ impl Color {
[(self.red * 255.) as u8, (self.green * 255.) as u8, (self.blue * 255.) as u8, (self.alpha * 255.) as u8]
}
// https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
/// Convert a [Color] to a hue, saturation, luminance and alpha (all between 0 and 1)
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla();
/// ```
pub fn to_hsla(&self) -> [f32; 4] {
let min_channel = self.red.min(self.green).min(self.blue);
let max_channel = self.red.max(self.green).max(self.blue);
let luminance = (min_channel + max_channel) / 2.;
let saturation = if min_channel == max_channel {
0.
} else if luminance <= 0.5 {
(max_channel - min_channel) / (max_channel + min_channel)
} else {
(max_channel - min_channel) / (2. - max_channel - min_channel)
};
let hue = if self.red > self.green && self.red > self.blue {
(self.green - self.blue) / (max_channel - min_channel)
} else if self.green > self.red && self.green > self.blue {
2. + (self.blue - self.red) / (max_channel - min_channel)
} else {
4. + (self.red - self.green) / (max_channel - min_channel)
} / 6.;
let hue = hue.rem_euclid(1.);
[hue, saturation, luminance, self.alpha]
}
// TODO: Readd formatting
/// Creates a color from a 8-character RGBA hex string (without a # prefix).
@ -191,3 +258,37 @@ impl Color {
Some(Color::from_rgb8(r, g, b))
}
}
#[test]
fn hsl_roundtrip() {
for (red, green, blue) in [
(24, 98, 118),
(69, 11, 89),
(54, 82, 38),
(47, 76, 50),
(25, 15, 73),
(62, 57, 33),
(55, 2, 18),
(12, 3, 82),
(91, 16, 98),
(91, 39, 82),
(97, 53, 32),
(76, 8, 91),
(54, 87, 19),
(56, 24, 88),
(14, 82, 34),
(61, 86, 31),
(73, 60, 75),
(95, 79, 88),
(13, 34, 4),
(82, 84, 84),
] {
let col = Color::from_rgb8(red, green, blue);
let [hue, saturation, luminance, alpha] = col.to_hsla();
let result = Color::from_hsla(hue, saturation, luminance, alpha);
assert!((col.r() - result.r()) < f32::EPSILON * 100.);
assert!((col.g() - result.g()) < f32::EPSILON * 100.);
assert!((col.b() - result.b()) < f32::EPSILON * 100.);
assert!((col.a() - result.a()) < f32::EPSILON * 100.);
}
}

View file

@ -14,6 +14,7 @@ num-traits = "0.2"
borrow_stack = { path = "../borrow_stack" }
dyn-clone = "1.0"
rand_chacha = "0.3.1"
log = "0.4"
[dependencies.serde]
version = "1.0"

View file

@ -62,7 +62,12 @@ impl DocumentNode {
NodeInput::Network => (ProtoNodeInput::Network, ConstructionArgs::Nodes(vec![])),
};
assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Network)), "recieved non resolved parameter");
assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Value(_))), "recieved value as parameter");
assert!(
!self.inputs.iter().any(|input| matches!(input, NodeInput::Value(_))),
"recieved value as parameter. inupts: {:#?}, construction_args: {:#?}",
&self.inputs,
&args
);
if let ConstructionArgs::Nodes(nodes) = &mut args {
nodes.extend(self.inputs.iter().map(|input| match input {
@ -143,7 +148,10 @@ impl NodeNetwork {
/// Recursively dissolve non primitive document nodes and return a single flattened network of nodes.
pub fn flatten_with_fns(&mut self, node: NodeId, map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy, gen_id: impl Fn() -> NodeId + Copy) {
let (id, mut node) = self.nodes.remove_entry(&node).expect("The node which was supposed to be flattened does not exist in the network");
let (id, mut node) = self
.nodes
.remove_entry(&node)
.unwrap_or_else(|| panic!("The node which was supposed to be flattened does not exist in the network, id {} network {:#?}", node, self));
match node.implementation {
DocumentNodeImplementation::Network(mut inner_network) => {

View file

@ -10,6 +10,7 @@ use dyn_any::{DynAny, Upcast};
pub enum TaggedValue {
String(String),
U32(u32),
F32(f32),
//Image(graphene_std::raster::Image),
Color(graphene_core::raster::color::Color),
}
@ -19,6 +20,7 @@ impl TaggedValue {
match self {
TaggedValue::String(x) => Box::new(x),
TaggedValue::U32(x) => Box::new(x),
TaggedValue::F32(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),
}
}

View file

@ -1,3 +1,6 @@
#[macro_use]
extern crate log;
pub mod node_registry;
pub mod document;

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use borrow_stack::FixedSizeStack;
use graphene_core::generic::FnNode;
use graphene_core::ops::AddNode;
@ -200,6 +202,44 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
}
})
}),
(
NodeIdentifier::new("graphene_core::raster::BrightenColorNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
info!("proto node {:?}", proto_node);
stack.push_fn(|nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Brighten Color Node constructed with out brightness input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_core::raster::BrightenColorNode::new(input_node));
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
},
),
(
NodeIdentifier::new("graphene_core::raster::HueShiftNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
info!("proto node {:?}", proto_node);
stack.push_fn(|nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Hue Shift Color Node constructed with out shift input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_core::raster::HueShiftNode::new(input_node));
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
},
),
(NodeIdentifier::new("graphene_std::raster::MapImageNode", &[]), |proto_node, stack| {
if let ConstructionArgs::Nodes(operation_node_id) = proto_node.construction_args {
stack.push_fn(move |nodes| {
@ -218,6 +258,45 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
unimplemented!()
}
}),
(NodeIdentifier::new("graph_craft::node_registry::GrayscaleImage", &[]), |proto_node, stack| {
stack.push_fn(move |nodes| {
let grayscale_node = DynAnyNode::new(FnNode::new(|mut image: Image| {
for pixel in &mut image.data {
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
image
}));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(grayscale_node).into_type_erased()
} else {
grayscale_node.into_type_erased()
}
})
}),
(NodeIdentifier::new("graph_craft::node_registry::HueShiftImage", &[]), |_proto_node, _stack| {
todo!();
// stack.push_fn(move |nodes| {
// let hue_shift_node = DynAnyNode::new(FnNode::new(|(mut image, amount): (Image, f32)| {
// for pixel in &mut image.data {
// let [mut hue, saturation, luminance, alpha] = pixel.to_hsla();
// hue += amount;
// *pixel = Color::from_hsla(hue, saturation, luminance, alpha);
// }
// image
// }));
// if let ProtoNodeInput::Node(node_id) = proto_node.input {
// let pre_node = nodes.get(node_id as usize).unwrap();
// (pre_node).then(hue_shift_node).into_type_erased()
// } else {
// hue_shift_node.into_type_erased()
// }
// })
}),
(
NodeIdentifier::new("graphene_std::raster::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
|_proto_node, stack| {

View file

@ -121,14 +121,26 @@ impl ProtoNode {
}
impl ProtoNetwork {
fn check_ref(&self, ref_id: &NodeId, id: &NodeId) {
assert!(
self.nodes.iter().any(|(check_id, _)| check_id == ref_id),
"Node id:{} has a reference which uses node id:{} which doesn't exist in network {:#?}",
id,
ref_id,
self
);
}
pub fn collect_outwards_edges(&self) -> HashMap<NodeId, Vec<NodeId>> {
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for (id, node) in &self.nodes {
if let ProtoNodeInput::Node(ref_id) = &node.input {
self.check_ref(ref_id, id);
edges.entry(*ref_id).or_default().push(*id)
}
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
for ref_id in ref_nodes {
self.check_ref(ref_id, id);
edges.entry(*ref_id).or_default().push(*id)
}
}
@ -140,10 +152,12 @@ impl ProtoNetwork {
let mut edges: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for (id, node) in &self.nodes {
if let ProtoNodeInput::Node(ref_id) = &node.input {
self.check_ref(ref_id, id);
edges.entry(*id).or_default().push(*ref_id)
}
if let ConstructionArgs::Nodes(ref_nodes) = &node.construction_args {
for ref_id in ref_nodes {
self.check_ref(ref_id, id);
edges.entry(*id).or_default().push(*ref_id)
}
}