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