Fold a color level into evenly spaced gradient stops

This commit is contained in:
Dennis Kobert
2026-08-21 21:48:21 +00:00
parent 2c1acbdc62
commit 2be252d7b4

View File

@@ -11,7 +11,7 @@ use glam::DAffine2;
use graphic_types::graphic::Graphic;
use graphic_types::Vector;
use raster_types::{CPU, Raster};
use vector_types::GradientStops;
use vector_types::{GradientStop, GradientStops};
/// Whether the walk can descend into a group: every run holds `Graphic`
/// elements.
@@ -129,6 +129,18 @@ fn wrap_extent(_content: ListIn<'_, Graphic>, _level: LevelIn) -> GPoll<Extent>
GPoll::Final(Extent::Exactly(1))
}
/// Rank-model colors-to-gradient: the color level folds into one gradient
/// with evenly spaced stops.
#[node_macro::node(category("Test"))]
fn to_gradient(_: impl Ctx + ExtractIndex + InjectIndex + Copy, colors: IList<Color>) -> GradientStops {
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
match colors.len() {
0 => GradientStops::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
1 => GradientStops::new(vec![stop(0., colors.get(0)), stop(1., colors.get(0))]),
total => GradientStops::new((0..total).map(|index| stop(index as f64 / (total - 1) as f64, colors.get(index)))),
}
}
/// One content row as the production vararg shape: a single-item legacy list
/// carrying the row's element only, so the list's dyn-hash is a complete
/// cache key over the observables.
@@ -280,6 +292,16 @@ mod tests {
node
}
fn install_flip<N: Node<ContextImpl<'static>>>(mut node: N, layout: &Layout) -> N {
let bundle = record::RecordLayout {
frame_bytes: layout.frame_bytes(),
plan: Vec::new(),
layout: layout.clone(),
};
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, bundle);
node
}
fn graphic_layout() -> Layout {
Layout::default().with_writes(1, record::element_write_hashed::<Graphic>(), &[record::FieldWrite::of::<Transform>(0)])
}
@@ -631,6 +653,65 @@ mod tests {
}
}
#[test]
fn colors_fold_into_evenly_spaced_stops() {
struct ColorSource {
layout: Layout,
colors: Vec<Color>,
}
impl<'e> Node<ContextImpl<'e>> for ColorSource {
type Output = RecordValue<'e>;
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
let color = self.colors[input.innermost_index() as usize];
let dst = stack::push(self.layout.frame_bytes());
unsafe { dst.cast::<Color>().write(color) };
stack::pop(dst);
GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }))
}
fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll<Extent> {
GPoll::Final(Extent::Exactly(self.colors.len()))
}
fn layout(&self) -> &Layout {
&self.layout
}
}
let arena = Arena::new(1 << 16).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let layout = Layout::default().with_writes(1, record::element_write_hashed::<Color>(), &[]);
let out = Layout::default().with_writes(0, record::element_write_hashed::<GradientStops>(), &[]);
let build = |colors: Vec<Color>| {
install_flip(
ToGradientNode::new(RecordSource::new(ColorSource { layout: layout.clone(), colors }, &layout, &layout), &layout),
&out,
)
};
let stops_of = |colors: Vec<Color>| {
let node = build(colors);
let GPoll::Final(value) = node.eval(&ctx) else {
panic!("expected a final record");
};
unsafe { record::borrow_element::<GradientStops>(out.rec(&value)) }.clone()
};
let three = stops_of(vec![Color::BLACK, Color::WHITE, Color::BLACK]);
assert_eq!(three.iter().map(|stop| stop.position).collect::<Vec<_>>(), vec![0., 0.5, 1.]);
assert_eq!(three.iter().map(|stop| stop.color).collect::<Vec<_>>(), vec![Color::BLACK, Color::WHITE, Color::BLACK]);
let single = stops_of(vec![Color::WHITE]);
assert_eq!(single.iter().map(|stop| (stop.position, stop.color)).collect::<Vec<_>>(), vec![(0., Color::WHITE), (1., Color::WHITE)]);
let empty = stops_of(Vec::new());
assert_eq!(empty.iter().map(|stop| (stop.position, stop.color)).collect::<Vec<_>>(), vec![(0., Color::BLACK), (1., Color::BLACK)]);
}
#[test]
fn a_group_converts_to_its_legacy_list() {
let arena = Arena::new(1 << 16).unwrap();