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 <ae@cy.md> 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 /// Additional properties for homogeneous colors. 51 static if (homogenous) 52 { 53 alias ChannelType = typeof(Fields.init.tupleof[0]); 54 enum channelBits = valueBits!ChannelType; 55 } 56 57 /// Return a Color instance with all fields set to "value". 58 static typeof(this) monochrome(ChannelType value) 59 { 60 typeof(this) r; 61 foreach (i, f; r.tupleof) 62 static if (__traits(identifier, r.tupleof[i]) == "a") 63 r.tupleof[i] = typeof(r.tupleof[i]).max; 64 else 65 r.tupleof[i] = value; 66 return r; 67 } 68 69 /// Additional properties for integer colors. 70 static if (is(ChannelType:uint)) 71 { 72 enum typeof(this) black = monochrome(0); 73 enum typeof(this) white = monochrome(ChannelType.max); 74 } 75 76 /// Interpolate between two colors. 77 /// See also: Gradient 78 static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1) 79 { 80 alias TryExpandNumericType!(ChannelType, P.sizeof*8) U; 81 typeof(this) r; 82 foreach (i, f; r.tupleof) 83 static if (r.tupleof[i].stringof != "r.x") // skip padding 84 r.tupleof[i] = cast(ChannelType).itpl(cast(U)c0.tupleof[i], cast(U)c1.tupleof[i], p, p0, p1); 85 return r; 86 } 87 88 /// Alpha-blend two colors. 89 static typeof(this) blend()(typeof(this) c0, typeof(this) c1) 90 if (is(typeof(a))) 91 { 92 alias A = typeof(c0.a); 93 A a = flipBits(cast(A)(c0.a.flipBits * c1.a.flipBits / A.max)); 94 if (!a) 95 return typeof(this).init; 96 A x = cast(A)(c1.a * A.max / a); 97 98 typeof(this) r; 99 foreach (i, f; r.tupleof) 100 static if (r.tupleof[i].stringof == "r.x") 101 {} // skip padding 102 else 103 static if (r.tupleof[i].stringof == "r.a") 104 r.a = a; 105 else 106 { 107 auto v0 = c0.tupleof[i]; 108 auto v1 = c1.tupleof[i]; 109 auto vr = .blend(v1, v0, x); 110 r.tupleof[i] = vr; 111 } 112 return r; 113 } 114 115 /// Alpha-blend a color with an alpha channel on top of one without. 116 static typeof(this) blend(C)(typeof(this) c0, C c1) 117 if (!is(typeof(a)) && is(typeof(c1.a))) 118 { 119 alias A = typeof(c1.a); 120 if (!c1.a) 121 return c0; 122 //A x = cast(A)(c1.a * A.max / a); 123 124 typeof(this) r; 125 foreach (i, ref f; r.tupleof) 126 { 127 enum name = __traits(identifier, r.tupleof[i]); 128 static if (name == "x") 129 {} // skip padding 130 else 131 static if (name == "a") 132 static assert(false); 133 else 134 { 135 auto v0 = __traits(getMember, c0, name); 136 auto v1 = __traits(getMember, c1, name); 137 f = .blend(v1, v0, c1.a); 138 } 139 } 140 return r; 141 } 142 143 /// Construct an RGB color from a typical hex string. 144 static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte)) 145 { 146 static typeof(this) fromHex(in char[] s) 147 { 148 import std.conv; 149 import std.exception; 150 151 enforce(s.length == 6 || (is(typeof(this.a) == ubyte) && s.length == 8), "Invalid color string"); 152 typeof(this) c; 153 c.r = s[0..2].to!ubyte(16); 154 c.g = s[2..4].to!ubyte(16); 155 c.b = s[4..6].to!ubyte(16); 156 static if (is(typeof(this.a) == ubyte)) 157 { 158 if (s.length == 8) 159 c.a = s[6..8].to!ubyte(16); 160 else 161 c.a = ubyte.max; 162 } 163 return c; 164 } 165 166 string toHex() const 167 { 168 import std..string; 169 return format("%02X%02X%02X", r, g, b); 170 } 171 } 172 173 /// Warning: overloaded operators preserve types and may cause overflows 174 typeof(this) opUnary(string op)() 175 if (op=="~" || op=="-") 176 { 177 typeof(this) r; 178 foreach (i, f; r.tupleof) 179 static if(r.tupleof[i].stringof != "r.x") // skip padding 180 r.tupleof[i] = cast(typeof(r.tupleof[i])) unary!(op[0])(this.tupleof[i]); 181 return r; 182 } 183 184 /// ditto 185 typeof(this) opOpAssign(string op)(int o) 186 { 187 foreach (i, f; this.tupleof) 188 static if(this.tupleof[i].stringof != "this.x") // skip padding 189 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`); 190 return this; 191 } 192 193 /// ditto 194 typeof(this) opOpAssign(string op, T)(T o) 195 if (is(T==struct) && structFields!T == structFields!Fields) 196 { 197 foreach (i, f; this.tupleof) 198 static if(this.tupleof[i].stringof != "this.x") // skip padding 199 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`); 200 return this; 201 } 202 203 /// ditto 204 typeof(this) opBinary(string op, T)(T o) 205 if (op != "~") 206 { 207 auto r = this; 208 mixin("r" ~ op ~ "=o;"); 209 return r; 210 } 211 212 /// Apply a custom operation for each channel. Example: 213 /// COLOR.op!q{(a + b) / 2}(colorA, colorB); 214 static typeof(this) op(string expr, T...)(T values) 215 { 216 static assert(values.length <= 10); 217 218 string genVars(string channel) 219 { 220 string result; 221 foreach (j, Tj; T) 222 { 223 static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)? 224 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~ channel ~ ";\n"; 225 else 226 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n"; 227 } 228 return result; 229 } 230 231 typeof(this) r; 232 foreach (i, f; r.tupleof) 233 static if(r.tupleof[i].stringof != "r.x") // skip padding 234 { 235 mixin(genVars(r.tupleof[i].stringof[2..$])); 236 r.tupleof[i] = mixin(expr); 237 } 238 return r; 239 } 240 241 /// Implements conversion to a similar color type. 242 T opCast(T)() const 243 if (is(T==struct) && structFields!T == structFields!Fields) 244 { 245 static if (is(T == typeof(this))) 246 return this; 247 else 248 { 249 T t; 250 foreach (i, f; this.tupleof) 251 t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i]; 252 return t; 253 } 254 } 255 256 /// Sum of all channels 257 ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum() 258 { 259 typeof(return) result; 260 foreach (i, f; this.tupleof) 261 static if (this.tupleof[i].stringof != "this.x") // skip padding 262 result += this.tupleof[i]; 263 return result; 264 } 265 266 /// Returns an instance of this color type 267 /// with all fields set at their minimum values. 268 static @property Color min() 269 { 270 Color result; 271 foreach (ref v; result.tupleof) 272 static if (is(typeof(typeof(v).min))) 273 v = typeof(v).min; 274 else 275 static if (is(typeof(typeof(v).max))) 276 v = -typeof(v).max; 277 return result; 278 } 279 280 /// Returns an instance of this color type 281 /// with all fields set at their maximum values. 282 static @property Color max() 283 { 284 Color result; 285 foreach (ref v; result.tupleof) 286 static if (is(typeof(typeof(v).max))) 287 v = typeof(v).max; 288 return result; 289 } 290 } 291 292 // The "x" has the special meaning of "padding" and is ignored in some circumstances 293 294 /// Definitions for common color types. 295 version(all) 296 { 297 alias Color!(ubyte , "r", "g", "b" ) RGB ; 298 alias Color!(ushort , "r", "g", "b" ) RGB16 ; 299 alias Color!(ubyte , "r", "g", "b", "x") RGBX ; 300 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ; 301 alias Color!(ubyte , "r", "g", "b", "a") RGBA ; 302 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ; 303 304 alias Color!(ubyte , "b", "g", "r" ) BGR ; 305 alias Color!(ubyte , "b", "g", "r", "x") BGRX ; 306 alias Color!(ubyte , "b", "g", "r", "a") BGRA ; 307 308 alias Color!(ubyte , "l" ) L8 ; 309 alias Color!(ushort , "l" ) L16 ; 310 alias Color!(ubyte , "l", "a" ) LA ; 311 alias Color!(ushort , "l", "a" ) LA16 ; 312 313 alias Color!(byte , "l" ) S8 ; 314 alias Color!(short , "l" ) S16 ; 315 316 alias Color!(float , "r", "g", "b" ) RGBf ; 317 alias Color!(double , "r", "g", "b" ) RGBd ; 318 } 319 320 unittest 321 { 322 static assert(RGB.sizeof == 3); 323 RGB[2] arr; 324 static assert(arr.sizeof == 6); 325 326 RGB hex = RGB.fromHex("123456"); 327 assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56); 328 329 BGRA hex2 = BGRA.fromHex("12345678"); 330 assert(hex2.r == 0x12 && hex2.g == 0x34 && hex2.b == 0x56 && hex2.a == 0x78); 331 332 assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9)); 333 334 RGB c = RGB(1, 1, 1); 335 c += 1; 336 assert(c == RGB(2, 2, 2)); 337 c += c; 338 assert(c == RGB(4, 4, 4)); 339 } 340 341 static assert(RGB.min == RGB( 0, 0, 0)); 342 static assert(RGB.max == RGB(255, 255, 255)); 343 344 unittest 345 { 346 import std.conv; 347 348 L8 r; 349 350 r = L8.itpl(L8(100), L8(200), 15, 10, 20); 351 assert(r == L8(150), text(r)); 352 } 353 354 unittest 355 { 356 import std.conv; 357 358 LA r; 359 360 r = LA.blend(LA(123, 0), 361 LA(111, 222)); 362 assert(r == LA(111, 222), text(r)); 363 364 r = LA.blend(LA(123, 213), 365 LA(111, 255)); 366 assert(r == LA(111, 255), text(r)); 367 368 r = LA.blend(LA( 0, 255), 369 LA(255, 100)); 370 assert(r == LA(100, 255), text(r)); 371 } 372 373 unittest 374 { 375 import std.conv; 376 377 L8 r; 378 379 r = L8.blend(L8(123), 380 LA(231, 0)); 381 assert(r == L8(123), text(r)); 382 383 r = L8.blend(L8(123), 384 LA(231, 255)); 385 assert(r == L8(231), text(r)); 386 387 r = L8.blend(L8( 0), 388 LA(255, 100)); 389 assert(r == L8(100), text(r)); 390 } 391 392 unittest 393 { 394 Color!(real, "r", "g", "b") c; 395 } 396 397 unittest 398 { 399 const RGB c; 400 RGB x = cast(RGB)c; 401 } 402 403 /// Obtains the type of each channel for homogenous colors. 404 template ChannelType(T) 405 { 406 /// 407 static if (is(T == struct)) 408 alias ChannelType = T.ChannelType; 409 else 410 alias ChannelType = T; 411 } 412 413 /// Resolves to a Color instance with a different ChannelType. 414 template ChangeChannelType(COLOR, T) 415 if (isNumeric!COLOR) 416 { 417 alias ChangeChannelType = T; 418 } 419 420 /// ditto 421 template ChangeChannelType(COLOR, T) 422 if (is(COLOR : Color!Spec, Spec...)) 423 { 424 static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color"); 425 alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]); 426 } 427 428 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16)); 429 static assert(is(ChangeChannelType!(int, ushort) == ushort)); 430 431 /// Wrapper around ExpandNumericType to only expand integer types. 432 template ExpandIntegerType(T, size_t bits) 433 { 434 /// 435 static if (is(T:real)) 436 alias ExpandIntegerType = T; 437 else 438 alias ExpandIntegerType = ExpandNumericType!(T, bits); 439 } 440 441 /// Resolves to a Color instance with its ChannelType expanded by BYTES bytes. 442 alias ExpandChannelType(COLOR, int BYTES) = 443 ChangeChannelType!(COLOR, 444 ExpandNumericType!(ChannelType!COLOR, BYTES * 8)); 445 446 static assert(is(ExpandChannelType!(RGB, 1) == RGB16)); 447 448 unittest 449 { 450 alias RGBf = ChangeChannelType!(RGB, float); 451 auto rgb = RGB(1, 2, 3); 452 import std.conv : to; 453 auto rgbf = rgb.to!RGBf(); 454 assert(rgbf.r == 1f); 455 assert(rgbf.g == 2f); 456 assert(rgbf.b == 3f); 457 } 458 459 460 // *************************************************************************** 461 462 /// Color storage unit for as-is storage. 463 alias PlainStorageUnit(Color) = Color[1]; 464 465 /// Color storage unit description for packed bit colors 466 /// (1-bit, 2-bit, 4-bit etc.) 467 struct BitStorageUnit(ValueType, size_t valueBits, StorageType, bool bigEndian) 468 { 469 StorageType storageValue; /// Raw value. 470 471 /// Array operations. 472 enum length = StorageType.sizeof * 8 / valueBits; 473 static assert(length * valueBits == StorageType.sizeof * 8, "Slack bits?"); 474 475 ValueType opIndex(size_t index) const 476 { 477 static if (bigEndian) 478 index = length - 1 - index; 479 auto shift = index * valueBits; 480 return cast(ValueType)((storageValue >> shift) & valueMask); 481 } /// ditto 482 483 ValueType opIndexAssign(ValueType value, size_t index) 484 { 485 static if (bigEndian) 486 index = length - 1 - index; 487 auto shift = index * valueBits; 488 StorageType mask = flipBits(cast(StorageType)(valueMask << shift)); 489 storageValue = (storageValue & mask) | cast(StorageType)(cast(StorageType)value << shift); 490 return value; 491 } /// ditto 492 private: 493 enum StorageType valueMask = ((cast(StorageType)1) << valueBits) - 1; 494 } 495 496 /// 8 monochrome bits packed into a byte, in the usual big-endian order. 497 alias OneBitStorageBE = BitStorageUnit!(bool, 1, ubyte, true); 498 /// As above, but in little-endian order. 499 alias OneBitStorageLE = BitStorageUnit!(bool, 1, ubyte, false); 500 501 /// Get the color value of a storage unit type. 502 alias StorageColor(StorageType) = typeof(StorageType.init[0]); 503 504 /// The number of bits that one individual color takes up. 505 enum size_t storageColorBits(StorageType) = StorageType.sizeof * 8 / StorageType.length; 506 507 /// True when we can take the address of an individual color within a storage unit. 508 enum bool isStorageColorLValue(StorageType) = is(typeof({ StorageType s = void; return &s[0]; }())); 509 510 /// Construct a `StorageType` with all colors set to the indicated value. 511 StorageType solidStorageUnit(StorageType)(StorageColor!StorageType color) 512 { 513 StorageType s; 514 foreach (i; 0 .. StorageType.length) 515 s[i] = color; 516 return s; 517 } 518 519 // *************************************************************************** 520 521 /// Calculate an interpolated color on a gradient with multiple points 522 struct Gradient(Value, Color) 523 { 524 /// Gradient points. 525 struct Point 526 { 527 Value value; /// Distance along the gradient. 528 Color color; /// Color at this point. 529 } 530 Point[] points; /// ditto 531 532 /// Obtain the value at the given position. 533 /// If `value` is before the first point, the first point's color is returned. 534 /// If `value` is after the last point, the last point's color is returned. 535 Color get(Value value) const 536 { 537 assert(points.length, "Gradient must have at least one point"); 538 539 if (value <= points[0].value) 540 return points[0].color; 541 542 for (size_t i = 1; i < points.length; i++) 543 { 544 assert(points[i-1].value <= points[i].value, 545 "Gradient values are not in ascending order"); 546 if (value < points[i].value) 547 return Color.itpl( 548 points[i-1].color, points[i].color, value, 549 points[i-1].value, points[i].value); 550 } 551 552 return points[$-1].color; 553 } 554 } 555 556 unittest 557 { 558 Gradient!(int, L8) grad; 559 grad.points = [ 560 grad.Point(0, L8(0)), 561 grad.Point(10, L8(100)), 562 ]; 563 564 assert(grad.get(-5) == L8( 0)); 565 assert(grad.get( 0) == L8( 0)); 566 assert(grad.get( 5) == L8( 50)); 567 assert(grad.get(10) == L8(100)); 568 assert(grad.get(15) == L8(100)); 569 } 570 571 unittest 572 { 573 Gradient!(float, L8) grad; 574 grad.points = [ 575 grad.Point(0.0f, L8( 0)), 576 grad.Point(0.5f, L8(10)), 577 grad.Point(1.0f, L8(30)), 578 ]; 579 580 assert(grad.get(0.00f) == L8( 0)); 581 assert(grad.get(0.25f) == L8( 5)); 582 assert(grad.get(0.50f) == L8( 10)); 583 assert(grad.get(0.75f) == L8( 20)); 584 assert(grad.get(1.00f) == L8( 30)); 585 } 586 587 // *************************************************************************** 588 589 // TODO: deprecate 590 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 ); }