mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
New node: 'Gradient to Colors' for unwrapping a gradient ramp into its colors (#4425)
* New node: 'Gradient to Colors' for unwrapping a gradient ramp into its colors * Update 'Gradient Midpoints' wording
This commit is contained in:
committed by
Dennis Kobert
parent
1356139559
commit
29540adc8f
@@ -1,10 +1,11 @@
|
||||
use crate::record::Inherited;
|
||||
use core_types::arena::Arena;
|
||||
use core_types::attribute::{Attr, EditorLayerPath, Name0, Named, Opacity, OpacityFill, Transform as TransformAttr, WireValue};
|
||||
use core_types::attribute::{Attr, Attribute, EditorLayerPath, Midpoint, Name0, Named, Opacity, OpacityFill, Position, Transform as TransformAttr, WireValue};
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::extent::{LevelIn, ListIn, ValueIn};
|
||||
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt};
|
||||
use core_types::list::List;
|
||||
use core_types::list::{ATTR_MIDPOINT, ATTR_POSITION};
|
||||
use core_types::node::Lane;
|
||||
use core_types::registry::types::Angle;
|
||||
use core_types::uuid::NodeId;
|
||||
@@ -15,6 +16,7 @@ use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
|
||||
use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use vector_types::gradient::{GradientForm as GradientFormValue, GradientHueDirection, GradientSpace, GradientSpread};
|
||||
use vector_types::markers::GradientCyclic;
|
||||
use vector_types::{Gradient, ReferencePoint};
|
||||
|
||||
fn arena_exhausted() -> Interrupt {
|
||||
@@ -559,10 +561,58 @@ pub fn flatten_gradient<'e>(
|
||||
flatten_leaf_lane(content, ctx.index() as usize)
|
||||
}
|
||||
|
||||
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
|
||||
/// A gradient over a level of colors: a `position` or `midpoint` column on the level places its stops, and a level carrying neither distributes them evenly.
|
||||
fn gradient_of_lanes(colors: core_types::node::List<'_, Color>) -> Gradient {
|
||||
let layout = colors.batch().layout();
|
||||
let has_position = layout.offset_of(Position::NAME, 0).is_some();
|
||||
let has_midpoint = layout.offset_of(Midpoint::NAME, 0).is_some();
|
||||
|
||||
let mut list = List::new();
|
||||
for index in 0..colors.len() {
|
||||
let lane = colors.lane(index);
|
||||
list.push(core_types::list::Item::new_from_element(colors.get(index)));
|
||||
if has_position {
|
||||
list.set_attribute(ATTR_POSITION, index, lane.attr::<Position>());
|
||||
}
|
||||
if has_midpoint {
|
||||
list.set_attribute(ATTR_MIDPOINT, index, lane.attr::<Midpoint>());
|
||||
}
|
||||
}
|
||||
Gradient::from(list)
|
||||
}
|
||||
|
||||
/// Constructs a gradient from a `Color[]`, where each color becomes a gradient stop. A `position` attribute on the colors places their stops along the ramp and a `midpoint` attribute skews each transition, while colors carrying neither are distributed evenly across the 0 to 1 range.
|
||||
#[node_macro::node(category("Gradient"), name("Colors to Gradient"))]
|
||||
pub fn colors_to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
|
||||
Gradient::from(colors.iter().collect::<Vec<_>>())
|
||||
gradient_of_lanes(colors)
|
||||
}
|
||||
|
||||
/// Unwraps each gradient into the `Color[]` of its stops, each carrying its `position` and `midpoint` along the ramp. Attributes belonging to the gradient as a whole (like spread and interpolation), rather than its individual color stops, are not preserved.
|
||||
#[node_macro::node(category("Gradient"), name("Gradient to Colors"), extent(gradient_to_colors_extent))]
|
||||
pub fn gradient_to_colors<'e>(
|
||||
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||
gradients: IList<Gradient>,
|
||||
) -> Result<IList<(Color, Attr<'e, Position>, Attr<'e, Midpoint>)>, Interrupt> {
|
||||
let mut remaining = ctx.index() as usize;
|
||||
for row in 0..gradients.len() {
|
||||
let gradient = gradients.element_ref(row);
|
||||
if remaining >= gradient.len() {
|
||||
remaining -= gradient.len();
|
||||
continue;
|
||||
}
|
||||
let gradient_cyclic = gradients.lane(row).attr::<GradientCyclic>();
|
||||
let Some(color) = gradient.color(remaining) else { break };
|
||||
return Ok((color, Attr(gradient.position(remaining, gradient_cyclic)), Attr(gradient.midpoint(remaining))));
|
||||
}
|
||||
Err(GraphError::past_end().into())
|
||||
}
|
||||
|
||||
/// The level holds every gradient's stops in order.
|
||||
fn gradient_to_colors_extent(gradients: ListIn<'_, Gradient>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => gradients.get().map(|gradients| Extent::Exactly((0..gradients.len()).map(|row| gradients.element_ref(row).len()).sum())),
|
||||
false => GPoll::Final(Extent::Exactly(1)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The gradient over a graphic level's color leaves, as [`colors_to_gradient`].
|
||||
|
||||
@@ -223,7 +223,10 @@ fn flatten_levels_extent(content: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::graphic::{ColorsToGradientNode, FlattenColorNode, FlattenGraphicNode, WrapGraphicNode, flatten_color_layout_meta, flatten_graphic_layout_meta, wrap_graphic_layout_meta};
|
||||
use crate::graphic::{
|
||||
ColorsToGradientNode, FlattenColorNode, FlattenGraphicNode, GradientToColorsNode, WrapGraphicNode, flatten_color_layout_meta, flatten_graphic_layout_meta, gradient_to_colors_layout_meta,
|
||||
wrap_graphic_layout_meta,
|
||||
};
|
||||
use core_types::arena::Arena;
|
||||
use core_types::attribute::Attribute as AttributeMarker;
|
||||
use core_types::context::{ContextImpl, ExtractArena};
|
||||
@@ -736,6 +739,46 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The stops of a gradient level unwrap to color lanes carrying their
|
||||
/// effective placement, which a `Colors to Gradient` reads back.
|
||||
#[test]
|
||||
fn gradient_stops_round_trip_through_color_lanes() {
|
||||
let frames = core_types::record::test_frames(1 << 16);
|
||||
let arena = Arena::new(1 << 16).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let mut gradient = Gradient::from(vec![Color::RED, Color::GREEN, Color::BLUE]);
|
||||
gradient.set_positions(&[0., 0.25, 1.]);
|
||||
gradient.set_midpoints(&[0.3, 0.5, 0.5]);
|
||||
let source = core_types::value::LeveledValueSource::new(vec![gradient.clone()]);
|
||||
let layout = Node::<ContextImpl>::layout(&source).clone();
|
||||
let colors = install(GradientToColorsNode::new(source), gradient_to_colors_layout_meta(), &[Some(&layout)]);
|
||||
let colors_layout = Node::<ContextImpl>::layout(&colors).clone();
|
||||
assert_eq!(colors.extent_at(&ctx, 0, &frames.reborrow()), GPoll::Final(Extent::Exactly(3)));
|
||||
|
||||
let head = ctx.index_head();
|
||||
for (lane, (color, position, midpoint)) in [(Color::RED, 0., 0.3), (Color::GREEN, 0.25, 0.5), (Color::BLUE, 1., 0.5)].into_iter().enumerate() {
|
||||
let GPoll::Final(record) = record::capture(&colors, &ctx.promoted(&head, lane as u64), &frames) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
assert_eq!(record.element::<Color>(), color, "lane {lane}");
|
||||
assert_eq!(record.attr::<core_types::attribute::Position>(), position, "lane {lane}");
|
||||
assert_eq!(record.attr::<core_types::attribute::Midpoint>(), midpoint, "lane {lane}");
|
||||
}
|
||||
|
||||
let out = Layout::default().with_writes(0, record::element_write_hashed::<Gradient>(), &[]);
|
||||
let restored = install_flip(ColorsToGradientNode::new(colors, &colors_layout), &out);
|
||||
let GPoll::Final(record) = record::capture(&restored, &ctx, &frames) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let restored = record.element::<Gradient>();
|
||||
assert_eq!(restored.positions(false), gradient.positions(false));
|
||||
assert_eq!(restored.midpoints(), gradient.midpoints());
|
||||
assert_eq!(restored.iter().map(|stop| stop.color).collect::<Vec<_>>(), vec![Color::RED, Color::GREEN, Color::BLUE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colors_fold_into_evenly_spaced_stops() {
|
||||
let frames = core_types::record::test_frames(1 << 16);
|
||||
|
||||
@@ -1245,9 +1245,9 @@ fn gradient_positions(_: impl Ctx, mut gradient: Gradient, positions: IList<f64>
|
||||
gradient
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Skews how rapidly the color flows across each interval between color stops, bunching up the transition toward one end instead of progressing uniformly. Each value places the halfway color within its corresponding interval, measured as a fraction of the distance (0 to 1) between the adjacent stops. A 0.5 midpoint keeps a uniform transition rate through the interval.
|
||||
///
|
||||
/// The final stop's midpoint controls the wrap back around to the first stop when the gradient is cyclic, and is otherwise ignored.
|
||||
/// Non-cyclic gradients have no interval following the last stop, meaning the midpoint is ignored in that position.
|
||||
///
|
||||
/// 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"))]
|
||||
|
||||
Reference in New Issue
Block a user