Add a gradient interpolation attribute with Stepped, Linear (existing), and Smooth modes (#4418)

* Delete dead code function

* Add a Gradient Interpolation axis with Stepped, Linear, and Smooth paths through the stops

* Keep the gradient interpolation attached when dragging stops in the picker

* Give the gradient popover's dropdowns the same tooltips as their row labels

* Order the gradient popover with Intrp. above Space

* Hold gradient stops in place when the cyclic flag is toggled

* Fix wrong Smooth gradient colors around stops sitting on the ramp boundaries

* Consolidate gradient settings plumbing and fix bugs

* Bundle whole-ramp gradient settings into GradientSettings across the editor plumbing

* Name the Smooth gradient interpolation's spline type in its tooltip

* Linearize the gamma hex stop colors recovered from imported Graphite SVGs

* Code review

* Aim the Smooth bake's seam subdivision at the copied boundary color

* Add GradientEvaluator to build gradient sampling state once per loop instead of per sample

* Correct the gradient evaluator's documented setup cost to O(n log n)
This commit is contained in:
Keavon Chambers
2026-08-07 16:43:36 -07:00
committed by Dennis Kobert
parent 244a7e0f78
commit 6e32e97ba1
24 changed files with 1552 additions and 750 deletions

View File

@@ -12,7 +12,8 @@ use rand::{Rng, SeedableRng};
use std::ops::{Add, Mul, Rem, Sub};
use vector_types::Gradient;
use vector_types::markers::{
GradientCyclic as GradientCyclicAttr, GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr,
GradientCyclic as GradientCyclicAttr, GradientForm as GradientFormAttr, GradientHueDirection as GradientHueDirectionAttr, GradientInterpolation as GradientInterpolationAttr,
GradientSpace as GradientSpaceAttr, GradientSpread as GradientSpreadAttr,
};
/// The struct that stores the context for the maths parser.
@@ -1228,6 +1229,12 @@ fn gradient_hue_direction(_: impl Ctx, gradient: Gradient, gradient_hue_directio
(gradient, Attr(gradient_hue_direction))
}
/// Sets how the color progresses across each interval between gradient stops: stepped, linear, or smoothstep.
#[node_macro::node(category("Gradient"))]
fn gradient_interpolation(_: impl Ctx, gradient: Gradient, gradient_interpolation: vector_types::GradientInterpolation) -> (Gradient, Attr<GradientInterpolationAttr>) {
(gradient, Attr(gradient_interpolation))
}
/// Sets the position of each of a gradient's stops, a factor from 0 to 1 along the gradient.
///
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each stop to its default evenly spaced position.
@@ -1263,11 +1270,14 @@ fn sample_gradient(
return Err(GraphError::past_end().into());
}
let gradient_spread = gradient.lane(0).attr::<GradientSpreadAttr>();
let gradient_space = gradient.lane(0).attr::<GradientSpaceAttr>();
let gradient_hue_direction = gradient.lane(0).attr::<GradientHueDirectionAttr>();
let gradient_cyclic = gradient.lane(0).attr::<GradientCyclicAttr>();
Ok(gradient.element_ref(0).evaluate(position, gradient_spread, gradient_cyclic, gradient_space, gradient_hue_direction))
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<GradientSpreadAttr>(),
cyclic: gradient.lane(0).attr::<GradientCyclicAttr>(),
space: gradient.lane(0).attr::<GradientSpaceAttr>(),
hue_direction: gradient.lane(0).attr::<GradientHueDirectionAttr>(),
interpolation: gradient.lane(0).attr::<GradientInterpolationAttr>(),
};
Ok(gradient.element_ref(0).evaluate(position, settings))
}
/// Constructs a footprint value which may be set to any transformation of a unit square describing a render area, and a render resolution at least 1x1 integer pixels.

View File

@@ -44,10 +44,10 @@ mod blend_std {
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, Default::default(), false, Default::default(), Default::default());
let under_color = under.evaluate(position, Default::default(), false, Default::default(), Default::default());
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 }
});

View File

@@ -23,16 +23,19 @@ fn gradient_map<T: Adjust<Color> + Clone + Send + Sync + core_types::CacheHash +
if gradient.is_empty() {
return image;
}
let gradient_spread = gradient.lane(0).attr::<vector_types::markers::GradientSpread>();
let gradient_space = gradient.lane(0).attr::<vector_types::markers::GradientSpace>();
let gradient_hue_direction = gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>();
let gradient_cyclic = gradient.lane(0).attr::<vector_types::markers::GradientCyclic>();
let gradient = gradient.element_ref(0);
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(),
cyclic: gradient.lane(0).attr::<vector_types::markers::GradientCyclic>(),
space: gradient.lane(0).attr::<vector_types::markers::GradientSpace>(),
hue_direction: gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>(),
interpolation: gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(),
};
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, gradient_spread, gradient_cyclic, gradient_space, gradient_hue_direction)
evaluator.evaluate(intensity as f64)
});
image

View File

@@ -45,17 +45,7 @@ use vector_types::vector::{PointDomain, RegionDomain};
/// The gradient color for one assign-colors position, replaying the
/// randomized draws up to it.
fn assign_color_at(
gradient: &Gradient,
gradient_cyclic: bool,
gradient_space: vector_types::GradientSpace,
gradient_hue_direction: vector_types::GradientHueDirection,
position: usize,
length: usize,
randomize: bool,
seed: SeedValue,
repeat_every: u32,
) -> Color {
fn assign_color_at(gradient: &Gradient, settings: vector_types::GradientSettings, position: usize, length: usize, randomize: bool, seed: SeedValue, repeat_every: u32) -> Color {
let factor = match randomize {
true => {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
@@ -71,7 +61,7 @@ fn assign_color_at(
_ => position as f64 % repeat_every as f64 / (repeat_every - 1) as f64,
},
};
gradient.evaluate(factor, Default::default(), gradient_cyclic, gradient_space, gradient_hue_direction)
gradient.evaluate(factor, settings)
}
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
@@ -114,30 +104,24 @@ fn assign_colors<'e>(
if gradient.is_empty() {
return Ok((content.lane(lane).map_element(element), Attr(existing_fill), Attr(existing_stroke)));
}
let gradient_cyclic = gradient.lane(0).attr::<vector_types::markers::GradientCyclic>();
let gradient_space = gradient.lane(0).attr::<vector_types::markers::GradientSpace>();
let gradient_hue_direction = gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>();
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(),
cyclic: gradient.lane(0).attr::<vector_types::markers::GradientCyclic>(),
space: gradient.lane(0).attr::<vector_types::markers::GradientSpace>(),
hue_direction: gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>(),
interpolation: gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(),
};
let gradient_element = gradient.element_ref(0);
let reversed;
let gradient_element = match reverse {
true => {
reversed = gradient_element.reversed();
reversed = gradient_element.reversed(settings.cyclic);
&reversed
}
false => gradient_element,
};
let color = assign_color_at(
gradient_element,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
lane,
content.len(),
randomize,
seed,
repeat_every,
);
let color = assign_color_at(gradient_element, settings, lane, content.len(), randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list();
let parked = park_paint(ctx.arena(), paint)?;
@@ -195,14 +179,18 @@ fn assign_colors_graphic<'e>(
if gradient.is_empty() {
return Ok(content.lane(lane).map_element(original.clone()));
}
let gradient_cyclic = gradient.lane(0).attr::<vector_types::markers::GradientCyclic>();
let gradient_space = gradient.lane(0).attr::<vector_types::markers::GradientSpace>();
let gradient_hue_direction = gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>();
let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(),
cyclic: gradient.lane(0).attr::<vector_types::markers::GradientCyclic>(),
space: gradient.lane(0).attr::<vector_types::markers::GradientSpace>(),
hue_direction: gradient.lane(0).attr::<vector_types::markers::GradientHueDirection>(),
interpolation: gradient.lane(0).attr::<vector_types::markers::GradientInterpolation>(),
};
let gradient_element = gradient.element_ref(0);
let reversed;
let gradient_element = match reverse {
true => {
reversed = gradient_element.reversed();
reversed = gradient_element.reversed(settings.cyclic);
&reversed
}
false => gradient_element,
@@ -243,17 +231,7 @@ fn assign_colors_graphic<'e>(
Some(mut rows) => {
for row in 0..rows.len() {
let has_stroke = rows.element(row).is_some_and(|vector| vector.stroke.is_some());
let color = assign_color_at(
gradient_element,
gradient_cyclic,
gradient_space,
gradient_hue_direction,
position + row,
length,
randomize,
seed,
repeat_every,
);
let color = assign_color_at(gradient_element, settings, position + row, length, randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list();
if fill {
set_paint_attribute_at(&mut rows, row, ATTR_FILL, paint.clone());