Merge origin/master into the async record refactor

Scaffolding merge for the reconcile; the final series to master is
authored fresh. Rank plumbing resolves to our axis-IR model, the node
macro and the LaneSource render walk stay ours, master's vector
restructure and gradient vocabulary are adopted, and the paint and
appearance adoption is deliberately deferred behind our fill and stroke
markers.
This commit is contained in:
Dennis Kobert
2026-09-08 15:03:57 +00:00
385 changed files with 34669 additions and 20078 deletions

View File

@@ -13,7 +13,7 @@ impl Adjust<Color> for Color {
mod adjust_std {
use super::*;
use raster_types::{CPU, Raster};
use vector_types::GradientStops;
use vector_types::Gradient;
impl Adjust<Color> for Raster<CPU> {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
@@ -22,11 +22,9 @@ mod adjust_std {
}
}
}
impl Adjust<Color> for GradientStops {
impl Adjust<Color> for Gradient {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for color in self.color.iter_mut() {
*color = map_fn(color);
}
*self = self.map_colors(map_fn);
}
}
}

View File

@@ -6,6 +6,8 @@ use core::fmt::Debug;
use glam::Vec3;
use no_std_types::color::{Color, linear_to_srgb, srgb_to_linear};
use no_std_types::context::Ctx;
#[cfg(not(feature = "std"))]
use no_std_types::list::ShaderItem as Item;
use no_std_types::registry::types::{AngleF32, PercentageF32, SignedPercentageF32};
use node_macro::BufferStruct;
use num_enum::{FromPrimitive, IntoPrimitive};
@@ -14,7 +16,7 @@ use num_traits::float::Float;
#[cfg(feature = "std")]
use raster_types::{CPU, Raster};
#[cfg(feature = "std")]
use vector_types::GradientStops;
use vector_types::Gradient;
// TODO: Implement the following:
// Color Balance
@@ -56,10 +58,13 @@ fn luminance<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::Cac
GradientStops,
)]
#[gpu_image]
mut input: T,
luminance_calc: LuminanceCalculation,
) -> T {
input.adjust(|color| {
input: Item<T>,
luminance_calc: Item<LuminanceCalculation>,
) -> Item<T> {
let mut input = input;
let luminance_calc = luminance_calc.into_element();
input.element_mut().adjust(|color| {
let luminance = match luminance_calc {
LuminanceCalculation::SRGB => color.luminance_rec_709(),
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
@@ -81,16 +86,20 @@ fn gamma_correction<T: Adjust<Color> + Clone + Send + Sync + no_std_types::conte
GradientStops,
)]
#[gpu_image]
mut input: T,
input: Item<T>,
#[default(2.2)]
#[range]
#[hard(0.0001..)]
#[soft(0.01..10)]
gamma: f32,
inverse: bool,
) -> T {
gamma: Item<f32>,
inverse: Item<bool>,
) -> Item<T> {
let mut input = input;
let gamma = gamma.into_element();
let inverse = inverse.into_element();
let exponent = if inverse { 1. / gamma } else { gamma };
input.adjust(|color| color.apply_gamma_exponent(exponent));
input.element_mut().adjust(|color| color.apply_gamma_exponent(exponent));
input
}
@@ -103,10 +112,13 @@ fn extract_channel<T: Adjust<Color> + Clone + Send + Sync + no_std_types::contex
GradientStops,
)]
#[gpu_image]
mut input: T,
channel: RedGreenBlueAlpha,
) -> T {
input.adjust(|color| {
input: Item<T>,
channel: Item<RedGreenBlueAlpha>,
) -> Item<T> {
let mut input = input;
let channel = channel.into_element();
input.element_mut().adjust(|color| {
let extracted_value = match channel {
RedGreenBlueAlpha::Red => color.r(),
RedGreenBlueAlpha::Green => color.g(),
@@ -127,9 +139,10 @@ fn make_opaque<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::C
GradientStops,
)]
#[gpu_image]
mut input: T,
) -> T {
input.adjust(|color| {
input: Item<T>,
) -> Item<T> {
let mut input = input;
input.element_mut().adjust(|color| {
if color.a() == 0. {
return color.with_alpha(1.);
}
@@ -149,10 +162,14 @@ fn brightness_contrast_classic<T: Adjust<Color> + Clone + Send + Sync + no_std_t
GradientStops,
)]
#[gpu_image]
mut input: T,
brightness: SignedPercentageF32,
contrast: SignedPercentageF32,
) -> T {
input: Item<T>,
brightness: Item<SignedPercentageF32>,
contrast: Item<SignedPercentageF32>,
) -> Item<T> {
let mut input = input;
let brightness = brightness.into_element();
let contrast = contrast.into_element();
let brightness = brightness / 255.;
let contrast = contrast / 100.;
@@ -160,7 +177,7 @@ fn brightness_contrast_classic<T: Adjust<Color> + Clone + Send + Sync + no_std_t
let offset = brightness * contrast + brightness - contrast / 2.;
input.adjust(|color| color.map_gamma_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)));
input.element_mut().adjust(|color| color.map_gamma_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)));
input
}
@@ -180,15 +197,20 @@ fn brightness_contrast<T: Adjust<Color> + Clone + Send + Sync + no_std_types::co
GradientStops,
)]
#[gpu_image]
mut input: T,
brightness: SignedPercentageF32,
contrast: SignedPercentageF32,
use_classic: bool,
) -> T {
input: Item<T>,
brightness: Item<SignedPercentageF32>,
contrast: Item<SignedPercentageF32>,
use_classic: Item<bool>,
) -> Item<T> {
let use_classic = use_classic.into_element();
if use_classic {
return brightness_contrast_classic(_ctx, input, brightness, contrast);
}
let mut input = input;
let brightness = brightness.into_element();
let contrast = contrast.into_element();
const WINDOW_SIZE: usize = 1024;
// Brightness LUT
@@ -239,7 +261,7 @@ fn brightness_contrast<T: Adjust<Color> + Clone + Send + Sync + no_std_types::co
});
let lut_max = (combined_lut.len() - 1) as f32;
input.adjust(|color| color.map_gamma_rgb(|c| combined_lut[(c * lut_max).round() as usize]));
input.element_mut().adjust(|color| color.map_gamma_rgb(|c| combined_lut[(c * lut_max).round() as usize]));
input
}
@@ -261,14 +283,21 @@ fn levels<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheH
GradientStops,
)]
#[gpu_image]
mut image: T,
#[default(0.)] shadows: PercentageF32,
#[default(50.)] midtones: PercentageF32,
#[default(100.)] highlights: PercentageF32,
#[default(0.)] output_minimums: PercentageF32,
#[default(100.)] output_maximums: PercentageF32,
) -> T {
image.adjust(|color| {
image: Item<T>,
#[default(0.)] shadows: Item<PercentageF32>,
#[default(50.)] midtones: Item<PercentageF32>,
#[default(100.)] highlights: Item<PercentageF32>,
#[default(0.)] output_minimums: Item<PercentageF32>,
#[default(100.)] output_maximums: Item<PercentageF32>,
) -> Item<T> {
let mut image = image;
let shadows = shadows.into_element();
let midtones = midtones.into_element();
let highlights = highlights.into_element();
let output_minimums = output_minimums.into_element();
let output_maximums = output_maximums.into_element();
image.element_mut().adjust(|color| {
// Levels math operates in gamma space
let [mut r, mut g, mut b, a] = color.to_gamma_srgb_channels();
@@ -340,34 +369,43 @@ fn black_and_white<T: Adjust<Color> + Clone + Send + Sync + no_std_types::contex
GradientStops,
)]
#[gpu_image]
mut image: T,
#[default(Color::BLACK)] tint: Color,
image: Item<T>,
#[default(Color::BLACK)] tint: Item<Color>,
#[default(40.)]
#[range]
#[soft(-200..300)]
reds: PercentageF32,
reds: Item<PercentageF32>,
#[default(60.)]
#[range]
#[soft(-200..300)]
yellows: PercentageF32,
yellows: Item<PercentageF32>,
#[default(40.)]
#[range]
#[soft(-200..300)]
greens: PercentageF32,
greens: Item<PercentageF32>,
#[default(60.)]
#[range]
#[soft(-200..300)]
cyans: PercentageF32,
cyans: Item<PercentageF32>,
#[default(20.)]
#[range]
#[soft(-200..300)]
blues: PercentageF32,
blues: Item<PercentageF32>,
#[default(80.)]
#[range]
#[soft(-200..300)]
magentas: PercentageF32,
) -> T {
image.adjust(|color| {
magentas: Item<PercentageF32>,
) -> Item<T> {
let mut image = image;
let tint = tint.into_element();
let reds = reds.into_element();
let yellows = yellows.into_element();
let greens = greens.into_element();
let cyans = cyans.into_element();
let blues = blues.into_element();
let magentas = magentas.into_element();
image.element_mut().adjust(|color| {
// Black & White channel weights are tuned for gamma-space values
let [r, g, b, alpha_part] = color.to_gamma_srgb_channels();
@@ -423,12 +461,17 @@ fn hue_saturation<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context
GradientStops,
)]
#[gpu_image]
mut input: T,
hue_shift: AngleF32,
saturation_shift: SignedPercentageF32,
lightness_shift: SignedPercentageF32,
) -> T {
input.adjust(|color| {
input: Item<T>,
hue_shift: Item<AngleF32>,
saturation_shift: Item<SignedPercentageF32>,
lightness_shift: Item<SignedPercentageF32>,
) -> Item<T> {
let mut input = input;
let hue_shift = hue_shift.into_element();
let saturation_shift = saturation_shift.into_element();
let lightness_shift = lightness_shift.into_element();
input.element_mut().adjust(|color| {
// HSL operates on gamma-space channels
let [hue, saturation, lightness, alpha] = color.to_hsla();
@@ -455,9 +498,10 @@ fn invert<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheH
GradientStops,
)]
#[gpu_image]
mut input: T,
) -> T {
input.adjust(|color| {
input: Item<T>,
) -> Item<T> {
let mut input = input;
input.element_mut().adjust(|color| {
// 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)
@@ -476,12 +520,17 @@ fn threshold<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::Cac
GradientStops,
)]
#[gpu_image]
mut image: T,
#[default(50.)] min_luminance: PercentageF32,
#[default(100.)] max_luminance: PercentageF32,
luminance_calc: LuminanceCalculation,
) -> T {
image.adjust(|color| {
image: Item<T>,
#[default(50.)] min_luminance: Item<PercentageF32>,
#[default(100.)] max_luminance: Item<PercentageF32>,
luminance_calc: Item<LuminanceCalculation>,
) -> Item<T> {
let mut image = image;
let min_luminance = min_luminance.into_element();
let max_luminance = max_luminance.into_element();
let luminance_calc = luminance_calc.into_element();
image.element_mut().adjust(|color| {
let min_luminance = srgb_to_linear(min_luminance / 100.);
let max_luminance = srgb_to_linear(max_luminance / 100.);
@@ -522,10 +571,13 @@ fn vibrance<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::Cach
GradientStops,
)]
#[gpu_image]
mut image: T,
vibrance: SignedPercentageF32,
) -> T {
image.adjust(|color| {
image: Item<T>,
vibrance: Item<SignedPercentageF32>,
) -> Item<T> {
let mut image = image;
let vibrance = vibrance.into_element();
image.element_mut().adjust(|color| {
let r_raw = color.r();
let g_raw = color.g();
let b_raw = color.b();
@@ -724,66 +776,73 @@ fn channel_mixer<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context:
GradientStops,
)]
#[gpu_image]
mut image: T,
image: Item<T>,
monochrome: bool,
monochrome: Item<bool>,
#[default(40.)]
#[name("Red")]
monochrome_r: f32,
monochrome_r: Item<f32>,
#[default(40.)]
#[name("Green")]
monochrome_g: f32,
monochrome_g: Item<f32>,
#[default(20.)]
#[name("Blue")]
monochrome_b: f32,
monochrome_b: Item<f32>,
#[default(0.)]
#[name("Constant")]
monochrome_c: f32,
monochrome_c: Item<f32>,
#[default(100.)]
#[name("(Red) Red")]
red_r: f32,
red_r: Item<f32>,
#[default(0.)]
#[name("(Red) Green")]
red_g: f32,
red_g: Item<f32>,
#[default(0.)]
#[name("(Red) Blue")]
red_b: f32,
red_b: Item<f32>,
#[default(0.)]
#[name("(Red) Constant")]
red_c: f32,
red_c: Item<f32>,
#[default(0.)]
#[name("(Green) Red")]
green_r: f32,
green_r: Item<f32>,
#[default(100.)]
#[name("(Green) Green")]
green_g: f32,
green_g: Item<f32>,
#[default(0.)]
#[name("(Green) Blue")]
green_b: f32,
green_b: Item<f32>,
#[default(0.)]
#[name("(Green) Constant")]
green_c: f32,
green_c: Item<f32>,
#[default(0.)]
#[name("(Blue) Red")]
blue_r: f32,
blue_r: Item<f32>,
#[default(0.)]
#[name("(Blue) Green")]
blue_g: f32,
blue_g: Item<f32>,
#[default(100.)]
#[name("(Blue) Blue")]
blue_b: f32,
blue_b: Item<f32>,
#[default(0.)]
#[name("(Blue) Constant")]
blue_c: f32,
blue_c: Item<f32>,
// Display-only properties (not used within the node)
_output_channel: RedGreenBlue,
) -> T {
image.adjust(|color| {
_output_channel: Item<RedGreenBlue>,
) -> Item<T> {
let mut image = image;
let monochrome = monochrome.into_element();
let (monochrome_r, monochrome_g, monochrome_b, monochrome_c) = (monochrome_r.into_element(), monochrome_g.into_element(), monochrome_b.into_element(), monochrome_c.into_element());
let (red_r, red_g, red_b, red_c) = (red_r.into_element(), red_g.into_element(), red_b.into_element(), red_c.into_element());
let (green_r, green_g, green_b, green_c) = (green_r.into_element(), green_g.into_element(), green_b.into_element(), green_c.into_element());
let (blue_r, blue_g, blue_b, blue_c) = (blue_r.into_element(), blue_g.into_element(), blue_b.into_element(), blue_c.into_element());
image.element_mut().adjust(|color| {
let [r, g, b, a] = color.to_gamma_srgb_channels();
let (out_r, out_g, out_b) = if monochrome {
@@ -856,58 +915,70 @@ fn selective_color<T: Adjust<Color> + Clone + Send + Sync + no_std_types::contex
GradientStops,
)]
#[gpu_image]
mut image: T,
image: Item<T>,
mode: RelativeAbsolute,
mode: Item<RelativeAbsolute>,
#[name("(Reds) Cyan")] r_c: f32,
#[name("(Reds) Magenta")] r_m: f32,
#[name("(Reds) Yellow")] r_y: f32,
#[name("(Reds) Black")] r_k: f32,
#[name("(Reds) Cyan")] r_c: Item<f32>,
#[name("(Reds) Magenta")] r_m: Item<f32>,
#[name("(Reds) Yellow")] r_y: Item<f32>,
#[name("(Reds) Black")] r_k: Item<f32>,
#[name("(Yellows) Cyan")] y_c: f32,
#[name("(Yellows) Magenta")] y_m: f32,
#[name("(Yellows) Yellow")] y_y: f32,
#[name("(Yellows) Black")] y_k: f32,
#[name("(Yellows) Cyan")] y_c: Item<f32>,
#[name("(Yellows) Magenta")] y_m: Item<f32>,
#[name("(Yellows) Yellow")] y_y: Item<f32>,
#[name("(Yellows) Black")] y_k: Item<f32>,
#[name("(Greens) Cyan")] g_c: f32,
#[name("(Greens) Magenta")] g_m: f32,
#[name("(Greens) Yellow")] g_y: f32,
#[name("(Greens) Black")] g_k: f32,
#[name("(Greens) Cyan")] g_c: Item<f32>,
#[name("(Greens) Magenta")] g_m: Item<f32>,
#[name("(Greens) Yellow")] g_y: Item<f32>,
#[name("(Greens) Black")] g_k: Item<f32>,
#[name("(Cyans) Cyan")] c_c: f32,
#[name("(Cyans) Magenta")] c_m: f32,
#[name("(Cyans) Yellow")] c_y: f32,
#[name("(Cyans) Black")] c_k: f32,
#[name("(Cyans) Cyan")] c_c: Item<f32>,
#[name("(Cyans) Magenta")] c_m: Item<f32>,
#[name("(Cyans) Yellow")] c_y: Item<f32>,
#[name("(Cyans) Black")] c_k: Item<f32>,
#[name("(Blues) Cyan")] b_c: f32,
#[name("(Blues) Magenta")] b_m: f32,
#[name("(Blues) Yellow")] b_y: f32,
#[name("(Blues) Black")] b_k: f32,
#[name("(Blues) Cyan")] b_c: Item<f32>,
#[name("(Blues) Magenta")] b_m: Item<f32>,
#[name("(Blues) Yellow")] b_y: Item<f32>,
#[name("(Blues) Black")] b_k: Item<f32>,
#[name("(Magentas) Cyan")] m_c: f32,
#[name("(Magentas) Magenta")] m_m: f32,
#[name("(Magentas) Yellow")] m_y: f32,
#[name("(Magentas) Black")] m_k: f32,
#[name("(Magentas) Cyan")] m_c: Item<f32>,
#[name("(Magentas) Magenta")] m_m: Item<f32>,
#[name("(Magentas) Yellow")] m_y: Item<f32>,
#[name("(Magentas) Black")] m_k: Item<f32>,
#[name("(Whites) Cyan")] w_c: f32,
#[name("(Whites) Magenta")] w_m: f32,
#[name("(Whites) Yellow")] w_y: f32,
#[name("(Whites) Black")] w_k: f32,
#[name("(Whites) Cyan")] w_c: Item<f32>,
#[name("(Whites) Magenta")] w_m: Item<f32>,
#[name("(Whites) Yellow")] w_y: Item<f32>,
#[name("(Whites) Black")] w_k: Item<f32>,
#[name("(Neutrals) Cyan")] n_c: f32,
#[name("(Neutrals) Magenta")] n_m: f32,
#[name("(Neutrals) Yellow")] n_y: f32,
#[name("(Neutrals) Black")] n_k: f32,
#[name("(Neutrals) Cyan")] n_c: Item<f32>,
#[name("(Neutrals) Magenta")] n_m: Item<f32>,
#[name("(Neutrals) Yellow")] n_y: Item<f32>,
#[name("(Neutrals) Black")] n_k: Item<f32>,
#[name("(Blacks) Cyan")] k_c: f32,
#[name("(Blacks) Magenta")] k_m: f32,
#[name("(Blacks) Yellow")] k_y: f32,
#[name("(Blacks) Black")] k_k: f32,
#[name("(Blacks) Cyan")] k_c: Item<f32>,
#[name("(Blacks) Magenta")] k_m: Item<f32>,
#[name("(Blacks) Yellow")] k_y: Item<f32>,
#[name("(Blacks) Black")] k_k: Item<f32>,
_colors: SelectiveColorChoice,
) -> T {
image.adjust(|color| {
_colors: Item<SelectiveColorChoice>,
) -> Item<T> {
let mut image = image;
let mode = mode.into_element();
let (r_c, r_m, r_y, r_k) = (r_c.into_element(), r_m.into_element(), r_y.into_element(), r_k.into_element());
let (y_c, y_m, y_y, y_k) = (y_c.into_element(), y_m.into_element(), y_y.into_element(), y_k.into_element());
let (g_c, g_m, g_y, g_k) = (g_c.into_element(), g_m.into_element(), g_y.into_element(), g_k.into_element());
let (c_c, c_m, c_y, c_k) = (c_c.into_element(), c_m.into_element(), c_y.into_element(), c_k.into_element());
let (b_c, b_m, b_y, b_k) = (b_c.into_element(), b_m.into_element(), b_y.into_element(), b_k.into_element());
let (m_c, m_m, m_y, m_k) = (m_c.into_element(), m_m.into_element(), m_y.into_element(), m_k.into_element());
let (w_c, w_m, w_y, w_k) = (w_c.into_element(), w_m.into_element(), w_y.into_element(), w_k.into_element());
let (n_c, n_m, n_y, n_k) = (n_c.into_element(), n_m.into_element(), n_y.into_element(), n_k.into_element());
let (k_c, k_m, k_y, k_k) = (k_c.into_element(), k_m.into_element(), k_y.into_element(), k_k.into_element());
image.element_mut().adjust(|color| {
let [r, g, b, a] = color.to_gamma_srgb_channels();
let min = |a: f32, b: f32, c: f32| a.min(b).min(c);
@@ -1000,13 +1071,15 @@ fn posterize<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::Cac
GradientStops,
)]
#[gpu_image]
mut input: T,
input: Item<T>,
#[default(4)]
#[hard(2..)]
levels: u32,
) -> T {
let levels = levels as f32;
input.adjust(|color| {
levels: Item<u32>,
) -> Item<T> {
let mut input = input;
let levels = levels.into_element() as f32;
input.element_mut().adjust(|color| {
let number_of_areas = levels.recip();
let size_of_areas = (levels - 1.).recip();
color.map_gamma_rgb(|c| (c / number_of_areas).floor() * size_of_areas)
@@ -1029,16 +1102,21 @@ fn exposure<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::Cach
GradientStops,
)]
#[gpu_image]
mut input: T,
exposure: f32,
offset: f32,
input: Item<T>,
exposure: Item<f32>,
offset: Item<f32>,
#[default(1.)]
#[range]
#[hard(0.0001..)]
#[soft(0.01..10)]
gamma_correction: f32,
) -> T {
input.adjust(|color| {
gamma_correction: Item<f32>,
) -> Item<T> {
let mut input = input;
let exposure = exposure.into_element();
let offset = offset.into_element();
let gamma_correction = gamma_correction.into_element();
input.element_mut().adjust(|color| {
let adjusted = color
// Exposure
.map_rgb(|c: f32| c * 2_f32.powf(exposure))

View File

@@ -6,7 +6,7 @@ use no_std_types::registry::types::PercentageF32;
#[cfg(feature = "std")]
use raster_types::{CPU, Raster};
#[cfg(feature = "std")]
use vector_types::{GradientStop, GradientStops};
use vector_types::{Gradient, GradientStop};
pub trait Blend<P: Pixel> {
fn blend(&self, under: &Self, blend_fn: impl Fn(P, P) -> P) -> Self;
@@ -36,73 +36,28 @@ mod blend_std {
}
}
impl Blend<Color> for GradientStops {
impl Blend<Color> for Gradient {
// TODO: This joining is unfaithful in several ways: it samples only at stop positions so midpoint curves flatten away;
// TODO: it evaluates both sources with default whole-ramp attributes rather than their own (which this element-level impl cannot read);
// TODO: and the output keeps over's attributes despite being sampled with defaults
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut combined_stops = self.position.iter().chain(under.position.iter()).copied().collect::<Vec<_>>();
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let mut combined_stops = self.positions(false).into_iter().chain(under.positions(false)).collect::<Vec<_>>();
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
combined_stops.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let over_evaluator = self.evaluator(Default::default());
let under_evaluator = under.evaluator(Default::default());
let stops = combined_stops.into_iter().map(|position| {
let over_color = self.evaluate(position);
let under_color = under.evaluate(position);
let color = blend_fn(over_color, under_color);
let color = blend_fn(over_evaluator.evaluate(position), under_evaluator.evaluate(position));
GradientStop { position, midpoint: 0.5, color }
});
GradientStops::new(stops)
// Positions stay explicit because eliding them needs the cyclic flag this impl can't read, and a wrong guess would relocate the stops
Gradient::new(stops)
}
}
}
#[inline(always)]
pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color {
let target_color = match blend_mode {
// Other utility blend modes (hidden from the normal list) - do not have alpha blend
BlendMode::Erase => return background.alpha_subtract(foreground),
BlendMode::Restore => return background.alpha_add(foreground),
BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground),
blend_mode => apply_blend_mode(foreground, background, blend_mode),
};
background.alpha_blend(target_color.apply_opacity(opacity))
}
pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color {
match blend_mode {
// Normal group
BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal),
// Darken group
BlendMode::Darken => background.blend_rgb(foreground, Color::blend_darken),
BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply),
BlendMode::ColorBurn => background.blend_rgb(foreground, Color::blend_color_burn),
BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn),
BlendMode::DarkerColor => background.blend_darker_color(foreground),
// Lighten group
BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten),
BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen),
BlendMode::ColorDodge => background.blend_rgb(foreground, Color::blend_color_dodge),
BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge),
BlendMode::LighterColor => background.blend_lighter_color(foreground),
// Contrast group
BlendMode::Overlay => foreground.blend_rgb(background, Color::blend_hardlight),
BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight),
BlendMode::HardLight => background.blend_rgb(foreground, Color::blend_hardlight),
BlendMode::VividLight => background.blend_rgb(foreground, Color::blend_vivid_light),
BlendMode::LinearLight => background.blend_rgb(foreground, Color::blend_linear_light),
BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light),
BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix),
// Inversion group
BlendMode::Difference => background.blend_rgb(foreground, Color::blend_difference),
BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion),
BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract),
BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide),
// Component group
BlendMode::Hue => background.blend_hue(foreground),
BlendMode::Saturation => background.blend_saturation(foreground),
BlendMode::Color => background.blend_color(foreground),
BlendMode::Luminosity => background.blend_luminosity(foreground),
// Other utility blend modes (hidden from the normal list) - do not have alpha blend
_ => panic!("Used blend mode without alpha blend"),
}
}
pub use no_std_types::blending::{apply_blend_mode, blend_colors};
#[cfg(feature = "std")]
#[node_macro::node(category("Raster"), cfg(feature = "std"))]
@@ -111,7 +66,7 @@ fn mix<T: Blend<Color> + Clone + Send + Sync + core_types::CacheHash + 'static>(
#[implementations(
Raster<CPU>,
Color,
GradientStops,
Gradient,
)]
#[gpu_image]
over: T,
@@ -119,7 +74,7 @@ fn mix<T: Blend<Color> + Clone + Send + Sync + core_types::CacheHash + 'static>(
#[implementations(
Raster<CPU>,
Color,
GradientStops,
Gradient,
)]
#[gpu_image]
under: T,
@@ -135,7 +90,7 @@ fn color_overlay<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context:
#[implementations(
Raster<CPU>,
Color,
GradientStops,
Gradient,
)]
#[gpu_image]
mut image: T,

View File

@@ -94,9 +94,9 @@ fn blur(
#[range]
#[hard(0..)]
#[soft(..100)]
radius: PixelLength,
radius: Item<PixelLength>,
/// Use a lower-quality box kernel instead of a circular Gaussian kernel. This is faster but produces boxy artifacts.
box_blur: bool,
box_blur: Item<bool>,
/// Opt to incorrectly apply the filter with color calculations in gamma space for compatibility with the results from other software.
gamma: bool,
) -> Raster<CPU> {

View File

@@ -1,9 +1,13 @@
//! Not immediately shader compatible due to needing [`GradientStops`] as a param, which needs [`Vec`]
//! Not immediately shader compatible due to needing [`Gradient`] as a param, which needs [`Vec`]
use crate::adjust::Adjust;
use core_types::{Color, Ctx};
use raster_types::{CPU, Raster};
use vector_types::GradientStops;
use vector_types::markers::{
GradientCyclic as GradientCyclicAttr, GradientHueDirection as GradientHueDirectionAttr, GradientInterpolation as GradientInterpolationAttr, GradientSpace as GradientSpaceAttr,
GradientSpread as GradientSpreadAttr,
};
use vector_types::{Gradient, GradientSettings};
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
@@ -14,21 +18,30 @@ fn gradient_map<T: Adjust<Color> + Clone + Send + Sync + core_types::CacheHash +
#[implementations(
Raster<CPU>,
Color,
GradientStops,
Gradient,
)]
mut image: T,
gradient: IList<GradientStops>,
#[default(Color::BLACK, Color::WHITE)] gradient: IList<Gradient>,
reverse: bool,
) -> T {
if gradient.is_empty() {
return image;
}
let gradient = gradient.element_ref(0);
// Master reads the whole-ramp settings off the item; ours ride the gradient's own lane.
let lane = gradient.lane(0);
let settings = GradientSettings {
spread: lane.attr::<GradientSpreadAttr>(),
cyclic: lane.attr::<GradientCyclicAttr>(),
space: lane.attr::<GradientSpaceAttr>(),
hue_direction: lane.attr::<GradientHueDirectionAttr>(),
interpolation: lane.attr::<GradientInterpolationAttr>(),
};
let evaluator = gradient.element_ref(0).evaluator(settings);
image.adjust(|color| {
let intensity = color.luminance_rec_709();
let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evaluate(intensity as f64)
evaluator.evaluate(intensity as f64)
});
image

View File

@@ -369,32 +369,32 @@ pub fn image(_: impl Ctx, resource: Resource) -> Raster<CPU> {
pub fn noise_pattern(
ctx: impl ExtractFootprint + Ctx,
_primary: (),
#[default(true)] clip: bool,
seed: u32,
#[default(true)] clip: Item<bool>,
seed: Item<u32>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_scale")]
#[default(10.)]
scale: f64,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_noise_type")] noise_type: NoiseType,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_domain_warp_type")] domain_warp_type: DomainWarpType,
scale: Item<f64>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_noise_type")] noise_type: Item<NoiseType>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_domain_warp_type")] domain_warp_type: Item<DomainWarpType>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_domain_warp_amplitude")]
#[default(100.)]
domain_warp_amplitude: f64,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_type")] fractal_type: FractalType,
domain_warp_amplitude: Item<f64>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_type")] fractal_type: Item<FractalType>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_octaves")]
#[default(3)]
fractal_octaves: u32,
fractal_octaves: Item<u32>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_lacunarity")]
#[default(2.)]
fractal_lacunarity: f64,
fractal_lacunarity: Item<f64>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_gain")]
#[default(0.5)]
fractal_gain: f64,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_weighted_strength")] fractal_weighted_strength: f64,
fractal_gain: Item<f64>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_fractal_weighted_strength")] fractal_weighted_strength: Item<f64>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_ping_pong_strength")]
#[default(2.)]
fractal_ping_pong_strength: f64,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_distance_function")] cellular_distance_function: CellularDistanceFunction,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_return_type")] cellular_return_type: CellularReturnType,
fractal_ping_pong_strength: Item<f64>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_distance_function")] cellular_distance_function: Item<CellularDistanceFunction>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_return_type")] cellular_return_type: Item<CellularReturnType>,
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_jitter")]
#[default(1.)]
cellular_jitter: f64,