Add a "Cyclic" gradient option that closes the ramp into a seamless loop (#4416)

* Add a Cyclic gradient attribute that wraps the stop list through the 1|0 boundary back to the first stop

* Show the wrap segment's midpoint diamond in the color picker spectrum strip when cyclic

* Keep the wrap midpoint diamond tracking the pointer by a wrapped strip width when dragged past the ends

* Give the Gradient tool's viewport overlay the same cyclic wrap midpoint diamond and drag behavior

* Correct the docs claiming the final stop's midpoint is always ignored now that cyclic uses it

* Move the gradient cyclic toggle onto the Ends row as a Link icon checkbox

* Update labels

* Register GradientHueDirection with the Data panel so its attribute column renders

* Reword wrap segment to wrapped interval and stray segment usages to interval

* Clean up comments

* Fix the color picker write-back losing default position elision and the blend path guessing the cyclic flag
This commit is contained in:
Keavon Chambers
2026-08-06 18:52:00 -07:00
committed by GitHub
parent 461ddbc872
commit 4b01abe36d
25 changed files with 863 additions and 257 deletions

View File

@@ -1401,6 +1401,14 @@ fn gradient_space(_: impl Ctx, gradient: Item<Gradient>, space: Item<vector_type
gradient
}
/// Sets whether each gradient in the input list treats its stops as a cycle, interpolating from the last stop back around to the first.
#[node_macro::node(category("Gradient"))]
fn gradient_cyclic(_: impl Ctx, gradient: Item<Gradient>, cyclic: Item<bool>) -> Item<Gradient> {
let mut gradient = gradient;
gradient.set_attribute(core_types::ATTR_GRADIENT_CYCLIC, *cyclic.element());
gradient
}
/// Sets which way around the hue wheel each gradient in the input list interpolates, for polar color spaces.
#[node_macro::node(category("Gradient"))]
fn gradient_hue_direction(_: impl Ctx, gradient: Item<Gradient>, hue_direction: Item<vector_types::GradientHueDirection>) -> Item<Gradient> {
@@ -1422,7 +1430,7 @@ fn gradient_positions(_: impl Ctx, gradient: Item<Gradient>, positions: List<f64
/// Sets the interpolation midpoint for each interval between gradient stops, a factor from 0 to 1 where the 0.5 default means linear interpolation and another value skews the transition speed toward one stop or the other.
///
/// The final stop belongs to no interval so its midpoint is ignored.
/// The final stop's midpoint controls the wrap back around to the first stop when the gradient is cyclic, and is otherwise ignored.
///
/// A list shorter than the stop count repeats its last value, a longer list is truncated, and an empty list sets each midpoint to its default of 0.5.
#[node_macro::node(category("Gradient"))]
@@ -1438,8 +1446,11 @@ fn gradient_midpoints(_: impl Ctx, gradient: Item<Gradient>, midpoints: List<f64
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 gradient_space = gradient.attribute_cloned_or_default::<vector_types::GradientSpace>(core_types::ATTR_GRADIENT_SPACE);
let gradient_cyclic = gradient.attribute_cloned_or_default::<bool>(core_types::ATTR_GRADIENT_CYCLIC);
let gradient_hue_direction = gradient.attribute_cloned_or_default::<vector_types::GradientHueDirection>(core_types::ATTR_GRADIENT_HUE_DIRECTION);
let color = gradient.element().evaluate(*position.element(), gradient_spread, gradient_space, gradient_hue_direction);
let color = gradient
.element()
.evaluate(*position.element(), gradient_spread, gradient_cyclic, gradient_space, gradient_hue_direction);
Item::new_from_element(color)
}