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 = ChannelType.sizeof*8; 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 /// Interpolate between two colors. 66 static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1) 67 { 68 alias UnsignedBitsType!(channelBits + P.sizeof*8) U; 69 alias Signed!U S; 70 typeof(this) r; 71 foreach (i, f; r.tupleof) 72 static if (r.tupleof[i].stringof != "r.x") // skip padding 73 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); 74 return r; 75 } 76 77 /// Alpha-blend two colors. 78 static typeof(this) blend()(typeof(this) c0, typeof(this) c1) 79 if (is(typeof(a))) 80 { 81 alias A = typeof(c0.a); 82 A a = ~cast(A)(~c0.a * ~c1.a / A.max); 83 if (!a) 84 return typeof(this).init; 85 A x = cast(A)(c1.a * A.max / a); 86 87 typeof(this) r; 88 foreach (i, f; r.tupleof) 89 static if (r.tupleof[i].stringof == "r.x") 90 {} // skip padding 91 else 92 static if (r.tupleof[i].stringof == "r.a") 93 r.a = a; 94 else 95 { 96 auto v0 = c0.tupleof[i]; 97 auto v1 = c1.tupleof[i]; 98 auto vr = .blend(v1, v0, x); 99 r.tupleof[i] = vr; 100 } 101 return r; 102 } 103 104 /// Construct an RGB color from a typical hex string. 105 static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte)) 106 static typeof(this) fromHex(in char[] s) 107 { 108 import std.conv; 109 import std.exception; 110 111 enforce(s.length == 6, "Invalid color string"); 112 typeof(this) c; 113 c.r = s[0..2].to!ubyte(16); 114 c.g = s[2..4].to!ubyte(16); 115 c.b = s[4..6].to!ubyte(16); 116 return c; 117 } 118 119 /// Warning: overloaded operators preserve types and may cause overflows 120 typeof(this) opUnary(string op)() 121 if (op=="~" || op=="-") 122 { 123 typeof(this) r; 124 foreach (i, f; r.tupleof) 125 static if(r.tupleof[i].stringof != "r.x") // skip padding 126 r.tupleof[i] = cast(typeof(r.tupleof[i])) mixin(op ~ `this.tupleof[i]`); 127 return r; 128 } 129 130 /// ditto 131 typeof(this) opOpAssign(string op)(int o) 132 { 133 foreach (i, f; this.tupleof) 134 static if(this.tupleof[i].stringof != "this.x") // skip padding 135 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`); 136 return this; 137 } 138 139 /// ditto 140 typeof(this) opOpAssign(string op, T)(T o) 141 if (is(T==struct) && structFields!T == structFields!Fields) 142 { 143 foreach (i, f; this.tupleof) 144 static if(this.tupleof[i].stringof != "this.x") // skip padding 145 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`); 146 return this; 147 } 148 149 /// ditto 150 typeof(this) opBinary(string op, T)(T o) 151 if (op != "~") 152 { 153 auto r = this; 154 mixin("r" ~ op ~ "=o;"); 155 return r; 156 } 157 158 /// Apply a custom operation for each channel. Example: 159 /// COLOR.op!q{(a + b) / 2}(colorA, colorB); 160 static typeof(this) op(string expr, T...)(T values) 161 { 162 static assert(values.length <= 10); 163 164 string genVars(string channel) 165 { 166 string result; 167 foreach (j, Tj; T) 168 { 169 static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)? 170 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~ channel ~ ";\n"; 171 else 172 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n"; 173 } 174 return result; 175 } 176 177 typeof(this) r; 178 foreach (i, f; r.tupleof) 179 static if(r.tupleof[i].stringof != "r.x") // skip padding 180 { 181 mixin(genVars(r.tupleof[i].stringof[2..$])); 182 r.tupleof[i] = mixin(expr); 183 } 184 return r; 185 } 186 187 T opCast(T)() 188 if (is(T==struct) && structFields!T == structFields!Fields) 189 { 190 T t; 191 foreach (i, f; this.tupleof) 192 t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i]; 193 return t; 194 } 195 196 /// Sum of all channels 197 UnsignedBitsType!(channelBits + ilog2(nextPowerOfTwo(channels))) sum() 198 { 199 typeof(return) result; 200 foreach (i, f; this.tupleof) 201 static if (this.tupleof[i].stringof != "this.x") // skip padding 202 result += this.tupleof[i]; 203 return result; 204 } 205 } 206 207 // The "x" has the special meaning of "padding" and is ignored in some circumstances 208 alias Color!(ubyte , "r", "g", "b" ) RGB ; 209 alias Color!(ushort , "r", "g", "b" ) RGB16 ; 210 alias Color!(ubyte , "r", "g", "b", "x") RGBX ; 211 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ; 212 alias Color!(ubyte , "r", "g", "b", "a") RGBA ; 213 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ; 214 215 alias Color!(ubyte , "b", "g", "r" ) BGR ; 216 alias Color!(ubyte , "b", "g", "r", "x") BGRX ; 217 alias Color!(ubyte , "b", "g", "r", "a") BGRA ; 218 219 alias Color!(ubyte , "l" ) L8 ; 220 alias Color!(ushort , "l" ) L16 ; 221 alias Color!(ubyte , "l", "a" ) LA ; 222 alias Color!(ushort , "l", "a" ) LA16 ; 223 224 alias Color!(byte , "l" ) S8 ; 225 alias Color!(short , "l" ) S16 ; 226 227 unittest 228 { 229 static assert(RGB.sizeof == 3); 230 RGB[2] arr; 231 static assert(arr.sizeof == 6); 232 233 RGB hex = RGB.fromHex("123456"); 234 assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56); 235 236 assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9)); 237 238 RGB c = RGB(1, 1, 1); 239 c += 1; 240 assert(c == RGB(2, 2, 2)); 241 c += c; 242 assert(c == RGB(4, 4, 4)); 243 } 244 245 unittest 246 { 247 import std.conv; 248 249 LA r; 250 251 r = LA.blend(LA(123, 0), 252 LA(111, 222)); 253 assert(r == LA(111, 222), text(r)); 254 255 r = LA.blend(LA(123, 213), 256 LA(111, 255)); 257 assert(r == LA(111, 255), text(r)); 258 259 r = LA.blend(LA( 0, 255), 260 LA(255, 100)); 261 assert(r == LA(100, 255), text(r)); 262 } 263 264 /// Obtains the type of each channel for homogenous colors. 265 template ChannelType(T) 266 { 267 static if (is(T == struct)) 268 alias ChannelType = T.ChannelType; 269 else 270 alias ChannelType = T; 271 } 272 273 /// Resolves to a Color instance with a different ChannelType. 274 template ChangeChannelType(COLOR, T) 275 if (isNumeric!COLOR) 276 { 277 alias ChangeChannelType = T; 278 } 279 280 /// ditto 281 template ChangeChannelType(COLOR, T) 282 if (is(COLOR : Color!Spec, Spec...)) 283 { 284 static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color"); 285 alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]); 286 } 287 288 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16)); 289 static assert(is(ChangeChannelType!(int, ushort) == ushort)); 290 291 alias ExpandChannelType(COLOR, int BYTES) = 292 ChangeChannelType!(COLOR, 293 UnsignedBitsType!((ChannelType!COLOR.sizeof + BYTES) * 8)); 294 295 static assert(is(ExpandChannelType!(RGB, 1) == RGB16)); 296 297 // *************************************************************************** 298 299 // TODO: deprecate 300 T blend(T)(T f, T b, T a) if (is(typeof(f*a+~b))) { return cast(T) ( ((f*a) + (b*~a)) / T.max ); }