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 @@ -368,7 +368,7 @@ impl ColorPickerMessageHandler {
gradient.set_midpoint(index as usize, position.clamp(MIN_MIDPOINT, MAX_MIDPOINT));
}
SpectrumInputUpdate::InsertMarker { position } => {
let new_index = gradient.insert_stop(position);
let new_index = gradient.insert_stop(position, self.gradient_interpolation);
self.active_marker_index = Some(new_index as u32);
self.active_marker_is_midpoint = false;
if let Some(color) = gradient.color(new_index) {
Expand Down
8 changes: 4 additions & 4 deletions editor/src/messages/tool/tool_messages/gradient_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,7 +1254,7 @@ impl Fsm for GradientToolFsmState {
// If click is on the line then insert point
if distance < (SELECTION_THRESHOLD * 2.) {
// Try and insert the new stop
if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport) {
if let Some(index) = insert_stop_at_point(&mut gradient, mouse, unit_to_viewport, appearance.gradient_interpolation) {
responses.add(DocumentMessage::StartTransaction);

let mut selected_gradient = SelectedGradient::new(gradient, appearance, source, layer, document);
Expand Down Expand Up @@ -1404,7 +1404,7 @@ impl Fsm for GradientToolFsmState {

if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
let mut new_gradient = gradient.clone();
if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport) {
if let Some(index) = insert_stop_at_point(&mut new_gradient, mouse, unit_to_viewport, appearance.gradient_interpolation) {
responses.add(DocumentMessage::StartTransaction);
transaction_started = true;

Expand Down Expand Up @@ -1715,10 +1715,10 @@ impl Fsm for GradientToolFsmState {
}
}

fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2) -> Option<usize> {
fn insert_stop_at_point(gradient: &mut Gradient, point: DVec2, unit_to_viewport: DAffine2, gradient_interpolation: GradientInterpolation) -> Option<usize> {
let (start, end) = gradient_handle_positions(unit_to_viewport);
let t = ((end - start).angle_to(point - start)).cos() * start.distance(point) / start.distance(end);
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t))
(0. ..=1.).contains(&t).then(|| gradient.insert_stop(t, gradient_interpolation))
}

fn dismiss_color_stop_color_picker(tool_data: &mut GradientToolData, responses: &mut VecDeque<Message>) {
Expand Down
47 changes: 29 additions & 18 deletions node-graph/libraries/vector-types/src/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,8 @@ impl Gradient {
/// Insert a new stop at the given position, sampling the gradient at that position to determine the new stop's color.
/// The new stop's midpoint is inherited from the interval it splits (or `0.5` if inserting at the very start).
/// Returns the index where the new stop was inserted.
pub fn insert_stop(&mut self, position: f64) -> usize {
let color = self.evaluate(position, Default::default());
pub fn insert_stop(&mut self, position: f64, gradient_interpolation: GradientInterpolation) -> usize {
let color = self.evaluate(position, Default::default(), gradient_interpolation);
let index = (0..self.len()).position(|i| self.position(i) > position).unwrap_or(self.len());
let midpoint = if index > 0 { self.midpoint(index - 1) } else { 0.5 };
self.insert_stop_values(position, midpoint, color)
Expand Down Expand Up @@ -631,7 +631,7 @@ impl Gradient {
}

/// Samples the gradient's color at `t`. Given a `t` outside the 0 to 1 range, the `gradient_spread` determines how the gradient extends.
pub fn evaluate(&self, t: f64, gradient_spread: GradientSpread) -> Color {
pub fn evaluate(&self, t: f64, gradient_spread: GradientSpread, gradient_interpolation: GradientInterpolation) -> Color {
let t = match gradient_spread {
GradientSpread::Pad => t.clamp(0., 1.),
GradientSpread::Repeat => t.rem_euclid(1.),
Expand Down Expand Up @@ -661,8 +661,7 @@ impl Gradient {
if t >= a.position && t <= b.position {
let normalized_t = (t - a.position) / (b.position - a.position);
let adjusted_t = apply_midpoint(normalized_t, a.midpoint);
// Sampling deliberately stays in linear light; the ramp's interpolation space attribute only shapes what the renderers draw
return a.color.lerp(&b.color, adjusted_t as f32);
return interpolate_stop_colors(a.color, b.color, adjusted_t as f32, gradient_interpolation);
}
}

Expand Down Expand Up @@ -966,7 +965,7 @@ mod tests {
fn default_is_empty_and_black_to_white_is_the_artist_starting_gradient() {
assert!(Gradient::default().is_empty());
assert_eq!(Gradient::black_to_white().positions(), vec![0., 1.]);
assert_eq!(Gradient::default().evaluate(0.5, Default::default()), Color::BLACK);
assert_eq!(Gradient::default().evaluate(0.5, Default::default(), Default::default()), Color::BLACK);
}

#[test]
Expand Down Expand Up @@ -1156,18 +1155,30 @@ mod tests {
fn clear_spread_evaluates_to_transparency_outside_the_unit_range() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);

assert_eq!(gradient.evaluate(-0.25, GradientSpread::Clear), Color::TRANSPARENT);
assert_eq!(gradient.evaluate(1.25, GradientSpread::Clear), Color::TRANSPARENT);
assert_eq!(gradient.evaluate(-0.25, GradientSpread::Clear, Default::default()), Color::TRANSPARENT);
assert_eq!(gradient.evaluate(1.25, GradientSpread::Clear, Default::default()), Color::TRANSPARENT);

for t in [0., 0.25, 1.] {
assert_eq!(
gradient.evaluate(t, GradientSpread::Clear),
gradient.evaluate(t, GradientSpread::Pad),
gradient.evaluate(t, GradientSpread::Clear, Default::default()),
gradient.evaluate(t, GradientSpread::Pad, Default::default()),
"inside the range Clear must match Pad at t = {t}"
);
}
}

#[test]
fn evaluate_follows_the_interpolation_space() {
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);

let linear = gradient.evaluate(0.5, Default::default(), GradientInterpolation::SrgbLinear);
let gamma = gradient.evaluate(0.5, Default::default(), GradientInterpolation::SrgbGamma);

assert_eq!(linear, Color::BLACK.lerp(&Color::WHITE, 0.5));
assert_eq!(gamma, Color::BLACK.lerp_gamma_srgb(&Color::WHITE, 0.5));
assert_ne!(linear, gamma, "the two spaces must produce different mid colors between black and white");
}

#[test]
fn gradient_ui_write_back_elides_default_attributes() {
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
Expand Down Expand Up @@ -1208,8 +1219,8 @@ mod tests {
assert_eq!(sample_positions.first(), Some(&0.));
assert_eq!(sample_positions.last(), Some(&1.));

assert_eq!(gradient.evaluate(0., Default::default()), Color::RED);
assert_eq!(gradient.evaluate(1., Default::default()), Color::WHITE);
assert_eq!(gradient.evaluate(0., Default::default(), Default::default()), Color::RED);
assert_eq!(gradient.evaluate(1., Default::default(), Default::default()), Color::WHITE);
}

#[test]
Expand All @@ -1219,8 +1230,8 @@ mod tests {

let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).iter().map(|(position, ..)| *position).collect();
assert_eq!(sample_positions, vec![0., 1.]);
assert_eq!(gradient.evaluate(0., Default::default()), Color::BLACK);
assert_eq!(gradient.evaluate(1., Default::default()), Color::WHITE);
assert_eq!(gradient.evaluate(0., Default::default(), Default::default()), Color::BLACK);
assert_eq!(gradient.evaluate(1., Default::default(), Default::default()), Color::WHITE);
}

#[test]
Expand All @@ -1230,7 +1241,7 @@ mod tests {

let sample_positions: Vec<f64> = gradient.interpolated_samples(GradientInterpolation::SrgbGamma).iter().map(|(position, ..)| *position).collect();
assert_eq!(sample_positions, vec![0., 1.]);
assert_eq!(gradient.evaluate(0.5, Default::default()), Color::WHITE.lerp(&Color::RED, 0.5));
assert_eq!(gradient.evaluate(0.5, Default::default(), Default::default()), Color::WHITE.lerp(&Color::RED, 0.5));

// A non-finite position is preserved as nondefault so write-back elision cannot resurrect the dropped stop
assert!(gradient.nondefault_positions().is_some());
Expand All @@ -1239,7 +1250,7 @@ mod tests {
let mut gradient = Gradient::from(vec![Color::WHITE, Color::RED]);
gradient.set_positions(&[f64::NAN, f64::NAN]);
assert!(gradient.interpolated_samples(GradientInterpolation::SrgbGamma).is_empty());
assert_eq!(gradient.evaluate(0.5, Default::default()), Color::BLACK);
assert_eq!(gradient.evaluate(0.5, Default::default(), Default::default()), Color::BLACK);
}

#[test]
Expand All @@ -1254,10 +1265,10 @@ mod tests {
#[test]
fn nan_midpoints_read_as_linear() {
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
let linear_result = gradient.evaluate(0.25, Default::default());
let linear_result = gradient.evaluate(0.25, Default::default(), Default::default());

gradient.set_midpoints(&[f64::NAN, f64::NAN]);
assert_eq!(gradient.evaluate(0.25, Default::default()), linear_result);
assert_eq!(gradient.evaluate(0.25, Default::default(), Default::default()), linear_result);
let no_nan_annotations = gradient
.interpolated_samples(GradientInterpolation::SrgbGamma)
.iter()
Expand Down
5 changes: 3 additions & 2 deletions node-graph/nodes/math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1425,11 +1425,12 @@ fn gradient_midpoints(_: impl Ctx, gradient: Item<Gradient>, midpoints: List<f64
gradient
}

/// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, Repeat, or Clear.
/// Evaluates the color at the specified position along the gradient, given a position from 0 (left) to 1 (right). Positions beyond that range follow the gradient's `gradient_spread` attribute: Pad (default), Reflect, Repeat, or Clear. Colors between stops blend in the gradient's `gradient_interpolation` color space.
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), #[default(Color::BLACK, Color::WHITE)] gradient: Item<Gradient>, position: Item<Fraction>) -> Item<Color> {
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
let color = gradient.element().evaluate(*position.element(), gradient_spread);
let gradient_interpolation = gradient.attribute_cloned_or_default::<vector_types::GradientInterpolation>(core_types::ATTR_GRADIENT_INTERPOLATION);
let color = gradient.element().evaluate(*position.element(), gradient_spread, gradient_interpolation);
Item::new_from_element(color)
}

Expand Down
7 changes: 5 additions & 2 deletions node-graph/nodes/raster/src/blending_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ mod blend_std {
}
}
impl Blend<Color> for Gradient {
// TODO: This joining is unfaithful in several ways: it samples only at stop positions so midpoint curves flatten away;
// it evaluates both sources with the default spread and interpolation rather than their own attributes (which this
// element-level impl cannot read); and the output keeps over's attributes despite being sampled in the default space
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut combined_stops = self.positions().into_iter().chain(under.positions()).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 stops = combined_stops.into_iter().map(|position| {
let over_color = self.evaluate(position, Default::default());
let under_color = under.evaluate(position, Default::default());
let over_color = self.evaluate(position, Default::default(), Default::default());
Comment thread
Keavon marked this conversation as resolved.
let under_color = under.evaluate(position, Default::default(), Default::default());
let color = blend_fn(over_color, under_color);
GradientStop { position, midpoint: 0.5, color }
});
Expand Down
3 changes: 2 additions & 1 deletion node-graph/nodes/raster/src/gradient_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ async fn gradient_map<T: Adjust<Color> + Send>(
) -> Item<T> {
let mut image = image;
let gradient_spread = gradient.attribute_cloned_or_default::<vector_types::GradientSpread>(core_types::ATTR_GRADIENT_SPREAD);
let gradient_interpolation = gradient.attribute_cloned_or_default::<vector_types::GradientInterpolation>(core_types::ATTR_GRADIENT_INTERPOLATION);
let gradient = gradient.into_element();
let reverse = reverse.into_element();

image.element_mut().adjust(|color| {
let intensity = color.luminance_rec_709();
let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evaluate(intensity as f64, gradient_spread)
gradient.evaluate(intensity as f64, gradient_spread, gradient_interpolation)
});

image
Expand Down
8 changes: 5 additions & 3 deletions node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher};
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
use core_types::list::{ATTR_FILL, ATTR_GRADIENT_INTERPOLATION, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId;
Expand Down Expand Up @@ -32,7 +32,7 @@ use vector_types::vector::misc::{
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
};
use vector_types::vector::style::{DashPattern, Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::style::{DashPattern, Gradient, GradientInterpolation, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::vector::{PointDomain, RegionDomain};

Expand Down Expand Up @@ -141,6 +141,7 @@ where

let mut content = content;
let length = content.vector_count();
let gradient_interpolation = gradient.attribute_cloned_or_default::<GradientInterpolation>(ATTR_GRADIENT_INTERPOLATION);
let element = gradient.into_element();
let gradient = if reverse { element.reversed() } else { element };

Expand All @@ -158,7 +159,8 @@ where
},
};

let color = gradient.evaluate(factor, Default::default());
// The factor spans 0..=1 inclusively, so the spread deliberately stays Pad (Repeat would wrap the final element onto the first stop's color)
let color = gradient.evaluate(factor, Default::default(), gradient_interpolation);
let paint = List::new_from_element(color).into_graphic_list();

if fill {
Expand Down
Loading