1 /**
2     Implement a gradient object, that holds a LUT.
3     Copyright: Chris Jones 2020.
4     Copyright: Guillaume Piolat 2018-2025.
5     License:   http://www.boost.org/LICENSE_1_0.txt
6 */
7 module dplug.canvas.gradient;
8 
9 import dplug.core.nogc;
10 import dplug.core.vec;
11 import dplug.canvas.misc;
12 import dplug.graphics.color;
13 
14 /**
15     Gradient class.
16 
17     Nothing here is a public API, it's only internal to
18     dplug:canvas.    
19     
20     A gradient owns:
21 
22       - a list of colours and positions (known as stops)
23         along a single dimension from 0 to 1.
24         The colors are a 32-bit RGBA quadruplet.
25 
26       - a look-up table of colors (LUT)
27         indexed by blitters.
28     
29     It has a lookup table for the rasterizer, built lazily
30     (was originally always 256 samples).
31 
32     FUTURE: size of look-up is dynamically choosen
33 */
34 class Gradient
35 {
36 public:
37 nothrow:
38 @nogc:
39 
40     static struct ColorStop
41     {
42         uint  color;
43         float pos;
44     }
45 
46     /**
47         Add a color stop.
48     */
49     void addStop(float pos, uint color)
50     {
51         m_stops.pushBack(ColorStop(color,clip(pos,0.0,1.0)));
52         m_changed = true;
53     }
54 
55     /**
56         Reset things, have zero color stop.
57         Invalidate look-up.
58     */
59     void reset()
60     {
61         m_stops.clearContents();
62         m_changed = true;
63     }
64 
65     /**
66         Returns: Numnber of color stops in this gradient. 
67     */
68     size_t length()
69     {
70         return m_stops.length;
71     }
72 
73     /**
74         Get look-up length.
75         This is invalidated in cases the color stops are 
76         redone with `.reset` or `.addStop`.
77     */
78     int lutLength()
79     {
80         if (m_changed) initLookup();
81         return cast(int)m_lookup.length;
82     }
83 
84     /**
85         Returns: Complete LUT, of size `lutLength()`.
86     */
87     uint[] getLookup()
88     {
89         if (m_changed) initLookup();
90         return m_lookup[0..$];
91     }
92 
93     /**
94         Returns: `true` if the color stops have changed,
95         and LUT would be recomputed at next `.getLookup()`.
96     */
97     bool hasChanged()
98     {
99         return m_changed;
100     }
101     
102 private:
103 
104     int computeLUTSize()
105     {
106         // PERF: hash of color stops would allow to keep
107         // the same LUT size.
108 
109         // heuristic to choose LUT size
110         enum MIN_LUT_SIZE = 32;
111 
112         // haven't seen a meaningful enhancement beyond 1024
113         enum MAX_LUT_SIZE = 1024; 
114 
115         int lutSize = MIN_LUT_SIZE;
116 
117         if (m_stops.length <= 1)
118             return MIN_LUT_SIZE;
119         else
120         {
121             foreach(size_t i; 1.. m_stops.length)
122             {
123                 float t0 = m_stops[i-1].pos;
124                 float t1 = m_stops[i].pos;
125                 float tdiff = abs_float(t0 - t1);
126                 if (tdiff < 0.0005)
127                     continue; // unlikely to make a difference
128 
129                 uint s0 = m_stops[i-1].color;
130                 uint s1 = m_stops[i].color;
131                 RGBA c0 = *cast(RGBA*)(&s0);
132                 RGBA c1 = *cast(RGBA*)(&s1);
133 
134                 // What's the maximum difference between those stops?
135                 int dr = abs_int(c0.r - c1.r);
136                 int dg = abs_int(c0.g - c1.g);
137                 int db = abs_int(c0.b - c1.b);
138                 int da = abs_int(c0.a - c1.a);
139 
140                 int maxD = dr;
141                 if (maxD < dg) maxD = dg;
142                 if (maxD < db) maxD = db;
143                 if (maxD < da) maxD = da;
144 
145                 // Approximate number of items to get small
146                 // difference of one level
147                 // VISUAL: add a factor there and tune it
148                 // Higher than 1.0 doesn't seem to make a 
149                 // different for now, FUTURE: test it again after
150                 // other enhancements.
151                 float n = maxD / tdiff;
152 
153                 int ni = cast(int)n;
154                 if (ni > MAX_LUT_SIZE)
155                 {
156                     ni = MAX_LUT_SIZE;
157                 }
158 
159                 if (lutSize < ni)
160                     lutSize = ni;
161             }
162             return lutSize;
163         }
164     }
165     
166 
167     void initLookup()
168     {
169         sortStopsInPlace(m_stops[]);
170 
171         int lutSize = computeLUTSize();
172         m_lookup.resize(lutSize);
173 
174         // PERF: we can skip a LUT table initialization if the
175         // stops and LUT size are carefully hashed.
176 
177         if (m_stops.length == 0)
178         {
179             foreach(ref c; m_lookup) c = 0;
180         }
181         else if (m_stops.length == 1)
182         {
183             foreach(ref c; m_lookup) c = m_stops[0].color;
184         }
185         else
186         {           
187             int idx = cast(int) (m_stops[0].pos*lutSize);
188 
189             int colorStop0 = m_stops[0].color;
190             for (int n = 0; n < idx; ++n)
191             {
192                 m_lookup[n] = colorStop0;
193             }
194 
195             foreach(size_t i; 1.. m_stops.length)
196             {
197                 int next = cast(int) (m_stops[i].pos*lutSize);
198 
199                 foreach(int j; idx..next)
200                 {
201                     // VISUAL: this computation compute a stop with only 8-bit operands
202                     // makes a difference?
203 
204                     enum bool integerGradient = false;
205 
206                     static if (integerGradient)
207                     {
208                         uint a = (256*(j-idx))/(next-idx);
209                         uint c0 = m_stops[i-1].color;
210                         uint c1 = m_stops[i].color;
211                         uint t0 = (c0 & 0xFF00FF)*(256-a) + (c1 & 0xFF00FF)*a;
212                         uint t1 = ((c0 >> 8) & 0xFF00FF)*(256-a) + ((c1 >> 8) & 0xFF00FF)*a;
213                         m_lookup[j] = ((t0 >> 8) & 0xFF00FF) | (t1 & 0xFF00FF00);
214                     }
215                     else
216                     {
217                         // FUTURE: useful?
218                         // This new one, doesn't look nicer, disabled
219                         uint s0 = m_stops[i-1].color;
220                         uint s1 = m_stops[i].color;
221                         RGBA c0 = *cast(RGBA*)(&s0);
222                         RGBA c1 = *cast(RGBA*)(&s1);
223                         float fa = cast(float)(j-idx)/(next-idx);
224                         float r = c0.r * (1.0f - fa) + c1.r * fa;
225                         float g = c0.g * (1.0f - fa) + c1.g * fa;
226                         float b = c0.b * (1.0f - fa) + c1.b * fa;
227                         float a = c0.a * (1.0f - fa) + c1.a * fa;
228                         RGBA res;
229                         res.r = cast(ubyte)(0.5f + r);
230                         res.g = cast(ubyte)(0.5f + g);
231                         res.b = cast(ubyte)(0.5f + b);
232                         res.a = cast(ubyte)(0.5f + a);
233                         m_lookup[j] = *cast(int*)(&res);
234                     }
235                 }
236                 idx = next;
237             }
238 
239             int colorStopLast = m_stops[$-1].color;
240             for (int n = idx; n < lutSize; ++n)
241             {
242                 m_lookup[n] = m_stops[$-1].color;
243             }
244         }
245         m_changed = false;
246     }
247 
248     Vec!ColorStop m_stops;
249     Vec!uint m_lookup;
250     bool m_changed = true;
251 
252     static void sortStopsInPlace(ColorStop[] stops)
253     {    
254         size_t i = 1;
255         while (i < stops.length)
256         {
257             size_t j = i;
258             while (j > 0 && (stops[j-1].pos > stops[j].pos))
259             {
260                 ColorStop tmp = stops[j-1];
261                 stops[j-1] = stops[j];
262                 stops[j] = tmp;
263                 j = j - 1;
264             }
265             i = i + 1;
266         }
267     }
268 
269     static float abs_float(float a) pure
270     {
271         return a < 0 ? -a : a;
272     }
273 
274     static int abs_int(int a) pure
275     {
276         return a < 0 ? -a : a;
277     }
278 }
279 
280 unittest
281 {
282     Gradient.ColorStop[3] stops = [Gradient.ColorStop(0xff0000, 1.0f),
283                                    Gradient.ColorStop(0x00ff00, 0.4f),
284                                    Gradient.ColorStop(0x0000ff, 0.0f)];
285     Gradient.sortStopsInPlace(stops[]);
286     assert(stops[0].pos == 0.0f);
287     assert(stops[1].pos == 0.4f);
288     assert(stops[2].pos == 1.0f);
289     Gradient.sortStopsInPlace([]);
290     Gradient.sortStopsInPlace(stops[0..1]);
291 }