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 }