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