Skip to content

Commit e39b6d9

Browse files
bors[bot]okaneco
andauthoredDec 15, 2019
Merge #158
158: Make `Take` iterator for gradient inclusive of both end colors, add tests r=Ogeon a=okaneco As mentioned in the third point of #156, this PR includes the bounds of the gradient along with more equal distribution of colors returned from the iterator. In the current version of the iterator, `(self.from_head) / (self.len)` will approach 1 but never equal 1 which results in the upper gradient bound being excluded. The current behavior of `take()` is surprising if one expects the last color step to be the max color of the gradient. This PR increases step distance so the user receives an iterator of colors that evenly traverses from the minimum bound of the gradient to the maximum. The step is increased by subtracting 1 from `self.len` in the calculation of `i`. The calculation for `i` will then include - `0` when `self.from_head` is `0` - `1` when `self.from_head == self.len - 1`. The assignment of `i` has a conditional for when `self.len` is `1`. This avoids division by 0 which results in NaN parameters for the iterator color returned in the case of `take(1)`. The check needed to be added to the `DoubleEndedIterator`, otherwise it would NaN on `1` as well. The behavior of `take(1)` before this PR is to supply the minimum color in the gradient. Pictured below is an example of the difference between current behavior and this PR in 3 sets of Lch gradients. The 1st, 3rd, and 5th rows are the current implementation of the iterator and the others are from the PR with the final step being inclusive of the maximum gradient color specified. The gradients are no longer skewed towards the beginning. The penultimate steps are very close to the current behavior's final step. ![image of 6 stepped gradients](https://raw.githubusercontent.com/okaneco/images/master/inclusive_grad.png) --- `gradient::test::simple_slice` fails as a result of this PR. ``` ---- gradient::test::simple_slice stdout ---- thread 'gradient::test::simple_slice' panicked at 'assert_relative_eq!(t1, t2) left = Rgb { red: 0.8888888888888888, green: 0.0, blue: 0.1111111111111111, standard: PhantomData } right = Rgb { red: 0.875, green: 0.0, blue: 0.125, standard: PhantomData } ', palette/src/gradient.rs:453:13 ``` ```rust #[test] fn simple_slice() { let g1 = Gradient::new(vec![ LinSrgb::new(1.0, 0.0, 0.0), LinSrgb::new(0.0, 0.0, 1.0), ]); let g2 = g1.slice(..0.5); let v1: Vec<_> = g1.take(10).take(5).collect(); let v2: Vec<_> = g2.take(5).collect(); for (t1, t2) in v1.iter().zip(v2.iter()) { assert_relative_eq!(t1, t2); } } ``` Co-authored-by: okaneco <47607823+okaneco@users.noreply.github.com>
2 parents 333f466 + 6839872 commit e39b6d9

File tree

4 files changed

+92
-11
lines changed

4 files changed

+92
-11
lines changed
 

‎README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ This results in the following three colors:
9999

100100
There is also a linear gradient type which makes it easy to interpolate between a series of colors. This gradient can be used in any color space and it can be used to make color sequence iterators.
101101

102-
The following example shows two gradients between the same two endpoints, but one is in RGB and the other in is HSV space.
102+
The following example shows three gradients between the same two endpoints, but the top is in RGB space while the middle and bottom are in HSV space. The bottom gradient is an example of using the color sequence iterator.
103103

104104
```Rust
105105
extern crate palette;
@@ -116,7 +116,7 @@ let grad2 = Gradient::new(vec![
116116
]);
117117
```
118118

119-
The RGB gradient goes through gray, while the HSV gradients changes only the hue:
119+
The RGB gradient goes through gray, while the HSV gradients only change hue:
120120

121121
![Gradient Comparison](gfx/readme_gradients.png)
122122

‎gfx/readme_gradients.png

123 Bytes
Loading

‎palette/examples/readme_examples.rs

+21-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ fn display_gradients<A: Mix<Scalar = f32> + Clone, B: Mix<Scalar = f32> + Clone>
9393
LinSrgb: From<A>,
9494
LinSrgb: From<B>,
9595
{
96-
let mut image = RgbImage::new(256, 64);
96+
let mut image = RgbImage::new(256, 96);
9797
{
9898
let mut sub_image = image.sub_image(0, 0, 256, 32);
9999
let (width, height) = sub_image.dimensions();
@@ -130,6 +130,26 @@ fn display_gradients<A: Mix<Scalar = f32> + Clone, B: Mix<Scalar = f32> + Clone>
130130
}
131131
}
132132

133+
{
134+
let mut sub_image = image.sub_image(0, 64, 256, 32);
135+
let swatch_size = 32;
136+
let mut v1 = Vec::new();
137+
for color in grad2.take(8) {
138+
let pix: [u8; 3] = Srgb::from_linear(LinSrgb::from(color))
139+
.into_format()
140+
.into_raw();
141+
v1.push(pix);
142+
}
143+
for (s, color) in v1.into_iter().enumerate() {
144+
for x in (s * swatch_size)..((s + 1) * swatch_size) {
145+
for y in 0..swatch_size {
146+
let pixel = sub_image.get_pixel_mut(x as u32, y as u32);
147+
*pixel = image::Rgb(color);
148+
}
149+
}
150+
}
151+
}
152+
133153
match image.save(filename) {
134154
Ok(()) => println!("see '{}' for the result", filename),
135155
Err(e) => println!("failed to write '{}': {}", filename, e),

‎palette/src/gradient.rs

+69-8
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,34 @@ impl<C: Mix + Clone> Gradient<C> {
9292
min_color.mix(max_color, factor)
9393
}
9494

95-
///Take `n` evenly spaced colors from the gradient, as an iterator.
95+
///Take `n` evenly spaced colors from the gradient, as an iterator. The
96+
///iterator includes both ends of the gradient, for `n > 1`, or just
97+
///the lower end of the gradient for `n = 0`.
98+
///
99+
///For example, `take(5)` will include point 0.0 of the gradient, three
100+
///intermediate colors, and point 1.0 spaced apart at 1/4 the distance
101+
///between colors 0.0 and 1.0 on the gradient.
102+
/// ```
103+
/// #[macro_use] extern crate approx;
104+
/// use palette::{Gradient, LinSrgb};
105+
///
106+
/// let gradient = Gradient::new(vec![
107+
/// LinSrgb::new(1.0, 1.0, 0.0),
108+
/// LinSrgb::new(0.0, 0.0, 1.0),
109+
/// ]);
110+
///
111+
/// let taken_colors: Vec<_> = gradient.take(5).collect();
112+
/// let colors = vec![
113+
/// LinSrgb::new(1.0, 1.0, 0.0),
114+
/// LinSrgb::new(0.75, 0.75, 0.25),
115+
/// LinSrgb::new(0.5, 0.5, 0.5),
116+
/// LinSrgb::new(0.25, 0.25, 0.75),
117+
/// LinSrgb::new(0.0, 0.0, 1.0),
118+
/// ];
119+
/// for (c1, c2) in taken_colors.iter().zip(colors.iter()) {
120+
/// assert_relative_eq!(c1, c2);
121+
/// }
122+
/// ```
96123
pub fn take(&self, n: usize) -> Take<C> {
97124
let (min, max) = self.domain();
98125

@@ -142,9 +169,14 @@ impl<'a, C: Mix + Clone> Iterator for Take<'a, C> {
142169

143170
fn next(&mut self) -> Option<C> {
144171
if self.from_head + self.from_end < self.len {
145-
let i = self.from + (self.diff / cast(self.len)) * cast(self.from_head);
146-
self.from_head += 1;
147-
Some(self.gradient.get(i))
172+
if self.len == 1 {
173+
self.from_head += 1;
174+
Some(self.gradient.get(self.from))
175+
} else {
176+
let i = self.from + (self.diff / cast(self.len - 1)) * cast(self.from_head);
177+
self.from_head += 1;
178+
Some(self.gradient.get(i))
179+
}
148180
} else {
149181
None
150182
}
@@ -160,9 +192,14 @@ impl<'a, C: Mix + Clone> ExactSizeIterator for Take<'a, C> {}
160192
impl<'a, C: Mix + Clone> DoubleEndedIterator for Take<'a, C> {
161193
fn next_back(&mut self) -> Option<Self::Item> {
162194
if self.from_head + self.from_end < self.len {
163-
let i = self.from + (self.diff / cast(self.len)) * cast(self.len - self.from_end - 1);
164-
self.from_end += 1;
165-
Some(self.gradient.get(i))
195+
if self.len == 1 {
196+
self.from_end += 1;
197+
Some(self.gradient.get(self.from))
198+
} else {
199+
let i = self.from + (self.diff / cast(self.len - 1)) * cast(self.len - self.from_end - 1);
200+
self.from_end += 1;
201+
Some(self.gradient.get(i))
202+
}
166203
} else {
167204
None
168205
}
@@ -439,7 +476,7 @@ mod test {
439476
]);
440477
let g2 = g1.slice(..0.5);
441478

442-
let v1: Vec<_> = g1.take(10).take(5).collect();
479+
let v1: Vec<_> = g1.take(9).take(5).collect();
443480
let v2: Vec<_> = g2.take(5).collect();
444481
for (t1, t2) in v1.iter().zip(v2.iter()) {
445482
assert_relative_eq!(t1, t2);
@@ -456,5 +493,29 @@ mod test {
456493
let v1: Vec<_> = g.take(10).collect::<Vec<_>>().iter().rev().cloned().collect();
457494
let v2: Vec<_> = g.take(10).rev().collect();
458495
assert_eq!(v1, v2);
496+
497+
//make sure `take(1).rev()` doesn't produce NaN results
498+
let v1: Vec<_> = g.take(1).collect::<Vec<_>>().iter().rev().cloned().collect();
499+
let v2: Vec<_> = g.take(1).rev().collect();
500+
assert_eq!(v1, v2);
501+
}
502+
503+
#[test]
504+
fn inclusive_take() {
505+
let g = Gradient::new(vec![
506+
LinSrgb::new(1.0, 1.0, 0.0),
507+
LinSrgb::new(0.0, 0.0, 1.0),
508+
]);
509+
510+
//take(0) returns None
511+
let v1: Vec<_> = g.take(0).collect();
512+
assert_eq!(v1.len(), 0);
513+
//`Take` produces minimum gradient boundary for n=1
514+
let v1: Vec<_> = g.take(1).collect();
515+
assert_eq!(v1[0], LinSrgb::new(1.0, 1.0, 0.0));
516+
//`Take` includes the maximum gradient color
517+
let v1: Vec<_> = g.take(5).collect();
518+
assert_eq!(v1[0], LinSrgb::new(1.0, 1.0, 0.0));
519+
assert_eq!(v1[4], LinSrgb::new(0.0, 0.0, 1.0));
459520
}
460521
}

0 commit comments

Comments
 (0)
Please sign in to comment.