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