1 /** 2 * Color type and operations. Port of ae.utils.graphics. 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 * Guillaume Piolat <contact@auburnsounds.com> 13 */ 14 15 module dplug.graphics.color; 16 17 import std.traits; 18 import std.math; 19 import std.algorithm : min, max, swap; 20 21 import dplug.core.math; 22 23 24 /// Evaluates to array of strings with name for each field. 25 @property string[] structFields(T)() 26 if (is(T == struct) || is(T == class)) 27 { 28 import std..string : split; 29 30 string[] fields; 31 foreach (i, f; T.init.tupleof) 32 { 33 string field = T.tupleof[i].stringof; 34 field = field.split(".")[$-1]; 35 fields ~= field; 36 } 37 return fields; 38 } 39 40 41 T itpl(T, U)(T low, T high, U r, U rLow, U rHigh) 42 { 43 return cast(T)(low + (cast(Signed!T)high-cast(Signed!T)low) * (cast(Signed!U)r - cast(Signed!U)rLow) / (cast(Signed!U)rHigh - cast(Signed!U)rLow)); 44 } 45 46 auto sqr(T)(T x) { return x*x; } 47 48 49 void sort2(T)(ref T x, ref T y) 50 { 51 if (x > y) 52 { 53 T z = x; 54 x = y; 55 y = z; 56 } 57 } 58 59 byte sign(T)(T x) 60 { 61 return x<0 ? -1 : (x>0 ? 1 : 0); 62 } 63 64 /// Integer log2. 65 private ubyte ilog2(T)(T n) 66 { 67 ubyte result = 0; 68 while (n >>= 1) 69 result++; 70 return result; 71 } 72 73 private T nextPowerOfTwo(T)(T x) 74 { 75 x |= x >> 1; 76 x |= x >> 2; 77 x |= x >> 4; 78 static if (T.sizeof > 1) 79 x |= x >> 8; 80 static if (T.sizeof > 2) 81 x |= x >> 16; 82 static if (T.sizeof > 4) 83 x |= x >> 32; 84 return x + 1; 85 } 86 87 /// Like std.typecons.Tuple, but a template mixin. 88 /// Unlike std.typecons.Tuple, names may not be omitted - but repeating types may be. 89 /// Example: FieldList!(ubyte, "r", "g", "b", ushort, "a"); 90 mixin template FieldList(Fields...) 91 { 92 mixin(GenFieldList!(void, Fields)); 93 } 94 95 template GenFieldList(T, Fields...) 96 { 97 static if (Fields.length == 0) 98 enum GenFieldList = ""; 99 else 100 { 101 static if (is(typeof(Fields[0]) == string)) 102 enum GenFieldList = T.stringof ~ " " ~ Fields[0] ~ ";\n" ~ GenFieldList!(T, Fields[1..$]); 103 else 104 enum GenFieldList = GenFieldList!(Fields[0], Fields[1..$]); 105 } 106 } 107 108 unittest 109 { 110 struct S 111 { 112 mixin FieldList!(ubyte, "r", "g", "b", ushort, "a"); 113 } 114 S s; 115 static assert(is(typeof(s.r) == ubyte)); 116 static assert(is(typeof(s.g) == ubyte)); 117 static assert(is(typeof(s.b) == ubyte)); 118 static assert(is(typeof(s.a) == ushort)); 119 } 120 121 122 /// Return the number of bits used to store the value part, i.e. 123 /// T.sizeof*8 for integer parts and the mantissa size for 124 /// floating-point types. 125 template valueBits(T) 126 { 127 static if (is(T : ulong)) 128 enum valueBits = T.sizeof * 8; 129 else 130 static if (is(T : real)) 131 enum valueBits = T.mant_dig; 132 else 133 static assert(false, "Don't know how many value bits there are in " ~ T.stringof); 134 } 135 136 static assert(valueBits!uint == 32); 137 static assert(valueBits!double == 53); 138 139 140 141 /// Instantiates to a color type. 142 /// FieldTuple is the color specifier, as parsed by 143 /// the FieldList template from ae.utils.meta. 144 /// By convention, each field's name indicates its purpose: 145 /// - x: padding 146 /// - a: alpha 147 /// - l: lightness (or grey, for monochrome images) 148 /// - others (r, g, b, etc.): color information 149 150 // MAYDO: figure out if we need all these methods in the color type itself 151 // - code such as gamma conversion needs to create color types 152 // - ReplaceType can't copy methods 153 // - even if we move out all conventional methods, that still leaves operator overloading 154 155 struct Color(FieldTuple...) 156 { 157 alias Spec = FieldTuple; 158 mixin FieldList!FieldTuple; 159 160 // A "dumb" type to avoid cyclic references. 161 private struct Fields { mixin FieldList!FieldTuple; } 162 163 /// Whether or not all channel fields have the same base type. 164 // Only "true" supported for now, may change in the future (e.g. for 5:6:5) 165 enum homogenous = true; 166 167 /// The number of fields in this color type. 168 enum channels = Fields.init.tupleof.length; 169 170 static if (homogenous) 171 { 172 alias ChannelType = typeof(Fields.init.tupleof[0]); 173 enum channelBits = valueBits!ChannelType; 174 } 175 176 /// Return a Color instance with all fields set to "value". 177 static typeof(this) monochrome(ChannelType value) 178 { 179 typeof(this) r; 180 foreach (i, f; r.tupleof) 181 r.tupleof[i] = value; 182 return r; 183 } 184 185 /// Interpolate between two colors. 186 static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1) 187 { 188 alias ExpandNumericType!(ChannelType, P.sizeof*8) U; 189 alias Signed!U S; 190 typeof(this) r; 191 foreach (i, f; r.tupleof) 192 static if (r.tupleof[i].stringof != "r.x") // skip padding 193 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); 194 return r; 195 } 196 197 /// Alpha-blend two colors. 198 static typeof(this) blend()(typeof(this) c0, typeof(this) c1) 199 if (is(typeof(a))) 200 { 201 alias A = typeof(c0.a); 202 A a = ~cast(A)(~c0.a * ~c1.a / A.max); 203 if (!a) 204 return typeof(this).init; 205 A x = cast(A)(c1.a * A.max / a); 206 207 typeof(this) r; 208 foreach (i, f; r.tupleof) 209 static if (r.tupleof[i].stringof == "r.x") 210 {} // skip padding 211 else 212 static if (r.tupleof[i].stringof == "r.a") 213 r.a = a; 214 else 215 { 216 auto v0 = c0.tupleof[i]; 217 auto v1 = c1.tupleof[i]; 218 auto vr = .blend(v1, v0, x); 219 r.tupleof[i] = vr; 220 } 221 return r; 222 } 223 224 /// Construct an RGB color from a typical hex string. 225 static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte)) 226 { 227 static typeof(this) fromHex(in char[] s) 228 { 229 import std.conv; 230 import std.exception; 231 232 enforce(s.length == 6, "Invalid color string"); 233 typeof(this) c; 234 c.r = s[0..2].to!ubyte(16); 235 c.g = s[2..4].to!ubyte(16); 236 c.b = s[4..6].to!ubyte(16); 237 return c; 238 } 239 240 string toHex() const 241 { 242 import std..string; 243 return format("%02X%02X%02X", r, g, b); 244 } 245 } 246 247 /// Warning: overloaded operators preserve types and may cause overflows 248 typeof(this) opUnary(string op)() 249 if (op=="~" || op=="-") 250 { 251 typeof(this) r; 252 foreach (i, f; r.tupleof) 253 static if(r.tupleof[i].stringof != "r.x") // skip padding 254 r.tupleof[i] = cast(typeof(r.tupleof[i])) mixin(op ~ `this.tupleof[i]`); 255 return r; 256 } 257 258 /// ditto 259 typeof(this) opOpAssign(string op)(int o) 260 { 261 foreach (i, f; this.tupleof) 262 static if(this.tupleof[i].stringof != "this.x") // skip padding 263 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`); 264 return this; 265 } 266 267 /// ditto 268 typeof(this) opOpAssign(string op, T)(T o) 269 if (is(T==struct) && structFields!T == structFields!Fields) 270 { 271 foreach (i, f; this.tupleof) 272 static if(this.tupleof[i].stringof != "this.x") // skip padding 273 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`); 274 return this; 275 } 276 277 /// ditto 278 typeof(this) opBinary(string op, T)(T o) 279 if (op != "~") 280 { 281 auto r = this; 282 mixin("r" ~ op ~ "=o;"); 283 return r; 284 } 285 286 /// Apply a custom operation for each channel. Example: 287 /// COLOR.op!q{(a + b) / 2}(colorA, colorB); 288 static typeof(this) op(string expr, T...)(T values) 289 { 290 static assert(values.length <= 10); 291 292 string genVars(string channel) 293 { 294 string result; 295 foreach (j, Tj; T) 296 { 297 static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)? 298 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~ channel ~ ";\n"; 299 else 300 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n"; 301 } 302 return result; 303 } 304 305 typeof(this) r; 306 foreach (i, f; r.tupleof) 307 static if(r.tupleof[i].stringof != "r.x") // skip padding 308 { 309 mixin(genVars(r.tupleof[i].stringof[2..$])); 310 r.tupleof[i] = mixin(expr); 311 } 312 return r; 313 } 314 315 T opCast(T)() 316 if (is(T==struct) && structFields!T == structFields!Fields) 317 { 318 T t; 319 foreach (i, f; this.tupleof) 320 t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i]; 321 return t; 322 } 323 324 /// Sum of all channels 325 ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum() 326 { 327 typeof(return) result; 328 foreach (i, f; this.tupleof) 329 static if (this.tupleof[i].stringof != "this.x") // skip padding 330 result += this.tupleof[i]; 331 return result; 332 } 333 } 334 335 // The "x" has the special meaning of "padding" and is ignored in some circumstances 336 alias Color!(ubyte , "r", "g", "b" ) RGB ; 337 alias Color!(ubyte , "r", "g", "b", "a") RGBA ; 338 339 340 alias Color!(ubyte , "l" ) L8 ; 341 alias Color!(ushort , "l" ) L16 ; 342 343 344 345 unittest 346 { 347 static assert(RGB.sizeof == 3); 348 RGB[2] arr; 349 static assert(arr.sizeof == 6); 350 351 RGB hex = RGB.fromHex("123456"); 352 assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56); 353 354 assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9)); 355 356 RGB c = RGB(1, 1, 1); 357 c += 1; 358 assert(c == RGB(2, 2, 2)); 359 c += c; 360 assert(c == RGB(4, 4, 4)); 361 } 362 363 unittest 364 { 365 import std.conv; 366 367 L8 r; 368 369 r = L8.itpl(L8(100), L8(200), 15, 10, 20); 370 assert(r == L8(150), text(r)); 371 } 372 373 374 unittest 375 { 376 Color!(real, "r", "g", "b") c; 377 } 378 379 /// Obtains the type of each channel for homogenous colors. 380 template ChannelType(T) 381 { 382 static if (is(T == struct)) 383 alias ChannelType = T.ChannelType; 384 else 385 alias ChannelType = T; 386 } 387 388 /// Resolves to a Color instance with a different ChannelType. 389 template ChangeChannelType(COLOR, T) 390 if (isNumeric!COLOR) 391 { 392 alias ChangeChannelType = T; 393 } 394 395 /// ditto 396 template ChangeChannelType(COLOR, T) 397 if (is(COLOR : Color!Spec, Spec...)) 398 { 399 static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color"); 400 alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]); 401 } 402 403 static assert(is(ChangeChannelType!(int, ushort) == ushort)); 404 405 406 /// Expand to a built-in numeric type of the same kind 407 /// (signed integer / unsigned integer / floating-point) 408 /// with at least the indicated number of bits of precision. 409 template ResizeNumericType(T, uint bits) 410 { 411 static if (is(T : ulong)) 412 static if (isSigned!T) 413 alias ResizeNumericType = SignedBitsType!bits; 414 else 415 alias ResizeNumericType = UnsignedBitsType!bits; 416 else 417 static if (is(T : real)) 418 { 419 static if (bits <= float.mant_dig) 420 alias ResizeNumericType = float; 421 else 422 static if (bits <= double.mant_dig) 423 alias ResizeNumericType = double; 424 else 425 static if (bits <= real.mant_dig) 426 alias ResizeNumericType = real; 427 else 428 static assert(0, "No floating-point type big enough to fit " ~ bits.stringof ~ " bits"); 429 } 430 else 431 static assert(false, "Don't know how to resize type: " ~ T.stringof); 432 } 433 434 static assert(is(ResizeNumericType!(float, double.mant_dig) == double)); 435 436 /// Expand to a built-in numeric type of the same kind 437 /// (signed integer / unsigned integer / floating-point) 438 /// with at least additionalBits more bits of precision. 439 alias ExpandNumericType(T, uint additionalBits) = 440 ResizeNumericType!(T, valueBits!T + additionalBits); 441 442 /// Unsigned integer type big enough to fit N bits of precision. 443 template UnsignedBitsType(uint bits) 444 { 445 static if (bits <= 8) 446 alias ubyte UnsignedBitsType; 447 else 448 static if (bits <= 16) 449 alias ushort UnsignedBitsType; 450 else 451 static if (bits <= 32) 452 alias uint UnsignedBitsType; 453 else 454 static if (bits <= 64) 455 alias ulong UnsignedBitsType; 456 else 457 static assert(0, "No integer type big enough to fit " ~ bits.stringof ~ " bits"); 458 } 459 460 template SignedBitsType(uint bits) 461 { 462 alias Signed!(UnsignedBitsType!bits) SignedBitsType; 463 } 464 465 466 /// Wrapper around ExpandNumericType to only expand numeric types. 467 template ExpandIntegerType(T, size_t bits) 468 { 469 static if (is(T:real)) 470 alias ExpandIntegerType = T; 471 else 472 alias ExpandIntegerType = ExpandNumericType!(T, bits); 473 } 474 475 alias ExpandChannelType(COLOR, int BYTES) = 476 ChangeChannelType!(COLOR, 477 ExpandNumericType!(ChannelType!COLOR, BYTES * 8)); 478 479 alias temp = ExpandChannelType!(RGB, 1); 480 //static assert(is(ExpandChannelType!(RGB, 1) == RGB16)); 481 482 unittest 483 { 484 alias RGBf = ChangeChannelType!(RGB, float); 485 auto rgb = RGB(1, 2, 3); 486 import std.conv : to; 487 auto rgbf = rgb.to!RGBf(); 488 assert(rgbf.r == 1f); 489 assert(rgbf.g == 2f); 490 assert(rgbf.b == 3f); 491 } 492 493 // *************************************************************************** 494 495 T blend(T)(T f, T b, T a) pure nothrow @nogc 496 if (is(typeof(f*a+~b))) 497 { 498 return cast(T) ( ((f*a) + (b*~a)) / T.max ); 499 }