mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
New node: Color Balance (#4527)
This commit is contained in:
@@ -919,6 +919,7 @@ fn static_node_properties() -> NodeProperties {
|
|||||||
map.insert("black_and_white_properties".to_string(), Box::new(node_properties::black_and_white_properties));
|
map.insert("black_and_white_properties".to_string(), Box::new(node_properties::black_and_white_properties));
|
||||||
map.insert("threshold_properties".to_string(), Box::new(node_properties::threshold_properties));
|
map.insert("threshold_properties".to_string(), Box::new(node_properties::threshold_properties));
|
||||||
map.insert("vibrance_properties".to_string(), Box::new(node_properties::vibrance_properties));
|
map.insert("vibrance_properties".to_string(), Box::new(node_properties::vibrance_properties));
|
||||||
|
map.insert("color_balance_properties".to_string(), Box::new(node_properties::color_balance_properties));
|
||||||
map.insert("fill_properties".to_string(), Box::new(node_properties::fill_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));
|
map.insert("stroke_properties".to_string(), Box::new(node_properties::stroke_properties));
|
||||||
map.insert("offset_path_properties".to_string(), Box::new(node_properties::offset_path_properties));
|
map.insert("offset_path_properties".to_string(), Box::new(node_properties::offset_path_properties));
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use graphene_std::color::SRGBA8;
|
|||||||
use graphene_std::extract_xy::XY;
|
use graphene_std::extract_xy::XY;
|
||||||
use graphene_std::raster::{
|
use graphene_std::raster::{
|
||||||
AdjustmentChannel, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
AdjustmentChannel, BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||||
SelectiveColorChoice,
|
SelectiveColorChoice, TonalRange,
|
||||||
};
|
};
|
||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::text::{Font, TextAlign};
|
use graphene_std::text::{Font, TextAlign};
|
||||||
@@ -318,6 +318,7 @@ pub(crate) fn property_from_type(
|
|||||||
Some(x) if id_is::<CellularReturnType>(x) => enum_choice::<CellularReturnType>().for_socket(default_info).disabled(false).property_row(),
|
Some(x) if id_is::<CellularReturnType>(x) => enum_choice::<CellularReturnType>().for_socket(default_info).disabled(false).property_row(),
|
||||||
Some(x) if id_is::<DomainWarpType>(x) => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
|
Some(x) if id_is::<DomainWarpType>(x) => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
|
||||||
Some(x) if id_is::<RelativeAbsolute>(x) => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
|
Some(x) if id_is::<RelativeAbsolute>(x) => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
|
||||||
|
Some(x) if id_is::<TonalRange>(x) => enum_choice::<TonalRange>().for_socket(default_info).disabled(false).property_row(),
|
||||||
Some(x) if id_is::<AdjustmentChannel>(x) => enum_choice::<AdjustmentChannel>().for_socket(default_info).disabled(false).property_row(),
|
Some(x) if id_is::<AdjustmentChannel>(x) => enum_choice::<AdjustmentChannel>().for_socket(default_info).disabled(false).property_row(),
|
||||||
Some(x) if id_is::<GridType>(x) => enum_choice::<GridType>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<GridType>(x) => enum_choice::<GridType>().for_socket(default_info).property_row(),
|
||||||
Some(x) if id_is::<StrokeCap>(x) => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<StrokeCap>(x) => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
||||||
@@ -1676,6 +1677,51 @@ pub(crate) fn vibrance_properties(node_id: NodeId, context: &mut NodePropertiesC
|
|||||||
)]
|
)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn color_balance_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
|
use graphene_std::raster::color_balance::*;
|
||||||
|
|
||||||
|
let mut tone_info = ParameterWidgetsInfo::new(node_id, ToneInput, true, context);
|
||||||
|
tone_info.exposable = false;
|
||||||
|
let tone = enum_choice::<TonalRange>().for_socket(tone_info).property_row();
|
||||||
|
let preserve_luminosity = bool_widget(ParameterWidgetsInfo::new(node_id, PreserveLuminosityInput, true, context), CheckboxInput::default());
|
||||||
|
|
||||||
|
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 color_balance_properties: {err}");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tone_choice = match document_node.input_value(ToneInput) {
|
||||||
|
Some(TaggedValue::TonalRange(choice)) => *choice,
|
||||||
|
_ => {
|
||||||
|
warn!("Color Balance node properties panel could not be displayed.");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only the selected tone's three sliders are shown
|
||||||
|
let parameters: [ParameterRef; 3] = match tone_choice {
|
||||||
|
TonalRange::Shadows => [ShadowsCyanRedInput.into(), ShadowsMagentaGreenInput.into(), ShadowsYellowBlueInput.into()],
|
||||||
|
TonalRange::Midtones => [MidtonesCyanRedInput.into(), MidtonesMagentaGreenInput.into(), MidtonesYellowBlueInput.into()],
|
||||||
|
TonalRange::Highlights => [HighlightsCyanRedInput.into(), HighlightsMagentaGreenInput.into(), HighlightsYellowBlueInput.into()],
|
||||||
|
};
|
||||||
|
let tracks = [
|
||||||
|
Gradient::from(vec![Color::CYAN, Color::RED]),
|
||||||
|
Gradient::from(vec![Color::MAGENTA, Color::GREEN]),
|
||||||
|
Gradient::from(vec![Color::YELLOW, Color::BLUE]),
|
||||||
|
];
|
||||||
|
let number_input = NumberInput::default().mode_increment().unit("%").min(-100.).max(100.);
|
||||||
|
|
||||||
|
let mut layout = vec![tone];
|
||||||
|
for (parameter, track) in parameters.into_iter().zip(tracks) {
|
||||||
|
layout.push(spectrum_slider_row(node_id, context, parameter, track, Color::WHITE, -100., 100., 0., number_input.clone()));
|
||||||
|
}
|
||||||
|
layout.push(LayoutGroup::row(preserve_luminosity));
|
||||||
|
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn black_and_white_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub(crate) fn black_and_white_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::raster::black_and_white::*;
|
use graphene_std::raster::black_and_white::*;
|
||||||
|
|
||||||
|
|||||||
@@ -562,6 +562,7 @@ tagged_value! {
|
|||||||
DomainWarpType(raster_nodes::adjustments::DomainWarpType),
|
DomainWarpType(raster_nodes::adjustments::DomainWarpType),
|
||||||
RelativeAbsolute(raster_nodes::adjustments::RelativeAbsolute),
|
RelativeAbsolute(raster_nodes::adjustments::RelativeAbsolute),
|
||||||
SelectiveColorChoice(raster_nodes::adjustments::SelectiveColorChoice),
|
SelectiveColorChoice(raster_nodes::adjustments::SelectiveColorChoice),
|
||||||
|
TonalRange(raster_nodes::adjustments::TonalRange),
|
||||||
AdjustmentChannel(raster_nodes::adjustments::AdjustmentChannel),
|
AdjustmentChannel(raster_nodes::adjustments::AdjustmentChannel),
|
||||||
GridType(vector::misc::GridType),
|
GridType(vector::misc::GridType),
|
||||||
ArcType(vector::misc::ArcType),
|
ArcType(vector::misc::ArcType),
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
RedGreenBlueAlpha,
|
RedGreenBlueAlpha,
|
||||||
RelativeAbsolute,
|
RelativeAbsolute,
|
||||||
SelectiveColorChoice,
|
SelectiveColorChoice,
|
||||||
|
TonalRange,
|
||||||
AdjustmentChannel,
|
AdjustmentChannel,
|
||||||
Stroke,
|
Stroke,
|
||||||
XY,
|
XY,
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ use raster_types::{CPU, Raster};
|
|||||||
use vector_types::Gradient;
|
use vector_types::Gradient;
|
||||||
|
|
||||||
// TODO: Implement the following:
|
// TODO: Implement the following:
|
||||||
// Color Balance
|
|
||||||
// Aims for interoperable compatibility with:
|
|
||||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27blnc%27%20%3D%20Color%20Balance
|
|
||||||
//
|
|
||||||
// Photo Filter
|
// Photo Filter
|
||||||
// Aims for interoperable compatibility with:
|
// Aims for interoperable compatibility with:
|
||||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27phfl%27%20%3D%20Photo%20Filter
|
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27phfl%27%20%3D%20Photo%20Filter
|
||||||
@@ -1235,11 +1231,156 @@ fn exposure<T: Adjust<Color>>(
|
|||||||
input
|
input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[repr(u32)]
|
||||||
|
#[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, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)]
|
||||||
|
#[widget(Dropdown)]
|
||||||
|
pub enum TonalRange {
|
||||||
|
Shadows,
|
||||||
|
#[default]
|
||||||
|
Midtones,
|
||||||
|
Highlights,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Levels-style tone curve: input black and white points on a 0..255 scale and a gamma exponent. For gamma above 1 the
|
||||||
|
/// power curve's slope is unbounded at black, so a cubic toe holds it to 2^gamma until past where that line meets the curve.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct LevelsCurve {
|
||||||
|
black: f32,
|
||||||
|
white: f32,
|
||||||
|
exponent: f32,
|
||||||
|
toe_end: f32,
|
||||||
|
toe_value: f32,
|
||||||
|
toe_slope_start: f32,
|
||||||
|
toe_slope_end: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LevelsCurve {
|
||||||
|
fn new(black: i32, white: i32, gamma: f32) -> Self {
|
||||||
|
Self::from_points(black as f32, white as f32, gamma)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `gamma` is the Levels dialog value (pixel exponent 1/gamma).
|
||||||
|
fn from_points(black: f32, white: f32, gamma: f32) -> Self {
|
||||||
|
let gamma = gamma.max(0.01);
|
||||||
|
let black = black.min(white - 1.);
|
||||||
|
let exponent = 1. / gamma;
|
||||||
|
|
||||||
|
let mut curve = Self {
|
||||||
|
black,
|
||||||
|
white,
|
||||||
|
exponent,
|
||||||
|
toe_end: 0.,
|
||||||
|
toe_value: 0.,
|
||||||
|
toe_slope_start: 0.,
|
||||||
|
toe_slope_end: 0.,
|
||||||
|
};
|
||||||
|
if gamma > 1. {
|
||||||
|
let slope = 2_f32.powf(gamma);
|
||||||
|
let intersection = 255. * slope.powf(-1. / (1. - exponent));
|
||||||
|
// The cubic toe joins the power curve at twice the intersection
|
||||||
|
if intersection > 1e-3 {
|
||||||
|
let toe_end = 2. * intersection;
|
||||||
|
curve.toe_end = toe_end;
|
||||||
|
curve.toe_value = 255. * (toe_end / 255.).powf(exponent);
|
||||||
|
curve.toe_slope_start = slope;
|
||||||
|
curve.toe_slope_end = exponent * (toe_end / 255.).powf(exponent - 1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
curve
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps one gamma-space channel value in 0..1.
|
||||||
|
fn apply(&self, value: f32) -> f32 {
|
||||||
|
let t = ((value * 255. - self.black) * 255. / (self.white - self.black)).clamp(0., 255.);
|
||||||
|
let y = if t < self.toe_end {
|
||||||
|
let u = t / self.toe_end;
|
||||||
|
let hermite_start = u * u * u - 2. * u * u + u;
|
||||||
|
let hermite_end_value = 3. * u * u - 2. * u * u * u;
|
||||||
|
let hermite_end_slope = u * u * u - u * u;
|
||||||
|
hermite_start * self.toe_end * self.toe_slope_start + hermite_end_value * self.toe_value + hermite_end_slope * self.toe_end * self.toe_slope_end
|
||||||
|
} else {
|
||||||
|
255. * (t / 255.).powf(self.exponent)
|
||||||
|
};
|
||||||
|
y / 255.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One channel's Levels parameters from its own slider values, and (with preserve_luminosity) the slider
|
||||||
|
/// extremes across all three channels. The halvings truncate toward zero, as PSD interop requires.
|
||||||
|
fn color_balance_curve(s: i32, m: i32, h: i32, s_max: i32, m_max: i32, m_min: i32, h_min: i32, preserve_luminosity: bool) -> LevelsCurve {
|
||||||
|
let (black, white, tone) = if preserve_luminosity {
|
||||||
|
(s_max - s, 255 - (h - h_min), m - (m_max + m_min) / 2)
|
||||||
|
} else {
|
||||||
|
(0.max(-s), 255 - 0.max(h), (s + h) / 2 + m)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rounding the derived gamma to hundredths, the precision of a PSD Levels record, is needed for compatible results
|
||||||
|
let gamma = (2_f32.powf(tone as f32 / 100.) * 100.).round() / 100.;
|
||||||
|
LevelsCurve::new(black, white, gamma)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aims for interoperable compatibility with:
|
||||||
|
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27blnc%27%20%3D%20Color%20Balance
|
||||||
|
//
|
||||||
|
// Every channel is a Levels curve whose black point, white point, and two-decimal gamma are derived from
|
||||||
|
// the nine sliders, see `color_balance_curve`.
|
||||||
|
#[node_macro::node(category("Raster: Adjustment"), properties("color_balance_properties"), shader_node(PerPixelAdjust))]
|
||||||
|
fn color_balance<T: Adjust<Color>>(
|
||||||
|
_: impl Ctx,
|
||||||
|
#[implementations(Raster<CPU>, Color, Gradient)]
|
||||||
|
#[gpu_image]
|
||||||
|
image: Item<T>,
|
||||||
|
|
||||||
|
#[name("(Shadows) Cyan-Red")] shadows_cyan_red: Item<SignedPercentageF32>,
|
||||||
|
#[name("(Shadows) Magenta-Green")] shadows_magenta_green: Item<SignedPercentageF32>,
|
||||||
|
#[name("(Shadows) Yellow-Blue")] shadows_yellow_blue: Item<SignedPercentageF32>,
|
||||||
|
|
||||||
|
#[name("(Midtones) Cyan-Red")] midtones_cyan_red: Item<SignedPercentageF32>,
|
||||||
|
#[name("(Midtones) Magenta-Green")] midtones_magenta_green: Item<SignedPercentageF32>,
|
||||||
|
#[name("(Midtones) Yellow-Blue")] midtones_yellow_blue: Item<SignedPercentageF32>,
|
||||||
|
|
||||||
|
#[name("(Highlights) Cyan-Red")] highlights_cyan_red: Item<SignedPercentageF32>,
|
||||||
|
#[name("(Highlights) Magenta-Green")] highlights_magenta_green: Item<SignedPercentageF32>,
|
||||||
|
#[name("(Highlights) Yellow-Blue")] highlights_yellow_blue: Item<SignedPercentageF32>,
|
||||||
|
|
||||||
|
#[default(true)] preserve_luminosity: Item<bool>,
|
||||||
|
|
||||||
|
// Display-only property (not used within the node)
|
||||||
|
_tone: Item<TonalRange>,
|
||||||
|
) -> Item<T> {
|
||||||
|
let mut image = image;
|
||||||
|
let preserve_luminosity = preserve_luminosity.into_element();
|
||||||
|
|
||||||
|
// The derivation below is integer arithmetic, so the sliders round to whole percentages first
|
||||||
|
let slider = |value: Item<SignedPercentageF32>| value.into_element().clamp(-100., 100.).round() as i32;
|
||||||
|
let (s_r, s_g, s_b) = (slider(shadows_cyan_red), slider(shadows_magenta_green), slider(shadows_yellow_blue));
|
||||||
|
let (m_r, m_g, m_b) = (slider(midtones_cyan_red), slider(midtones_magenta_green), slider(midtones_yellow_blue));
|
||||||
|
let (h_r, h_g, h_b) = (slider(highlights_cyan_red), slider(highlights_magenta_green), slider(highlights_yellow_blue));
|
||||||
|
|
||||||
|
let s_max = s_r.max(s_g).max(s_b);
|
||||||
|
let m_max = m_r.max(m_g).max(m_b);
|
||||||
|
let m_min = m_r.min(m_g).min(m_b);
|
||||||
|
let h_min = h_r.min(h_g).min(h_b);
|
||||||
|
let red = color_balance_curve(s_r, m_r, h_r, s_max, m_max, m_min, h_min, preserve_luminosity);
|
||||||
|
let green = color_balance_curve(s_g, m_g, h_g, s_max, m_max, m_min, h_min, preserve_luminosity);
|
||||||
|
let blue = color_balance_curve(s_b, m_b, h_b, s_max, m_max, m_min, h_min, preserve_luminosity);
|
||||||
|
|
||||||
|
image.element_mut().adjust(|color| {
|
||||||
|
// The curves operate on gamma-space channel values
|
||||||
|
let [r, g, b, a] = color.to_gamma_srgb_channels();
|
||||||
|
Color::from_gamma_srgb_channels(red.apply(r), green.apply(g), blue.apply(b), a)
|
||||||
|
});
|
||||||
|
image
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
mod _graphene_hash_impls {
|
mod _graphene_hash_impls {
|
||||||
use super::{
|
use super::{
|
||||||
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
AdjustmentChannel, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||||
SelectiveColorChoice,
|
SelectiveColorChoice, TonalRange,
|
||||||
};
|
};
|
||||||
graphene_hash::impl_via_hash!(
|
graphene_hash::impl_via_hash!(
|
||||||
LuminanceCalculation,
|
LuminanceCalculation,
|
||||||
@@ -1252,7 +1393,8 @@ mod _graphene_hash_impls {
|
|||||||
DomainWarpType,
|
DomainWarpType,
|
||||||
RelativeAbsolute,
|
RelativeAbsolute,
|
||||||
SelectiveColorChoice,
|
SelectiveColorChoice,
|
||||||
AdjustmentChannel
|
AdjustmentChannel,
|
||||||
|
TonalRange,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1271,3 +1413,73 @@ mod test {
|
|||||||
assert!((a - 0.5).abs() < 1e-5, "alpha was {a}");
|
assert!((a - 0.5).abs() < 1e-5, "alpha was {a}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "std", test))]
|
||||||
|
mod color_balance_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Runs Color Balance on one gamma-space RGB value (0..255) and returns the gamma-space result on the same scale.
|
||||||
|
fn run(input: [f32; 3], shadows: [f32; 3], midtones: [f32; 3], highlights: [f32; 3], preserve_luminosity: bool) -> [f32; 3] {
|
||||||
|
let color = Color::from_gamma_srgb_channels(input[0] / 255., input[1] / 255., input[2] / 255., 1.);
|
||||||
|
let result = color_balance(
|
||||||
|
(),
|
||||||
|
Item::new_from_element(color),
|
||||||
|
shadows[0].into(),
|
||||||
|
shadows[1].into(),
|
||||||
|
shadows[2].into(),
|
||||||
|
midtones[0].into(),
|
||||||
|
midtones[1].into(),
|
||||||
|
midtones[2].into(),
|
||||||
|
highlights[0].into(),
|
||||||
|
highlights[1].into(),
|
||||||
|
highlights[2].into(),
|
||||||
|
preserve_luminosity.into(),
|
||||||
|
TonalRange::Midtones.into(),
|
||||||
|
);
|
||||||
|
let [r, g, b, _] = result.into_element().to_gamma_srgb_channels();
|
||||||
|
[r * 255., g * 255., b * 255.]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matched to within one 8-bit level.
|
||||||
|
fn assert_close(actual: [f32; 3], expected: [f32; 3]) {
|
||||||
|
for (actual, expected) in actual.iter().zip(expected) {
|
||||||
|
assert!((actual - expected).abs() <= 1., "expected {expected}, got {actual}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn midtones_are_a_gamma_with_a_toe() {
|
||||||
|
let none = [0., 0., 0.];
|
||||||
|
assert_close(run([100., 100., 100.], none, [100., 0., 0.], none, false), [160., 100., 100.]);
|
||||||
|
assert_close(run([200., 200., 200.], none, [100., 0., 0.], none, false), [226., 200., 200.]);
|
||||||
|
assert_close(run([4., 4., 4.], none, [100., 0., 0.], none, false), [16., 4., 4.]);
|
||||||
|
assert_close(run([1., 1., 1.], none, [100., 0., 0.], none, false), [4., 1., 1.]);
|
||||||
|
assert_close(run([100., 100., 100.], none, [-100., 0., 0.], none, false), [39., 100., 100.]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadows_and_highlights_move_the_end_points() {
|
||||||
|
let none = [0., 0., 0.];
|
||||||
|
assert_close(run([100., 100., 100.], [-100., 0., 0.], none, none, false), [0., 100., 100.]);
|
||||||
|
assert_close(run([150., 150., 150.], [-100., 0., 0.], none, none, false), [52., 150., 150.]);
|
||||||
|
assert_close(run([200., 200., 200.], [-100., 0., 0.], none, none, false), [138., 200., 200.]);
|
||||||
|
assert_close(run([100., 100., 100.], none, none, [100., 0., 0.], false), [187., 100., 100.]);
|
||||||
|
assert_close(run([155., 155., 155.], none, none, [100., 0., 0.], false), [255., 155., 155.]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserve_luminosity_makes_sliders_relative() {
|
||||||
|
let none = [0., 0., 0.];
|
||||||
|
assert_close(run([90., 90., 90.], none, [-100., 0., 0.], none, true), [59., 122., 122.]);
|
||||||
|
assert_close(run([90., 90., 90.], [50., 0., 0.], none, none, true), [90., 50., 50.]);
|
||||||
|
assert_close(run([120., 120., 120.], none, none, [-100., 0., 0.], true), [120., 197., 197.]);
|
||||||
|
assert_close(run([90., 90., 90.], [100., 100., 100.], [100., 100., 100.], [100., 100., 100.], true), [90., 90., 90.]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combined_tones_use_integer_arithmetic() {
|
||||||
|
// Red: black 39, white 235, gamma 1.09; green: gamma 0.91; blue: white 210, gamma 1.13
|
||||||
|
let result = run([128., 128., 128.], [-39., 6., 42.], [21., 0., -25.], [20., -35., 45.], false);
|
||||||
|
assert_close(result, [124., 120., 164.]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user