Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use graphene_std::list::{Item, List, NodeIdPath};
use graphene_std::math::float_noise::round_away_float_noise;
use graphene_std::memo::IORecord;
use graphene_std::raster::{
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice,
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DesaturateMethod, DomainWarpType, FractalType, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
TonalRange,
};
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::text::TextAlign;
Expand Down Expand Up @@ -238,11 +238,12 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
List<ExtrudeJoiningAlgorithm>,
List<PointSpacingType>,
List<StringCapitalization>,
List<LuminanceCalculation>,
List<DesaturateMethod>,
List<RedGreenBlue>,
List<RedGreenBlueAlpha>,
List<RelativeAbsolute>,
List<SelectiveColorChoice>,
List<TonalRange>,
List<AdjustmentChannel>,
List<XY>,
List<ScaleType>,
Expand Down Expand Up @@ -295,11 +296,12 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
Item<ExtrudeJoiningAlgorithm>,
Item<PointSpacingType>,
Item<StringCapitalization>,
Item<LuminanceCalculation>,
Item<DesaturateMethod>,
Item<RedGreenBlue>,
Item<RedGreenBlueAlpha>,
Item<RelativeAbsolute>,
Item<SelectiveColorChoice>,
Item<TonalRange>,
Item<AdjustmentChannel>,
Item<XY>,
Item<ScaleType>,
Expand Down Expand Up @@ -1065,11 +1067,12 @@ impl_table_item_layout_for_choice_enum!(
ExtrudeJoiningAlgorithm,
PointSpacingType,
StringCapitalization,
LuminanceCalculation,
DesaturateMethod,
RedGreenBlue,
RedGreenBlueAlpha,
RelativeAbsolute,
SelectiveColorChoice,
TonalRange,
AdjustmentChannel,
XY,
ScaleType,
Expand Down Expand Up @@ -1284,11 +1287,12 @@ macro_rules! known_item_types {
ExtrudeJoiningAlgorithm,
PointSpacingType,
StringCapitalization,
LuminanceCalculation,
DesaturateMethod,
RedGreenBlue,
RedGreenBlueAlpha,
RelativeAbsolute,
SelectiveColorChoice,
TonalRange,
AdjustmentChannel,
XY,
ScaleType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use graphene_std::animation::RealTimeMode;
use graphene_std::color::SRGBA8;
use graphene_std::extract_xy::XY;
use graphene_std::raster::{
AdjustmentChannel, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
AdjustmentChannel, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DesaturateMethod, DomainWarpType, FractalType, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice, TonalRange,
};
use graphene_std::raster_types::Image;
Expand Down Expand Up @@ -332,7 +332,7 @@ pub(crate) fn property_from_type(
Some(x) if id_is::<PointSpacingType>(x) => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if id_is::<BooleanOperation>(x) => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if id_is::<CentroidType>(x) => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
Some(x) if id_is::<LuminanceCalculation>(x) => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
Some(x) if id_is::<DesaturateMethod>(x) => enum_choice::<DesaturateMethod>().for_socket(default_info).property_row(),
Some(x) if id_is::<QRCodeErrorCorrectionLevel>(x) => enum_choice::<QRCodeErrorCorrectionLevel>().for_socket(default_info).property_row(),
Some(x) if id_is::<ScaleType>(x) => enum_choice::<ScaleType>().for_socket(default_info).property_row(),
Some(x) if id_is::<InterpolationDistribution>(x) => enum_choice::<InterpolationDistribution>().for_socket(default_info).property_row(),
Expand Down
3 changes: 2 additions & 1 deletion editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,9 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
],
},
NodeReplacement {
node: graphene_std::raster_nodes::adjustments::luminance::IDENTIFIER,
node: graphene_std::raster_nodes::adjustments::desaturate::IDENTIFIER,
aliases: &[
"raster_nodes::adjustments::LuminanceNode",
"graphene_raster_nodes::adjustments::LuminanceNode",
"graphene_core::raster::adjustments::LuminanceNode",
"graphene_core::raster::LuminanceNode",
Expand Down
3 changes: 2 additions & 1 deletion node-graph/graph-craft/src/document/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,8 @@ tagged_value! {
// ENUM TYPES
// ==========
BlendMode(core_types::blending::BlendMode),
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
#[serde(alias = "LuminanceCalculation")]
DesaturateMethod(raster_nodes::adjustments::DesaturateMethod),
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
XY(graphene_core::extract_xy::XY),
StringCapitalization(text_nodes::StringCapitalization),
Expand Down
2 changes: 1 addition & 1 deletion node-graph/interpreted-executor/src/node_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
ExtrudeJoiningAlgorithm,
PointSpacingType,
StringCapitalization,
LuminanceCalculation,
DesaturateMethod,
RedGreenBlue,
RedGreenBlueAlpha,
RelativeAbsolute,
Expand Down
33 changes: 20 additions & 13 deletions node-graph/libraries/no-std-types/src/color/color_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ impl Luminance for RGBA16F {
type LuminanceChannel = f32;
#[inline(always)]
fn luminance(&self) -> f32 {
// TODO: verify this is correct for sRGB
0.2126 * self.red() + 0.7152 * self.green() + 0.0722 * self.blue()
}
}
Expand Down Expand Up @@ -541,37 +540,36 @@ impl Color {
}

/// Relative luminance using Rec.709 / sRGB-primary weights, computed on linear-light RGB.
// From https://stackoverflow.com/a/56678483/775283
#[inline(always)]
pub fn luminance_rec_709(&self) -> f32 {
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue
}

/// Luma using Rec.601 SDTV coefficients.
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
#[inline(always)]
pub fn luminance_rec_601(&self) -> f32 {
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
0.299 * self.red + 0.587 * self.green + 0.114 * self.blue
}

/// Luma using rounded Rec.601 coefficients (`0.3 / 0.59 / 0.11`), as used by some legacy image processing.
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
#[inline(always)]
pub fn luminance_rec_601_rounded(&self) -> f32 {
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
0.3 * self.red + 0.59 * self.green + 0.11 * self.blue
}

/// Perceptual lightness (CIE L*) of the Rec.709 luminance, normalized to 0..1.
// From https://stackoverflow.com/a/56678483/775283
/// Perceptual lightness (OkLab L) of the linear-light RGB, 0..1.
Comment thread
Keavon marked this conversation as resolved.
#[inline(always)]
pub fn luminance_perceptual(&self) -> f32 {
let luminance = self.luminance_rec_709();
pub fn lightness_oklab(&self) -> f32 {
// From https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab

if luminance <= 0.008856 {
(luminance * 903.3) / 100.
} else {
(luminance.cbrt() * 116. - 16.) / 100.
}
let long = 0.41222147 * self.red + 0.53633254 * self.green + 0.05144599 * self.blue;
let medium = 0.2119035 * self.red + 0.6806995 * self.green + 0.10739696 * self.blue;
let short = 0.08830246 * self.red + 0.28171884 * self.green + 0.6299787 * self.blue;

0.21045426 * long.cbrt() + 0.7936178 * medium.cbrt() - 0.004072047 * short.cbrt()
}

/// Construct an opaque grayscale color where R = G = B = `luminance`.
Expand Down Expand Up @@ -1062,6 +1060,15 @@ impl Color {
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn oklab_lightness_spans_black_to_white() {
assert!(Color::BLACK.lightness_oklab().abs() < 1e-4);
assert!((Color::WHITE.lightness_oklab() - 1.).abs() < 1e-4);
// A gray keeps L at the cube root of its linear value, since the three cone responses sum to it
assert!((Color::from_luminance(0.18).lightness_oklab() - 0.18_f32.cbrt()).abs() < 1e-3);
}

#[test]
fn hsl_roundtrip() {
for (red, green, blue) in [
Expand Down
84 changes: 66 additions & 18 deletions node-graph/nodes/raster/src/adjustments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,52 @@ use vector_types::Gradient;
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27clrL%27%20%3D%20Color%20Lookup
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6

/// Conversion from a color to grayscale.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)]
#[widget(Dropdown)]
#[repr(u32)]
pub enum LuminanceCalculation {
pub enum DesaturateMethod {
/// Light level of the color, the Y (luminance) of Rec. 709, which weights the linear-light RGB channels by `0.2126, 0.7152, 0.0722`.
///
/// Accessibility contrast ratios and SVG luminance masks use this.
#[default]
#[label("sRGB")]
SRGB,
Perceptual,
AverageChannels,
MinimumChannels,
MaximumChannels,
#[label("Luminance (Rec. 709)")]
#[cfg_attr(feature = "serde", serde(alias = "SRGB"))]
LuminanceRec709,
/// Light level approximation for the color, the Y′ (luma) of Rec. 709, which weights the gamma-encoded RGB channels by `0.2126, 0.7152, 0.0722`.
///
/// CSS filter functions such as `grayscale()` use this.
#[label("Luma (Rec. 709)")]
LumaRec709,
/// Light level approximation for the color, the Y′ (luma) of Rec. 601, which weights the gamma-encoded RGB channels by `0.299, 0.587, 0.114`.
#[label("Luma (Rec. 601)")]
LumaRec601,
/// Perceptually uniform scale from black to white, the L (lightness) of OkLab.
#[label("Lightness (OkLab)")]
#[cfg_attr(feature = "serde", serde(alias = "Perceptual"))]
LightnessOkLab,
/// Mean of the three linear-light RGB channels.
#[menu_separator]
#[cfg_attr(feature = "serde", serde(alias = "AverageChannels"))]
ChannelsAverage,
/// Smallest of the three linear-light RGB channels.
#[cfg_attr(feature = "serde", serde(alias = "MinimumChannels"))]
ChannelsMinimum,
/// Largest of the three linear-light RGB channels, the V (value) of HSV.
#[cfg_attr(feature = "serde", serde(alias = "MaximumChannels"))]
ChannelsMaximum,
/// Midpoint of the largest and smallest gamma-encoded RGB channels, the L (lightness) of HSL.
///
/// The classic "Desaturate" command of many image editors uses this.
#[label("Lightness (HSL)")]
LightnessHsl,
}

#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
fn luminance<T: Adjust<Color>>(
fn desaturate<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
Raster<CPU>,
Expand All @@ -61,18 +89,38 @@ fn luminance<T: Adjust<Color>>(
)]
#[gpu_image]
input: Item<T>,
luminance_calc: Item<LuminanceCalculation>,
method: Item<DesaturateMethod>,
) -> Item<T> {
let mut input = input;
let luminance_calc = luminance_calc.into_element();
let method = method.into_element();

input.element_mut().adjust(|color| {
let luminance = match luminance_calc {
LuminanceCalculation::SRGB => color.luminance_rec_709(),
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
LuminanceCalculation::MaximumChannels => color.maximum_rgb_channels(),
// Gamma-encoded formulas are decoded as if they were a gray
let gamma = || color.to_gamma_srgb_channels();
let luminance = match method {
DesaturateMethod::LuminanceRec709 => color.luminance_rec_709(),
DesaturateMethod::LumaRec709 => {
let [r, g, b, _] = gamma();
srgb_to_linear(0.2126 * r + 0.7152 * g + 0.0722 * b)
}
DesaturateMethod::LumaRec601 => {
let [r, g, b, _] = gamma();
srgb_to_linear(0.299 * r + 0.587 * g + 0.114 * b)
}
DesaturateMethod::LightnessOkLab => {
// A gray's OkLab lightness is the cube root of its linear value, so cubing gives the gray of equal lightness
let lightness = color.lightness_oklab();
lightness * lightness * lightness

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer a lightness.powi(3)

Suggested change
lightness * lightness * lightness
lightness.powi(3)

}
DesaturateMethod::ChannelsAverage => color.average_rgb_channels(),
DesaturateMethod::ChannelsMinimum => color.minimum_rgb_channels(),
DesaturateMethod::ChannelsMaximum => color.maximum_rgb_channels(),
DesaturateMethod::LightnessHsl => {
// The transfer curve is monotonic, so the extremes are found first and only they are encoded
let max = linear_to_srgb(color.maximum_rgb_channels());
let min = linear_to_srgb(color.minimum_rgb_channels());
srgb_to_linear((max + min) / 2.)
}
};
color.map_rgb(|_| luminance)
});
Expand Down Expand Up @@ -1401,11 +1449,11 @@ fn color_balance<T: Adjust<Color>>(
#[cfg(feature = "std")]
mod _graphene_hash_impls {
use super::{
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DesaturateMethod, DomainWarpType, FractalType, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice, TonalRange,
};
graphene_hash::impl_via_hash!(
LuminanceCalculation,
DesaturateMethod,
RedGreenBlue,
RedGreenBlueAlpha,
NoiseType,
Expand Down
Loading