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 /// Alpha-blend a color with an alpha channel on top of one without. 111 static typeof(this) blend(C)(typeof(this) c0, C c1) 112 if (!is(typeof(a)) && is(typeof(c1.a))) 113 { 114 alias A = typeof(c1.a); 115 if (!c1.a) 116 return c0; 117 //A x = cast(A)(c1.a * A.max / a); 118 119 typeof(this) r; 120 foreach (i, ref f; r.tupleof) 121 { 122 enum name = __traits(identifier, r.tupleof[i]); 123 static if (name == "x") 124 {} // skip padding 125 else 126 static if (name == "a") 127 static assert(false); 128 else 129 { 130 auto v0 = __traits(getMember, c0, name); 131 auto v1 = __traits(getMember, c1, name); 132 f = .blend(v1, v0, c1.a); 133 } 134 } 135 return r; 136 } 137 138 /// Construct an RGB color from a typical hex string. 139 static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte)) 140 { 141 static typeof(this) fromHex(in char[] s) 142 { 143 import std.conv; 144 import std.exception; 145 146 enforce(s.length == 6, "Invalid color string"); 147 typeof(this) c; 148 c.r = s[0..2].to!ubyte(16); 149 c.g = s[2..4].to!ubyte(16); 150 c.b = s[4..6].to!ubyte(16); 151 return c; 152 } 153 154 string toHex() const 155 { 156 import std.string; 157 return format("%02X%02X%02X", r, g, b); 158 } 159 } 160 161 /// Warning: overloaded operators preserve types and may cause overflows 162 typeof(this) opUnary(string op)() 163 if (op=="~" || op=="-") 164 { 165 typeof(this) r; 166 foreach (i, f; r.tupleof) 167 static if(r.tupleof[i].stringof != "r.x") // skip padding 168 r.tupleof[i] = cast(typeof(r.tupleof[i])) mixin(op ~ `this.tupleof[i]`); 169 return r; 170 } 171 172 /// ditto 173 typeof(this) opOpAssign(string op)(int o) 174 { 175 foreach (i, f; this.tupleof) 176 static if(this.tupleof[i].stringof != "this.x") // skip padding 177 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`); 178 return this; 179 } 180 181 /// ditto 182 typeof(this) opOpAssign(string op, T)(T o) 183 if (is(T==struct) && structFields!T == structFields!Fields) 184 { 185 foreach (i, f; this.tupleof) 186 static if(this.tupleof[i].stringof != "this.x") // skip padding 187 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`); 188 return this; 189 } 190 191 /// ditto 192 typeof(this) opBinary(string op, T)(T o) 193 if (op != "~") 194 { 195 auto r = this; 196 mixin("r" ~ op ~ "=o;"); 197 return r; 198 } 199 200 /// Apply a custom operation for each channel. Example: 201 /// COLOR.op!q{(a + b) / 2}(colorA, colorB); 202 static typeof(this) op(string expr, T...)(T values) 203 { 204 static assert(values.length <= 10); 205 206 string genVars(string channel) 207 { 208 string result; 209 foreach (j, Tj; T) 210 { 211 static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)? 212 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~ channel ~ ";\n"; 213 else 214 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n"; 215 } 216 return result; 217 } 218 219 typeof(this) r; 220 foreach (i, f; r.tupleof) 221 static if(r.tupleof[i].stringof != "r.x") // skip padding 222 { 223 mixin(genVars(r.tupleof[i].stringof[2..$])); 224 r.tupleof[i] = mixin(expr); 225 } 226 return r; 227 } 228 229 T opCast(T)() 230 if (is(T==struct) && structFields!T == structFields!Fields) 231 { 232 T t; 233 foreach (i, f; this.tupleof) 234 t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i]; 235 return t; 236 } 237 238 /// Sum of all channels 239 ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum() 240 { 241 typeof(return) result; 242 foreach (i, f; this.tupleof) 243 static if (this.tupleof[i].stringof != "this.x") // skip padding 244 result += this.tupleof[i]; 245 return result; 246 } 247 248 static @property Color min() 249 { 250 Color result; 251 foreach (ref v; result.tupleof) 252 static if (is(typeof(typeof(v).min))) 253 v = typeof(v).min; 254 else 255 static if (is(typeof(typeof(v).max))) 256 v = -typeof(v).max; 257 return result; 258 } 259 260 static @property Color max() 261 { 262 Color result; 263 foreach (ref v; result.tupleof) 264 static if (is(typeof(typeof(v).max))) 265 v = typeof(v).max; 266 return result; 267 } 268 } 269 270 // The "x" has the special meaning of "padding" and is ignored in some circumstances 271 alias Color!(ubyte , "r", "g", "b" ) RGB ; 272 alias Color!(ushort , "r", "g", "b" ) RGB16 ; 273 alias Color!(ubyte , "r", "g", "b", "x") RGBX ; 274 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ; 275 alias Color!(ubyte , "r", "g", "b", "a") RGBA ; 276 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ; 277 278 alias Color!(ubyte , "b", "g", "r" ) BGR ; 279 alias Color!(ubyte , "b", "g", "r", "x") BGRX ; 280 alias Color!(ubyte , "b", "g", "r", "a") BGRA ; 281 282 alias Color!(ubyte , "l" ) L8 ; 283 alias Color!(ushort , "l" ) L16 ; 284 alias Color!(ubyte , "l", "a" ) LA ; 285 alias Color!(ushort , "l", "a" ) LA16 ; 286 287 alias Color!(byte , "l" ) S8 ; 288 alias Color!(short , "l" ) S16 ; 289 290 alias Color!(float , "r", "g", "b" ) RGBf ; 291 alias Color!(double , "r", "g", "b" ) RGBd ; 292 293 unittest 294 { 295 static assert(RGB.sizeof == 3); 296 RGB[2] arr; 297 static assert(arr.sizeof == 6); 298 299 RGB hex = RGB.fromHex("123456"); 300 assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56); 301 302 assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9)); 303 304 RGB c = RGB(1, 1, 1); 305 c += 1; 306 assert(c == RGB(2, 2, 2)); 307 c += c; 308 assert(c == RGB(4, 4, 4)); 309 } 310 311 static assert(RGB.min == RGB( 0, 0, 0)); 312 static assert(RGB.max == RGB(255, 255, 255)); 313 314 unittest 315 { 316 import std.conv; 317 318 L8 r; 319 320 r = L8.itpl(L8(100), L8(200), 15, 10, 20); 321 assert(r == L8(150), text(r)); 322 } 323 324 unittest 325 { 326 import std.conv; 327 328 LA r; 329 330 r = LA.blend(LA(123, 0), 331 LA(111, 222)); 332 assert(r == LA(111, 222), text(r)); 333 334 r = LA.blend(LA(123, 213), 335 LA(111, 255)); 336 assert(r == LA(111, 255), text(r)); 337 338 r = LA.blend(LA( 0, 255), 339 LA(255, 100)); 340 assert(r == LA(100, 255), text(r)); 341 } 342 343 unittest 344 { 345 import std.conv; 346 347 L8 r; 348 349 r = L8.blend(L8(123), 350 LA(231, 0)); 351 assert(r == L8(123), text(r)); 352 353 r = L8.blend(L8(123), 354 LA(231, 255)); 355 assert(r == L8(231), text(r)); 356 357 r = L8.blend(L8( 0), 358 LA(255, 100)); 359 assert(r == L8(100), text(r)); 360 } 361 362 unittest 363 { 364 Color!(real, "r", "g", "b") c; 365 } 366 367 /// Obtains the type of each channel for homogenous colors. 368 template ChannelType(T) 369 { 370 static if (is(T == struct)) 371 alias ChannelType = T.ChannelType; 372 else 373 alias ChannelType = T; 374 } 375 376 /// Resolves to a Color instance with a different ChannelType. 377 template ChangeChannelType(COLOR, T) 378 if (isNumeric!COLOR) 379 { 380 alias ChangeChannelType = T; 381 } 382 383 /// ditto 384 template ChangeChannelType(COLOR, T) 385 if (is(COLOR : Color!Spec, Spec...)) 386 { 387 static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color"); 388 alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]); 389 } 390 391 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16)); 392 static assert(is(ChangeChannelType!(int, ushort) == ushort)); 393 394 /// Wrapper around ExpandNumericType to only expand numeric types. 395 template ExpandIntegerType(T, size_t bits) 396 { 397 static if (is(T:real)) 398 alias ExpandIntegerType = T; 399 else 400 alias ExpandIntegerType = ExpandNumericType!(T, bits); 401 } 402 403 alias ExpandChannelType(COLOR, int BYTES) = 404 ChangeChannelType!(COLOR, 405 ExpandNumericType!(ChannelType!COLOR, BYTES * 8)); 406 407 static assert(is(ExpandChannelType!(RGB, 1) == RGB16)); 408 409 unittest 410 { 411 alias RGBf = ChangeChannelType!(RGB, float); 412 auto rgb = RGB(1, 2, 3); 413 import std.conv : to; 414 auto rgbf = rgb.to!RGBf(); 415 assert(rgbf.r == 1f); 416 assert(rgbf.g == 2f); 417 assert(rgbf.b == 3f); 418 } 419 420 // *************************************************************************** 421 422 // TODO: deprecate 423 T blend(T)(T f, T b, T a) if (is(typeof(f*a+~b))) { return cast(T) ( ((f*a) + (b*~a)) / T.max ); }