1use std::collections::BTreeMap;
2
3use crate::{
4 image::ImageStore,
5 paint::{GradientStop, MultiStopGradient},
6 Color, ErrorKind, ImageFlags, ImageId, ImageInfo, ImageSource, Renderer,
7};
8
9/// GradientStore holds image ids for multi-stop gradients. The actual image/textures
10/// are contained by the Canvas's ImageStore.
11//
12// If many gradients are used in a frame, we could combine them into a single texture
13// and update the texture immediately prior to giving the renderer the command list.
14pub(crate) struct GradientStore {
15 this_frame: BTreeMap<MultiStopGradient, ImageId>,
16 prev_frame: BTreeMap<MultiStopGradient, ImageId>,
17}
18impl GradientStore {
19 /// Create a new empty gradient store
20 pub fn new() -> GradientStore {
21 GradientStore {
22 this_frame: BTreeMap::new(),
23 prev_frame: BTreeMap::new(),
24 }
25 }
26
27 /// Lookup or add a multi-stop gradient in this gradient store.
28 pub fn lookup_or_add<R: Renderer>(
29 &mut self,
30 colors: &MultiStopGradient,
31 images: &mut ImageStore<R::Image>,
32 renderer: &mut R,
33 ) -> Result<ImageId, ErrorKind> {
34 if let Some(gradient_image_id) = self.prev_frame.remove(colors) {
35 // See if we already have this texture from the previous frame. If we find
36 // it then we migrate it to the current frame so we don't release it and
37 // return the texture id to the caller.
38 self.this_frame.insert(colors.clone(), gradient_image_id);
39 Ok(gradient_image_id)
40 } else if let Some(gradient_image_id) = self.this_frame.get(colors) {
41 // See if we already used this gradient in this frame, and return the texture
42 // id if we do.
43 Ok(*gradient_image_id)
44 } else {
45 // We need to allocate a texture and synthesize the gradient image.
46 let info = ImageInfo::new(ImageFlags::REPEAT_Y, 256, 1, crate::PixelFormat::Rgba8);
47 let gradient_image_id = images.alloc(renderer, info)?;
48 let image = linear_gradient_stops(colors);
49 images.update(renderer, gradient_image_id, ImageSource::Rgba(image.as_ref()), 0, 0)?;
50
51 self.this_frame.insert(colors.clone(), gradient_image_id);
52 Ok(gradient_image_id)
53 }
54 }
55
56 /// Release the textures that were not used in the most recently rendered frame. This
57 /// method should be called when all the commands have been submitted.
58 pub fn release_old_gradients<R: Renderer>(&mut self, images: &mut ImageStore<R::Image>, renderer: &mut R) {
59 let mut prev_textures = BTreeMap::new();
60 std::mem::swap(&mut prev_textures, &mut self.prev_frame);
61 for (_, gradient_image_id) in prev_textures {
62 images.remove(renderer, gradient_image_id);
63 }
64 // Move the "this_frame" textures to "prev_frame". "prev_frame" is already empty.
65 std::mem::swap(&mut self.this_frame, &mut self.prev_frame);
66 }
67}
68
69#[allow(clippy::many_single_char_names)]
70// Gradient filling, adapted from https://github.com/lieff/lvg/blob/master/render/common.c#L147
71fn gradient_span(dest: &mut [rgb::RGBA8; 256], color0: Color, color1: Color, offset0: f32, offset1: f32) {
72 let s0o = offset0.max(0.0).min(1.0);
73 let s1o = offset1.max(0.0).min(1.0);
74
75 if s1o < s0o {
76 return;
77 }
78
79 let s = (s0o * 256.0) as usize;
80 let e = (s1o * 256.0) as usize;
81
82 let mut r = color0.r;
83 let mut g = color0.g;
84 let mut b = color0.b;
85 let mut a = color0.a;
86
87 let steps = (e - s) as f32;
88
89 let dr = (color1.r - r) / steps;
90 let dg = (color1.g - g) / steps;
91 let db = (color1.b - b) / steps;
92 let da = (color1.a - a) / steps;
93
94 #[allow(clippy::needless_range_loop)]
95 for i in s..e {
96 // The output must be premultiplied, but we don't premultiply until this point
97 // so that we can do gradients from transparent colors correctly -- for example
98 // if we have a stop that is fully transparent red and it transitions to opaque
99 // blue, we should see some red in the gradient. If we premultiply the stops
100 // then we won't see any red, because we will have already multiplied it to zero.
101 // This way we'll get the red contribution.
102 dest[i] = rgb::RGBA8::new(
103 (r * a * 255.0) as u8,
104 (g * a * 255.0) as u8,
105 (b * a * 255.0) as u8,
106 (a * 255.0) as u8,
107 );
108 r += dr;
109 g += dg;
110 b += db;
111 a += da;
112 }
113}
114fn linear_gradient_stops(gradient: &MultiStopGradient) -> imgref::Img<Vec<rgb::RGBA8>> {
115 let mut dest = [rgb::RGBA8::new(0, 0, 0, 0); 256];
116
117 // Fill the gradient up to the first stop.
118 let first_stop = gradient.get(0);
119 if first_stop.0 > 0.0 {
120 let s0 = first_stop.0;
121 let color0 = first_stop.1;
122 gradient_span(&mut dest, color0, color0, 0.0, s0);
123 }
124
125 // Iterate over the stops in overlapping pairs and fill out the rest of the
126 // gradient. If the stop position is > 1.0 then we have exhausted the stops
127 // and should break. As a special case, if the second stop is > 1.0 then we
128 // fill the current color to the end of the gradient.
129 for [GradientStop(s0, color0), GradientStop(s1, color1)] in gradient.pairs() {
130 // Catch the case where the last stop doesn't go all the way to 1.0 and
131 // pad it.
132 if s0 < 1.0 && s1 > 1.0 {
133 gradient_span(&mut dest, color0, color0, s0, 1.0);
134 } else {
135 gradient_span(&mut dest, color0, color1, s0, s1);
136 }
137
138 // If the first stop is >1.0 then we're done.
139 if s0 > 1.0 {
140 break;
141 };
142 }
143 imgref::Img::new(dest.to_vec(), 256, 1)
144}
145