1 /** 2 * Color type and operations. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.graphics.color; 15 16 import std.traits; 17 18 import ae.utils.math; 19 import ae.utils.meta; 20 21 /// Instantiates to a color type. 22 /// FieldTuple is the color specifier, as parsed by 23 /// the FieldList template from ae.utils.meta. 24 /// By convention, each field's name indicates its purpose: 25 /// - x: padding 26 /// - a: alpha 27 /// - l: lightness (or grey, for monochrome images) 28 /// - others (r, g, b, etc.): color information 29 30 // TODO: figure out if we need alll these methods in the color type itself 31 // - code such as gamma conversion needs to create color types 32 // - ReplaceType can't copy methods 33 // - even if we move out all conventional methods, that still leaves operator overloading 34 35 struct Color(FieldTuple...) 36 { 37 alias Spec = FieldTuple; 38 mixin FieldList!FieldTuple; 39 40 // A "dumb" type to avoid cyclic references. 41 private struct Fields { mixin FieldList!FieldTuple; } 42 43 /// Whether or not all channel fields have the same base type. 44 // Only "true" supported for now, may change in the future (e.g. for 5:6:5) 45 enum homogenous = isHomogenous!Fields(); 46 47 /// The number of fields in this color type. 48 enum channels = Fields.init.tupleof.length; 49 50 static if (homogenous) 51 { 52 alias ChannelType = typeof(Fields.init.tupleof[0]); 53 enum channelBits = valueBits!ChannelType; 54 } 55 56 /// Return a Color instance with all fields set to "value". 57 static typeof(this) monochrome(ChannelType value) 58 { 59 typeof(this) r; 60 foreach (i, f; r.tupleof) 61 r.tupleof[i] = value; 62 return r; 63 } 64 65 static if (is(ChannelType:uint)) 66 { 67 enum typeof(this) black = monochrome(0); 68 enum typeof(this) white = monochrome(ChannelType.max); 69 } 70 71 /// Interpolate between two colors. 72 static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1) 73 { 74 alias ExpandNumericType!(ChannelType, P.sizeof*8) U; 75 alias Signed!U S; 76 typeof(this) r; 77 foreach (i, f; r.tupleof) 78 static if (r.tupleof[i].stringof != "r.x") // skip padding 79 r.tupleof[i] = cast(ChannelType).itpl(cast(U)c0.tupleof[i], cast(U)c1.tupleof[i], cast(S)p, cast(S)p0, cast(S)p1); 80 return r; 81 } 82 83 /// Alpha-blend two colors. 84 static typeof(this) blend()(typeof(this) c0, typeof(this) c1) 85 if (is(typeof(a))) 86 { 87 alias A = typeof(c0.a); 88 A a = ~cast(A)(~c0.a * ~c1.a / A.max); 89 if (!a) 90 return typeof(this).init; 91 A x = cast(A)(c1.a * A.max / a); 92 93 typeof(this) r; 94 foreach (i, f; r.tupleof) 95 static if (r.tupleof[i].stringof == "r.x") 96 {} // skip padding 97 else 98 static if (r.tupleof[i].stringof == "r.a") 99 r.a = a; 100 else 101 { 102 auto v0 = c0.tupleof[i]; 103 auto v1 = c1.tupleof[i]; 104 auto vr = .blend(v1, v0, x); 105 r.tupleof[i] = vr; 106 } 107 return r; 108 } 109 110 /// Construct an RGB color from a typical hex string. 111 static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte)) 112 { 113 static typeof(this) fromHex(in char[] s) 114 { 115 import std.conv; 116 import std.exception; 117 118 enforce(s.length == 6, "Invalid color string"); 119 typeof(this) c; 120 c.r = s[0..2].to!ubyte(16); 121 c.g = s[2..4].to!ubyte(16); 122 c.b = s[4..6].to!ubyte(16); 123 return c; 124 } 125 126 string toHex() const 127 { 128 import std.string; 129 return format("%02X%02X%02X", r, g, b); 130 } 131 } 132 133 /// Warning: overloaded operators preserve types and may cause overflows 134 typeof(this) opUnary(string op)() 135 if (op=="~" || op=="-") 136 { 137 typeof(this) r; 138 foreach (i, f; r.tupleof) 139 static if(r.tupleof[i].stringof != "r.x") // skip padding 140 r.tupleof[i] = cast(typeof(r.tupleof[i])) mixin(op ~ `this.tupleof[i]`); 141 return r; 142 } 143 144 /// ditto 145 typeof(this) opOpAssign(string op)(int o) 146 { 147 foreach (i, f; this.tupleof) 148 static if(this.tupleof[i].stringof != "this.x") // skip padding 149 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`); 150 return this; 151 } 152 153 /// ditto 154 typeof(this) opOpAssign(string op, T)(T o) 155 if (is(T==struct) && structFields!T == structFields!Fields) 156 { 157 foreach (i, f; this.tupleof) 158 static if(this.tupleof[i].stringof != "this.x") // skip padding 159 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`); 160 return this; 161 } 162 163 /// ditto 164 typeof(this) opBinary(string op, T)(T o) 165 if (op != "~") 166 { 167 auto r = this; 168 mixin("r" ~ op ~ "=o;"); 169 return r; 170 } 171 172 /// Apply a custom operation for each channel. Example: 173 /// COLOR.op!q{(a + b) / 2}(colorA, colorB); 174 static typeof(this) op(string expr, T...)(T values) 175 { 176 static assert(values.length <= 10); 177 178 string genVars(string channel) 179 { 180 string result; 181 foreach (j, Tj; T) 182 { 183 static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)? 184 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~ channel ~ ";\n"; 185 else 186 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n"; 187 } 188 return result; 189 } 190 191 typeof(this) r; 192 foreach (i, f; r.tupleof) 193 static if(r.tupleof[i].stringof != "r.x") // skip padding 194 { 195 mixin(genVars(r.tupleof[i].stringof[2..$])); 196 r.tupleof[i] = mixin(expr); 197 } 198 return r; 199 } 200 201 T opCast(T)() 202 if (is(T==struct) && structFields!T == structFields!Fields) 203 { 204 T t; 205 foreach (i, f; this.tupleof) 206 t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i]; 207 return t; 208 } 209 210 /// Sum of all channels 211 ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum() 212 { 213 typeof(return) result; 214 foreach (i, f; this.tupleof) 215 static if (this.tupleof[i].stringof != "this.x") // skip padding 216 result += this.tupleof[i]; 217 return result; 218 } 219 220 static @property Color min() 221 { 222 Color result; 223 foreach (ref v; result.tupleof) 224 static if (is(typeof(typeof(v).min))) 225 v = typeof(v).min; 226 else 227 static if (is(typeof(typeof(v).max))) 228 v = -typeof(v).max; 229 return result; 230 } 231 232 static @property Color max() 233 { 234 Color result; 235 foreach (ref v; result.tupleof) 236 static if (is(typeof(typeof(v).max))) 237 v = typeof(v).max; 238 return result; 239 } 240 } 241 242 // The "x" has the special meaning of "padding" and is ignored in some circumstances 243 alias Color!(ubyte , "r", "g", "b" ) RGB ; 244 alias Color!(ushort , "r", "g", "b" ) RGB16 ; 245 alias Color!(ubyte , "r", "g", "b", "x") RGBX ; 246 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ; 247 alias Color!(ubyte , "r", "g", "b", "a") RGBA ; 248 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ; 249 250 alias Color!(ubyte , "b", "g", "r" ) BGR ; 251 alias Color!(ubyte , "b", "g", "r", "x") BGRX ; 252 alias Color!(ubyte , "b", "g", "r", "a") BGRA ; 253 254 alias Color!(ubyte , "l" ) L8 ; 255 alias Color!(ushort , "l" ) L16 ; 256 alias Color!(ubyte , "l", "a" ) LA ; 257 alias Color!(ushort , "l", "a" ) LA16 ; 258 259 alias Color!(byte , "l" ) S8 ; 260 alias Color!(short , "l" ) S16 ; 261 262 alias Color!(float , "r", "g", "b" ) RGBf ; 263 alias Color!(double , "r", "g", "b" ) RGBd ; 264 265 unittest 266 { 267 static assert(RGB.sizeof == 3); 268 RGB[2] arr; 269 static assert(arr.sizeof == 6); 270 271 RGB hex = RGB.fromHex("123456"); 272 assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56); 273 274 assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9)); 275 276 RGB c = RGB(1, 1, 1); 277 c += 1; 278 assert(c == RGB(2, 2, 2)); 279 c += c; 280 assert(c == RGB(4, 4, 4)); 281 } 282 283 static assert(RGB.min == RGB( 0, 0, 0)); 284 static assert(RGB.max == RGB(255, 255, 255)); 285 286 unittest 287 { 288 import std.conv; 289 290 L8 r; 291 292 r = L8.itpl(L8(100), L8(200), 15, 10, 20); 293 assert(r == L8(150), text(r)); 294 } 295 296 unittest 297 { 298 import std.conv; 299 300 LA r; 301 302 r = LA.blend(LA(123, 0), 303 LA(111, 222)); 304 assert(r == LA(111, 222), text(r)); 305 306 r = LA.blend(LA(123, 213), 307 LA(111, 255)); 308 assert(r == LA(111, 255), text(r)); 309 310 r = LA.blend(LA( 0, 255), 311 LA(255, 100)); 312 assert(r == LA(100, 255), text(r)); 313 } 314 315 unittest 316 { 317 Color!(real, "r", "g", "b") c; 318 } 319 320 /// Obtains the type of each channel for homogenous colors. 321 template ChannelType(T) 322 { 323 static if (is(T == struct)) 324 alias ChannelType = T.ChannelType; 325 else 326 alias ChannelType = T; 327 } 328 329 /// Resolves to a Color instance with a different ChannelType. 330 template ChangeChannelType(COLOR, T) 331 if (isNumeric!COLOR) 332 { 333 alias ChangeChannelType = T; 334 } 335 336 /// ditto 337 template ChangeChannelType(COLOR, T) 338 if (is(COLOR : Color!Spec, Spec...)) 339 { 340 static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color"); 341 alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]); 342 } 343 344 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16)); 345 static assert(is(ChangeChannelType!(int, ushort) == ushort)); 346 347 /// Wrapper around ExpandNumericType to only expand numeric types. 348 template ExpandIntegerType(T, size_t bits) 349 { 350 static if (is(T:real)) 351 alias ExpandIntegerType = T; 352 else 353 alias ExpandIntegerType = ExpandNumericType!(T, bits); 354 } 355 356 alias ExpandChannelType(COLOR, int BYTES) = 357 ChangeChannelType!(COLOR, 358 ExpandNumericType!(ChannelType!COLOR, BYTES * 8)); 359 360 static assert(is(ExpandChannelType!(RGB, 1) == RGB16)); 361 362 unittest 363 { 364 alias RGBf = ChangeChannelType!(RGB, float); 365 auto rgb = RGB(1, 2, 3); 366 import std.conv : to; 367 auto rgbf = rgb.to!RGBf(); 368 assert(rgbf.r == 1f); 369 assert(rgbf.g == 2f); 370 assert(rgbf.b == 3f); 371 } 372 373 // *************************************************************************** 374 375 // TODO: deprecate 376 T blend(T)(T f, T b, T a) if (is(typeof(f*a+~b))) { return cast(T) ( ((f*a) + (b*~a)) / T.max ); }