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 static if (__traits(identifier, r.tupleof[i]) == "a") 62 r.tupleof[i] = typeof(r.tupleof[i]).max; 63 else 64 r.tupleof[i] = value; 65 return r; 66 } 67 68 static if (is(ChannelType:uint)) 69 { 70 enum typeof(this) black = monochrome(0); 71 enum typeof(this) white = monochrome(ChannelType.max); 72 } 73 74 /// Interpolate between two colors. 75 /// See also: Gradient 76 static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1) 77 { 78 alias TryExpandNumericType!(ChannelType, P.sizeof*8) U; 79 typeof(this) r; 80 foreach (i, f; r.tupleof) 81 static if (r.tupleof[i].stringof != "r.x") // skip padding 82 r.tupleof[i] = cast(ChannelType).itpl(cast(U)c0.tupleof[i], cast(U)c1.tupleof[i], p, p0, p1); 83 return r; 84 } 85 86 /// Alpha-blend two colors. 87 static typeof(this) blend()(typeof(this) c0, typeof(this) c1) 88 if (is(typeof(a))) 89 { 90 alias A = typeof(c0.a); 91 A a = flipBits(cast(A)(c0.a.flipBits * c1.a.flipBits / A.max)); 92 if (!a) 93 return typeof(this).init; 94 A x = cast(A)(c1.a * A.max / a); 95 96 typeof(this) r; 97 foreach (i, f; r.tupleof) 98 static if (r.tupleof[i].stringof == "r.x") 99 {} // skip padding 100 else 101 static if (r.tupleof[i].stringof == "r.a") 102 r.a = a; 103 else 104 { 105 auto v0 = c0.tupleof[i]; 106 auto v1 = c1.tupleof[i]; 107 auto vr = .blend(v1, v0, x); 108 r.tupleof[i] = vr; 109 } 110 return r; 111 } 112 113 /// Alpha-blend a color with an alpha channel on top of one without. 114 static typeof(this) blend(C)(typeof(this) c0, C c1) 115 if (!is(typeof(a)) && is(typeof(c1.a))) 116 { 117 alias A = typeof(c1.a); 118 if (!c1.a) 119 return c0; 120 //A x = cast(A)(c1.a * A.max / a); 121 122 typeof(this) r; 123 foreach (i, ref f; r.tupleof) 124 { 125 enum name = __traits(identifier, r.tupleof[i]); 126 static if (name == "x") 127 {} // skip padding 128 else 129 static if (name == "a") 130 static assert(false); 131 else 132 { 133 auto v0 = __traits(getMember, c0, name); 134 auto v1 = __traits(getMember, c1, name); 135 f = .blend(v1, v0, c1.a); 136 } 137 } 138 return r; 139 } 140 141 /// Construct an RGB color from a typical hex string. 142 static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte)) 143 { 144 static typeof(this) fromHex(in char[] s) 145 { 146 import std.conv; 147 import std.exception; 148 149 enforce(s.length == 6 || (is(typeof(this.a) == ubyte) && s.length == 8), "Invalid color string"); 150 typeof(this) c; 151 c.r = s[0..2].to!ubyte(16); 152 c.g = s[2..4].to!ubyte(16); 153 c.b = s[4..6].to!ubyte(16); 154 static if (is(typeof(this.a) == ubyte)) 155 { 156 if (s.length == 8) 157 c.a = s[6..8].to!ubyte(16); 158 else 159 c.a = ubyte.max; 160 } 161 return c; 162 } 163 164 string toHex() const 165 { 166 import std.string; 167 return format("%02X%02X%02X", r, g, b); 168 } 169 } 170 171 /// Warning: overloaded operators preserve types and may cause overflows 172 typeof(this) opUnary(string op)() 173 if (op=="~" || op=="-") 174 { 175 typeof(this) r; 176 foreach (i, f; r.tupleof) 177 static if(r.tupleof[i].stringof != "r.x") // skip padding 178 r.tupleof[i] = cast(typeof(r.tupleof[i])) unary!(op[0])(this.tupleof[i]); 179 return r; 180 } 181 182 /// ditto 183 typeof(this) opOpAssign(string op)(int o) 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`); 188 return this; 189 } 190 191 /// ditto 192 typeof(this) opOpAssign(string op, T)(T o) 193 if (is(T==struct) && structFields!T == structFields!Fields) 194 { 195 foreach (i, f; this.tupleof) 196 static if(this.tupleof[i].stringof != "this.x") // skip padding 197 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`); 198 return this; 199 } 200 201 /// ditto 202 typeof(this) opBinary(string op, T)(T o) 203 if (op != "~") 204 { 205 auto r = this; 206 mixin("r" ~ op ~ "=o;"); 207 return r; 208 } 209 210 /// Apply a custom operation for each channel. Example: 211 /// COLOR.op!q{(a + b) / 2}(colorA, colorB); 212 static typeof(this) op(string expr, T...)(T values) 213 { 214 static assert(values.length <= 10); 215 216 string genVars(string channel) 217 { 218 string result; 219 foreach (j, Tj; T) 220 { 221 static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)? 222 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~ channel ~ ";\n"; 223 else 224 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n"; 225 } 226 return result; 227 } 228 229 typeof(this) r; 230 foreach (i, f; r.tupleof) 231 static if(r.tupleof[i].stringof != "r.x") // skip padding 232 { 233 mixin(genVars(r.tupleof[i].stringof[2..$])); 234 r.tupleof[i] = mixin(expr); 235 } 236 return r; 237 } 238 239 T opCast(T)() const 240 if (is(T==struct) && structFields!T == structFields!Fields) 241 { 242 static if (is(T == typeof(this))) 243 return this; 244 else 245 { 246 T t; 247 foreach (i, f; this.tupleof) 248 t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i]; 249 return t; 250 } 251 } 252 253 /// Sum of all channels 254 ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum() 255 { 256 typeof(return) result; 257 foreach (i, f; this.tupleof) 258 static if (this.tupleof[i].stringof != "this.x") // skip padding 259 result += this.tupleof[i]; 260 return result; 261 } 262 263 static @property Color min() 264 { 265 Color result; 266 foreach (ref v; result.tupleof) 267 static if (is(typeof(typeof(v).min))) 268 v = typeof(v).min; 269 else 270 static if (is(typeof(typeof(v).max))) 271 v = -typeof(v).max; 272 return result; 273 } 274 275 static @property Color max() 276 { 277 Color result; 278 foreach (ref v; result.tupleof) 279 static if (is(typeof(typeof(v).max))) 280 v = typeof(v).max; 281 return result; 282 } 283 } 284 285 // The "x" has the special meaning of "padding" and is ignored in some circumstances 286 alias Color!(ubyte , "r", "g", "b" ) RGB ; 287 alias Color!(ushort , "r", "g", "b" ) RGB16 ; 288 alias Color!(ubyte , "r", "g", "b", "x") RGBX ; 289 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ; 290 alias Color!(ubyte , "r", "g", "b", "a") RGBA ; 291 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ; 292 293 alias Color!(ubyte , "b", "g", "r" ) BGR ; 294 alias Color!(ubyte , "b", "g", "r", "x") BGRX ; 295 alias Color!(ubyte , "b", "g", "r", "a") BGRA ; 296 297 alias Color!(ubyte , "l" ) L8 ; 298 alias Color!(ushort , "l" ) L16 ; 299 alias Color!(ubyte , "l", "a" ) LA ; 300 alias Color!(ushort , "l", "a" ) LA16 ; 301 302 alias Color!(byte , "l" ) S8 ; 303 alias Color!(short , "l" ) S16 ; 304 305 alias Color!(float , "r", "g", "b" ) RGBf ; 306 alias Color!(double , "r", "g", "b" ) RGBd ; 307 308 unittest 309 { 310 static assert(RGB.sizeof == 3); 311 RGB[2] arr; 312 static assert(arr.sizeof == 6); 313 314 RGB hex = RGB.fromHex("123456"); 315 assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56); 316 317 BGRA hex2 = BGRA.fromHex("12345678"); 318 assert(hex2.r == 0x12 && hex2.g == 0x34 && hex2.b == 0x56 && hex2.a == 0x78); 319 320 assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9)); 321 322 RGB c = RGB(1, 1, 1); 323 c += 1; 324 assert(c == RGB(2, 2, 2)); 325 c += c; 326 assert(c == RGB(4, 4, 4)); 327 } 328 329 static assert(RGB.min == RGB( 0, 0, 0)); 330 static assert(RGB.max == RGB(255, 255, 255)); 331 332 unittest 333 { 334 import std.conv; 335 336 L8 r; 337 338 r = L8.itpl(L8(100), L8(200), 15, 10, 20); 339 assert(r == L8(150), text(r)); 340 } 341 342 unittest 343 { 344 import std.conv; 345 346 LA r; 347 348 r = LA.blend(LA(123, 0), 349 LA(111, 222)); 350 assert(r == LA(111, 222), text(r)); 351 352 r = LA.blend(LA(123, 213), 353 LA(111, 255)); 354 assert(r == LA(111, 255), text(r)); 355 356 r = LA.blend(LA( 0, 255), 357 LA(255, 100)); 358 assert(r == LA(100, 255), text(r)); 359 } 360 361 unittest 362 { 363 import std.conv; 364 365 L8 r; 366 367 r = L8.blend(L8(123), 368 LA(231, 0)); 369 assert(r == L8(123), text(r)); 370 371 r = L8.blend(L8(123), 372 LA(231, 255)); 373 assert(r == L8(231), text(r)); 374 375 r = L8.blend(L8( 0), 376 LA(255, 100)); 377 assert(r == L8(100), text(r)); 378 } 379 380 unittest 381 { 382 Color!(real, "r", "g", "b") c; 383 } 384 385 unittest 386 { 387 const RGB c; 388 RGB x = cast(RGB)c; 389 } 390 391 /// Obtains the type of each channel for homogenous colors. 392 template ChannelType(T) 393 { 394 static if (is(T == struct)) 395 alias ChannelType = T.ChannelType; 396 else 397 alias ChannelType = T; 398 } 399 400 /// Resolves to a Color instance with a different ChannelType. 401 template ChangeChannelType(COLOR, T) 402 if (isNumeric!COLOR) 403 { 404 alias ChangeChannelType = T; 405 } 406 407 /// ditto 408 template ChangeChannelType(COLOR, T) 409 if (is(COLOR : Color!Spec, Spec...)) 410 { 411 static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color"); 412 alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]); 413 } 414 415 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16)); 416 static assert(is(ChangeChannelType!(int, ushort) == ushort)); 417 418 /// Wrapper around ExpandNumericType to only expand numeric types. 419 template ExpandIntegerType(T, size_t bits) 420 { 421 static if (is(T:real)) 422 alias ExpandIntegerType = T; 423 else 424 alias ExpandIntegerType = ExpandNumericType!(T, bits); 425 } 426 427 alias ExpandChannelType(COLOR, int BYTES) = 428 ChangeChannelType!(COLOR, 429 ExpandNumericType!(ChannelType!COLOR, BYTES * 8)); 430 431 static assert(is(ExpandChannelType!(RGB, 1) == RGB16)); 432 433 unittest 434 { 435 alias RGBf = ChangeChannelType!(RGB, float); 436 auto rgb = RGB(1, 2, 3); 437 import std.conv : to; 438 auto rgbf = rgb.to!RGBf(); 439 assert(rgbf.r == 1f); 440 assert(rgbf.g == 2f); 441 assert(rgbf.b == 3f); 442 } 443 444 // *************************************************************************** 445 446 /// Calculate an interpolated color on a gradient with multiple points 447 struct Gradient(Value, Color) 448 { 449 struct Point 450 { 451 Value value; 452 Color color; 453 } 454 Point[] points; 455 456 Color get(Value value) const 457 { 458 assert(points.length, "Gradient must have at least one point"); 459 460 if (value <= points[0].value) 461 return points[0].color; 462 463 for (int i = 0; i < points.length - 1; i++) 464 { 465 assert(points[i].value <= points[i+1].value, 466 "Gradient values are not in ascending order"); 467 if (value < points[i+1].value) 468 return Color.itpl(points[i].color, points[i+1].color, value, points[i].value, points[i+1].value); 469 } 470 471 return points[$-1].color; 472 } 473 } 474 475 unittest 476 { 477 Gradient!(int, L8) grad; 478 grad.points = [ 479 grad.Point(0, L8(0)), 480 grad.Point(10, L8(100)), 481 ]; 482 483 assert(grad.get(-5) == L8( 0)); 484 assert(grad.get( 0) == L8( 0)); 485 assert(grad.get( 5) == L8( 50)); 486 assert(grad.get(10) == L8(100)); 487 assert(grad.get(15) == L8(100)); 488 } 489 490 unittest 491 { 492 Gradient!(float, L8) grad; 493 grad.points = [ 494 grad.Point(0.0f, L8( 0)), 495 grad.Point(0.5f, L8(10)), 496 grad.Point(1.0f, L8(30)), 497 ]; 498 499 assert(grad.get(0.00f) == L8( 0)); 500 assert(grad.get(0.25f) == L8( 5)); 501 assert(grad.get(0.50f) == L8( 10)); 502 assert(grad.get(0.75f) == L8( 20)); 503 assert(grad.get(1.00f) == L8( 30)); 504 } 505 506 // *************************************************************************** 507 508 // TODO: deprecate 509 T blend(T)(T f, T b, T a) if (is(typeof(f*a+flipBits(b)))) { return cast(T) ( ((f*a) + (b*flipBits(a))) / T.max ); }