1 | use std::collections::BTreeMap; |
2 | |
3 | use 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. |
14 | pub(crate) struct GradientStore { |
15 | this_frame: BTreeMap<MultiStopGradient, ImageId>, |
16 | prev_frame: BTreeMap<MultiStopGradient, ImageId>, |
17 | } |
18 | impl 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 |
71 | fn 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 | } |
114 | fn 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 | |