diff --git a/crates/color/src/palette_io.rs b/crates/color/src/palette_io.rs index 065e06eed..80f740321 100644 --- a/crates/color/src/palette_io.rs +++ b/crates/color/src/palette_io.rs @@ -181,7 +181,9 @@ fn css_comment(s: &str) -> String { fn write_css(lib: &SwatchLibrary, keep: impl Fn(&&Swatch) -> bool) -> String { let mut out = format!("/* {} */\n:root {{\n", css_comment(&lib.name)); - let mut taken: Vec = vec![]; + let mut taken: HashSet = HashSet::new(); + // The next number to try for each base, so repeated names don't search the ones used before. + let mut next: HashMap = HashMap::new(); let mut props = |out: &mut String, list: &[Swatch]| { for w in list.iter().filter(&keep) { let value = match &w.paint { @@ -198,8 +200,18 @@ fn write_css(lib: &SwatchLibrary, keep: impl Fn(&&Swatch) -> bool) -> String { }; // Names differing only in case or punctuation map to one property: number the others. let base = css_property(&w.name); - let name = (1..).map(|i| if i == 1 { base.clone() } else { format!("{base}-{i}") }).find(|n| !taken.contains(n)).unwrap_or(base); - taken.push(name.clone()); + let name = if taken.insert(base.clone()) { + base + } else { + let n = next.entry(base.clone()).or_insert(1); + loop { + *n += 1; + let name = format!("{base}-{n}"); + if taken.insert(name.clone()) { + break name; + } + } + }; let note = if w.spot { " /* spot */" } else { "" }; out.push_str(&format!(" {name}: {value};{note}\n")); } diff --git a/crates/effects/src/pixel/mod.rs b/crates/effects/src/pixel/mod.rs index 87335670c..8885c9def 100644 --- a/crates/effects/src/pixel/mod.rs +++ b/crates/effects/src/pixel/mod.rs @@ -932,22 +932,41 @@ pub fn dilate_plane(a: &mut Vec, w: usize, h: usize, radius: f64) { /// One horizontal pass of [`dilate_plane`] over the rows of `w` values of a plane: the largest /// value within `whole`, blended by `frac` towards the largest within `whole + 1`. fn max_plane(src: &[f32], dst: &mut [f32], w: usize, whole: usize, frac: f32) { + let (mut inner, mut ring) = (vec![0.0f32; w], vec![0.0f32; w]); for (row, out) in src.chunks_exact(w).zip(dst.chunks_exact_mut(w)) { - for (x, o) in out.iter_mut().enumerate() { - let lo = x.saturating_sub(whole + 1); - let hi = (x + whole + 1).min(w - 1); - let (mut inner, mut ring) = (row[x], row[x]); - for (j, v) in row[lo..=hi].iter().enumerate() { - if (lo + j).abs_diff(x) <= whole { - inner = inner.max(*v); - } - ring = ring.max(*v); - } - *o = inner + (ring - inner) * frac; + window_max(row, whole, &mut inner); + window_max(row, whole + 1, &mut ring); + for ((o, i), r) in out.iter_mut().zip(&inner).zip(&ring) { + *o = i + (r - i) * frac; } } } +/// For each `x`, the largest of `row[x - radius..=x + radius]` (cut at the row's ends) into `out`, +/// in time linear in the row (a sliding window, not a scan of the window for each `x`). +fn window_max(row: &[f32], radius: usize, out: &mut [f32]) { + use std::collections::VecDeque; + // Indices of the window's values that can still be its largest, falling in value. + let mut keep: VecDeque = VecDeque::new(); + let mut next = 0; + for (x, o) in out.iter_mut().enumerate().take(row.len()) { + let hi = x.saturating_add(radius).min(row.len().saturating_sub(1)); + while next <= hi { + let v = row[next]; + while keep.back().is_some_and(|&b| row[b] <= v) { + keep.pop_back(); + } + keep.push_back(next); + next += 1; + } + let lo = x.saturating_sub(radius); + while keep.front().is_some_and(|&f| f < lo) { + keep.pop_front(); + } + *o = keep.front().map_or(row[x], |&f| row[f]); + } +} + /// Premultiplied RGBA at 16 bits (×257): blurs and averages that keep their tones. pub(super) type Px16 = [u16; 4]; @@ -1061,3 +1080,22 @@ mod tests; mod tests_paint; #[cfg(test)] mod tests_sketch; + +#[cfg(test)] +mod window_max_tests { + use super::window_max; + + #[test] + fn the_sliding_maximum_matches_a_scan_of_each_window() { + let row: Vec = (0..40u32).map(|i| ((i * 7919) % 31) as f32).collect(); + for radius in [0, 1, 2, 5, 39, 100] { + let mut out = vec![0.0; row.len()]; + window_max(&row, radius, &mut out); + for (x, got) in out.iter().enumerate() { + let (lo, hi) = (x.saturating_sub(radius), (x + radius).min(row.len() - 1)); + let want = row[lo..=hi].iter().copied().fold(f32::MIN, f32::max); + assert_eq!(*got, want, "radius {radius} at {x}"); + } + } + } +} diff --git a/crates/effects/src/stylize.rs b/crates/effects/src/stylize.rs index 2db14d10f..bdb5f8623 100644 --- a/crates/effects/src/stylize.rs +++ b/crates/effects/src/stylize.rs @@ -184,9 +184,14 @@ pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData { row: u64, } let mut chains: Vec = vec![]; - let mut y = y0 + step / 2.0; let mut row = 0u64; - while y < y1 { + // Rows are counted, not summed: adding a step that is below the spacing of the numbers at `y` + // would never move it. + while row <= 2001 { + let y = y0 + step / 2.0 + row as f64 * step; + if y.partial_cmp(&y1) != Some(std::cmp::Ordering::Less) { + break; + } let mut xs: Vec = vec![]; for pl in &polys { for w in pl.windows(2) { @@ -225,7 +230,6 @@ pub fn scribble(path: &PathData, b: Rect, p: &Value) -> PathData { } } row += 1; - y += step; } let lines: Vec = chains .into_iter()