mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-07 02:51:41 +00:00
perf(color): sort gradient stops once per diamond texture (#1894)
Gradient::sample cloned, sanitised and sorted every stop on each call, and the diamond texture sampled it for each of its 65 536 pixels, so a file's gradient with many stops cost that sort 65 536 times. sample_sorted samples stops already sorted, finding the segment by binary search; sample delegates to it and the diamond texture sorts once. Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
1 parent
ad20c56874
commit
8717728a87
3 files changed
+65
-19
No files matched your search
+24
-18
@@ -376,26 +376,32 @@ impl Gradient {
|
||||
s
|
||||
}
|
||||
|
||||
/// The colour at `t` (0..1) along the gradient.
|
||||
/// The colour at `t` (0..1) along the gradient. Sorts the stops on every call: to sample one
|
||||
/// gradient many times, sort once with [`Self::sorted_stops`] and use [`sample_sorted`].
|
||||
pub fn sample(&self, t: f32) -> Color {
|
||||
let stops = self.sorted_stops();
|
||||
let t = clamp01(t);
|
||||
let mut prev = match stops.first() {
|
||||
Some(s) => *s,
|
||||
None => return Color::BLACK,
|
||||
};
|
||||
if t <= prev.position {
|
||||
return prev.color;
|
||||
sample_sorted(&self.sorted_stops(), t)
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour at `t` (0..1) along stops already sorted by [`Gradient::sorted_stops`]: found by a
|
||||
/// binary search, so a renderer sampling a file's gradient per pixel doesn't scan or sort its
|
||||
/// stops each time.
|
||||
pub fn sample_sorted(stops: &[ColorStop], t: f32) -> Color {
|
||||
let t = clamp01(t);
|
||||
let Some(first) = stops.first() else { return Color::BLACK };
|
||||
if t <= first.position {
|
||||
return first.color;
|
||||
}
|
||||
// The first stop after the first one at or past `t`, and the stop before it.
|
||||
let rest = stops.get(1..).unwrap_or_default();
|
||||
let i = rest.partition_point(|s| s.position < t);
|
||||
match (rest.get(i), stops.get(i)) {
|
||||
(Some(s), Some(prev)) => {
|
||||
let span = s.position - prev.position;
|
||||
let f = if span > 1e-6 { (t - prev.position) / span } else { 1.0 };
|
||||
prev.color.lerp(s.color, f)
|
||||
}
|
||||
for s in stops.iter().skip(1) {
|
||||
if t <= s.position {
|
||||
let span = s.position - prev.position;
|
||||
let f = if span > 1e-6 { (t - prev.position) / span } else { 1.0 };
|
||||
return prev.color.lerp(s.color, f);
|
||||
}
|
||||
prev = *s;
|
||||
}
|
||||
prev.color
|
||||
_ => stops.last().map_or(Color::BLACK, |s| s.color),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,44 @@ fn gradient_sampling() {
|
||||
assert_eq!(g.sample(0.5).to_hex(), "808080");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sampling_sorted_stops_matches_a_scan() {
|
||||
// The scan `sample` used to make: the first stop at or past `t`, mixed with the one before.
|
||||
fn scan(stops: &[ColorStop], t: f32) -> Color {
|
||||
let t = if t.is_nan() { 0.0 } else { t.clamp(0.0, 1.0) };
|
||||
let mut prev = stops[0];
|
||||
if t <= prev.position {
|
||||
return prev.color;
|
||||
}
|
||||
for s in &stops[1..] {
|
||||
if t <= s.position {
|
||||
let span = s.position - prev.position;
|
||||
let f = if span > 1e-6 { (t - prev.position) / span } else { 1.0 };
|
||||
return prev.color.lerp(s.color, f);
|
||||
}
|
||||
prev = *s;
|
||||
}
|
||||
prev.color
|
||||
}
|
||||
let grey = |v: f32| Color { r: v, g: v, b: v, a: 1.0 };
|
||||
let positions = [0.3, 0.0, 0.3, 1.0, 0.7, f32::NAN, 0.5, 0.5, 0.9];
|
||||
let g = Gradient {
|
||||
kind: GradientKind::Diamond,
|
||||
stops: positions.iter().enumerate().map(|(i, p)| ColorStop { position: *p, color: grey(i as f32 / 10.0) }).collect(),
|
||||
from: (0.5, 0.5),
|
||||
to: (1.0, 0.5),
|
||||
width: (0.5, 1.0),
|
||||
};
|
||||
let sorted = g.sorted_stops();
|
||||
for i in -10..=110 {
|
||||
let t = i as f32 / 100.0;
|
||||
assert_eq!(sample_sorted(&sorted, t), scan(&sorted, t), "t = {t}");
|
||||
assert_eq!(g.sample(t), scan(&sorted, t), "t = {t}");
|
||||
}
|
||||
assert_eq!(sample_sorted(&sorted, f32::NAN), scan(&sorted, f32::NAN));
|
||||
assert_eq!(sample_sorted(&[], 0.5), Color::BLACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paint_serde_shape() {
|
||||
let p = Paint::solid(Color::from_hex("FF0000").unwrap());
|
||||
|
||||
@@ -79,11 +79,13 @@ fn diamond_pixmap(g: &Gradient, opacity: f32) -> Option<Arc<Pixmap>> {
|
||||
const N: u32 = 256;
|
||||
let mut p = Pixmap::new(N, N)?;
|
||||
let half = f64::from(N) / 2.0;
|
||||
// Sorted once for the 65 536 samples below, not once per sample.
|
||||
let stops = g.sorted_stops();
|
||||
for (i, px) in p.data_mut().as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let x = ((i as u32 % N) as f64 + 0.5 - half) / half;
|
||||
let y = ((i as u32 / N) as f64 + 0.5 - half) / half;
|
||||
let t = (x.abs() + y.abs()) as f32;
|
||||
let c = g.sample(t);
|
||||
let c = figcraft_color::sample_sorted(&stops, t);
|
||||
let a = c.a * opacity;
|
||||
let q = |v: f32| (v * a * 255.0).round().clamp(0.0, 255.0) as u8;
|
||||
px.copy_from_slice(&[q(c.r), q(c.g), q(c.b), (a * 255.0).round().clamp(0.0, 255.0) as u8]);
|
||||
|
||||
Reference in new issue
Block a user