mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-18 18:38:05 +08:00
Deprecate all usages of the Color struct representing gamma space values, fixing round-trip precision bugs (#4149)
* Deprecate all usages of the Color struct representing gamma space values, fixing round-trip precision bugs * Code review fixes
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
use base64::Engine;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use canvas_utils::{Canvas, CanvasHandle};
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::list::{Item, List};
|
||||
#[cfg(target_family = "wasm")]
|
||||
use core_types::math::bbox::Bbox;
|
||||
@@ -123,7 +124,15 @@ fn string_to_bytes(_: impl Ctx, string: String) -> List<u8> {
|
||||
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
|
||||
fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
|
||||
let Some(image) = image.element(0) else { return List::new() };
|
||||
image.data.iter().flat_map(|color| color.to_rgba8_srgb()).map(Item::new_from_element).collect()
|
||||
image
|
||||
.data
|
||||
.iter()
|
||||
.flat_map(|color| {
|
||||
let SRGBA8 { red, green, blue, alpha } = (*color).into();
|
||||
[red, green, blue, alpha]
|
||||
})
|
||||
.map(Item::new_from_element)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
|
||||
@@ -154,7 +163,11 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List<Raster<CPU>> {
|
||||
let image = Image {
|
||||
data: image
|
||||
.chunks(4)
|
||||
.map(|pixel| Color::from_unassociated_alpha(pixel[0], pixel[1], pixel[2], pixel[3]).to_linear_srgb())
|
||||
.map(|pixel| {
|
||||
// Decoded bytes are unassociated gamma sRGB; premultiply in gamma then lift to linear
|
||||
let a = pixel[3];
|
||||
Color::from_gamma_srgb_channels(pixel[0] * a, pixel[1] * a, pixel[2] * a, a)
|
||||
})
|
||||
.collect(),
|
||||
width: image.width(),
|
||||
height: image.height(),
|
||||
|
||||
@@ -823,7 +823,8 @@ fn rgba_to_color(_: impl Ctx, _primary: (), red: Fraction, green: Fraction, blue
|
||||
let blue = (blue as f32).clamp(0., 1.);
|
||||
let alpha = (alpha as f32).clamp(0., 1.);
|
||||
|
||||
List::new_from_element(Color::from_rgbaf32_unchecked(red, green, blue, alpha))
|
||||
// RGB user inputs are interpreted as sRGB display values; lift to linear-light for the internal `Color`
|
||||
List::new_from_element(Color::from_gamma_srgb_channels(red, green, blue, alpha))
|
||||
}
|
||||
|
||||
/// Constructs a color value from hue, saturation, value, and alpha components given as numbers from 0 to 1.
|
||||
@@ -848,11 +849,11 @@ fn hsla_to_color(_: impl Ctx, _primary: (), hue: Fraction, #[default(1.)] satura
|
||||
List::new_from_element(Color::from_hsla(hue, saturation, lightness, alpha))
|
||||
}
|
||||
|
||||
/// Constructs a color value from an sRGB color code string, such as `#RRGGBB` or `#RRGGBBAA`. Invalid hex code strings produce no color.
|
||||
/// Constructs a color value from a CSS color string. Accepts hex (`#RRGGBB`, `#RRGGBBAA`, plus bare and shorthand variants), CSS named colors (like `red`), and functional notations (`rgb(...)`, `hsl(...)`, etc.). Invalid inputs produce no color.
|
||||
#[node_macro::node(category("Color"), name("Hex to Color"))]
|
||||
fn hex_to_color(_: impl Ctx, hex_code: String) -> List<Color> {
|
||||
match Color::from_hex_str(&hex_code) {
|
||||
Some(c) => List::new_from_element(c),
|
||||
match core_types::misc::parse_css_color(&hex_code) {
|
||||
Some(color) => List::new_from_element(color),
|
||||
None => List::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::cubic_spline::CubicSplines;
|
||||
use core::fmt::Debug;
|
||||
#[cfg(feature = "std")]
|
||||
use core_types::list::List;
|
||||
use glam::{Vec3, Vec4};
|
||||
use no_std_types::color::Color;
|
||||
use glam::Vec3;
|
||||
use no_std_types::color::{Color, linear_to_srgb, srgb_to_linear};
|
||||
use no_std_types::context::Ctx;
|
||||
use no_std_types::registry::types::{AngleF32, PercentageF32, SignedPercentageF32};
|
||||
use node_macro::BufferStruct;
|
||||
@@ -63,7 +63,7 @@ fn luminance<T: Adjust<Color>>(
|
||||
) -> T {
|
||||
input.adjust(|color| {
|
||||
let luminance = match luminance_calc {
|
||||
LuminanceCalculation::SRGB => color.luminance_srgb(),
|
||||
LuminanceCalculation::SRGB => color.luminance_rec_709(),
|
||||
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
|
||||
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
|
||||
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
|
||||
@@ -91,7 +91,7 @@ fn gamma_correction<T: Adjust<Color>>(
|
||||
inverse: bool,
|
||||
) -> T {
|
||||
let exponent = if inverse { 1. / gamma } else { gamma };
|
||||
input.adjust(|color| color.gamma(exponent));
|
||||
input.adjust(|color| color.apply_gamma_exponent(exponent));
|
||||
input
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ fn brightness_contrast_classic<T: Adjust<Color>>(
|
||||
|
||||
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());
|
||||
input.adjust(|color| color.map_gamma_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)));
|
||||
|
||||
input
|
||||
}
|
||||
@@ -240,7 +240,7 @@ fn brightness_contrast<T: Adjust<Color>>(
|
||||
});
|
||||
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.adjust(|color| color.map_gamma_rgb(|c| combined_lut[(c * lut_max).round() as usize]));
|
||||
|
||||
input
|
||||
}
|
||||
@@ -270,7 +270,8 @@ fn levels<T: Adjust<Color>>(
|
||||
#[default(100.)] output_maximums: PercentageF32,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
let color = color.to_gamma_srgb();
|
||||
// Levels math operates in gamma space
|
||||
let [mut r, mut g, mut b, a] = color.to_gamma_srgb_channels();
|
||||
|
||||
// Input Range (Range: 0-1)
|
||||
let input_shadows = shadows / 100.;
|
||||
@@ -301,15 +302,24 @@ fn levels<T: Adjust<Color>>(
|
||||
|
||||
// Input levels (Range: 0-1)
|
||||
let highlights_minus_shadows = (input_highlights - input_shadows).clamp(f32::EPSILON, 1.);
|
||||
let color = color.map_rgb(|c| ((c - input_shadows).max(0.) / highlights_minus_shadows).min(1.));
|
||||
let input_map = |c: f32| ((c - input_shadows).max(0.) / highlights_minus_shadows).min(1.);
|
||||
r = input_map(r);
|
||||
g = input_map(g);
|
||||
b = input_map(b);
|
||||
|
||||
// Midtones (Range: 0-1)
|
||||
let color = color.gamma(gamma);
|
||||
// Midtones gamma curve (Range: 0-1)
|
||||
let inverse_gamma = 1. / gamma.max(0.0001);
|
||||
r = r.powf(inverse_gamma);
|
||||
g = g.powf(inverse_gamma);
|
||||
b = b.powf(inverse_gamma);
|
||||
|
||||
// Output levels (Range: 0-1)
|
||||
let color = color.map_rgb(|c| c * (output_maximums - output_minimums) + output_minimums);
|
||||
let output_map = |c: f32| c * (output_maximums - output_minimums) + output_minimums;
|
||||
r = output_map(r);
|
||||
g = output_map(g);
|
||||
b = output_map(b);
|
||||
|
||||
color.to_linear_srgb()
|
||||
Color::from_gamma_srgb_channels(r, g, b, a)
|
||||
});
|
||||
image
|
||||
}
|
||||
@@ -353,7 +363,8 @@ fn black_and_white<T: Adjust<Color>>(
|
||||
magentas: PercentageF32,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
let color = color.to_gamma_srgb();
|
||||
// Black & White channel weights are tuned for gamma-space values
|
||||
let [r, g, b, alpha_part] = color.to_gamma_srgb_channels();
|
||||
|
||||
let reds = reds / 100.;
|
||||
let yellows = yellows / 100.;
|
||||
@@ -362,12 +373,11 @@ fn black_and_white<T: Adjust<Color>>(
|
||||
let blues = blues / 100.;
|
||||
let magentas = magentas / 100.;
|
||||
|
||||
let gray_base = color.r().min(color.g()).min(color.b());
|
||||
let gray_base = r.min(g).min(b);
|
||||
|
||||
let red_part = color.r() - gray_base;
|
||||
let green_part = color.g() - gray_base;
|
||||
let blue_part = color.b() - gray_base;
|
||||
let alpha_part = color.a();
|
||||
let red_part = r - gray_base;
|
||||
let green_part = g - gray_base;
|
||||
let blue_part = b - gray_base;
|
||||
|
||||
let additional = if red_part == 0. {
|
||||
let cyan_part = green_part.min(blue_part);
|
||||
@@ -383,11 +393,15 @@ fn black_and_white<T: Adjust<Color>>(
|
||||
let luminance = gray_base + additional;
|
||||
|
||||
// TODO: Fix "Color" blend mode implementation so it matches the expected behavior perfectly (it's currently close)
|
||||
let color = tint.with_luminance(luminance);
|
||||
// Apply luminance substitution in gamma space
|
||||
let [tr, tg, tb, _] = tint.to_gamma_srgb_channels();
|
||||
let tint_luma_rec_601 = 0.3 * tr + 0.59 * tg + 0.11 * tb;
|
||||
let delta = luminance - tint_luma_rec_601;
|
||||
let result_r = (tr + delta).clamp(0., 1.);
|
||||
let result_g = (tg + delta).clamp(0., 1.);
|
||||
let result_b = (tb + delta).clamp(0., 1.);
|
||||
|
||||
let color = Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), alpha_part);
|
||||
|
||||
color.to_linear_srgb()
|
||||
Color::from_gamma_srgb_channels(result_r, result_g, result_b, alpha_part)
|
||||
});
|
||||
image
|
||||
}
|
||||
@@ -410,20 +424,17 @@ fn hue_saturation<T: Adjust<Color>>(
|
||||
lightness_shift: SignedPercentageF32,
|
||||
) -> T {
|
||||
input.adjust(|color| {
|
||||
let color = color.to_gamma_srgb();
|
||||
|
||||
// HSL operates on gamma-space channels
|
||||
let [hue, saturation, lightness, alpha] = color.to_hsla();
|
||||
|
||||
let color = Color::from_hsla(
|
||||
Color::from_hsla(
|
||||
(hue + hue_shift / 360.) % 1.,
|
||||
// TODO: Improve the way saturation works (it's slightly off)
|
||||
(saturation + saturation_shift / 100.).clamp(0., 1.),
|
||||
// TODO: Fix the way lightness works (it's very off)
|
||||
(lightness + lightness_shift / 100.).clamp(0., 1.),
|
||||
alpha,
|
||||
);
|
||||
|
||||
color.to_linear_srgb()
|
||||
)
|
||||
});
|
||||
input
|
||||
}
|
||||
@@ -442,11 +453,9 @@ fn invert<T: Adjust<Color>>(
|
||||
mut input: T,
|
||||
) -> T {
|
||||
input.adjust(|color| {
|
||||
let color = color.to_gamma_srgb();
|
||||
|
||||
let color = color.map_rgb(|c| color.a() - c);
|
||||
|
||||
color.to_linear_srgb()
|
||||
// Invert in gamma space relative to alpha
|
||||
let [r, g, b, a] = color.to_gamma_srgb_channels();
|
||||
Color::from_gamma_srgb_channels(a - r, a - g, a - b, a)
|
||||
});
|
||||
input
|
||||
}
|
||||
@@ -468,11 +477,11 @@ fn threshold<T: Adjust<Color>>(
|
||||
luminance_calc: LuminanceCalculation,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
let min_luminance = Color::srgb_to_linear(min_luminance / 100.);
|
||||
let max_luminance = Color::srgb_to_linear(max_luminance / 100.);
|
||||
let min_luminance = srgb_to_linear(min_luminance / 100.);
|
||||
let max_luminance = srgb_to_linear(max_luminance / 100.);
|
||||
|
||||
let luminance = match luminance_calc {
|
||||
LuminanceCalculation::SRGB => color.luminance_srgb(),
|
||||
LuminanceCalculation::SRGB => color.luminance_rec_709(),
|
||||
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
|
||||
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
|
||||
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
|
||||
@@ -512,17 +521,22 @@ fn vibrance<T: Adjust<Color>>(
|
||||
vibrance: SignedPercentageF32,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
let r_raw = color.r();
|
||||
let g_raw = color.g();
|
||||
let b_raw = color.b();
|
||||
let alpha_in = color.a();
|
||||
|
||||
let vibrance = vibrance / 100.;
|
||||
// Slow the effect down by half when it's negative, since artifacts begin appearing past -50%.
|
||||
// So this scales the 0% to -50% range to 0% to -100%.
|
||||
let slowed_vibrance = if vibrance >= 0. { vibrance } else { vibrance * 0.5 };
|
||||
|
||||
let channel_max = color.r().max(color.g()).max(color.b());
|
||||
let channel_min = color.r().min(color.g()).min(color.b());
|
||||
let channel_max = r_raw.max(g_raw).max(b_raw);
|
||||
let channel_min = r_raw.min(g_raw).min(b_raw);
|
||||
let channel_difference = channel_max - channel_min;
|
||||
|
||||
let scale_multiplier = if channel_max == color.r() {
|
||||
let green_blue_difference = (color.g() - color.b()).abs();
|
||||
let scale_multiplier = if channel_max == r_raw {
|
||||
let green_blue_difference = (g_raw - b_raw).abs();
|
||||
let t = (green_blue_difference / channel_difference).min(1.);
|
||||
t * 0.5 + 0.5
|
||||
} else {
|
||||
@@ -532,30 +546,48 @@ fn vibrance<T: Adjust<Color>>(
|
||||
let channel_reduction = channel_min * scale;
|
||||
let scale = 1. + scale * (1. - channel_difference);
|
||||
|
||||
let luminance_initial = color.to_linear_srgb().luminance_srgb();
|
||||
let altered_color = color.map_rgb(|c| c * scale - channel_reduction).to_linear_srgb();
|
||||
let luminance = altered_color.luminance_srgb();
|
||||
let altered_color = altered_color.map_rgb(|c| c * luminance_initial / luminance);
|
||||
let r_lin0 = srgb_to_linear(r_raw);
|
||||
let g_lin0 = srgb_to_linear(g_raw);
|
||||
let b_lin0 = srgb_to_linear(b_raw);
|
||||
let luminance_initial = 0.2126 * r_lin0 + 0.7152 * g_lin0 + 0.0722 * b_lin0;
|
||||
|
||||
let channel_max = altered_color.r().max(altered_color.g()).max(altered_color.b());
|
||||
let altered_color = if Color::linear_to_srgb(channel_max) > 1. {
|
||||
let mut alt_r = srgb_to_linear(r_raw * scale - channel_reduction);
|
||||
let mut alt_g = srgb_to_linear(g_raw * scale - channel_reduction);
|
||||
let mut alt_b = srgb_to_linear(b_raw * scale - channel_reduction);
|
||||
let luminance = 0.2126 * alt_r + 0.7152 * alt_g + 0.0722 * alt_b;
|
||||
// Skip the luminance-preservation scaling when the result is black (e.g. black input pixel), avoiding division by zero.
|
||||
if luminance > 0. {
|
||||
alt_r *= luminance_initial / luminance;
|
||||
alt_g *= luminance_initial / luminance;
|
||||
alt_b *= luminance_initial / luminance;
|
||||
}
|
||||
|
||||
let channel_max = alt_r.max(alt_g).max(alt_b);
|
||||
if linear_to_srgb(channel_max) > 1. {
|
||||
let scale = (1. - luminance) / (channel_max - luminance);
|
||||
altered_color.map_rgb(|c| (c - luminance) * scale + luminance)
|
||||
} else {
|
||||
altered_color
|
||||
};
|
||||
let altered_color = altered_color.to_gamma_srgb();
|
||||
alt_r = (alt_r - luminance) * scale + luminance;
|
||||
alt_g = (alt_g - luminance) * scale + luminance;
|
||||
alt_b = (alt_b - luminance) * scale + luminance;
|
||||
}
|
||||
|
||||
alt_r = linear_to_srgb(alt_r);
|
||||
alt_g = linear_to_srgb(alt_g);
|
||||
alt_b = linear_to_srgb(alt_b);
|
||||
|
||||
if vibrance >= 0. {
|
||||
altered_color
|
||||
Color::from_rgbaf32_unchecked(alt_r, alt_g, alt_b, alpha_in)
|
||||
} else {
|
||||
// TODO: The result ends up a bit darker than it should be, further investigation is needed
|
||||
let luminance = color.luminance_rec_601();
|
||||
|
||||
// Near -0% vibrance we mostly use `altered_color`.
|
||||
// Near -100% vibrance, we mostly use half the desaturated luminance color and half `altered_color`.
|
||||
// TODO: The result ends up a bit darker than it should be, further investigation is needed.
|
||||
// Mix in gamma space (matching `alt_*`), so the luminance is computed from gamma channels too.
|
||||
let [gr, gg, gb, _] = color.to_gamma_srgb_channels();
|
||||
let luminance = 0.299 * gr + 0.587 * gg + 0.114 * gb;
|
||||
let factor = -slowed_vibrance;
|
||||
altered_color.map_rgb(|c| c * (1. - factor) + luminance * factor)
|
||||
Color::from_rgbaf32_unchecked(
|
||||
alt_r * (1. - factor) + luminance * factor,
|
||||
alt_g * (1. - factor) + luminance * factor,
|
||||
alt_b * (1. - factor) + luminance * factor,
|
||||
alpha_in,
|
||||
)
|
||||
}
|
||||
});
|
||||
image
|
||||
@@ -747,16 +779,14 @@ fn channel_mixer<T: Adjust<Color>>(
|
||||
_output_channel: RedGreenBlue,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
let color = color.to_gamma_srgb();
|
||||
let [r, g, b, a] = color.to_gamma_srgb_channels();
|
||||
|
||||
let (r, g, b, a) = color.components();
|
||||
|
||||
let color = if monochrome {
|
||||
let (out_r, out_g, out_b) = if monochrome {
|
||||
let (monochrome_r, monochrome_g, monochrome_b, monochrome_c) = (monochrome_r / 100., monochrome_g / 100., monochrome_b / 100., monochrome_c / 100.);
|
||||
|
||||
let gray = (r * monochrome_r + g * monochrome_g + b * monochrome_b + monochrome_c).clamp(0., 1.);
|
||||
|
||||
Color::from_rgbaf32_unchecked(gray, gray, gray, a)
|
||||
(gray, gray, gray)
|
||||
} else {
|
||||
let (red_r, red_g, red_b, red_c) = (red_r / 100., red_g / 100., red_b / 100., red_c / 100.);
|
||||
let (green_r, green_g, green_b, green_c) = (green_r / 100., green_g / 100., green_b / 100., green_c / 100.);
|
||||
@@ -766,10 +796,10 @@ fn channel_mixer<T: Adjust<Color>>(
|
||||
let green = (r * green_r + g * green_g + b * green_b + green_c).clamp(0., 1.);
|
||||
let blue = (r * blue_r + g * blue_g + b * blue_b + blue_c).clamp(0., 1.);
|
||||
|
||||
Color::from_rgbaf32_unchecked(red, green, blue, a)
|
||||
(red, green, blue)
|
||||
};
|
||||
|
||||
color.to_linear_srgb()
|
||||
Color::from_gamma_srgb_channels(out_r, out_g, out_b, a)
|
||||
});
|
||||
image
|
||||
}
|
||||
@@ -873,9 +903,7 @@ fn selective_color<T: Adjust<Color>>(
|
||||
_colors: SelectiveColorChoice,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
let color = color.to_gamma_srgb();
|
||||
|
||||
let (r, g, b, a) = color.components();
|
||||
let [r, g, b, a] = color.to_gamma_srgb_channels();
|
||||
|
||||
let min = |a: f32, b: f32, c: f32| a.min(b).min(c);
|
||||
let max = |a: f32, b: f32, c: f32| a.max(b).max(c);
|
||||
@@ -945,9 +973,9 @@ fn selective_color<T: Adjust<Color>>(
|
||||
}
|
||||
|
||||
let rgb = Vec3::new(r, g, b);
|
||||
let color = Color::from_vec4(Vec4::from(((sum + rgb).clamp(Vec3::ZERO, Vec3::ONE), a)));
|
||||
let out = (sum + rgb).clamp(Vec3::ZERO, Vec3::ONE);
|
||||
|
||||
color.to_linear_srgb()
|
||||
Color::from_gamma_srgb_channels(out.x, out.y, out.z, a)
|
||||
});
|
||||
image
|
||||
}
|
||||
@@ -973,15 +1001,11 @@ fn posterize<T: Adjust<Color>>(
|
||||
levels: u32,
|
||||
) -> T {
|
||||
input.adjust(|color| {
|
||||
let color = color.to_gamma_srgb();
|
||||
|
||||
let levels = levels as f32;
|
||||
// `hard_min(2)` constrains the widget but doesn't bind the data-flow input (a saved doc or upstream node could still feed 0 or 1, producing inf/NaN below).
|
||||
let levels = (levels as f32).max(2.);
|
||||
let number_of_areas = levels.recip();
|
||||
let size_of_areas = (levels - 1.).recip();
|
||||
let channel = |channel: f32| (channel / number_of_areas).floor() * size_of_areas;
|
||||
let color = color.map_rgb(channel);
|
||||
|
||||
color.to_linear_srgb()
|
||||
color.map_gamma_rgb(|c| (c / number_of_areas).floor() * size_of_areas)
|
||||
});
|
||||
input
|
||||
}
|
||||
@@ -1016,7 +1040,7 @@ fn exposure<T: Adjust<Color>>(
|
||||
// Offset
|
||||
.map_rgb(|c: f32| c + offset)
|
||||
// Gamma correction
|
||||
.gamma(gamma_correction);
|
||||
.apply_gamma_exponent(gamma_correction);
|
||||
|
||||
adjusted.map_rgb(|c: f32| c.clamp(0., 1.))
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode,
|
||||
blend_mode => apply_blend_mode(foreground, background, blend_mode),
|
||||
};
|
||||
|
||||
background.alpha_blend(target_color.to_associated_alpha(opacity))
|
||||
background.alpha_blend(target_color.apply_opacity(opacity))
|
||||
}
|
||||
|
||||
pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use core_types::color::Color;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use core_types::color::{Alpha, Color, Pixel, RGB};
|
||||
use core_types::context::Ctx;
|
||||
use core_types::list::List;
|
||||
use core_types::registry::types::PixelLength;
|
||||
@@ -6,6 +7,84 @@ use raster_types::Image;
|
||||
use raster_types::{Bitmap, BitmapMut};
|
||||
use raster_types::{CPU, Raster};
|
||||
|
||||
/// Working-buffer pixel for the blur algorithms' `gamma` mode: premultiplied sRGB-gamma `f32` channels.
|
||||
/// Only used internally so the working buffer's color space is reflected in the type instead of stuffed into `Color` (which is linear-light by invariant).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
|
||||
struct PremultipliedGammaPixel {
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
a: f32,
|
||||
}
|
||||
|
||||
impl Pixel for PremultipliedGammaPixel {}
|
||||
|
||||
impl RGB for PremultipliedGammaPixel {
|
||||
type ColorChannel = f32;
|
||||
fn red(&self) -> f32 {
|
||||
self.r
|
||||
}
|
||||
fn green(&self) -> f32 {
|
||||
self.g
|
||||
}
|
||||
fn blue(&self) -> f32 {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
|
||||
impl Alpha for PremultipliedGammaPixel {
|
||||
type AlphaChannel = f32;
|
||||
const TRANSPARENT: Self = Self { r: 0., g: 0., b: 0., a: 0. };
|
||||
fn alpha(&self) -> f32 {
|
||||
self.a
|
||||
}
|
||||
fn multiplied_alpha(&self, mult: f32) -> Self {
|
||||
Self {
|
||||
r: self.r * mult,
|
||||
g: self.g * mult,
|
||||
b: self.b * mult,
|
||||
a: self.a * mult,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn premultiply_gamma(buffer: Image<Color>) -> Image<PremultipliedGammaPixel> {
|
||||
Image {
|
||||
width: buffer.width,
|
||||
height: buffer.height,
|
||||
data: buffer
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|px| {
|
||||
let [r, g, b, a] = px.to_gamma_srgb_channels();
|
||||
PremultipliedGammaPixel { r: r * a, g: g * a, b: b * a, a }
|
||||
})
|
||||
.collect(),
|
||||
base64_string: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unpremultiply_gamma_to_linear(buffer: Image<PremultipliedGammaPixel>) -> Image<Color> {
|
||||
Image {
|
||||
width: buffer.width,
|
||||
height: buffer.height,
|
||||
data: buffer
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|px| {
|
||||
if px.a > 0. {
|
||||
let inv_a = 1. / px.a;
|
||||
Color::from_gamma_srgb_channels(px.r * inv_a, px.g * inv_a, px.b * inv_a, px.a)
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
base64_string: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Blurs the image with a Gaussian or box blur kernel filter.
|
||||
#[node_macro::node(category("Raster: Filter"))]
|
||||
async fn blur(
|
||||
@@ -98,26 +177,49 @@ fn gaussian_kernel(radius: f64) -> Vec<f64> {
|
||||
gaussian_kernel
|
||||
}
|
||||
|
||||
fn gaussian_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
||||
if gamma {
|
||||
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
|
||||
} else {
|
||||
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
|
||||
}
|
||||
|
||||
let (width, height) = original_buffer.dimensions();
|
||||
|
||||
// Create 1D gaussian kernel
|
||||
fn gaussian_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
||||
let kernel = gaussian_kernel(radius);
|
||||
if gamma {
|
||||
let working = premultiply_gamma(buffer);
|
||||
let blurred = gaussian_separable(working, &kernel, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a });
|
||||
unpremultiply_gamma_to_linear(blurred)
|
||||
} else {
|
||||
let mut working = buffer;
|
||||
working.map_pixels(|px| px.apply_opacity(px.a()));
|
||||
let mut blurred = gaussian_separable(working, &kernel, Color::from_rgbaf32_unchecked);
|
||||
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
||||
blurred
|
||||
}
|
||||
}
|
||||
|
||||
fn box_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
||||
if gamma {
|
||||
let working = premultiply_gamma(buffer);
|
||||
let blurred = box_separable(working, radius, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a });
|
||||
unpremultiply_gamma_to_linear(blurred)
|
||||
} else {
|
||||
let mut working = buffer;
|
||||
working.map_pixels(|px| px.apply_opacity(px.a()));
|
||||
let mut blurred = box_separable(working, radius, Color::from_rgbaf32_unchecked);
|
||||
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
||||
blurred
|
||||
}
|
||||
}
|
||||
|
||||
fn gaussian_separable<P, F>(buffer: Image<P>, kernel: &[f64], construct: F) -> Image<P>
|
||||
where
|
||||
P: Pixel + Copy + RGB<ColorChannel = f32> + Alpha<AlphaChannel = f32>,
|
||||
F: Fn(f32, f32, f32, f32) -> P,
|
||||
{
|
||||
let (width, height) = buffer.dimensions();
|
||||
let half_kernel = kernel.len() / 2;
|
||||
|
||||
// Intermediate buffer for horizontal and vertical passes
|
||||
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
|
||||
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
|
||||
let mut x_axis = Image::new(width, height, P::default());
|
||||
let mut y_axis = Image::new(width, height, P::default());
|
||||
|
||||
for pass in [false, true] {
|
||||
let (max, old_buffer, current_buffer) = match pass {
|
||||
false => (width, &original_buffer, &mut x_axis),
|
||||
false => (width, &buffer, &mut x_axis),
|
||||
true => (height, &x_axis, &mut y_axis),
|
||||
};
|
||||
let pass = pass as usize;
|
||||
@@ -140,41 +242,32 @@ fn gaussian_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize
|
||||
let (r, g, b, a) = if weight_sum > 0. {
|
||||
((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32)
|
||||
} else {
|
||||
let px = old_buffer.get_pixel(x, y).unwrap();
|
||||
(px.r(), px.g(), px.b(), px.a())
|
||||
};
|
||||
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
|
||||
current_buffer.set_pixel(x, y, construct(r, g, b, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gamma {
|
||||
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
|
||||
} else {
|
||||
y_axis.map_pixels(|px| px.to_unassociated_alpha());
|
||||
}
|
||||
|
||||
y_axis
|
||||
}
|
||||
|
||||
fn box_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
||||
if gamma {
|
||||
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
|
||||
} else {
|
||||
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
|
||||
}
|
||||
|
||||
let (width, height) = original_buffer.dimensions();
|
||||
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
|
||||
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
|
||||
fn box_separable<P, F>(buffer: Image<P>, radius: f64, construct: F) -> Image<P>
|
||||
where
|
||||
P: Pixel + Copy + RGB<ColorChannel = f32> + Alpha<AlphaChannel = f32>,
|
||||
F: Fn(f32, f32, f32, f32) -> P,
|
||||
{
|
||||
let (width, height) = buffer.dimensions();
|
||||
let mut x_axis = Image::new(width, height, P::default());
|
||||
let mut y_axis = Image::new(width, height, P::default());
|
||||
|
||||
for pass in [false, true] {
|
||||
let (max, old_buffer, current_buffer) = match pass {
|
||||
false => (width, &original_buffer, &mut x_axis),
|
||||
false => (width, &buffer, &mut x_axis),
|
||||
true => (height, &x_axis, &mut y_axis),
|
||||
};
|
||||
let pass = pass as usize;
|
||||
@@ -196,17 +289,11 @@ fn box_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: boo
|
||||
}
|
||||
|
||||
let (r, g, b, a) = ((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32);
|
||||
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
|
||||
current_buffer.set_pixel(x, y, construct(r, g, b, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gamma {
|
||||
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
|
||||
} else {
|
||||
y_axis.map_pixels(|px| px.to_unassociated_alpha());
|
||||
}
|
||||
|
||||
y_axis
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ async fn gradient_map<T: Adjust<Color>>(
|
||||
let Some(gradient) = gradient.element(0) else { return image };
|
||||
|
||||
image.adjust(|color| {
|
||||
let intensity = color.luminance_srgb();
|
||||
let intensity = color.luminance_rec_709();
|
||||
let intensity = if reverse { 1. - intensity } else { intensity };
|
||||
gradient.evaluate(intensity as f64).to_linear_srgb()
|
||||
gradient.evaluate(intensity as f64)
|
||||
});
|
||||
|
||||
image
|
||||
|
||||
@@ -16,7 +16,8 @@ async fn image_color_palette(
|
||||
let bins = GRID * GRID * GRID;
|
||||
|
||||
let mut histogram = vec![0; (bins + 1.) as usize];
|
||||
let mut color_bins = vec![Vec::new(); (bins + 1.) as usize];
|
||||
// Each bin stores `(red, green, blue, alpha)` tuples in sRGB gamma space; averaging in gamma space gives perceptually-uniform binning.
|
||||
let mut color_bins: Vec<Vec<[f32; 4]>> = vec![Vec::new(); (bins + 1.) as usize];
|
||||
|
||||
for element in image.iter_element_values() {
|
||||
for pixel in element.data.iter() {
|
||||
@@ -27,7 +28,7 @@ async fn image_color_palette(
|
||||
let bin = (r * GRID + g * GRID + b * GRID) as usize;
|
||||
|
||||
histogram[bin] += 1;
|
||||
color_bins[bin].push(pixel.to_gamma_srgb());
|
||||
color_bins[bin].push(pixel.to_gamma_srgb_channels());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,24 +40,21 @@ async fn image_color_palette(
|
||||
.flat_map(|&i| {
|
||||
let list = &color_bins[i];
|
||||
|
||||
let mut r = 0.;
|
||||
let mut g = 0.;
|
||||
let mut b = 0.;
|
||||
let mut a = 0.;
|
||||
let [mut r, mut g, mut b, mut a] = [0.; 4];
|
||||
|
||||
for color in list.iter() {
|
||||
r += color.r();
|
||||
g += color.g();
|
||||
b += color.b();
|
||||
a += color.a();
|
||||
for &[cr, cg, cb, ca] in list.iter() {
|
||||
r += cr;
|
||||
g += cg;
|
||||
b += cb;
|
||||
a += ca;
|
||||
}
|
||||
|
||||
r /= list.len() as f32;
|
||||
g /= list.len() as f32;
|
||||
b /= list.len() as f32;
|
||||
a /= list.len() as f32;
|
||||
let len = list.len() as f32;
|
||||
let [r, g, b, a] = [r / len, g / len, b / len, a / len];
|
||||
|
||||
Color::from_rgbaf32(r, g, b, a).map(Item::new_from_element).into_iter()
|
||||
// Reject NaN/out-of-range averages, then lift the gamma-space bin centroid to linear-light
|
||||
let in_gamut = a <= 1. && ![r, g, b, a].iter().any(|c| c.is_sign_negative() || !c.is_finite());
|
||||
in_gamut.then(|| Color::from_gamma_srgb_channels(r, g, b, a)).map(Item::new_from_element).into_iter()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user