Remove the old node macro and fix/clean up several raster nodes (#2650)

* Fix several broken raster nodes and clean up leftover old node system code

* Migrate Brightness/Contrast to the new node macro, and fix it

* Remove last usages of old_node_fn

* Remove old_node_fn
This commit is contained in:
Keavon Chambers 2025-05-17 21:24:32 -07:00 committed by GitHub
parent 77f8bfd9ed
commit a8e209e44c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 511 additions and 1423 deletions

View file

@ -16,7 +16,7 @@ use graph_craft::document::value::*;
use graph_craft::document::*;
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlue, RedGreenBlueAlpha};
use graphene_core::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::transform::Footprint;
use graphene_core::vector::VectorDataTable;
@ -94,22 +94,6 @@ static DOCUMENT_NODE_TYPES: once_cell::sync::Lazy<Vec<DocumentNodeDefinition>> =
/// The [`DocumentNode`] is the instance while these [`DocumentNodeDefinition`]s are the "classes" or "blueprints" from which the instances are built.
fn static_nodes() -> Vec<DocumentNodeDefinition> {
let mut custom = vec![
DocumentNodeDefinition {
identifier: "Default Network",
category: "General",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork::default()),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata::default()),
..Default::default()
},
},
description: Cow::Borrowed("A default node network you can use to create your own custom nodes."),
properties: None,
},
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Identity",
@ -129,6 +113,43 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes."),
properties: Some("identity_properties"),
},
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Monitor",
category: "Debug",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
inputs: vec![NodeInput::value(TaggedValue::None, true)],
manual_composition: Some(generic!(T)),
skip_deduplication: true,
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![("In", "TODO").into()],
output_names: vec!["Out".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("The Monitor node is used by the editor to access the data flowing through it."),
properties: Some("monitor_properties"),
},
DocumentNodeDefinition {
identifier: "Default Network",
category: "General",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork::default()),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata::default()),
..Default::default()
},
},
description: Cow::Borrowed("A default node network you can use to create your own custom nodes."),
properties: None,
},
DocumentNodeDefinition {
identifier: "Cache",
category: "General",
@ -210,27 +231,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("TODO"),
properties: None,
},
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Monitor",
category: "Debug",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
inputs: vec![NodeInput::value(TaggedValue::None, true)],
manual_composition: Some(generic!(T)),
skip_deduplication: true,
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![("In", "TODO").into()],
output_names: vec!["Out".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("The Monitor node is used by the editor to access the data flowing through it."),
properties: Some("monitor_properties"),
},
DocumentNodeDefinition {
identifier: "Merge",
category: "General",
@ -837,92 +837,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("Generates different noise patterns."),
properties: None,
},
// TODO: This needs to work with resolution-aware data.
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Mask",
category: "Raster",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_std::raster::MaskImageNode"),
inputs: vec![
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
],
manual_composition: Some(generic!(T)),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![
("Image", "TODO").into(),
PropertiesRow::with_override("Stencil", "TODO", WidgetOverride::Custom("mask_stencil".to_string())),
],
output_names: vec!["Image".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
// TODO: This needs to work with resolution-aware data.
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Insert Channel",
category: "Raster",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_std::raster::InsertChannelNode"),
inputs: vec![
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::RedGreenBlue(RedGreenBlue::default()), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![
("Image", "TODO").into(),
PropertiesRow::with_override("Insertion", "TODO", WidgetOverride::Hidden),
("Into", "TODO").into(),
],
output_names: vec!["Image".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
// TODO: This needs to work with resolution-aware data.
DocumentNodeDefinition {
identifier: "Combine Channels",
category: "Raster",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_std::raster::CombineChannelsNode"),
inputs: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![
("None", "TODO").into(),
("Red", "TODO").into(),
("Green", "TODO").into(),
("Blue", "TODO").into(),
("Alpha", "TODO").into(),
],
output_names: vec!["Image".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Split Channels",
category: "Raster",
@ -1937,28 +1851,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("TODO"),
properties: None,
},
#[cfg(feature = "gpu")]
DocumentNodeDefinition {
identifier: "GPU Image",
category: "Debug: GPU",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_std::executor::MapGpuSingleImageNode"),
inputs: vec![
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::DocumentNode(DocumentNode::default()), true),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![("Image", "TODO").into(), ("Node", "TODO").into()],
output_names: vec!["Image".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Extract",
category: "Debug",
@ -1977,40 +1869,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27brit%27%20%3D%20Brightness/Contrast
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Padding-,Brightness%20and%20Contrast,-Key%20is%20%27brit
identifier: "Brightness/Contrast",
category: "Raster: Adjustment",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_core::raster::BrightnessContrastNode"),
inputs: vec![
NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true),
NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::F64(0.), false),
NodeInput::value(TaggedValue::Bool(false), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![
("Image", "TODO").into(),
PropertiesRow::with_override("Brightness", "TODO", WidgetOverride::Custom("brightness".to_string())),
PropertiesRow::with_override("Brightness", "TODO", WidgetOverride::Custom("contrast".to_string())),
("Use Classic", "TODO").into(),
],
output_names: vec!["Image".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=levl%27%20%3D%20Levels-,%27curv%27%20%3D%20Curves,-%27expA%27%20%3D%20Exposure
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Max%20input%20range-,Curves,-Curves%20settings%20files
//
// Some further analysis available at:
// https://geraldbakker.nl/psnumbers/curves.html
// TODO: Fix this, it's currently broken
// DocumentNodeDefinition {
// identifier: "Curves",
@ -2034,51 +1898,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
// properties: None,
// },
// (*IMAGINATE_NODE).clone(),
DocumentNodeDefinition {
identifier: "Line",
category: "Vector: Shape",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::proto("graphene_core::vector::generator_nodes::LineNode"),
manual_composition: Some(concrete!(Context)),
inputs: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::value(TaggedValue::DVec2(DVec2::new(0., -50.)), false),
NodeInput::value(TaggedValue::DVec2(DVec2::new(0., 50.)), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_properties: vec![
("None", "TODO").into(),
PropertiesRow::with_override(
"Start",
"TODO",
WidgetOverride::Vec2(Vec2InputSettings {
x: "X".to_string(),
y: "Y".to_string(),
unit: " px".to_string(),
..Default::default()
}),
),
PropertiesRow::with_override(
"End",
"TODO",
WidgetOverride::Vec2(Vec2InputSettings {
x: "X".to_string(),
y: "Y".to_string(),
unit: " px".to_string(),
..Default::default()
}),
),
],
output_names: vec!["Vector".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Path",
category: "Vector",
@ -2948,6 +2767,7 @@ pub static NODE_OVERRIDES: once_cell::sync::Lazy<NodeProperties> = once_cell::sy
/// Defines the logic for inputs to display a custom properties panel widget.
fn static_node_properties() -> NodeProperties {
let mut map: NodeProperties = HashMap::new();
map.insert("brightness_contrast_properties".to_string(), Box::new(node_properties::brightness_contrast_properties));
map.insert("channel_mixer_properties".to_string(), Box::new(node_properties::channel_mixer_properties));
map.insert("fill_properties".to_string(), Box::new(node_properties::fill_properties));
map.insert("stroke_properties".to_string(), Box::new(node_properties::stroke_properties));

View file

@ -164,6 +164,7 @@ pub(crate) fn property_from_type(
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
Some("Resolution") => vector2_widget(default_info, "W", "H", " px", Some(64.)),
Some("PixelSize") => vector2_widget(default_info, "X", "Y", " px", None),
// For all other types, use TypeId-based matching
_ => {
@ -967,6 +968,56 @@ pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesCo
})
}
pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
log::error!("Could not get document node in brightness_contrast_properties: {err}");
return Vec::new();
}
};
// Use Classic
let use_classic_index = 3;
let use_classic = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, use_classic_index, true, context), CheckboxInput::default());
let use_classic_value = match document_node.inputs[use_classic_index].as_value() {
Some(TaggedValue::Bool(use_classic_choice)) => *use_classic_choice,
_ => false,
};
// Brightness
let brightness_index = 1;
let brightness = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, brightness_index, true, context),
NumberInput::default()
.unit("%")
.mode_range()
.display_decimal_places(2)
.range_min(Some(if use_classic_value { -100. } else { -150. }))
.range_max(Some(if use_classic_value { 100. } else { 150. })),
);
// Contrast
let contrast_index = 2;
let contrast = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, contrast_index, true, context),
NumberInput::default()
.unit("%")
.mode_range()
.display_decimal_places(2)
.range_min(Some(if use_classic_value { -100. } else { -50. }))
.range_max(Some(100.)),
);
let layout = vec![
LayoutGroup::Row { widgets: brightness },
LayoutGroup::Row { widgets: contrast },
LayoutGroup::Row { widgets: use_classic },
];
layout
}
pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,

View file

@ -461,7 +461,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
}
};
const REPLACEMENTS: [(&str, &str); 35] = [
const REPLACEMENTS: [(&str, &str); 36] = [
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
@ -501,6 +501,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
("graphene_std::executor::BlendGpuImageNode", "graphene_std::gpu_nodes::BlendGpuImageNode"),
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
];
let mut network = document.network_interface.document_network().clone();
network.generate_node_paths(&[]);

View file

@ -585,16 +585,28 @@ impl<'i, N: for<'a> Node<'a, I> + Clone, I: 'i> Clone for TypeNode<N, I, <N as N
impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Node<'i, I>>::Output> {}
// Into
pub struct IntoNode<O> {
_o: PhantomData<O>,
pub struct IntoNode<O>(PhantomData<O>);
impl<_O> IntoNode<_O> {
#[cfg(feature = "alloc")]
pub const fn new() -> Self {
Self(core::marker::PhantomData)
}
}
#[cfg(feature = "alloc")]
#[node_macro::old_node_fn(IntoNode<_O>)]
async fn into<I, _O>(input: I) -> _O
impl<_O> Default for IntoNode<_O> {
fn default() -> Self {
Self::new()
}
}
impl<'input, I: 'input, _O: 'input> Node<'input, I> for IntoNode<_O>
where
I: Into<_O> + Sync + Send,
{
input.into()
type Output = ::dyn_any::DynFuture<'input, _O>;
#[inline]
fn eval(&'input self, input: I) -> Self::Output {
Box::pin(async move { input.into() })
}
}
#[cfg(test)]

View file

@ -14,8 +14,6 @@ use spirv_std::num_traits::float::Float;
pub mod adjustments;
pub mod bbox;
#[cfg(not(target_arch = "spirv"))]
pub mod brightness_contrast;
#[cfg(not(target_arch = "spirv"))]
pub mod brush_cache;
pub mod color;
#[cfg(not(target_arch = "spirv"))]

View file

@ -1,7 +1,8 @@
#![allow(clippy::too_many_arguments)]
use crate::raster::curve::{CubicSplines, CurveManipulatorGroup};
#[cfg(feature = "alloc")]
use crate::raster::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
use crate::raster::curve::{Curve, ValueMapperNode};
#[cfg(feature = "alloc")]
use crate::raster::image::{Image, ImageFrameTable};
use crate::raster::{Channel, Color, Pixel};
@ -326,10 +327,100 @@ fn make_opaque<T: Adjust<Color>>(
}
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27%20%3D%20Brightness/Contrast-,%27levl%27%20%3D%20Levels,-%27curv%27%20%3D%20Curves
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27brit%27%20%3D%20Brightness/Contrast
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Padding-,Brightness%20and%20Contrast,-Key%20is%20%27brit
//
// Some further analysis available at:
// https://geraldbakker.nl/psnumbers/brightness-contrast.html
#[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"))]
fn brightness_contrast<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
Color,
ImageFrameTable<Color>,
GradientStops,
)]
mut input: T,
brightness: SignedPercentage,
contrast: SignedPercentage,
use_classic: bool,
) -> T {
if use_classic {
let brightness = brightness as f32 / 255.;
let contrast = contrast as f32 / 100.;
let contrast = if contrast > 0. { (contrast * core::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast };
let offset = brightness * contrast + brightness - contrast / 2.;
input.adjust(|color| color.to_gamma_srgb().map_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)).to_linear_srgb());
return input;
}
const WINDOW_SIZE: usize = 1024;
// Brightness LUT
let brightness_is_negative = brightness < 0.;
// We clamp the brightness before the two curve X-axis points `130 - brightness * 26` and `233 - brightness * 48` intersect.
// Beyond the point of intersection, the cubic spline fitting becomes invalid and fails an assertion, which we need to avoid.
// See the intersection of the red lines at x = 103/22*100 = 468.18182 in the graph: https://www.desmos.com/calculator/ekvz4zyd9c
let brightness = (brightness.abs() / 100.).min(103. / 22. - 0.00001) as f32;
let brightness_curve_points = CubicSplines {
x: [0., 130. - brightness * 26., 233. - brightness * 48., 255.].map(|x| x / 255.),
y: [0., 130. + brightness * 51., 233. + brightness * 10., 255.].map(|x| x / 255.),
};
let brightness_curve_solutions = brightness_curve_points.solve();
let mut brightness_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
brightness_curve_points.interpolate(x, &brightness_curve_solutions)
});
// Special handling for when brightness is negative
if brightness_is_negative {
brightness_lut = core::array::from_fn(|i| {
let mut x = i;
while x > 1 && brightness_lut[x] > i as f32 / WINDOW_SIZE as f32 {
x -= 1;
}
x as f32 / WINDOW_SIZE as f32
});
}
// Contrast LUT
// Unlike with brightness, the X-axis points `64` and `192` don't intersect at any contrast value, because they are constants.
// So we don't have to worry about clamping the contrast value to avoid invalid cubic spline fitting.
// See the graph: https://www.desmos.com/calculator/iql9vsca56
let contrast = contrast as f32 / 100.;
let contrast_curve_points = CubicSplines {
x: [0., 64., 192., 255.].map(|x| x / 255.),
y: [0., 64. - contrast * 30., 192. + contrast * 30., 255.].map(|x| x / 255.),
};
let contrast_curve_solutions = contrast_curve_points.solve();
let contrast_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
contrast_curve_points.interpolate(x, &contrast_curve_solutions)
});
// Composed brightness and contrast LUTs
let combined_lut = brightness_lut.map(|brightness| {
let index_in_contrast_lut = (brightness * (contrast_lut.len() - 1) as f32).round() as usize;
contrast_lut[index_in_contrast_lut]
});
let lut_max = (combined_lut.len() - 1) as f32;
input.adjust(|color| color.to_gamma_srgb().map_rgb(|c| combined_lut[(c * lut_max).round() as usize]).to_linear_srgb());
input
}
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=levl%27%20%3D%20Levels
//
// Algorithm from:
// https://stackoverflow.com/questions/39510072/algorithm-for-adjustment-of-image-levels
//
// Some further analysis available at:
// https://geraldbakker.nl/psnumbers/levels.html
#[node_macro::node(category("Raster: Adjustment"))]
fn levels<T: Adjust<Color>>(
_: impl Ctx,
@ -748,7 +839,7 @@ async fn gradient_map<T: Adjust<Color>>(
image.adjust(|color| {
let intensity = color.luminance_srgb();
let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evaluate(intensity as f64)
gradient.evaluate(intensity as f64).to_linear_srgb()
});
image
@ -762,7 +853,7 @@ async fn gradient_map<T: Adjust<Color>>(
// https://stackoverflow.com/questions/33966121/what-is-the-algorithm-for-vibrance-filters
// The results of this implementation are very close to correct, but not quite perfect.
//
// A bit of additional analysis can be found at here:
// Some further analysis available at:
// https://www.photo-mark.com/notes/analyzing-photoshop-vibrance-and-saturation/
//
// This algorithm is currently lacking a "Saturation" parameter which is needed for interoperability.

View file

@ -1,256 +0,0 @@
use crate::raster::curve::CubicSplines;
use crate::{Color, Node};
// LEGACY BRIGHTNESS/CONTRAST
pub struct BrightnessContrastLegacyMapperNode {
contrast: f32,
combined: f32,
}
impl<'i> Node<'i, Color> for BrightnessContrastLegacyMapperNode {
type Output = Color;
fn eval(&'i self, color: Color) -> Color {
let color = color.to_gamma_srgb();
let color = color.map_rgb(|c| (c + c * self.contrast + self.combined).clamp(0., 1.));
color.to_linear_srgb()
}
}
#[derive(Debug, Clone, Copy)]
pub struct GenerateBrightnessContrastLegacyMapperNode<Brightness, Contrast> {
brightness: Brightness,
contrast: Contrast,
}
#[node_macro::old_node_fn(GenerateBrightnessContrastLegacyMapperNode)]
fn brightness_contrast_legacy(_primary: (), brightness: f64, contrast: f64) -> BrightnessContrastLegacyMapperNode {
let brightness = brightness as f32 / 255.;
let contrast = contrast as f32 / 100.;
let contrast = if contrast > 0. { (contrast * core::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast };
let combined = brightness * contrast + brightness - contrast / 2.;
BrightnessContrastLegacyMapperNode { contrast, combined }
}
// NORMAL BRIGHTNESS/CONTRAST
pub struct BrightnessContrastMapperNode {
combined_lut: [f32; WINDOW_SIZE],
}
impl<'i> Node<'i, Color> for BrightnessContrastMapperNode {
type Output = Color;
fn eval(&'i self, color: Color) -> Color {
let color = color.to_gamma_srgb();
let color = color.map_rgb(|c| {
let index_in_combined_lut = (c * (self.combined_lut.len() - 1) as f32).round() as usize;
self.combined_lut[index_in_combined_lut]
});
color.to_linear_srgb()
}
}
#[derive(Debug, Clone, Copy)]
pub struct GenerateBrightnessContrastMapperNode<Brightness, Contrast> {
brightness: Brightness,
contrast: Contrast,
}
// TODO: Replace this node implementation with one that reuses the more generalized Curves adjustment node.
// TODO: It will be necessary to ensure the tests below are faithfully translated in a way that ensures identical results.
#[node_macro::old_node_fn(GenerateBrightnessContrastMapperNode)]
fn brightness_contrast(_primary: (), brightness: f64, contrast: f64) -> BrightnessContrastMapperNode {
// Brightness LUT
let brightness_is_negative = brightness < 0.;
let brightness = brightness.abs() as f32 / 100.;
let brightness_curve_points = CubicSplines {
x: [0., 130. - brightness * 26., 233. - brightness * 48., 255.].map(|x| x / 255.),
y: [0., 130. + brightness * 51., 233. + brightness * 10., 255.].map(|x| x / 255.),
};
let brightness_curve_solutions = brightness_curve_points.solve();
let mut brightness_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
brightness_curve_points.interpolate(x, &brightness_curve_solutions)
});
// Special handling for when brightness is negative
if brightness_is_negative {
brightness_lut = core::array::from_fn(|i| {
let mut x = i;
while x > 1 && brightness_lut[x] > i as f32 / WINDOW_SIZE as f32 {
x -= 1;
}
x as f32 / WINDOW_SIZE as f32
});
}
// Contrast LUT
let contrast = contrast as f32 / 100.;
let contrast_curve_points = CubicSplines {
x: [0., 64., 192., 255.].map(|x| x / 255.),
y: [0., 64. - contrast * 30., 192. + contrast * 30., 255.].map(|x| x / 255.),
};
let contrast_curve_solutions = contrast_curve_points.solve();
let contrast_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
contrast_curve_points.interpolate(x, &contrast_curve_solutions)
});
// Composed brightness and contrast LUTs
let combined_lut = brightness_lut.map(|brightness| {
let index_in_contrast_lut = (brightness * (contrast_lut.len() - 1) as f32).round() as usize;
contrast_lut[index_in_contrast_lut]
});
BrightnessContrastMapperNode { combined_lut }
}
const WINDOW_SIZE: usize = 1024;
mod tests {
#[allow(unused_imports)]
use super::*;
#[allow(unused_imports)]
use crate::value::ClonedNode;
#[test]
fn brightness_contrast_legacy_tests() {
let string_data = |string: &str| string.split(',').map(|s| s.parse().unwrap()).collect::<Vec<u8>>();
let brightness_contrast_legacy_map = |brightness, contrast| -> [u8; 256] {
let brightness = ClonedNode::new(brightness);
let contrast = ClonedNode::new(contrast);
let generate_brightness_contrast_legacy_node = GenerateBrightnessContrastLegacyMapperNode::new(brightness, contrast);
let mapper = generate_brightness_contrast_legacy_node.eval(());
let color = |r| Color::from_rgbaf32_unchecked(r, 0., 0., 1.);
core::array::from_fn(|x| (mapper.eval(color(x as f32 / 255.)).r() * 255.).round() as u8)
};
assert_eq!(brightness_contrast_legacy_map(-150., 100.), [0; 256]);
assert_eq!(brightness_contrast_legacy_map(-77., 100.), {
let mut x = [0; 153].into_iter().chain([2, 20, 65, 143]).chain([255; 99]);
core::array::from_fn(|_| x.next().unwrap())
});
assert_eq!(brightness_contrast_legacy_map(0., 100.), {
let mut x = [0; 54].into_iter().chain([13, 107]).chain([255; 200]);
core::array::from_fn(|_| x.next().unwrap())
});
assert_eq!(brightness_contrast_legacy_map(53., 100.), {
let mut x = [0; 18].into_iter().chain([132]).chain([255; 237]);
core::array::from_fn(|_| x.next().unwrap())
});
assert_eq!(brightness_contrast_legacy_map(150., 100.), [255; 256]);
assert_eq!(
brightness_contrast_legacy_map(-150., 0.),
[0; 86].into_iter().chain(string_data("1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,6,6,6,6,6,6,6,7,7,7,7,7,8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,11,11,11,11,11,12,12,12,12,13,13,13,13,13,14,14,14,14,15,15,15,15,15,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,21,21,21,21,22,22,22,22,23,23,23,23,24,24,24,25,25,25,25,26,26,26,27,27,27,27,28,28,28,29,29,29,30,30,30,30,31,31,31,32,32,32,33,33,33,33,34,34,34,35,35,35,36,36")).collect::<Vec<_>>().as_slice(),
);
assert_eq!(
brightness_contrast_legacy_map(-77., 0.),
string_data("0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4,4,5,5,5,6,6,6,6,7,7,7,8,8,8,9,9,9,10,10,11,11,11,12,12,12,13,13,13,14,14,15,15,15,16,16,17,17,17,18,18,19,19,20,20,20,21,21,22,22,23,23,24,24,24,25,25,26,26,27,27,28,28,29,29,30,30,31,31,31,32,32,33,33,34,34,35,35,36,36,37,37,38,38,39,39,40,40,41,41,42,42,43,44,44,45,45,46,46,47,47,48,48,49,49,50,50,51,51,52,53,53,54,54,55,55,56,56,57,57,58,59,59,60,60,61,61,62,62,63,64,64,65,65,66,66,67,67,68,69,69,70,70,71,71,72,73,73,74,74,75,76,76,77,77,78,78,79,80,80,81,81,82,83,83,84,84,85,85,86,87,87,88,88,89,90,90,91,91,92,93,93,94,94,95,96,96,97,98,98,99,99,100,101,101,102,102,103,104,104,105,105,106,107,107,108,109,109,110,110,111,112,112,113,114").as_slice(),
);
assert_eq!(brightness_contrast_legacy_map(0., 0.), core::array::from_fn(|i| i as u8));
assert_eq!(
brightness_contrast_legacy_map(53., 0.),
string_data("9,14,18,21,24,27,29,32,34,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,66,68,70,72,73,75,77,79,80,82,84,85,87,89,90,92,94,95,97,99,100,102,104,105,107,108,110,111,113,115,116,118,119,121,122,124,126,127,129,130,132,133,135,136,138,139,141,142,144,145,147,148,150,151,153,154,156,157,159,160,161,163,164,166,167,169,170,172,173,175,176,177,179,180,182,183,185,186,187,189,190,192,193,195,196,197,199,200,202,203,204,206,207,209,210,211,213,214,216,217,218,220,221,223,224,225,227,228,230,231,232,234,235,236,238,239,241,242,243,245,246,247,249,250,251,253,254").into_iter().chain([255; 105]).collect::<Vec<_>>().as_slice(),
);
assert_eq!(
brightness_contrast_legacy_map(150., 0.),
string_data("78,93,105,114,122,129,135,141,147,152,157,162,167,171,176,180,184,188,192,196,200,204,208,211,215,218,222,225,229,232,236,239,242,245,249,252")
.into_iter()
.chain([255; 220])
.collect::<Vec<_>>()
.as_slice(),
);
assert_eq!(brightness_contrast_legacy_map(-150., -100.), [55; 256]);
assert_eq!(brightness_contrast_legacy_map(-77., -100.), [55; 256]);
assert_eq!(brightness_contrast_legacy_map(0., -100.), [55; 256]);
assert_eq!(brightness_contrast_legacy_map(53., -100.), [55; 256]);
assert_eq!(brightness_contrast_legacy_map(150., -100.), [55; 256]);
}
#[test]
fn brightness_contrast_tests() {
let string_data = |string: &str| string.split(',').map(|s| s.parse().unwrap()).collect::<Vec<u8>>();
let brightness_contrast_map = |brightness, contrast| -> [u8; 1024] {
let brightness = ClonedNode::new(brightness);
let contrast = ClonedNode::new(contrast);
let generate_brightness_contrast_node = GenerateBrightnessContrastMapperNode::new(brightness, contrast);
let mapper = generate_brightness_contrast_node.eval(());
let color = |r| Color::from_rgbaf32_unchecked(r, 0., 0., 1.);
core::array::from_fn(|x| (mapper.eval(color(x as f32 / 1023.)).r() * 255.).round() as u8)
};
assert_eq!(
&brightness_contrast_map(-150., 100.),
string_data("0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,25,25,25,25,25,25,25,26,26,26,26,26,26,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,29,29,29,29,29,29,30,30,30,30,30,30,30,30,31,31,31,31,31,31,32,32,32,32,33,33,33,33,33,33,33,33,34,34,34,34,35,35,35,35,36,36,36,36,36,36,37,37,37,37,38,38,38,38,39,39,39,39,40,40,40,40,41,41,41,41,42,42,42,42,43,43,43,43,44,44,44,44,45,45,45,45,46,46,47,47,47,47,48,48,49,49,49,49,50,50,50,50,51,51,52,52,52,53,53,53,53,54,54,55,55,56,56,57,57,57,57,58,58,59,59,60,60,61,61,61,62,62,63,63,64,64,65,65,67,67,68,68,69,69,70,70,72,72,72,73,73,74,74,76,76,77,77,79,79,81,81,82,82,82,84,84,86,86,88,88,90,90,92,92,94,94,94,97,97,99,99,101,101,104,104,107,107,110,110,110,113,113,116,116,119,119,123,123,126,126,126,130,130,133,133,137,137,141,141,145,145,145,149,149,154,154,158,158,163,163,163,168,168,172,172,177,177,182,182,182,188,188,193,193,198,198,204,204,204,209,209,215,215,220,220,226,226,226,232,232,237,237,243,243,249,249").as_slice()
);
assert_eq!(
brightness_contrast_map(-77., 100.),
string_data("0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,21,21,21,21,21,21,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,25,25,25,25,25,25,25,26,26,26,26,26,26,27,27,27,27,27,27,28,28,28,28,28,28,28,29,29,29,29,29,29,30,30,30,30,30,30,30,31,31,31,31,31,31,31,32,32,32,32,33,33,33,33,33,33,33,34,34,34,34,34,35,35,35,35,36,36,36,36,36,36,36,37,37,37,37,38,38,38,38,38,38,39,39,39,39,40,40,40,40,40,40,41,41,41,41,42,42,42,42,42,42,43,43,43,43,44,44,44,44,44,44,45,45,45,45,46,46,46,46,46,47,47,47,47,47,47,48,48,48,49,49,49,49,50,50,50,50,50,51,51,51,52,52,52,52,52,52,53,53,53,53,53,54,54,54,55,55,55,55,55,55,56,56,56,57,57,57,57,57,57,58,58,58,59,59,59,59,59,60,60,60,61,61,61,62,62,62,62,62,63,63,63,64,64,64,64,65,65,65,65,66,66,66,66,67,67,67,68,68,68,68,68,69,69,69,70,70,70,71,71,71,71,72,72,72,73,73,73,73,73,74,74,74,75,75,76,76,76,77,77,77,77,78,78,78,79,79,79,80,80,80,80,81,81,81,82,82,82,82,83,83,83,84,84,84,85,85,85,85,86,86,86,87,87,88,88,88,88,89,89,89,90,90,90,90,91,92,92,93,93,93,93,94,94,94,95,95,95,95,96,97,97,97,97,98,98,98,98,99,100,100,100,100,101,101,101,101,102,103,103,104,104,104,104,105,105,105,106,106,107,107,107,107,108,108,109,109,109,110,110,111,111,112,112,112,112,113,113,113,114,114,115,115,115,115,117,117,117,117,118,118,118,119,119,120,120,120,120,121,121,122,122,123,123,124,124,124,124,125,126,126,126,126,128,128,128,128,129,129,130,130,130,130,131,131,132,132,133,133,134,134,134,135,135,136,136,137,137,138,138,139,139,139,139,140,140,141,141,142,142,143,143,143,143,144,144,145,145,146,146,147,147,148,148,149,149,149,149,150,150,151,151,152,152,153,153,154,154,155,155,156,156,156,156,158,158,159,159,159,159,160,160,161,161,162,162,163,163,164,164,165,165,166,166,167,167,168,168,169,169,170,170,171,171,171,171,172,172,173,173,174,174,175,175,176,176,177,177,178,178,179,179,180,180,180,181,181,182,182,183,183,184,184,185,185,186,186,187,187,188,188,189,189,190,190,191,191,191,193,193,193,193,194,194,196,196,196,196,198,198,198,198,200,200,201,201,201,202,202,203,203,204,204,205,205,206,206,207,207,208,208,208,209,209,210,210,211,211,212,212,213,213,214,214,214,215,215,217,217,218,218,218,218,220,220,221,221,221,222,222,223,223,224,224,225,225,226,226,226,227,227,228,228,230,230,231,231,232,232,232,233,233,234,234,235,235,236,236,236,237,237,238,238,239,239,240,240,240,242,242,243,243,244,244,245,245,245,246,246,247,247,248,248,249,249,249,250,250,251,251,253,253,254,254").as_slice()
);
assert_eq!(
brightness_contrast_map(0., 100.),
string_data("0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,6,6,6,6,6,6,7,7,7,7,7,7,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,11,11,11,11,12,12,12,12,12,13,13,13,13,14,14,14,14,15,15,15,16,16,16,16,17,17,17,17,18,18,18,19,19,19,19,20,20,20,21,21,21,22,22,22,22,23,23,23,24,24,24,25,25,25,26,26,26,27,27,27,28,28,28,29,29,29,30,30,30,31,31,31,32,32,33,33,33,34,34,34,35,35,36,36,36,37,37,37,38,38,39,39,39,40,40,41,41,41,42,42,43,43,43,44,44,45,45,45,46,46,46,47,47,48,48,49,49,49,50,50,51,51,52,52,52,53,53,53,54,55,55,55,56,56,57,57,57,58,58,59,59,59,60,60,61,61,62,62,63,63,64,64,65,65,65,66,66,67,67,68,68,68,69,69,70,70,71,71,72,72,73,73,73,74,74,74,75,75,76,76,77,77,78,78,79,79,80,80,81,81,82,82,83,83,83,83,84,84,85,85,86,86,87,87,88,88,88,89,89,90,90,91,92,92,93,93,93,94,94,95,95,96,96,97,97,97,98,98,99,99,100,100,100,101,101,102,102,103,104,104,104,105,105,106,106,107,107,107,108,108,109,109,109,110,111,111,112,112,112,113,113,114,114,115,115,115,116,117,117,117,118,118,119,119,119,120,120,121,121,121,122,123,123,124,124,124,125,125,126,126,126,127,128,128,128,129,129,130,130,130,131,131,131,132,133,133,133,134,134,135,135,135,136,137,137,137,138,138,139,139,139,140,140,140,141,142,142,142,143,143,144,144,144,145,145,145,146,147,147,147,148,148,148,149,149,149,150,150,151,151,152,152,153,153,153,154,154,154,155,155,155,156,156,156,157,158,158,158,159,159,159,160,160,160,161,161,161,162,162,163,163,163,164,164,165,165,165,166,166,166,167,167,168,168,168,169,169,169,170,170,170,171,171,171,172,172,172,173,173,173,174,174,174,175,175,175,176,176,176,177,177,177,178,178,178,179,179,179,180,180,180,180,181,181,181,182,182,182,183,183,183,184,184,184,185,185,185,186,186,186,187,187,187,188,188,188,188,188,189,189,189,190,190,190,191,191,191,191,192,192,192,193,193,193,193,193,194,194,194,195,195,195,196,196,196,196,196,197,197,197,198,198,198,198,198,199,199,199,200,200,200,200,201,201,201,201,201,202,202,202,203,203,203,203,203,204,204,204,204,204,205,205,205,206,206,206,206,206,207,207,207,207,208,208,208,208,208,209,209,209,209,209,210,210,210,210,210,211,211,211,211,211,212,212,212,212,213,213,213,213,213,214,214,214,214,214,215,215,215,215,215,215,216,216,216,216,216,217,217,217,217,217,218,218,218,218,218,218,218,219,219,219,219,219,219,220,220,220,220,220,221,221,221,221,221,221,222,222,222,222,222,222,222,223,223,223,223,223,224,224,224,224,224,224,224,224,225,225,225,225,225,226,226,226,226,226,226,226,226,227,227,227,227,227,227,227,228,228,228,228,228,228,229,229,229,229,229,229,229,230,230,230,230,230,230,230,230,231,231,231,231,231,231,231,231,232,232,232,232,232,232,232,233,233,233,233,233,233,233,233,233,233,234,234,234,234,234,234,234,234,235,235,235,235,235,235,235,235,236,236,236,236,236,236,236,236,236,236,237,237,237,237,237,237,237,237,238,238,238,238,238,238,238,238,238,238,239,239,239,239,239,239,239,239,239,239,240,240,240,240,240,240,240,240,240,240,241,241,241,241,241,241,241,241,241,241,242,242,242,242,242,242,242,242,242,242,243,243,243,243,243,243,243,243,243,243,243,244,244,244,244,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,245,245,245,246,246,246,246,246,246,246,246,246,246,246,246,247,247,247,247,247,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,248,248,248,249,249,249,249,249,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,255,255,255,255,255,255,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(53., 100.),
string_data("0,0,0,0,0,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,4,4,4,4,5,5,5,6,6,6,7,7,7,8,8,8,9,9,10,10,11,11,12,12,12,13,13,14,15,15,16,16,17,17,18,19,19,19,20,21,22,22,23,23,24,25,25,26,27,27,28,29,29,30,31,31,32,33,34,34,35,36,36,37,38,39,40,40,41,42,43,43,44,45,46,46,47,48,49,50,50,51,52,53,53,55,55,56,57,57,59,59,60,61,62,63,63,65,65,66,67,68,68,69,70,71,72,73,73,74,75,76,77,77,79,79,80,81,82,83,83,84,85,86,86,87,88,89,90,90,92,93,93,94,95,96,96,98,98,99,99,101,101,102,102,104,105,105,106,107,108,108,109,110,111,112,112,113,114,115,115,116,117,118,119,119,120,120,121,123,123,124,124,125,126,126,128,128,129,129,130,131,131,132,133,134,135,135,136,137,137,138,138,139,140,141,142,142,143,143,144,145,145,146,147,147,148,148,149,149,150,152,152,153,153,154,154,155,155,156,157,158,158,159,159,160,160,161,161,162,162,163,164,165,165,166,166,167,167,168,168,168,169,169,170,170,171,171,172,173,173,174,174,175,175,176,176,177,177,177,178,178,179,179,180,180,181,181,182,182,183,183,183,184,184,185,185,186,186,187,187,187,188,188,188,189,189,190,190,191,191,191,192,192,193,193,193,193,194,194,195,195,196,196,196,196,197,197,198,198,198,198,199,199,200,200,201,201,201,201,201,202,202,202,203,203,203,204,204,204,204,205,205,206,206,206,206,207,207,207,207,208,208,208,208,209,209,209,210,210,210,210,210,211,211,211,211,212,212,212,213,213,213,213,213,214,214,214,214,215,215,215,215,216,216,216,216,216,217,217,217,217,218,218,218,218,218,218,218,219,219,219,219,219,220,220,220,220,221,221,221,221,221,221,222,222,222,222,222,222,222,223,223,223,223,224,224,224,224,224,224,224,225,225,225,225,225,226,226,226,226,226,226,226,227,227,227,227,227,227,227,227,228,228,228,228,228,229,229,229,229,229,229,229,229,229,230,230,230,230,230,230,230,231,231,231,231,231,231,231,231,232,232,232,232,232,232,232,232,232,233,233,233,233,233,233,233,233,233,233,234,234,234,234,234,234,234,234,234,234,235,235,235,235,235,235,235,235,235,236,236,236,236,236,236,236,236,236,236,236,237,237,237,237,237,237,237,237,237,238,238,238,238,238,238,238,238,238,238,238,238,238,239,239,239,239,239,239,239,239,239,239,239,239,239,240,240,240,240,240,240,240,240,240,240,240,240,240,240,240,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(150., 100.),
string_data("0,0,0,1,1,1,2,2,3,4,5,6,7,9,10,12,14,16,18,20,22,25,28,30,33,36,38,41,44,47,50,53,56,59,62,65,68,71,74,77,81,84,87,90,94,96,99,102,105,108,112,114,117,119,123,125,128,130,133,135,138,140,143,146,148,150,152,154,157,159,161,163,165,167,169,170,172,174,176,177,179,181,182,184,186,187,188,189,191,192,193,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,209,210,211,211,213,214,214,215,215,216,216,217,218,218,219,219,220,221,221,222,222,222,223,224,224,224,225,225,226,226,226,227,227,228,228,228,229,229,230,230,230,231,231,231,232,232,232,232,233,233,233,233,233,234,234,234,235,235,235,235,236,236,236,236,236,237,237,237,237,238,238,238,238,238,238,239,239,239,239,239,239,239,240,240,240,240,240,240,241,241,241,241,241,241,241,242,242,242,242,242,242,242,242,243,243,243,243,243,243,243,243,243,243,244,244,244,244,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,245,245,245,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(-150., 0.),
string_data("0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,26,26,26,26,26,26,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,31,31,31,32,32,32,32,32,32,32,32,32,32,32,33,33,33,33,33,33,33,33,33,33,33,33,33,34,34,34,34,34,34,34,34,34,35,35,35,35,35,35,35,35,35,35,35,36,36,36,36,36,36,36,36,37,37,37,37,37,37,37,37,37,37,37,37,38,38,38,38,38,38,38,39,39,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,41,42,42,42,42,42,42,43,43,43,43,43,43,43,43,44,44,44,44,44,44,44,44,45,45,45,45,45,45,46,46,46,46,46,46,47,47,47,47,47,47,48,48,48,48,48,48,49,49,49,49,50,50,50,50,50,50,51,51,51,51,52,52,52,52,52,52,53,53,53,53,53,54,54,54,54,54,54,55,55,55,55,56,56,56,56,57,57,57,57,58,58,58,58,58,59,59,60,60,60,60,61,61,62,62,62,62,63,63,64,64,64,64,64,65,65,66,66,67,67,67,67,69,69,69,69,70,70,70,71,71,72,72,73,73,74,74,76,76,77,77,77,78,78,79,79,81,81,82,82,84,84,85,85,85,87,87,89,89,91,91,92,92,94,94,94,97,97,99,99,101,101,103,103,106,106,106,108,108,111,111,114,114,117,117,117,120,120,124,124,127,127,131,131,131,135,135,140,140,145,145,151,151,151,157,157,164,164,172,172,180,180,180,191,191,203,203,218,218,235,235").as_slice()
);
assert_eq!(
brightness_contrast_map(-77., 0.),
string_data("0,0,0,0,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,24,24,24,25,25,25,25,25,25,25,25,25,25,26,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,32,32,32,32,32,32,32,32,32,33,33,33,33,33,33,33,33,33,33,34,34,34,34,34,34,34,35,35,35,35,35,35,35,35,35,35,36,36,36,36,36,36,36,36,37,37,37,37,37,37,37,37,37,38,38,38,38,38,38,38,38,38,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,44,44,44,44,44,44,44,44,45,45,45,45,45,45,45,45,45,46,46,46,46,46,46,47,47,47,47,47,47,47,47,48,48,48,48,48,48,48,48,48,49,49,49,49,49,49,50,50,50,50,50,50,50,50,50,51,51,51,51,51,51,52,52,52,52,52,52,52,53,53,53,53,53,53,53,53,54,54,54,54,54,54,54,54,55,55,55,55,55,55,55,56,56,56,56,56,56,56,56,57,57,57,57,57,57,58,58,58,58,58,58,58,58,59,59,59,59,59,59,59,60,60,60,60,60,60,61,61,61,61,61,61,61,62,62,62,62,62,62,63,63,63,63,63,63,63,64,64,64,64,64,64,65,65,65,65,65,65,65,66,66,66,66,66,67,67,67,67,67,67,67,68,68,68,68,68,69,69,69,69,69,69,70,70,70,70,70,70,71,71,71,71,71,71,72,72,72,72,72,72,73,73,73,73,73,74,74,74,74,74,74,74,75,75,75,75,75,76,76,76,76,76,77,77,77,77,77,77,78,78,78,78,78,79,79,79,79,79,80,80,80,80,80,80,81,81,81,82,82,82,82,82,82,82,82,83,83,83,84,84,84,84,84,84,85,85,85,85,85,86,86,86,86,87,87,87,87,87,88,88,88,88,88,88,89,89,89,89,90,90,90,91,91,91,91,91,91,92,92,92,92,92,92,93,93,93,93,94,94,94,95,95,95,95,96,96,96,96,97,97,97,97,98,98,98,98,98,98,99,99,99,100,100,100,100,101,101,101,101,102,102,102,102,103,103,103,103,104,104,104,104,105,105,105,105,106,106,106,106,107,107,107,107,108,108,108,108,109,109,109,109,110,110,111,111,111,111,112,112,112,112,113,113,113,113,114,114,115,115,115,115,116,116,116,116,117,117,118,118,119,119,119,119,120,120,120,120,121,121,122,122,123,123,123,123,124,124,124,124,125,125,126,126,127,127,127,127,128,128,129,129,130,130,130,130,130,131,131,132,132,133,133,134,134,134,134,135,135,136,136,137,137,138,138,139,139,139,140,140,141,141,141,141,143,143,143,143,145,145,145,145,147,147,148,148,148,149,149,150,150,151,151,152,152,153,153,154,154,156,156,156,157,157,158,158,159,159,161,161,162,162,163,163,163,165,165,166,166,168,168,169,169,171,171,172,172,172,174,174,176,176,177,177,179,179,181,181,181,183,183,185,185,187,187,189,189,191,191,191,193,193,196,196,198,198,200,200,200,202,202,205,205,208,208,210,210,210,213,213,216,216,219,219,222,222,222,225,225,229,229,232,232,236,236,236,239,239,243,243,247,247,250,250").as_slice()
);
assert_eq!(
brightness_contrast_map(0., 0.),
string_data("0,0,1,1,1,1,2,2,2,2,3,3,3,3,3,4,4,4,4,5,5,5,6,6,6,6,6,7,7,7,7,8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11,12,12,12,13,13,13,13,14,14,14,14,14,15,15,15,15,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,22,22,22,22,23,23,23,23,24,24,24,24,25,25,25,25,26,26,26,26,27,27,27,27,28,28,28,28,29,29,29,29,30,30,30,30,31,31,31,31,32,32,32,32,33,33,33,33,34,34,34,34,35,35,35,35,36,36,36,36,37,37,37,37,38,38,38,38,39,39,39,39,40,40,40,40,41,41,41,41,42,42,42,42,43,43,43,43,44,44,44,44,45,45,45,45,46,46,46,46,47,47,47,47,48,48,48,48,49,49,49,49,50,50,50,50,51,51,51,51,52,52,52,52,53,53,53,53,54,54,54,54,55,55,55,55,56,56,56,56,57,57,57,57,58,58,58,58,58,59,59,59,60,60,60,60,61,61,61,61,62,62,62,62,63,63,63,63,64,64,64,64,65,65,65,65,66,66,66,66,66,67,67,67,67,68,68,68,69,69,69,69,70,70,70,70,71,71,71,71,71,72,72,72,73,73,73,73,74,74,74,74,74,75,75,75,76,76,76,76,76,77,77,77,78,78,78,78,78,79,79,79,80,80,80,80,80,81,81,81,82,82,82,82,82,83,83,83,84,84,84,84,85,85,85,85,85,86,86,86,87,87,87,87,88,88,88,88,88,89,89,89,89,90,90,90,91,91,91,91,92,92,92,92,92,93,93,93,93,94,94,94,94,95,95,95,95,96,96,96,97,97,97,97,98,98,98,98,99,99,99,99,100,100,100,100,101,101,101,101,102,102,102,102,103,103,103,103,104,104,104,104,105,105,105,105,106,106,106,106,107,107,107,107,108,108,108,108,108,109,109,109,109,110,110,110,110,111,111,111,111,112,112,112,112,113,113,113,113,113,114,114,115,115,115,115,115,116,116,116,116,117,117,117,117,118,118,118,119,119,119,119,119,120,120,120,120,121,121,121,121,122,122,122,123,123,123,123,123,124,124,124,124,125,125,125,126,126,126,126,126,127,127,127,127,128,128,128,129,129,129,129,129,130,130,130,130,130,131,131,131,132,132,132,132,133,133,133,134,134,134,134,134,135,135,135,135,135,136,136,136,137,137,137,137,137,138,138,138,139,139,139,139,139,140,140,140,141,141,141,141,141,142,142,142,143,143,143,143,143,144,144,144,145,145,145,145,145,146,146,146,147,147,147,147,148,148,148,148,148,149,149,149,150,150,150,150,150,151,151,151,152,152,152,152,153,153,153,153,153,154,154,154,155,155,155,156,156,156,156,156,156,157,157,157,158,158,158,158,158,159,159,159,159,160,160,160,161,161,161,161,162,162,162,162,162,163,163,163,164,164,164,164,165,165,165,165,165,166,166,166,166,167,167,167,168,168,168,168,169,169,169,169,169,170,170,170,170,171,171,171,172,172,172,172,173,173,173,173,173,174,174,174,174,175,175,175,175,176,176,176,177,177,177,177,178,178,178,178,179,179,179,179,179,180,180,180,180,181,181,181,181,182,182,182,183,183,183,183,184,184,184,184,185,185,185,185,186,186,186,186,186,187,187,187,187,188,188,188,188,189,189,189,189,190,190,190,190,191,191,191,191,192,192,192,193,193,193,193,194,194,194,194,195,195,195,195,196,196,196,196,197,197,197,197,198,198,198,198,198,198,199,199,199,199,200,200,200,200,201,201,201,201,202,202,202,202,203,203,203,203,204,204,204,204,205,205,205,205,206,206,206,206,207,207,207,207,208,208,208,208,209,209,209,209,210,210,210,210,211,211,211,211,212,212,212,212,213,213,213,213,214,214,215,215,215,215,216,216,216,216,217,217,217,217,218,218,218,218,218,219,219,219,219,220,220,220,220,221,221,221,221,222,222,222,222,223,223,223,223,223,224,224,224,224,225,225,225,225,226,226,227,227,227,227,228,228,228,228,228,229,229,229,229,230,230,230,230,231,231,231,231,232,232,232,232,232,233,233,233,233,234,234,235,235,235,235,235,236,236,236,236,237,237,237,237,238,238,238,238,238,239,239,239,239,240,240,241,241,241,241,241,242,242,242,242,243,243,243,243,244,244,244,244,244,245,245,245,245,246,246,246,247,247,247,247,248,248,248,248,248,249,249,249,249,250,250,250,250,250,251,251,252,252,252,252,253,253,253,253,253,254,254,254,254,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(53., 0.),
string_data("0,0,1,1,2,2,3,3,4,4,5,5,6,6,6,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,24,24,24,25,25,26,27,27,28,28,29,29,29,30,31,31,32,32,33,33,33,34,35,35,36,36,37,37,38,38,39,39,40,40,41,41,42,42,43,43,43,44,44,45,45,46,46,47,48,48,48,49,50,50,50,51,51,52,52,53,53,54,54,55,55,56,56,56,57,57,58,58,59,59,60,60,61,61,62,62,63,63,63,64,64,65,65,66,66,67,67,67,68,69,69,70,70,71,71,71,72,72,73,73,74,74,74,75,75,76,76,76,77,78,78,79,79,79,80,80,81,81,81,82,82,83,83,84,84,85,85,85,86,86,87,87,88,88,88,89,89,90,90,91,91,91,92,92,93,93,93,94,94,95,95,96,96,96,97,97,98,98,99,99,100,100,100,101,101,101,102,102,103,103,104,104,104,105,105,106,106,106,107,107,107,108,108,108,109,110,110,110,111,111,111,112,112,113,113,113,114,114,115,115,115,116,116,116,117,117,118,118,119,119,119,120,120,120,120,121,121,121,122,122,123,123,123,124,124,125,125,126,126,126,127,127,127,127,128,128,129,129,129,130,130,130,131,131,132,132,132,132,133,133,134,134,134,135,135,135,135,136,136,137,137,137,138,138,138,139,139,139,140,140,141,141,141,141,142,142,143,143,143,143,144,144,145,145,145,145,146,146,147,147,148,148,148,148,148,149,149,149,150,150,150,151,151,151,152,152,153,153,153,153,154,154,154,154,155,155,156,156,156,156,157,157,158,158,158,158,158,159,159,159,160,160,160,161,161,162,162,162,162,162,163,163,163,164,164,164,165,165,165,165,165,166,166,167,167,167,168,168,168,168,169,169,169,169,170,170,170,170,171,171,171,172,172,172,173,173,173,173,173,174,174,174,175,175,175,175,176,176,176,177,177,177,177,178,178,178,179,179,179,179,179,180,180,180,180,180,181,181,182,182,182,182,182,183,183,183,184,184,184,184,185,185,185,185,186,186,186,186,186,186,187,187,187,188,188,188,188,189,189,189,189,189,190,190,190,191,191,191,191,192,192,192,192,192,193,193,193,193,194,194,194,194,194,195,195,195,196,196,196,196,196,197,197,197,197,198,198,198,198,198,198,198,198,199,199,199,199,199,200,200,200,200,201,201,201,201,201,202,202,202,202,202,203,203,203,203,203,204,204,204,204,205,205,205,205,205,206,206,206,206,206,206,206,207,207,207,207,208,208,208,208,208,209,209,209,209,209,210,210,210,210,210,210,210,211,211,211,211,211,212,212,212,212,212,212,212,213,213,213,213,213,214,214,214,215,215,215,215,215,215,215,216,216,216,216,216,216,216,217,217,217,217,217,218,218,218,218,218,218,218,219,219,219,219,219,219,219,220,220,220,220,220,220,220,221,221,221,221,221,221,221,222,222,222,222,222,222,222,223,223,223,223,223,223,223,223,223,224,224,224,224,224,224,224,225,225,225,225,225,225,225,226,226,226,226,226,227,227,227,227,227,227,227,227,228,228,228,228,228,228,228,228,228,229,229,229,229,229,229,229,229,229,230,230,230,230,230,230,230,230,230,231,231,231,231,231,231,231,231,231,231,232,232,232,232,232,232,232,232,232,233,233,233,233,233,233,233,233,233,233,234,234,234,234,234,234,235,235,235,235,235,235,235,235,235,236,236,236,236,236,236,236,236,236,236,236,236,237,237,237,237,237,237,237,237,237,237,238,238,238,238,238,238,238,238,238,238,238,238,239,239,239,239,239,239,239,239,239,239,239,239,240,240,240,240,240,240,241,241,241,241,241,241,241,241,241,241,241,241,242,242,242,242,242,242,242,242,242,242,242,242,242,242,243,243,243,243,243,243,243,243,243,243,243,243,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,245,245,245,245,246,246,246,246,246,246,246,246,246,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,255,255,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(150., 0.),
string_data("0,1,2,3,4,6,8,9,11,13,15,16,18,20,22,24,26,28,29,32,33,35,37,39,41,43,44,46,48,50,52,54,56,57,59,61,62,64,66,67,69,71,73,75,76,78,80,81,82,85,86,88,89,91,92,94,95,97,98,100,102,103,104,106,107,109,110,111,113,115,116,117,119,120,121,122,124,125,126,127,129,130,131,133,134,135,136,137,138,139,141,142,143,144,145,146,147,148,149,150,151,153,153,154,156,156,157,158,159,160,161,162,163,164,165,165,166,167,168,169,170,170,171,172,173,173,174,175,176,177,178,178,179,179,180,181,181,182,183,184,184,185,186,186,187,187,188,189,189,190,191,191,191,192,193,193,194,195,195,196,196,197,197,198,198,198,199,199,200,200,201,201,202,202,203,203,203,204,204,205,205,206,206,207,207,207,208,208,209,209,209,209,210,210,211,211,211,212,212,212,212,213,213,214,214,215,215,215,215,216,216,217,217,217,217,218,218,218,218,219,219,219,220,220,220,220,220,221,221,221,221,222,222,222,222,222,223,223,223,223,224,224,224,224,224,225,225,225,225,225,226,226,226,227,227,227,227,227,228,228,228,228,228,228,229,229,229,229,229,229,229,230,230,230,230,230,230,230,231,231,231,231,231,231,231,231,232,232,232,232,232,232,232,232,233,233,233,233,233,233,233,233,233,234,234,234,234,234,235,235,235,235,235,235,235,235,235,235,236,236,236,236,236,236,236,236,236,236,236,236,237,237,237,237,237,237,237,237,237,237,237,238,238,238,238,238,238,238,238,238,238,238,238,238,238,239,239,239,239,239,239,239,239,239,239,239,239,239,239,239,240,240,240,240,240,240,240,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,246,246,246,246,246,246,246,246,246,246,246,246,246,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(-150., -100.),
string_data("0,0,0,0,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,24,24,24,24,24,24,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,26,26,26,26,26,26,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,31,31,31,31,31,31,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,33,33,33,33,33,33,33,33,33,33,33,33,33,33,33,33,33,33,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,35,35,35,35,35,35,35,35,35,35,35,35,35,35,35,35,35,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,37,37,37,37,37,37,37,37,37,37,37,37,37,37,37,37,37,37,38,38,38,38,38,38,38,38,38,38,38,38,38,38,38,38,38,38,38,38,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,55,55,55,55,55,55,55,55,55,55,55,55,55,55,56,56,56,56,56,56,56,56,56,56,56,56,56,57,57,57,57,57,57,57,57,57,57,58,58,58,58,58,58,58,58,58,59,59,59,59,59,59,59,59,59,60,60,60,60,60,60,61,61,61,61,61,61,61,62,62,62,62,63,63,63,63,64,64,64,64,64,65,65,66,66,66,66,67,67,68,68,68,69,69,70,70,71,71,72,72,73,73,73,75,75,76,76,78,78,80,80,80,82,82,84,84,87,87,90,90,90,93,93,97,97,101,101,107,107,107,113,113,121,121,130,130,141,141,141,154,154,172,172,194,194,222,222").as_slice()
);
assert_eq!(
brightness_contrast_map(-77., -100.),
string_data("0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6,6,7,7,7,7,8,8,8,8,8,9,9,9,9,10,10,10,10,10,11,11,11,11,12,12,12,12,12,13,13,13,13,13,14,14,14,14,15,15,15,15,15,16,16,16,16,16,16,17,17,17,17,17,18,18,18,18,18,19,19,19,19,19,20,20,20,20,20,20,20,21,21,21,21,21,22,22,22,22,22,22,23,23,23,23,23,24,24,24,24,24,24,24,25,25,25,25,25,25,26,26,26,26,26,26,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,32,32,32,32,32,32,32,32,33,33,33,33,33,33,33,33,33,34,34,34,34,34,34,34,34,35,35,35,35,35,35,35,35,35,36,36,36,36,36,36,36,36,36,36,36,37,37,37,37,37,37,37,37,37,38,38,38,38,38,38,38,38,38,38,38,38,39,39,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,44,44,44,44,44,44,44,44,44,44,44,44,44,44,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,47,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,49,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,51,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,53,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,54,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,59,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,61,62,62,62,62,62,62,62,62,62,62,62,62,62,62,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,63,64,64,64,64,64,64,64,64,64,64,64,64,64,65,65,65,65,65,65,65,65,65,65,65,65,65,66,66,66,66,66,66,66,66,66,66,66,67,67,67,67,67,67,67,67,67,67,67,67,68,68,68,68,68,68,68,68,68,69,69,69,69,69,69,69,69,69,69,70,70,70,70,70,70,70,70,70,71,71,71,71,71,71,71,71,72,72,72,72,72,72,72,72,73,73,73,73,73,73,73,73,74,74,74,74,74,74,75,75,75,75,75,75,75,75,76,76,76,76,76,76,77,77,77,77,77,77,78,78,78,78,78,78,79,79,79,79,79,79,80,80,80,80,81,81,81,81,81,81,82,82,82,82,83,83,83,83,84,84,84,84,85,85,85,85,86,86,86,86,87,87,87,87,88,88,88,88,88,89,89,90,90,90,90,91,91,91,91,92,92,93,93,94,94,94,94,95,95,96,96,96,97,97,97,97,98,98,99,99,100,100,101,101,102,102,103,103,104,104,104,105,105,106,106,107,107,108,108,109,109,110,110,112,112,112,113,113,114,114,116,116,117,117,119,119,120,120,120,122,122,124,124,125,125,127,127,129,129,131,131,131,133,133,135,135,137,137,139,139,141,141,141,144,144,146,146,149,149,152,152,154,154,154,158,158,161,161,164,164,167,167,167,171,171,175,175,179,179,183,183,183,187,187,191,191,196,196,201,201,201,206,206,212,212,217,217,222,222,222,229,229,234,234,241,241,247,247").as_slice()
);
assert_eq!(
brightness_contrast_map(0., -100.),
string_data("0,0,1,1,2,3,3,4,5,5,6,7,7,8,8,9,10,10,11,12,12,13,13,14,15,15,16,16,17,17,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,25,26,26,27,27,28,28,28,29,29,30,30,30,31,31,31,32,32,32,33,33,33,34,34,34,35,35,35,35,36,36,36,36,37,37,37,38,38,38,38,39,39,39,39,39,40,40,40,40,40,41,41,41,41,42,42,42,42,42,42,43,43,43,43,43,43,44,44,44,44,44,44,45,45,45,45,45,45,45,46,46,46,46,46,46,46,47,47,47,47,47,47,47,47,48,48,48,48,48,48,48,48,49,49,49,49,49,49,49,49,49,49,50,50,50,50,50,50,50,50,50,50,51,51,51,51,51,51,51,51,51,51,51,52,52,52,52,52,52,52,52,52,52,52,52,53,53,53,53,53,53,53,53,53,53,53,53,53,54,54,54,54,54,54,54,54,54,54,54,54,54,55,55,55,55,55,55,55,55,55,55,55,55,55,55,56,56,56,56,56,56,56,56,56,56,56,56,56,57,57,57,57,57,57,57,57,57,57,57,57,57,57,58,58,58,58,58,58,58,58,58,58,58,58,58,58,59,59,59,59,59,59,59,59,59,59,59,59,59,59,60,60,60,60,60,60,60,60,60,60,60,60,60,61,61,61,61,61,61,61,61,61,61,61,61,62,62,62,62,62,62,62,62,62,62,62,63,63,63,63,63,63,63,63,63,63,63,63,64,64,64,64,64,64,64,64,64,64,64,65,65,65,65,65,65,65,65,65,65,65,66,66,66,66,66,66,66,66,66,66,67,67,67,67,67,67,67,67,67,67,68,68,68,68,68,68,68,68,68,69,69,69,69,69,69,69,69,69,70,70,70,70,70,70,70,70,70,71,71,71,71,71,71,71,71,72,72,72,72,72,72,72,72,73,73,73,73,73,73,73,73,74,74,74,74,74,74,74,75,75,75,75,75,75,75,75,76,76,76,76,76,76,76,77,77,77,77,77,77,77,78,78,78,78,78,78,78,79,79,79,79,79,79,80,80,80,80,80,80,80,81,81,81,81,81,81,82,82,82,82,82,82,83,83,83,83,83,83,84,84,84,84,84,84,85,85,85,85,85,85,86,86,86,86,86,86,87,87,87,87,88,88,88,88,88,88,88,89,89,89,89,90,90,90,90,90,91,91,91,91,91,91,92,92,92,92,92,93,93,93,93,94,94,94,94,94,95,95,95,95,95,96,96,96,96,96,97,97,97,97,97,98,98,98,98,99,99,99,99,100,100,100,100,101,101,101,101,101,102,102,102,102,103,103,103,103,103,104,104,104,105,105,105,105,105,106,106,106,107,107,107,107,107,108,108,108,108,109,109,109,110,110,110,110,110,111,111,111,112,112,112,112,113,113,113,114,114,114,114,114,115,115,115,115,116,116,116,117,117,117,117,118,118,118,119,119,119,119,120,120,120,121,121,121,121,122,122,122,123,123,123,123,124,124,124,125,125,125,125,126,126,126,126,127,127,127,128,128,128,128,129,129,129,130,130,131,131,131,131,132,132,132,133,133,133,133,134,134,134,134,135,135,136,136,136,137,137,137,137,138,138,138,138,139,140,140,140,140,141,141,141,141,142,142,143,143,143,144,144,144,144,145,145,146,146,146,146,147,147,147,147,148,148,149,149,149,150,150,151,151,151,151,152,152,152,152,153,153,154,154,154,154,155,155,156,156,156,157,157,158,158,158,158,159,159,160,160,160,160,161,161,162,162,162,162,163,163,164,164,165,165,165,165,166,166,167,167,167,167,168,168,169,169,169,169,170,170,171,171,172,172,172,172,173,173,174,174,175,175,175,175,176,176,177,177,178,178,178,178,179,179,180,180,181,181,181,181,182,182,183,183,184,184,184,184,185,185,186,186,187,187,187,187,188,188,189,189,190,190,191,191,191,191,192,192,193,193,194,194,194,195,195,195,195,196,196,197,197,198,198,199,199,199,199,200,200,201,201,202,202,203,203,203,204,204,204,204,205,205,206,206,207,207,208,208,209,209,209,209,210,210,210,211,211,212,212,213,213,214,214,215,215,215,215,216,216,216,217,217,218,218,219,219,220,220,221,221,222,222,222,222,222,223,223,224,224,225,225,226,226,227,227,227,228,228,229,229,230,230,231,231,231,231,231,232,232,233,233,234,234,235,235,236,236,236,237,237,238,238,239,239,240,240,240,241,241,242,242,243,243,244,244,244,244,244,245,245,246,246,247,247,247,248,248,249,249,250,250,251,251,251,252,252,253,253,254,254,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(53., -100.),
string_data("0,1,1,2,3,5,6,7,9,10,11,12,13,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,30,31,32,33,33,34,34,35,36,36,37,37,38,38,39,39,40,40,41,41,41,42,42,43,43,43,44,44,44,44,45,45,45,46,46,46,47,47,47,47,48,48,48,48,48,49,49,49,49,50,50,50,50,50,51,51,51,51,51,51,52,52,52,52,52,52,53,53,53,53,53,53,53,54,54,54,54,54,54,55,55,55,55,55,55,55,55,56,56,56,56,56,56,56,57,57,57,57,57,57,57,58,58,58,58,58,58,58,58,59,59,59,59,59,59,59,60,60,60,60,60,60,60,60,61,61,61,61,61,61,62,62,62,62,62,62,62,63,63,63,63,63,63,63,64,64,64,64,64,64,65,65,65,65,65,65,66,66,66,66,66,66,67,67,67,67,67,67,68,68,68,68,68,69,69,69,69,69,69,70,70,70,70,70,71,71,71,71,71,71,72,72,72,72,73,73,73,73,73,74,74,74,74,74,75,75,75,75,76,76,76,76,76,77,77,77,77,78,78,78,78,78,79,79,79,79,80,80,80,80,81,81,81,81,82,82,82,82,82,83,83,83,83,84,84,84,85,85,85,85,86,86,86,86,87,87,87,87,88,88,88,88,89,89,89,90,90,90,90,91,91,91,91,92,92,92,93,93,93,93,94,94,94,95,95,95,95,96,96,96,97,97,97,97,98,98,98,99,99,100,100,100,100,101,101,101,102,102,102,103,103,103,104,104,104,104,105,105,105,105,106,106,107,107,107,107,108,108,109,109,109,110,110,110,110,110,111,111,112,112,112,113,113,114,114,114,114,115,115,115,115,116,116,117,117,117,118,118,119,119,119,119,120,120,120,121,121,121,122,122,122,123,123,123,124,124,124,125,125,125,125,126,126,127,127,127,128,128,128,128,129,129,129,130,131,131,131,131,132,132,132,133,133,133,134,134,134,134,135,136,136,136,136,137,137,137,138,138,138,138,139,139,140,140,140,141,141,141,141,142,143,143,143,143,143,144,144,144,145,145,146,146,146,147,147,147,147,147,148,149,149,149,149,150,150,151,151,151,151,152,152,152,152,152,153,154,154,154,154,154,155,156,156,156,156,156,157,158,158,158,158,158,159,159,159,160,160,160,161,161,161,162,162,162,162,162,163,164,164,164,165,165,165,165,165,166,167,167,167,167,167,168,168,168,169,169,169,169,169,170,170,171,171,171,172,172,172,172,172,173,173,173,174,175,175,175,175,175,176,176,176,177,177,177,177,178,178,178,178,179,179,180,180,180,181,181,181,181,181,182,182,182,182,183,183,183,184,184,184,184,184,185,185,185,186,186,186,186,187,187,187,187,187,188,188,188,189,189,189,190,190,190,190,191,191,191,191,191,191,191,192,193,193,193,193,194,194,194,195,195,195,195,195,195,195,196,196,196,196,197,197,197,198,198,198,198,199,199,199,199,199,199,199,200,200,200,201,201,201,201,202,202,202,203,203,203,203,203,203,204,204,204,204,204,204,204,205,205,205,206,206,206,206,207,207,207,207,207,208,208,208,208,209,209,209,209,209,209,209,209,209,210,210,210,210,211,211,211,211,212,212,212,212,212,213,213,213,213,214,214,214,214,214,215,215,215,215,215,215,215,215,215,215,216,216,216,216,217,217,217,217,217,218,218,218,218,219,219,219,219,219,219,220,220,220,220,220,220,221,221,221,222,222,222,222,222,222,222,222,222,222,222,222,223,223,223,223,223,223,224,224,224,224,225,225,225,225,225,225,226,226,226,226,226,226,227,227,227,227,227,227,228,228,228,228,228,228,229,229,229,229,229,229,230,230,230,230,230,230,231,231,231,231,231,231,231,231,231,231,231,231,232,232,232,232,232,232,233,233,233,233,233,233,233,233,234,234,234,234,234,234,235,235,235,235,235,235,236,236,236,236,236,236,237,237,237,237,237,237,237,237,237,238,238,238,238,238,238,239,239,239,239,239,239,240,240,240,240,240,240,240,240,240,241,241,241,241,241,241,242,242,242,242,242,242,242,242,242,243,243,243,243,243,243,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,245,246,246,246,246,246,246,247,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,248,249,249,249,249,249,249,250,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,255,255,255,255").as_slice()
);
assert_eq!(
brightness_contrast_map(150., -100.),
string_data("0,1,3,6,10,14,18,22,25,28,31,33,36,38,39,41,42,44,45,46,47,48,49,50,50,51,51,52,53,53,54,54,55,56,56,57,57,58,58,59,59,60,60,61,61,62,62,63,63,64,65,65,66,66,67,68,68,69,70,70,71,72,73,74,74,75,76,76,77,78,79,80,81,81,82,83,84,85,86,87,88,89,90,91,92,93,93,94,95,96,97,98,99,100,101,103,103,104,105,107,107,109,110,110,112,113,114,114,116,116,118,119,120,121,122,123,123,125,126,126,128,128,129,131,132,132,133,134,136,136,137,138,139,140,140,141,142,143,144,145,146,146,147,148,149,150,151,152,152,153,154,155,155,156,157,158,159,160,160,161,162,162,163,164,165,165,166,167,167,168,169,169,170,170,172,172,172,173,174,175,175,176,176,178,178,178,179,179,181,181,181,181,182,183,184,184,184,185,185,186,186,187,187,188,188,189,189,190,190,191,191,192,192,193,193,194,194,195,195,195,196,196,197,197,198,198,198,199,199,199,199,200,200,201,201,201,202,203,203,203,204,204,204,204,204,205,205,206,206,206,207,207,207,208,208,209,209,209,209,209,209,210,210,210,211,211,211,211,212,212,212,213,213,213,213,214,214,214,215,215,215,215,215,215,215,215,216,216,216,216,217,217,217,217,218,218,218,218,218,219,219,219,219,220,220,220,220,220,221,221,221,221,222,222,222,222,222,222,222,222,222,222,222,222,223,223,223,223,223,223,224,224,224,224,224,225,225,225,225,225,225,226,226,226,226,226,226,227,227,227,227,227,227,227,227,228,228,228,228,228,228,228,229,229,229,229,229,229,229,229,230,230,230,230,230,230,230,231,231,231,231,231,231,231,231,231,231,231,231,231,231,231,231,231,232,232,232,232,232,232,232,232,232,233,233,233,233,233,233,233,233,233,234,234,234,234,234,234,234,234,234,234,235,235,235,235,235,235,235,235,235,236,236,236,236,236,236,236,236,236,236,236,237,237,237,237,237,237,237,237,237,237,237,237,238,238,238,238,238,238,238,238,238,238,238,239,239,239,239,239,239,239,239,239,239,239,239,239,240,240,240,240,240,240,240,240,240,240,240,240,240,241,241,241,241,241,241,241,241,241,241,241,241,241,241,241,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,242,243,243,243,243,243,243,243,243,243,243,243,243,243,243,243,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,245,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,246,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,248,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255").as_slice()
);
}
}

View file

@ -28,6 +28,8 @@ pub mod types {
pub type SeedValue = u32;
/// Non-negative integer vector2 with px unit
pub type Resolution = glam::UVec2;
/// DVec2 with px unit
pub type PixelSize = glam::DVec2;
}
// Translation struct between macro and definition

View file

@ -1,7 +1,7 @@
use super::misc::{ArcType, AsU64, GridType};
use super::{PointId, SegmentId, StrokeId};
use crate::Ctx;
use crate::registry::types::Angle;
use crate::registry::types::{Angle, PixelSize};
use crate::vector::{HandleId, VectorData, VectorDataTable};
use bezier_rs::Subpath;
use glam::DVec2;
@ -130,7 +130,7 @@ fn star<T: AsU64>(
}
#[node_macro::node(category("Vector: Shape"))]
fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: DVec2, #[default((0., 50.))] end: DVec2) -> VectorDataTable {
fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: PixelSize, #[default((0., 50.))] end: PixelSize) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
}
@ -206,7 +206,7 @@ fn grid<T: GridSpacing>(
// Add current point to the grid with offset for odd columns
let current_index = vector_data.point_domain.ids().len();
let a_angles_eaten = ((x + 1) / 2) as f64;
let a_angles_eaten = x.div_ceil(2) as f64;
let b_angles_eaten = (x / 2) as f64;
let offset_y_fraction = b_angles_eaten * tan_b - a_angles_eaten * tan_a;

View file

@ -5,7 +5,7 @@ use super::style::{Fill, Gradient, GradientStops, Stroke};
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
use crate::instances::{Instance, InstanceMut, Instances};
use crate::raster::image::ImageFrameTable;
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, SeedValue};
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, ReferencePoint, Transform, TransformMut};
use crate::vector::PointDomain;
@ -207,7 +207,7 @@ async fn repeat<I: 'n + Send>(
#[implementations(GraphicGroupTable, VectorDataTable, ImageFrameTable<Color>)] instance: Instances<I>,
#[default(100., 100.)]
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
direction: DVec2,
direction: PixelSize,
angle: Angle,
#[default(4)] instances: IntegerCount,
) -> GraphicGroupTable

View file

@ -1,4 +1,4 @@
use crate::raster::{BlendImageTupleNode, ExtendImageToBoundsNode, blend_image_closure};
use crate::raster::{BlendImageTupleNode, blend_image_closure, extend_image_to_bounds};
use glam::{DAffine2, DVec2};
use graph_craft::generic::FnNode;
use graph_craft::proto::FutureWrapperNode;
@ -8,7 +8,7 @@ use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::raster::{Alpha, Bitmap, BlendMode, Color, Pixel, Sample};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
use graphene_core::value::{ClonedNode, ValueNode};
use graphene_core::vector::VectorDataTable;
use graphene_core::vector::brush_stroke::{BrushStroke, BrushStyle};
use graphene_core::{Ctx, GraphicElement, Node};
@ -225,7 +225,7 @@ async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: I
background_bounds = bounds.transform();
}
let mut actual_image = ExtendImageToBoundsNode::new(ClonedNode::new(background_bounds)).eval(brush_plan.background);
let mut actual_image = extend_image_to_bounds((), brush_plan.background, background_bounds);
let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1);
for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() {
// Create brush texture.
@ -262,7 +262,7 @@ async fn brush(_: impl Ctx, image_frame_table: ImageFrameTable<Color>, bounds: I
);
let blit_target = if idx == 0 {
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
ExtendImageToBoundsNode::new(CopiedNode::new(stroke_to_layer)).eval(target)
extend_image_to_bounds((), target, stroke_to_layer)
} else {
use crate::raster::empty_image;
empty_image((), stroke_to_layer, Color::TRANSPARENT)

View file

@ -1,19 +1,15 @@
use crate::wasm_application_io::WasmApplicationIo;
use dyn_any::StaticTypeSized;
use glam::{DAffine2, DVec2, Mat2, Vec2};
use gpu_executor::{ComputePassDimensions, StorageBufferOptions};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::*;
use graph_craft::proto::*;
use graphene_core::application_io::ApplicationIo;
use graphene_core::raster::BlendMode;
use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::raster::{BlendMode, Pixel};
use graphene_core::transform::Transform;
use graphene_core::transform::TransformMut;
use graphene_core::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use wgpu_executor::{Bindgroup, PipelineLayout, Shader, ShaderIO, ShaderInput, WgpuExecutor, WgpuShaderInput};
use std::sync::Arc;
use wgpu_executor::{Bindgroup, PipelineLayout, Shader, ShaderIO, ShaderInput, WgpuExecutor};
// TODO: Move to graph-craft
#[node_macro::node(category("Debug: GPU"))]
@ -39,240 +35,6 @@ async fn compile_gpu<'a: 'n>(_: impl Ctx, node: &'a DocumentNode, typing_context
Ok(compilation_client::compile(proto_networks, input_types, output_types, io).await.unwrap())
}
pub struct MapGpuNode<Node, EditorApi> {
node: Node,
editor_api: EditorApi,
cache: Mutex<HashMap<String, ComputePass>>,
}
struct ComputePass {
pipeline_layout: PipelineLayout,
readback_buffer: Option<Arc<WgpuShaderInput>>,
}
impl Clone for ComputePass {
fn clone(&self) -> Self {
Self {
pipeline_layout: self.pipeline_layout.clone(),
readback_buffer: self.readback_buffer.clone(),
}
}
}
#[node_macro::old_node_impl(MapGpuNode)]
async fn map_gpu<'a: 'input>(image: ImageFrameTable<Color>, node: DocumentNode, editor_api: &'a graphene_core::application_io::EditorApi<WasmApplicationIo>) -> ImageFrameTable<Color> {
let image_frame_table = &image;
let image = image.one_instance_ref().instance;
log::debug!("Executing gpu node");
let executor = &editor_api.application_io.as_ref().and_then(|io| io.gpu_executor()).unwrap();
#[cfg(feature = "image-compare")]
let img: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(image.data.clone())).unwrap().into();
// TODO: The cache should be based on the network topology not the node name
let compute_pass_descriptor = if self.cache.lock().as_ref().unwrap().contains_key("placeholder") {
self.cache.lock().as_ref().unwrap().get("placeholder").unwrap().clone()
} else {
let name = "placeholder".to_string();
let Ok(compute_pass_descriptor) = create_compute_pass_descriptor(node, image_frame_table, executor).await else {
log::error!("Error creating compute pass descriptor in 'map_gpu()");
return ImageFrameTable::one_empty_image();
};
self.cache.lock().as_mut().unwrap().insert(name, compute_pass_descriptor.clone());
log::error!("created compute pass");
compute_pass_descriptor
};
let compute_pass = executor
.create_compute_pass(
&compute_pass_descriptor.pipeline_layout,
compute_pass_descriptor.readback_buffer.clone(),
ComputePassDimensions::XY(image.width / 12 + 1, image.height / 8 + 1),
)
.unwrap();
executor.execute_compute_pipeline(compute_pass).unwrap();
log::debug!("executed pipeline");
log::debug!("reading buffer");
let result = executor.read_output_buffer(compute_pass_descriptor.readback_buffer.clone().unwrap()).await.unwrap();
let colors = bytemuck::pod_collect_to_vec::<u8, Color>(result.as_slice());
log::debug!("first color: {:?}", colors[0]);
#[cfg(feature = "image-compare")]
let img2: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(colors.clone())).unwrap().into();
#[cfg(feature = "image-compare")]
let score = image_compare::rgb_hybrid_compare(&img.into_rgb8(), &img2.into_rgb8()).unwrap();
#[cfg(feature = "image-compare")]
log::debug!("score: {:?}", score.score);
let new_image = Image {
data: colors,
width: image.width,
height: image.height,
..Default::default()
};
let mut result = ImageFrameTable::new(new_image);
*result.transform_mut() = image_frame_table.transform();
*result.one_instance_mut().alpha_blending = *image_frame_table.one_instance_ref().alpha_blending;
result
}
impl<Node, EditorApi> MapGpuNode<Node, EditorApi> {
pub fn new(node: Node, editor_api: EditorApi) -> Self {
Self {
node,
editor_api,
cache: Mutex::new(HashMap::new()),
}
}
}
async fn create_compute_pass_descriptor<T: Clone + Pixel + StaticTypeSized>(node: DocumentNode, image: &ImageFrameTable<T>, executor: &&WgpuExecutor) -> Result<ComputePass, String>
where
GraphicElement: From<Image<T>>,
T::Static: Pixel,
{
let image = image.one_instance_ref().instance;
let compiler = graph_craft::graphene_compiler::Compiler {};
let inner_network = NodeNetwork::value_network(node);
log::debug!("inner_network: {inner_network:?}");
let network = NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::Inline(InlineRust::new("i1[(_global_index.y * i0 + _global_index.x) as usize]".into(), concrete![Color]))],
implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::network(concrete!(u32), 0)],
implementation: DocumentNodeImplementation::ProtoNode("graphene_core::ops::IdentityNode".into()),
..Default::default()
},
// DocumentNode {
// name: "Index".into(),
// // inputs: vec![NodeInput::Network(concrete!(UVec3))],
// inputs: vec![NodeInput::Inline(InlineRust::new("i1.x as usize".into(), concrete![u32]))],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
// ..Default::default()
// },
// DocumentNode {
// name: "Get Node".into(),
// inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(0), 0)],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::storage::GetNode".into()),
// ..Default::default()
// },
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::Network(inner_network),
..Default::default()
},
// DocumentNode {
// name: "Save Node".into(),
// inputs: vec![
// NodeInput::node(NodeId(5), 0),
// NodeInput::Inline(InlineRust::new(
// "|x| o0[(_global_index.y * i1 + _global_index.x) as usize] = x".into(),
// // "|x|()".into(),
// Type::Fn(Box::new(concrete!(PackedPixel)), Box::new(concrete!(()))),
// )),
// ],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::generic::FnMutNode".into()),
// ..Default::default()
// },
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
};
log::debug!("compiling network");
let proto_networks: Result<Vec<_>, _> = compiler.compile(network.clone()).collect();
log::debug!("compiling shader");
let shader = compilation_client::compile(
proto_networks?,
vec![concrete!(u32), concrete!(Color)],
vec![concrete!(Color)],
ShaderIO {
inputs: vec![
ShaderInput::UniformBuffer((), concrete!(u32)),
ShaderInput::StorageBuffer((), concrete!(Color)),
ShaderInput::OutputBuffer((), concrete!(Color)),
],
output: ShaderInput::OutputBuffer((), concrete!(Color)),
},
)
.await
.unwrap();
let len: usize = image.data.len();
let storage_buffer = executor
.create_storage_buffer(
image.data.clone(),
StorageBufferOptions {
cpu_writable: false,
gpu_writable: true,
cpu_readable: false,
storage: true,
},
)
.unwrap();
// let canvas = editor_api.application_io.create_surface();
// let surface = unsafe { executor.create_surface(canvas) }.unwrap();
// let surface_id = surface.surface_id;
// let texture = executor.create_texture_buffer(image.clone(), TextureBufferOptions::Texture).unwrap();
// // executor.create_render_pass(texture, surface).unwrap();
// let frame = SurfaceFrame {
// surface_id,
// transform: image.transform,
// };
// return frame;
log::debug!("creating buffer");
let width_uniform = executor.create_uniform_buffer(image.width).unwrap();
let storage_buffer = Arc::new(storage_buffer);
let output_buffer = executor.create_output_buffer(len, concrete!(Color), false).unwrap();
let output_buffer = Arc::new(output_buffer);
let readback_buffer = executor.create_output_buffer(len, concrete!(Color), true).unwrap();
let readback_buffer = Arc::new(readback_buffer);
log::debug!("created buffer");
let bind_group = Bindgroup {
buffers: vec![width_uniform.into(), storage_buffer],
};
let shader = Shader {
source: shader.spirv_binary.into(),
name: "gpu::eval",
io: shader.io,
};
log::debug!("loading shader");
let shader = executor.load_shader(shader).unwrap();
log::debug!("loaded shader");
let pipeline = PipelineLayout {
shader: shader.into(),
entry_point: "eval".to_string(),
bind_group: bind_group.into(),
output_buffer,
};
log::debug!("created pipeline");
Ok(ComputePass {
pipeline_layout: pipeline,
readback_buffer: Some(readback_buffer),
})
}
#[node_macro::node(category("Debug: GPU"))]
async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, background: ImageFrameTable<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrameTable<Color> {
let foreground_transform = foreground.transform();
@ -457,3 +219,237 @@ async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, backgr
result
}
// struct ComputePass {
// pipeline_layout: PipelineLayout,
// readback_buffer: Option<Arc<WgpuShaderInput>>,
// }
// impl Clone for ComputePass {
// fn clone(&self) -> Self {
// Self {
// pipeline_layout: self.pipeline_layout.clone(),
// readback_buffer: self.readback_buffer.clone(),
// }
// }
// }
// pub struct MapGpuNode<Node, EditorApi> {
// node: Node,
// editor_api: EditorApi,
// cache: Mutex<HashMap<String, ComputePass>>,
// }
// #[node_macro::old_node_impl(MapGpuNode)]
// async fn map_gpu<'a: 'input>(image: ImageFrameTable<Color>, node: DocumentNode, editor_api: &'a graphene_core::application_io::EditorApi<WasmApplicationIo>) -> ImageFrameTable<Color> {
// let image_frame_table = &image;
// let image = image.one_instance_ref().instance;
// log::debug!("Executing gpu node");
// let executor = &editor_api.application_io.as_ref().and_then(|io| io.gpu_executor()).unwrap();
// #[cfg(feature = "image-compare")]
// let img: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(image.data.clone())).unwrap().into();
// // TODO: The cache should be based on the network topology not the node name
// let compute_pass_descriptor = if self.cache.lock().as_ref().unwrap().contains_key("placeholder") {
// self.cache.lock().as_ref().unwrap().get("placeholder").unwrap().clone()
// } else {
// let name = "placeholder".to_string();
// let Ok(compute_pass_descriptor) = create_compute_pass_descriptor(node, image_frame_table, executor).await else {
// log::error!("Error creating compute pass descriptor in 'map_gpu()");
// return ImageFrameTable::one_empty_image();
// };
// self.cache.lock().as_mut().unwrap().insert(name, compute_pass_descriptor.clone());
// log::error!("created compute pass");
// compute_pass_descriptor
// };
// let compute_pass = executor
// .create_compute_pass(
// &compute_pass_descriptor.pipeline_layout,
// compute_pass_descriptor.readback_buffer.clone(),
// ComputePassDimensions::XY(image.width / 12 + 1, image.height / 8 + 1),
// )
// .unwrap();
// executor.execute_compute_pipeline(compute_pass).unwrap();
// log::debug!("executed pipeline");
// log::debug!("reading buffer");
// let result = executor.read_output_buffer(compute_pass_descriptor.readback_buffer.clone().unwrap()).await.unwrap();
// let colors = bytemuck::pod_collect_to_vec::<u8, Color>(result.as_slice());
// log::debug!("first color: {:?}", colors[0]);
// #[cfg(feature = "image-compare")]
// let img2: image::DynamicImage = image::Rgba32FImage::from_raw(image.width, image.height, bytemuck::cast_vec(colors.clone())).unwrap().into();
// #[cfg(feature = "image-compare")]
// let score = image_compare::rgb_hybrid_compare(&img.into_rgb8(), &img2.into_rgb8()).unwrap();
// #[cfg(feature = "image-compare")]
// log::debug!("score: {:?}", score.score);
// let new_image = Image {
// data: colors,
// width: image.width,
// height: image.height,
// ..Default::default()
// };
// let mut result = ImageFrameTable::new(new_image);
// *result.transform_mut() = image_frame_table.transform();
// *result.one_instance_mut().alpha_blending = *image_frame_table.one_instance_ref().alpha_blending;
// result
// }
// impl<Node, EditorApi> MapGpuNode<Node, EditorApi> {
// pub fn new(node: Node, editor_api: EditorApi) -> Self {
// Self {
// node,
// editor_api,
// cache: Mutex::new(HashMap::new()),
// }
// }
// }
// async fn create_compute_pass_descriptor<T: Clone + Pixel + StaticTypeSized>(node: DocumentNode, image: &ImageFrameTable<T>, executor: &&WgpuExecutor) -> Result<ComputePass, String>
// where
// GraphicElement: From<Image<T>>,
// T::Static: Pixel,
// {
// let image = image.one_instance_ref().instance;
// let compiler = graph_craft::graphene_compiler::Compiler {};
// let inner_network = NodeNetwork::value_network(node);
// log::debug!("inner_network: {inner_network:?}");
// let network = NodeNetwork {
// exports: vec![NodeInput::node(NodeId(2), 0)],
// nodes: [
// DocumentNode {
// inputs: vec![NodeInput::Inline(InlineRust::new("i1[(_global_index.y * i0 + _global_index.x) as usize]".into(), concrete![Color]))],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
// ..Default::default()
// },
// DocumentNode {
// inputs: vec![NodeInput::network(concrete!(u32), 0)],
// implementation: DocumentNodeImplementation::ProtoNode("graphene_core::ops::IdentityNode".into()),
// ..Default::default()
// },
// // DocumentNode {
// // name: "Index".into(),
// // // inputs: vec![NodeInput::Network(concrete!(UVec3))],
// // inputs: vec![NodeInput::Inline(InlineRust::new("i1.x as usize".into(), concrete![u32]))],
// // implementation: DocumentNodeImplementation::ProtoNode("graphene_core::value::CopiedNode".into()),
// // ..Default::default()
// // },
// // DocumentNode {
// // name: "Get Node".into(),
// // inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(0), 0)],
// // implementation: DocumentNodeImplementation::ProtoNode("graphene_core::storage::GetNode".into()),
// // ..Default::default()
// // },
// DocumentNode {
// inputs: vec![NodeInput::node(NodeId(0), 0)],
// implementation: DocumentNodeImplementation::Network(inner_network),
// ..Default::default()
// },
// // DocumentNode {
// // name: "Save Node".into(),
// // inputs: vec![
// // NodeInput::node(NodeId(5), 0),
// // NodeInput::Inline(InlineRust::new(
// // "|x| o0[(_global_index.y * i1 + _global_index.x) as usize] = x".into(),
// // // "|x|()".into(),
// // Type::Fn(Box::new(concrete!(PackedPixel)), Box::new(concrete!(()))),
// // )),
// // ],
// // implementation: DocumentNodeImplementation::ProtoNode("graphene_core::generic::FnMutNode".into()),
// // ..Default::default()
// // },
// ]
// .into_iter()
// .enumerate()
// .map(|(id, node)| (NodeId(id as u64), node))
// .collect(),
// ..Default::default()
// };
// log::debug!("compiling network");
// let proto_networks: Result<Vec<_>, _> = compiler.compile(network.clone()).collect();
// log::debug!("compiling shader");
// let shader = compilation_client::compile(
// proto_networks?,
// vec![concrete!(u32), concrete!(Color)],
// vec![concrete!(Color)],
// ShaderIO {
// inputs: vec![
// ShaderInput::UniformBuffer((), concrete!(u32)),
// ShaderInput::StorageBuffer((), concrete!(Color)),
// ShaderInput::OutputBuffer((), concrete!(Color)),
// ],
// output: ShaderInput::OutputBuffer((), concrete!(Color)),
// },
// )
// .await
// .unwrap();
// let len: usize = image.data.len();
// let storage_buffer = executor
// .create_storage_buffer(
// image.data.clone(),
// StorageBufferOptions {
// cpu_writable: false,
// gpu_writable: true,
// cpu_readable: false,
// storage: true,
// },
// )
// .unwrap();
// // let canvas = editor_api.application_io.create_surface();
// // let surface = unsafe { executor.create_surface(canvas) }.unwrap();
// // let surface_id = surface.surface_id;
// // let texture = executor.create_texture_buffer(image.clone(), TextureBufferOptions::Texture).unwrap();
// // // executor.create_render_pass(texture, surface).unwrap();
// // let frame = SurfaceFrame {
// // surface_id,
// // transform: image.transform,
// // };
// // return frame;
// log::debug!("creating buffer");
// let width_uniform = executor.create_uniform_buffer(image.width).unwrap();
// let storage_buffer = Arc::new(storage_buffer);
// let output_buffer = executor.create_output_buffer(len, concrete!(Color), false).unwrap();
// let output_buffer = Arc::new(output_buffer);
// let readback_buffer = executor.create_output_buffer(len, concrete!(Color), true).unwrap();
// let readback_buffer = Arc::new(readback_buffer);
// log::debug!("created buffer");
// let bind_group = Bindgroup {
// buffers: vec![width_uniform.into(), storage_buffer],
// };
// let shader = Shader {
// source: shader.spirv_binary.into(),
// name: "gpu::eval",
// io: shader.io,
// };
// log::debug!("loading shader");
// let shader = executor.load_shader(shader).unwrap();
// log::debug!("loaded shader");
// let pipeline = PipelineLayout {
// shader: shader.into(),
// entry_point: "eval".to_string(),
// bind_group: bind_group.into(),
// output_buffer,
// };
// log::debug!("created pipeline");
// Ok(ComputePass {
// pipeline_layout: pipeline,
// readback_buffer: Some(readback_buffer),
// })
// }

View file

@ -4,7 +4,7 @@ use glam::{DAffine2, DVec2, Vec2};
use graphene_core::raster::bbox::Bbox;
use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::raster::{
Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue, Sample,
Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, Sample,
};
use graphene_core::transform::{Transform, TransformMut};
use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint, GraphicElement, Node};
@ -12,7 +12,6 @@ use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::fmt::Debug;
use std::hash::Hash;
use std::marker::PhantomData;
#[derive(Debug, DynAny)]
pub enum Error {
@ -90,95 +89,28 @@ fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFra
result
}
#[derive(Debug, Clone, Copy)]
pub struct MapImageNode<P, MapFn> {
map_fn: MapFn,
_p: PhantomData<P>,
}
#[node_macro::old_node_fn(MapImageNode<_P>)]
fn map_image<MapFn, _P, Img: BitmapMut<Pixel = _P>>(image: Img, map_fn: &'input MapFn) -> Img
where
MapFn: for<'any_input> Node<'any_input, _P, Output = _P> + 'input,
{
let mut image = image;
image.map_pixels(|c| map_fn.eval(c));
image
}
#[node_macro::node]
fn insert_channel<
// _P is the color of the input image.
_P: RGBMut,
_S: Pixel + Luminance,
// Input image
Input: BitmapMut<Pixel = _P>,
Insertion: Bitmap<Pixel = _S>,
>(
#[node_macro::node(category("Raster"))]
fn combine_channels<_I, Red, Green, Blue, Alpha>(
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
#[implementations(ImageFrameTable<Color>)] insertion: Insertion,
target_channel: RedGreenBlue,
) -> Input
where
_P::ColorChannel: Linear,
{
if insertion.width() == 0 {
return image;
}
if insertion.width() != image.width() || insertion.height() != image.height() {
log::warn!("Stencil and image have different sizes. This is not supported.");
return image;
}
for y in 0..image.height() {
for x in 0..image.width() {
let image_pixel = image.get_pixel_mut(x, y).unwrap();
let insertion_pixel = insertion.get_pixel(x, y).unwrap();
match target_channel {
RedGreenBlue::Red => image_pixel.set_red(insertion_pixel.l().cast_linear_channel()),
RedGreenBlue::Green => image_pixel.set_green(insertion_pixel.l().cast_linear_channel()),
RedGreenBlue::Blue => image_pixel.set_blue(insertion_pixel.l().cast_linear_channel()),
}
}
}
image
}
#[node_macro::node]
fn combine_channels<
// _P is the color of the input image.
_P: RGBMut + AlphaMut,
_S: Pixel + Luminance,
// Input image
Input: BitmapMut<Pixel = _P>,
Red: Bitmap<Pixel = _S>,
Green: Bitmap<Pixel = _S>,
Blue: Bitmap<Pixel = _S>,
Alpha: Bitmap<Pixel = _S>,
>(
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
_primary: (),
#[implementations(ImageFrameTable<Color>)] red: Red,
#[implementations(ImageFrameTable<Color>)] green: Green,
#[implementations(ImageFrameTable<Color>)] blue: Blue,
#[implementations(ImageFrameTable<Color>)] alpha: Alpha,
) -> Input
) -> ImageFrameTable<Color>
where
_P::ColorChannel: Linear,
_I: Pixel + Luminance,
Red: Bitmap<Pixel = _I>,
Green: Bitmap<Pixel = _I>,
Blue: Bitmap<Pixel = _I>,
Alpha: Bitmap<Pixel = _I>,
{
let dimensions = [red.dim(), green.dim(), blue.dim(), alpha.dim()];
if dimensions.iter().all(|&(x, _)| x == 0) {
return image;
if dimensions.iter().any(|&(x, y)| x == 0 || y == 0) || dimensions.iter().any(|&(x, y)| dimensions.iter().any(|&(other_x, other_y)| x != other_x || y != other_y)) {
return ImageFrameTable::one_empty_image();
}
if dimensions.iter().any(|&(x, y)| x != image.width() || y != image.height()) {
log::warn!("Stencil and image have different sizes. This is not supported.");
return image;
}
let mut image = Image::new(red.width(), red.height(), Color::TRANSPARENT);
for y in 0..image.height() {
for x in 0..image.width() {
@ -198,26 +130,30 @@ where
}
}
image
ImageFrameTable::new(image)
}
#[node_macro::node()]
fn mask_image<
// _P is the color of the input image. It must have an alpha channel because that is going to
// be modified by the mask
#[node_macro::node(category("Raster"))]
fn mask<_P, _S, Input, Stencil>(
_: impl Ctx,
/// The image to be masked.
#[implementations(ImageFrameTable<Color>)]
mut image: Input,
/// The stencil to be used for masking.
#[implementations(ImageFrameTable<Color>)]
#[expose]
stencil: Stencil,
) -> Input
where
// _P is the color of the input image. It must have an alpha channel because that is going to be modified by the mask.
_P: Alpha,
// _S is the color of the stencil. It must have a luminance channel because that is used to
// mask the input image
// _S is the color of the stencil. It must have a luminance channel because that is used to mask the input image.
_S: Luminance,
// Input image
Input: Transform + BitmapMut<Pixel = _P>,
// Stencil
Stencil: Transform + Sample<Pixel = _S>,
>(
_: impl Ctx,
#[implementations(ImageFrameTable<Color>)] mut image: Input,
#[implementations(ImageFrameTable<Color>)] stencil: Stencil,
) -> Input {
{
let image_size = DVec2::new(image.width() as f64, image.height() as f64);
let mask_size = stencil.transform().decompose_scale();
@ -313,13 +249,8 @@ where
background
}
#[derive(Debug, Clone, Copy)]
pub struct ExtendImageToBoundsNode<Bounds> {
bounds: Bounds,
}
#[node_macro::old_node_fn(ExtendImageToBoundsNode)]
fn extend_image_to_bounds(image: ImageFrameTable<Color>, bounds: DAffine2) -> ImageFrameTable<Color> {
#[node_macro::node(category(""))]
fn extend_image_to_bounds(_: impl Ctx, image: ImageFrameTable<Color>, bounds: DAffine2) -> ImageFrameTable<Color> {
let image_aabb = Bbox::unit().affine_transform(image.transform()).to_axis_aligned_bbox();
let bounds_aabb = Bbox::unit().affine_transform(bounds.transform()).to_axis_aligned_bbox();
if image_aabb.contains(bounds_aabb.start) && image_aabb.contains(bounds_aabb.end) {

View file

@ -2,19 +2,17 @@ use dyn_any::StaticType;
use glam::{DVec2, UVec2};
use graph_craft::document::value::RenderOutput;
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_core::fn_type;
use graphene_core::raster::color::Color;
use graphene_core::raster::image::ImageFrameTable;
use graphene_core::raster::*;
use graphene_core::value::{ClonedNode, ValueNode};
use graphene_core::vector::VectorDataTable;
use graphene_core::{Artboard, GraphicGroupTable, concrete, generic};
use graphene_core::{Cow, ProtoNodeIdentifier, Type};
use graphene_core::{Node, NodeIO, NodeIOTypes};
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, FutureWrapperNode, IntoTypeErasedNode};
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, IntoTypeErasedNode};
use graphene_std::application_io::ImageTexture;
use graphene_std::wasm_application_io::*;
use node_registry_macros::{async_node, into_node};
@ -22,7 +20,7 @@ use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(feature = "gpu")]
use wgpu_executor::{ShaderInputFrame, WgpuExecutor};
use wgpu_executor::ShaderInputFrame;
use wgpu_executor::{WgpuSurface, WindowHandle};
// TODO: turn into hashmap
@ -91,37 +89,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => WgpuSurface]),
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => Option<WgpuSurface>]),
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => ImageTexture]),
// Filters
// TODO: Move these filters to the new node macro and put them in `graphene_core::raster::adjustments`, then add them to the document upgrade script which moves many of the adjustment nodes from `graphene_core::raster` to `graphene_core::raster::adjustments`
(
ProtoNodeIdentifier::new("graphene_core::raster::BrightnessContrastNode"),
|args| {
Box::pin(async move {
use graphene_core::raster::brightness_contrast::*;
let brightness: DowncastBothNode<(), f64> = DowncastBothNode::new(args[0].clone());
let brightness = ClonedNode::new(brightness.eval(()).await);
let contrast: DowncastBothNode<(), f64> = DowncastBothNode::new(args[1].clone());
let contrast = ClonedNode::new(contrast.eval(()).await);
let use_legacy: DowncastBothNode<(), bool> = DowncastBothNode::new(args[2].clone());
if use_legacy.eval(()).await {
let generate_brightness_contrast_legacy_mapper_node = GenerateBrightnessContrastLegacyMapperNode::new(brightness, contrast);
let map_image_frame_node = graphene_std::raster::MapImageNode::new(ValueNode::new(generate_brightness_contrast_legacy_mapper_node.eval(())));
let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
let any: DynAnyNode<ImageFrameTable<Color>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
any.into_type_erased()
} else {
let generate_brightness_contrast_mapper_node = GenerateBrightnessContrastMapperNode::new(brightness, contrast);
let map_image_frame_node = graphene_std::raster::MapImageNode::new(ValueNode::new(generate_brightness_contrast_mapper_node.eval(())));
let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
let any: DynAnyNode<ImageFrameTable<Color>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
any.into_type_erased()
}
})
},
NodeIOTypes::new(concrete!(ImageFrameTable<Color>), concrete!(ImageFrameTable<Color>), vec![fn_type!(f64), fn_type!(f64), fn_type!(bool)]),
),
(
ProtoNodeIdentifier::new("graphene_core::structural::ComposeNode"),
|args| {
@ -145,26 +112,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => ShaderInputFrame]),
#[cfg(feature = "gpu")]
into_node!(from: &WasmEditorApi, to: &WgpuExecutor),
#[cfg(feature = "gpu")]
(
ProtoNodeIdentifier::new("graphene_std::executor::MapGpuSingleImageNode"),
|args| {
Box::pin(async move {
let document_node: DowncastBothNode<(), graph_craft::document::DocumentNode> = DowncastBothNode::new(args[0].clone());
let editor_api: DowncastBothNode<(), &WasmEditorApi> = DowncastBothNode::new(args[1].clone());
let node = graphene_std::gpu_nodes::MapGpuNode::new(document_node, editor_api);
let any: DynAnyNode<ImageFrameTable<Color>, _, _> = graphene_std::any::DynAnyNode::new(node);
any.into_type_erased()
})
},
NodeIOTypes::new(
concrete!(ImageFrameTable<Color>),
concrete!(ImageFrameTable<Color>),
vec![fn_type!(graph_craft::document::DocumentNode), fn_type!(WasmEditorApi)],
),
),
#[cfg(feature = "gpu")]
(
ProtoNodeIdentifier::new(stringify!(wgpu_executor::CreateGpuSurfaceNode<_>)),
|args| {

View file

@ -1,80 +0,0 @@
//! This has all been copied out of node_registry.rs to avoid leaving many lines of commented out code in that file. It's left here instead for future reference.
// (
// ProtoNodeIdentifier::new("graphene_core::raster::CurvesNode"),
// |args| {
// use graphene_core::raster::curve::Curve;
// use graphene_core::raster::GenerateCurvesNode;
// let curve: DowncastBothNode<(), Curve> = DowncastBothNode::new(args[0].clone());
// Box::pin(async move {
// let curve = ClonedNode::new(curve.eval(()).await);
// let generate_curves_node = GenerateCurvesNode::new(curve, ClonedNode::new(0_f32));
// let map_image_frame_node = graphene_std::raster::MapImageNode::new(ValueNode::new(generate_curves_node.eval(())));
// let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
// let any: DynAnyNode<ImageFrameTable<Luma>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
// any.into_type_erased()
// })
// },
// NodeIOTypes::new(concrete!(ImageFrameTable<Luma>), concrete!(ImageFrameTable<Luma>), vec![fn_type!(graphene_core::raster::curve::Curve)]),
// ),
// TODO: Use channel split and merge for this instead of using LuminanceMut for the whole color.
// (
// ProtoNodeIdentifier::new("graphene_core::raster::CurvesNode"),
// |args| {
// use graphene_core::raster::curve::Curve;
// use graphene_core::raster::GenerateCurvesNode;
// let curve: DowncastBothNode<(), Curve> = DowncastBothNode::new(args[0].clone());
// Box::pin(async move {
// let curve = ValueNode::new(ClonedNode::new(curve.eval(()).await));
// let generate_curves_node = GenerateCurvesNode::new(FutureWrapperNode::new(curve), FutureWrapperNode::new(ClonedNode::new(0_f32)));
// let map_image_frame_node = graphene_std::raster::MapImageNode::new(FutureWrapperNode::new(ValueNode::new(generate_curves_node.eval(()))));
// let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
// let any: DynAnyNode<ImageFrameTable<Color>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
// any.into_type_erased()
// })
// },
// NodeIOTypes::new(
// concrete!(ImageFrameTable<Color>),
// concrete!(ImageFrameTable<Color>),
// vec![fn_type!(graphene_core::raster::curve::Curve)],
// ),
// ),
// (
// ProtoNodeIdentifier::new("graphene_std::raster::ImaginateNode"),
// |args: Vec<graph_craft::proto::SharedNodeContainer>| {
// Box::pin(async move {
// use graphene_std::raster::ImaginateNode;
// macro_rules! instantiate_imaginate_node {
// ($($i:expr,)*) => { ImaginateNode::new($(graphene_std::any::input_node(args[$i].clone()),)* ) };
// }
// let node: ImaginateNode<Color, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _> = instantiate_imaginate_node!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,);
// let any = graphene_std::any::DynAnyNode::new(node);
// any.into_type_erased()
// })
// },
// NodeIOTypes::new(
// concrete!(ImageFrameTable<Color>),
// concrete!(ImageFrameTable<Color>),
// vec![
// fn_type!(&WasmEditorApi),
// fn_type!(ImaginateController),
// fn_type!(f64),
// fn_type!(Option<DVec2>),
// fn_type!(u32),
// fn_type!(ImaginateSamplingMethod),
// fn_type!(f64),
// fn_type!(String),
// fn_type!(String),
// fn_type!(bool),
// fn_type!(f64),
// fn_type!(bool),
// fn_type!(f64),
// fn_type!(ImaginateMaskStartingFill),
// fn_type!(bool),
// fn_type!(bool),
// fn_type!(u64),
// ],
// ),
// ),

View file

@ -1,19 +1,20 @@
// TODO: Deprecate and remove this file
use proc_macro::TokenStream;
use proc_macro_error2::proc_macro_error;
use proc_macro2::Span;
use quote::{ToTokens, format_ident, quote};
use syn::{
AngleBracketedGenericArguments, AssocType, FnArg, GenericArgument, GenericParam, Ident, ItemFn, Lifetime, Pat, PatIdent, PathArguments, PathSegment, PredicateType, ReturnType, Token, TraitBound,
Type, TypeImplTrait, TypeParam, TypeParamBound, TypeTuple, WhereClause, WherePredicate, parse_macro_input, punctuated::Punctuated, token::Comma,
};
use syn::GenericParam;
mod codegen;
mod derive_choice_type;
mod parsing;
mod validation;
/// Used to create a node definition.
#[proc_macro_error]
#[proc_macro_attribute]
pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream {
// Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct
parsing::new_node_fn(attr.into(), item.into()).into()
}
/// Generate meta-information for an enum.
///
/// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported.
@ -27,429 +28,3 @@ mod validation;
pub fn derive_choice_type(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// A macro used to construct a proto node implementation from the given struct and the decorated function.
///
/// This works by generating two `impl` blocks for the given struct:
///
/// - `impl TheGivenStruct`:
/// Attaches a `new` constructor method to the struct.
/// - `impl Node for TheGivenStruct`:
/// Implements the [`Node`] trait for the struct, with the `eval` method inside which is a modified version of the decorated function. See below for how the function is modified.
///
/// # Usage of this and similar macros
///
/// You'll use this macro most commonly when writing proto nodes. It's a convenient combination of the [`node_new`] and [`node_impl`] proc macros, which handles both of the bullet points above, respectively. There can only be one constructor method, but additional functions decorated by the [`node_impl`] macro can be added to implement different functionality across multiple type signatures.
///
/// # Useful hint
///
/// It can be helpful to run the "rust-analyzer: Expand macro recursively at carat" command from the VS Code command palette (or your editor's equivalent) to see the generated code of the macro to understand how the translation magic works.
///
/// # How generics and type signatures are handled
///
/// The given struct has various fields, each of them generic. These correspond with the node's parameters (the secondary inputs, but not the primary input). We can implement multiple functions with different type signatures, each each of these are converted by the [`node_impl`] macro into separate `impl` blocks for different `Node` traits.
///
/// ## Type signature translation
///
/// The conversion into an `impl Node` corresponding with the decorated function's type signature involves:
///
/// - Mapping the type of the function's first argument (the node's primary input) to the impl'd `Node`'s generic type, e.g.:
///
/// ```ignore
/// Node<'input, Color>
/// ```
///
/// for a `Color` primary input type.
/// - Mapping the type of the function's remaining arguments (the node's secondary inputs) to the given struct fields' generic types, e.g.:
///
/// ```ignore
/// TheGivenStruct<S0, S1>
/// where S0: Node<'input, (), Output = f64>,
/// where S1: Node<'input, (), Output = f64>,
/// ```
///
/// for two `f64` secondary input types. Since Graphene works by having each function evaluate its upstream node as a lambda that returns output data, these secondary inputs are not directly `f64` values but rather `Node`s that output `f64` values when evaluated (in this case, with an empty input of `()`).
/// - Mapping the function's return type to the impl'd `Node` trait's associated type, e.g.:
///
/// ```ignore
/// Output = Color
/// ```
///
/// for a `Color` secondary output type.
///
/// ## `eval()` method generation
///
/// The conversion of the decorated function's body into the `eval` method within the `impl Node` block involves the following steps:
///
/// - The function's body gets copied over to the interior of the `eval` method.
/// - The function's argument list only has its first argument (the node's primary input) copied over to the `eval` function signature. The remaining arguments (the node's secondary inputs) are not copied over as `eval` function arguments.
/// - A series of `let` declarations are added before the copied-over function body, one for each secondary input. They look like `let secondaryA: SomeOutputType = self.secondaryA.eval(someInput);`. Each one is calling the `eval()` method on its corresponding struct field, obtaining the evaluated value of that secondary input node that gets used in the function body in the lines below these `let` declarations.
///
/// This process is necessary because the arguments in the original decorated function don't really exist with the actual values. Instead, they live as fields in the struct and they are `Node`s that output the actual values only once evaluated. So with the magic performed by this macro, the function body can written pretending to be working with the actual secondary input values, but the real types are `impl Node<SomeInputType, Output = SomeOutputType>` and they live in `self` as struct fields.
///
/// The function body runs with the actual primary input value from the `eval` method's argument and the secondary input values from the `eval` method's `let` declarations. The result looks like this:
///
/// ```ignore
/// fn eval(&'input self, color: Color) -> Self::Output {
/// let secondaryA = self.secondaryA.eval(());
/// let secondaryB = self.secondaryB.eval(());
/// {
/// Color::from_rgbaf32_unchecked(
/// color.r() / secondaryA,
/// color.g() / secondaryA,
/// color.b() / secondaryA,
/// color.a() * secondaryB,
/// )
/// }
/// }
/// ```
///
/// There is one exception where a `let` declaration is not added if an opt-out is desired. Any argument given to the decorated function may be of type `impl Node<SomeInputType, Output = SomeOutputType>` which will tell the macro not to add a `let` declaration for that argument. This allows for manually calling `eval` on the struct field in the function body, like `self.secondaryA.eval(())`.
///
/// When a `let` declaration is generated automatically, this is called **automatic composition**. When opting out, this is called **manual composition**.
#[proc_macro_attribute]
pub fn old_node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
// Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct
let node_impl = node_impl_proxy(attr.clone(), item.clone());
// Performs the `node_new` macro's functionality of attaching a `new` constructor method to the node struct
let mut new_constructor = node_new_impl(attr, item);
// Combines the two pieces of Rust source code
new_constructor.extend(node_impl);
new_constructor
}
#[proc_macro_error]
#[proc_macro_attribute]
pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream {
// Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct
parsing::new_node_fn(attr.into(), item.into()).into()
}
/// Attaches an `impl TheGivenStruct` block to the node struct, containing a `new` constructor method. This is almost always called by the combined [`node_fn`] macro instead of using this one, however it can be used separately if needed. See that macro's documentation for more information.
#[proc_macro_attribute]
pub fn old_node_new(attr: TokenStream, item: TokenStream) -> TokenStream {
node_new_impl(attr, item)
}
/// Attaches an `impl Node for TheGivenStruct` block to the node struct, containing an implementation of the node's `eval` method for a certain type signature. This can be called with multiple separate functions each having different type signatures. The [`node_fn`] macro calls this macro as well as defining a `new` constructor method on the node struct, which is a necessary part of defining a proto node; therefore you will most likely call that macro on the first decorated function and this macro on any additional decorated functions to provide additional type signatures for the proto node. See that macro's documentation for more information.
#[proc_macro_attribute]
pub fn old_node_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
node_impl_proxy(attr, item)
}
fn node_new_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
let node = parse_macro_input!(attr as syn::PathSegment);
let function = parse_macro_input!(item as ItemFn);
let node = &node;
let node_name = &node.ident;
let mut args = node_args(node);
let arg_idents = args
.iter()
.filter(|x| x.to_token_stream().to_string().starts_with('_'))
.map(|arg| Ident::new(arg.to_token_stream().to_string().to_lowercase().as_str(), Span::call_site()))
.collect::<Vec<_>>();
let (_, _, parameter_pat_ident_patterns) = parse_inputs(&function, false);
let parameter_idents = parameter_pat_ident_patterns.iter().map(|pat_ident| &pat_ident.ident).collect::<Vec<_>>();
// Extract the output type of the entire node - `()` by default
let struct_generics = (0..parameter_pat_ident_patterns.len())
.map(|x| {
let ident = format_ident!("S{x}");
ident
})
.collect::<Punctuated<_, Comma>>();
for ident in struct_generics.iter() {
args.push(Type::Verbatim(quote::quote!(#ident)));
}
let function_attributes = &function.attrs;
let struct_generics_iter = struct_generics.iter();
quote::quote! {
#[automatically_derived]
impl <#(#args),*> #node_name<#(#args),*>
{
#(#function_attributes)*
pub const fn new(#(#parameter_idents: #struct_generics_iter),*) -> Self{
Self {
#(#parameter_idents,)*
#(#arg_idents: core::marker::PhantomData,)*
}
}
}
}
.into()
}
fn node_args(node: &syn::PathSegment) -> Vec<Type> {
match node.arguments.clone() {
PathArguments::AngleBracketed(args) => args
.args
.into_iter()
.map(|arg| match arg {
syn::GenericArgument::Type(ty) => ty,
_ => panic!("Only types are allowed as arguments"),
})
.collect::<Vec<_>>(),
_ => Default::default(),
}
}
fn node_impl_proxy(attr: TokenStream, item: TokenStream) -> TokenStream {
let fn_item = item.clone();
let function = parse_macro_input!(fn_item as ItemFn);
if function.sig.asyncness.is_some() {
node_impl_impl(attr, item, Asyncness::AllAsync)
} else {
node_impl_impl(attr, item, Asyncness::Sync)
}
}
enum Asyncness {
Sync,
AllAsync,
}
fn node_impl_impl(attr: TokenStream, item: TokenStream, asyncness: Asyncness) -> TokenStream {
// let node_name = parse_macro_input!(attr as Ident);
let node = parse_macro_input!(attr as syn::PathSegment);
let function = parse_macro_input!(item as ItemFn);
let node = &node;
let node_name = &node.ident;
let mut args = node_args(node);
let async_out = match asyncness {
Asyncness::Sync => false,
Asyncness::AllAsync => true,
};
let async_in = matches!(asyncness, Asyncness::AllAsync);
let body = &function.block;
let mut type_generics = function.sig.generics.params.clone();
let mut where_clause = function.sig.generics.where_clause.clone().unwrap_or(WhereClause {
where_token: Token![where](Span::call_site()),
predicates: Default::default(),
});
type_generics.iter_mut().for_each(|x| {
if let GenericParam::Type(t) = x {
t.bounds.insert(0, TypeParamBound::Lifetime(Lifetime::new("'input", Span::call_site())));
}
});
let (primary_input, parameter_inputs, parameter_pat_ident_patterns) = parse_inputs(&function, true);
let primary_input_ty = &primary_input.ty;
let Pat::Ident(PatIdent {
ident: primary_input_ident,
mutability: primary_input_mutability,
..
}) = &*primary_input.pat
else {
panic!("Expected ident as primary input.");
};
// Extract the output type of the entire node - `()` by default
let output = if let ReturnType::Type(_, ty) = &function.sig.output {
ty.to_token_stream()
} else {
quote::quote!(())
};
let num_inputs = parameter_inputs.len();
let struct_generics = (0..num_inputs).map(|x| format_ident!("S{x}")).collect::<Vec<_>>();
let parameter_types = parameter_inputs.iter().map(|x| *x.ty.clone()).collect::<Vec<Type>>();
for ident in struct_generics.iter() {
args.push(Type::Verbatim(quote::quote!(#ident)));
}
// Generics are simply `S0` through to `Sn-1` where n is the number of secondary inputs
let node_generics = construct_node_generics(&struct_generics, true);
let generics = type_generics.into_iter().chain(node_generics.iter().cloned()).collect::<Punctuated<_, Comma>>();
// Bindings for all of the above generics to a node with an input of `()` and an output of the type in the function
let node_bounds = if async_in {
input_node_bounds(parameter_types, node_generics, |lifetime, in_ty, out_ty| {
quote! {
Node<'any_input, #in_ty, Output: core::future::Future<Output = #out_ty> + ::dyn_any::WasmNotSend > + ::dyn_any::WasmNotSend + #lifetime
}
})
} else {
input_node_bounds(parameter_types, node_generics, |lifetime, in_ty, out_ty| quote! {Node<#lifetime, #in_ty, Output = #out_ty>})
};
where_clause.predicates.extend(node_bounds);
let output = if async_out {
quote::quote!( ::dyn_any::DynFuture<'input, #output>)
} else {
quote::quote!(#output)
};
let parameter_idents = parameter_pat_ident_patterns.iter().map(|pat_ident| &pat_ident.ident).collect::<Vec<_>>();
let parameter_mutability = parameter_pat_ident_patterns.iter().map(|pat_ident| &pat_ident.mutability).collect::<Vec<_>>();
let futures = quote::quote!(#(let #parameter_mutability #parameter_idents = self.#parameter_idents.eval(());)*);
let parameters = if matches!(asyncness, Asyncness::AllAsync) {
quote::quote!(#(let #parameter_mutability #parameter_idents = #parameter_idents.await;)*)
} else {
quote::quote!(#(let #parameter_mutability #parameter_idents = #parameter_idents;)*)
};
let mut body_with_inputs = quote::quote!(
#futures
#body
);
if async_out && !body.to_token_stream().to_string().contains("async") {
body_with_inputs = quote::quote!(
#futures
Box::pin(async move { #parameters #body })
);
}
quote::quote! {
#[automatically_derived]
impl <'input, #generics> Node<'input, #primary_input_ty> for #node_name<#(#args),*>
#where_clause
{
type Output = #output;
#[inline]
fn eval(&'input self, #primary_input_mutability #primary_input_ident: #primary_input_ty) -> Self::Output {
#body_with_inputs
}
}
}
.into()
}
fn parse_inputs(function: &ItemFn, remove_impl_node: bool) -> (&syn::PatType, Vec<&syn::PatType>, Vec<&PatIdent>) {
let mut function_inputs = function.sig.inputs.iter().filter_map(|arg| if let FnArg::Typed(typed_arg) = arg { Some(typed_arg) } else { None });
// Extract primary input as first argument
let primary_input = function_inputs.next().expect("Primary input required - set to `()` if not needed.");
// Extract secondary inputs as all other arguments
let parameter_inputs = function_inputs.collect::<Vec<_>>();
let parameter_pat_ident_patterns = parameter_inputs
.iter()
.filter(|input| !matches!(&*input.ty, Type::ImplTrait(_)) || !remove_impl_node)
.map(|input| {
let Pat::Ident(pat_ident) = &*input.pat else {
panic!("Expected ident for secondary input.");
};
pat_ident
})
.collect::<Vec<_>>();
(primary_input, parameter_inputs, parameter_pat_ident_patterns)
}
fn path(elements: &[&str]) -> syn::Path {
syn::Path {
leading_colon: None,
segments: Punctuated::from_iter(elements.iter().map(|element| PathSegment {
ident: Ident::new(element, Span::mixed_site()),
arguments: PathArguments::None,
})),
}
}
fn construct_node_generics(struct_generics: &[Ident], add_sync: bool) -> Vec<GenericParam> {
let mut bounds = vec![
TypeParamBound::Lifetime(Lifetime::new("'input", Span::call_site())),
TypeParamBound::Trait(TraitBound {
paren_token: None,
modifier: syn::TraitBoundModifier::None,
lifetimes: None,
path: path(&["dyn_any", "WasmNotSend"]),
}),
];
if add_sync {
bounds.push(TypeParamBound::Trait(TraitBound {
paren_token: None,
modifier: syn::TraitBoundModifier::None,
lifetimes: None,
path: path(&["dyn_any", "WasmNotSync"]),
}))
};
struct_generics
.iter()
.cloned()
.map(|ident| {
GenericParam::Type(TypeParam {
attrs: vec![],
ident,
colon_token: Some(Default::default()),
bounds: Punctuated::from_iter(bounds.clone()),
eq_token: None,
default: None,
})
})
.collect()
}
fn input_node_bounds(parameter_inputs: Vec<Type>, node_generics: Vec<GenericParam>, trait_bound: impl Fn(Lifetime, Type, Type) -> proc_macro2::TokenStream) -> Vec<WherePredicate> {
parameter_inputs
.iter()
.zip(&node_generics)
.map(|(ty, name)| {
let GenericParam::Type(generic_ty) = name else {
panic!("Expected type generic.");
};
let ident = &generic_ty.ident;
let (lifetime, in_ty, out_ty) = match ty.clone() {
Type::ImplTrait(TypeImplTrait { bounds, .. }) if bounds.len() == 1 => {
let TypeParamBound::Trait(TraitBound { ref path, .. }) = bounds[0] else {
panic!("impl Traits other then Node are not supported")
};
let node_segment = path.segments.last().expect("Found an empty path in the impl Trait arg");
assert_eq!(node_segment.ident.to_string(), "Node", "Only impl Node is supported as an argument");
let PathArguments::AngleBracketed(AngleBracketedGenericArguments { ref args, .. }) = node_segment.arguments else {
panic!("Node must have generic arguments")
};
let mut args_iter = args.iter();
let lifetime = if args.len() == 2 {
Lifetime::new("'input", Span::call_site())
} else if let Some(GenericArgument::Lifetime(node_lifetime)) = args_iter.next() {
node_lifetime.clone()
} else {
panic!("Invalid arguments for Node trait")
};
let Some(GenericArgument::Type(in_ty)) = args_iter.next() else {
panic!("Expected type argument in Node<> declaration")
};
let Some(GenericArgument::AssocType(AssocType { ty: out_ty, .. })) = args_iter.next() else {
panic!("Expected Output = in Node declaration")
};
(lifetime, in_ty.clone(), out_ty.clone())
}
ty => (
Lifetime::new("'input", Span::call_site()),
Type::Tuple(TypeTuple {
paren_token: syn::token::Paren(Span::call_site()),
elems: Punctuated::new(),
}),
ty,
),
};
let bound = trait_bound(lifetime, in_ty, out_ty);
WherePredicate::Type(PredicateType {
lifetimes: Some(syn::parse_quote!(for<'any_input>)),
bounded_ty: Type::Verbatim(ident.to_token_stream()),
colon_token: Default::default(),
bounds: syn::parse_quote!(#bound),
})
})
.collect()
}

View file

@ -27,7 +27,7 @@ use proc_macro::TokenStream;
///
/// # Helper attributes
/// - `#[sub_discriminant]`: only usable on variants with a single field; instead of no fields, the discriminant of the single field will be included in the discriminant,
/// acting as a sub-discriminant.
/// acting as a sub-discriminant.
/// - `#[discriminant_attr(…)]`: usable on the enum itself or on any variant; applies `#[…]` in its place on the discriminant.
///
/// # Attributes on the Discriminant
@ -76,7 +76,7 @@ pub fn derive_discriminant(input_item: TokenStream) -> TokenStream {
///
/// # Helper Attributes
/// - `#[parent(<Type>, <Expr>)]` (**required**): declare the parent type (`<Type>`)
/// and a function (`<Expr>`, has to evaluate to a single arg function) for converting a value of this type to the parent type
/// and a function (`<Expr>`, has to evaluate to a single arg function) for converting a value of this type to the parent type
/// - `#[parent_is_top]`: Denote that the parent type has no further parent type (this is required because otherwise the `From` impls for parent and top parent would overlap)
///
/// # Example
@ -198,18 +198,18 @@ pub fn derive_message(input_item: TokenStream) -> TokenStream {
/// # Usage
/// There are three possible argument syntaxes you can use:
/// 1. no arguments: this is for the top-level message enum. It derives `ToDiscriminant`, `AsMessage` on the discriminant, and implements `TransitiveChild` on both
/// (the parent and top parent being the respective types themselves).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// (the parent and top parent being the respective types themselves).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// 2. two arguments: this is for message enums whose direct parent is the top level message enum. The syntax is `#[impl_message(<Type>, <Ident>)]`,
/// where `<Type>` is the parent message type and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both (adding `#[parent_is_top]` to both).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// where `<Type>` is the parent message type and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both (adding `#[parent_is_top]` to both).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// 3. three arguments: this is for all other message enums that are transitive children of the top level message enum. The syntax is
/// `#[impl_message(<Type>, <Type>, <Ident>)]`, where the first `<Type>` is the top parent message type, the second `<Type>` is the parent message type
/// and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both.
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// **This third option will likely change in the future**
/// `#[impl_message(<Type>, <Type>, <Ident>)]`, where the first `<Type>` is the top parent message type, the second `<Type>` is the parent message type
/// and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both.
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// **This third option will likely change in the future**
#[proc_macro_attribute]
pub fn impl_message(attr: TokenStream, input_item: TokenStream) -> TokenStream {
TokenStream::from(combined_message_attrs_impl(attr.into(), input_item.into()).unwrap_or_else(|err| err.to_compile_error()))