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.meta : AliasSeq; 17 import std.traits; 18 19 import ae.utils.math; 20 import ae.utils.meta; 21 22 /// Represents a tuple of samples, usually used to represent 23 /// a single color in some color space. 24 /// This helper type allows manipulating such tuples more easily, 25 /// and has special behavior for common color representations 26 /// (e.g. special treatment of the "a" field as an alpha channel, 27 /// and construction from hex strings for R/G/B colors). 28 /// `FieldTuple` is a field spec, as parsed by `ae.utils.meta.FieldList`. 29 /// By convention, each field's name indicates its purpose: 30 /// - `x`: padding 31 /// - `a`: alpha 32 /// - `l`: lightness (or grey, for monochrome images) 33 /// - others (`r`, `g`, `b`, etc.): color information 34 35 // TODO: figure out if we need alll these methods in the color type itself 36 // - code such as gamma conversion needs to create color types 37 // - ReplaceType can't copy methods 38 // - even if we move out all conventional methods, that still leaves operator overloading 39 40 struct Color(FieldTuple...) 41 { 42 alias Spec = FieldTuple; /// 43 mixin FieldList!FieldTuple; 44 45 // A "dumb" type to avoid cyclic references. 46 private struct Fields { mixin FieldList!FieldTuple; } 47 48 /// Whether or not all channel fields have the same base type. 49 // Only "true" supported for now, may change in the future (e.g. for 5:6:5) 50 enum homogeneous = isHomogeneous!Fields(); 51 deprecated alias homogenous = homogeneous; 52 53 /// The number of fields in this color type. 54 enum channels = Fields.init.tupleof.length; 55 56 /// Additional properties for homogeneous colors. 57 static if (homogeneous) 58 { 59 alias ChannelType = typeof(Fields.init.tupleof[0]); 60 static if (is(typeof(valueBits!ChannelType))) 61 enum channelBits = valueBits!ChannelType; 62 } 63 64 /// Return a Color instance with all fields set to "value". 65 static typeof(this) monochrome(ChannelType value) 66 { 67 typeof(this) r; 68 foreach (i, f; r.tupleof) 69 static if (__traits(identifier, r.tupleof[i]) == "a") 70 r.tupleof[i] = typeof(r.tupleof[i]).max; 71 else 72 r.tupleof[i] = value; 73 return r; 74 } 75 76 /// Additional properties for integer colors. 77 static if (is(ChannelType:uint)) 78 { 79 enum typeof(this) black = monochrome(0); 80 enum typeof(this) white = monochrome(ChannelType.max); 81 } 82 83 /// Interpolate between two colors. 84 /// See also: Gradient 85 static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1) 86 { 87 alias TryExpandNumericType!(ChannelType, P.sizeof*8) U; 88 typeof(this) r; 89 foreach (i, f; r.tupleof) 90 static if (r.tupleof[i].stringof != "r.x") // skip padding 91 r.tupleof[i] = cast(ChannelType).itpl(cast(U)c0.tupleof[i], cast(U)c1.tupleof[i], p, p0, p1); 92 return r; 93 } 94 95 /// Alpha-blend two colors. 96 static typeof(this) blend()(typeof(this) c0, typeof(this) c1) 97 if (is(typeof(a))) 98 { 99 alias A = typeof(c0.a); 100 A a = flipBits(cast(A)(c0.a.flipBits * c1.a.flipBits / A.max)); 101 if (!a) 102 return typeof(this).init; 103 A x = cast(A)(c1.a * A.max / a); 104 105 typeof(this) r; 106 foreach (i, f; r.tupleof) 107 static if (r.tupleof[i].stringof == "r.x") 108 {} // skip padding 109 else 110 static if (r.tupleof[i].stringof == "r.a") 111 r.a = a; 112 else 113 { 114 auto v0 = c0.tupleof[i]; 115 auto v1 = c1.tupleof[i]; 116 auto vr = ._blend(v1, v0, x); 117 r.tupleof[i] = vr; 118 } 119 return r; 120 } 121 122 /// Alpha-blend a color with an alpha channel on top of one without. 123 static typeof(this) blend(C)(typeof(this) c0, C c1) 124 if (!is(typeof(a)) && is(typeof(c1.a))) 125 { 126 alias A = typeof(c1.a); 127 if (!c1.a) 128 return c0; 129 //A x = cast(A)(c1.a * A.max / a); 130 131 typeof(this) r; 132 foreach (i, ref f; r.tupleof) 133 { 134 enum name = __traits(identifier, r.tupleof[i]); 135 static if (name == "x") 136 {} // skip padding 137 else 138 static if (name == "a") 139 static assert(false); 140 else 141 { 142 auto v0 = __traits(getMember, c0, name); 143 auto v1 = __traits(getMember, c1, name); 144 f = ._blend(v1, v0, c1.a); 145 } 146 } 147 return r; 148 } 149 150 /// Construct an RGB color from a typical hex string. 151 static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte)) 152 { 153 static typeof(this) fromHex(in char[] s) 154 { 155 import std.conv; 156 import std.exception; 157 158 enforce(s.length == 6 || (is(typeof(this.a) == ubyte) && s.length == 8), "Invalid color string"); 159 typeof(this) c; 160 c.r = s[0..2].to!ubyte(16); 161 c.g = s[2..4].to!ubyte(16); 162 c.b = s[4..6].to!ubyte(16); 163 static if (is(typeof(this.a) == ubyte)) 164 { 165 if (s.length == 8) 166 c.a = s[6..8].to!ubyte(16); 167 else 168 c.a = ubyte.max; 169 } 170 return c; 171 } 172 173 string toHex() const 174 { 175 import std.string; 176 return format("%02X%02X%02X", r, g, b); 177 } 178 } 179 180 /// Warning: overloaded operators preserve types and may cause overflows 181 typeof(this) opUnary(string op)() 182 if (op=="~" || op=="-") 183 { 184 typeof(this) r; 185 foreach (i, f; r.tupleof) 186 static if(r.tupleof[i].stringof != "r.x") // skip padding 187 r.tupleof[i] = cast(typeof(r.tupleof[i])) unary!(op[0])(this.tupleof[i]); 188 return r; 189 } 190 191 /// ditto 192 typeof(this) opOpAssign(string op)(int o) 193 { 194 foreach (i, f; this.tupleof) 195 static if(this.tupleof[i].stringof != "this.x") // skip padding 196 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`); 197 return this; 198 } 199 200 /// ditto 201 typeof(this) opOpAssign(string op, T)(T o) 202 if (is(T==struct) && structFields!T == structFields!Fields) 203 { 204 foreach (i, f; this.tupleof) 205 static if(this.tupleof[i].stringof != "this.x") // skip padding 206 this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`); 207 return this; 208 } 209 210 /// ditto 211 typeof(this) opBinary(string op, T)(T o) 212 if (op != "~" && op != "in") 213 { 214 auto r = this; 215 mixin("r" ~ op ~ "=o;"); 216 return r; 217 } 218 219 /// Apply a custom operation for each channel. Example: 220 /// COLOR.op!q{(a + b) / 2}(colorA, colorB); 221 static typeof(this) op(string expr, T...)(T values) 222 { 223 static assert(values.length <= 10); 224 225 string genVars(string channel) 226 { 227 string result; 228 foreach (j, Tj; T) 229 { 230 static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)? 231 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~ channel ~ ";\n"; 232 else 233 result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n"; 234 } 235 return result; 236 } 237 238 typeof(this) r; 239 foreach (i, f; r.tupleof) 240 static if(r.tupleof[i].stringof != "r.x") // skip padding 241 { 242 mixin(genVars(r.tupleof[i].stringof[2..$])); 243 r.tupleof[i] = mixin(expr); 244 } 245 return r; 246 } 247 248 /// Perform a per-channel hard-cast. 249 /// Provides continuity with casting of D basic types. 250 /// If this is not needed, consider using `channelMap` instead. 251 T opCast(T)() const 252 if (is(T==struct) && structFields!T == structFields!Fields) 253 { 254 static if (is(T == typeof(this))) 255 return this; 256 else 257 { 258 T t; 259 foreach (i, f; this.tupleof) 260 t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i]; 261 return t; 262 } 263 } 264 265 /// Sum of all channels 266 static if (is(ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))))) 267 ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum() 268 { 269 typeof(return) result; 270 foreach (i, f; this.tupleof) 271 static if (this.tupleof[i].stringof != "this.x") // skip padding 272 result += this.tupleof[i]; 273 return result; 274 } 275 276 /// Returns an instance of this color type 277 /// with all fields set at their minimum values. 278 static @property Color min() 279 { 280 Color result; 281 foreach (ref v; result.tupleof) 282 static if (is(typeof(typeof(v).min))) 283 v = typeof(v).min; 284 else 285 static if (is(typeof(typeof(v).max))) 286 v = -typeof(v).max; 287 return result; 288 } 289 290 /// Returns an instance of this color type 291 /// with all fields set at their maximum values. 292 static @property Color max() 293 { 294 Color result; 295 foreach (ref v; result.tupleof) 296 static if (is(typeof(typeof(v).max))) 297 v = typeof(v).max; 298 return result; 299 } 300 } 301 302 // The "x" has the special meaning of "padding" and is ignored in some circumstances 303 304 /// Definitions for common color types. 305 version(all) 306 { 307 alias Color!(ubyte , "r", "g", "b" ) RGB ; 308 alias Color!(ushort , "r", "g", "b" ) RGB16 ; 309 alias Color!(ubyte , "r", "g", "b", "x") RGBX ; 310 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ; 311 alias Color!(ubyte , "r", "g", "b", "a") RGBA ; 312 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ; 313 314 alias Color!(ubyte , "b", "g", "r" ) BGR ; 315 alias Color!(ubyte , "b", "g", "r", "x") BGRX ; 316 alias Color!(ubyte , "b", "g", "r", "a") BGRA ; 317 318 alias Color!(ubyte , "l" ) L8 ; 319 alias Color!(ushort , "l" ) L16 ; 320 alias Color!(ubyte , "l", "a" ) LA ; 321 alias Color!(ushort , "l", "a" ) LA16 ; 322 323 alias Color!(byte , "l" ) S8 ; 324 alias Color!(short , "l" ) S16 ; 325 326 alias Color!(float , "r", "g", "b" ) RGBf ; 327 alias Color!(double , "r", "g", "b" ) RGBd ; 328 } 329 330 unittest 331 { 332 static assert(RGB.sizeof == 3); 333 RGB[2] arr; 334 static assert(arr.sizeof == 6); 335 336 RGB hex = RGB.fromHex("123456"); 337 assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56); 338 339 BGRA hex2 = BGRA.fromHex("12345678"); 340 assert(hex2.r == 0x12 && hex2.g == 0x34 && hex2.b == 0x56 && hex2.a == 0x78); 341 342 assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9)); 343 344 RGB c = RGB(1, 1, 1); 345 c += 1; 346 assert(c == RGB(2, 2, 2)); 347 c += c; 348 assert(c == RGB(4, 4, 4)); 349 } 350 351 static assert(RGB.min == RGB( 0, 0, 0)); 352 static assert(RGB.max == RGB(255, 255, 255)); 353 354 unittest 355 { 356 import std.conv; 357 358 L8 r; 359 360 r = L8.itpl(L8(100), L8(200), 15, 10, 20); 361 assert(r == L8(150), text(r)); 362 } 363 364 unittest 365 { 366 import std.conv; 367 368 LA r; 369 370 r = LA.blend(LA(123, 0), 371 LA(111, 222)); 372 assert(r == LA(111, 222), text(r)); 373 374 r = LA.blend(LA(123, 213), 375 LA(111, 255)); 376 assert(r == LA(111, 255), text(r)); 377 378 r = LA.blend(LA( 0, 255), 379 LA(255, 100)); 380 assert(r == LA(100, 255), text(r)); 381 } 382 383 unittest 384 { 385 import std.conv; 386 387 L8 r; 388 389 r = L8.blend(L8(123), 390 LA(231, 0)); 391 assert(r == L8(123), text(r)); 392 393 r = L8.blend(L8(123), 394 LA(231, 255)); 395 assert(r == L8(231), text(r)); 396 397 r = L8.blend(L8( 0), 398 LA(255, 100)); 399 assert(r == L8(100), text(r)); 400 } 401 402 unittest 403 { 404 Color!(real, "r", "g", "b") c; 405 } 406 407 deprecated unittest 408 { 409 const RGB c; 410 RGB x = cast(RGB)c; 411 } 412 413 /// Obtains the type of each channel for homogeneous colors. 414 template ChannelType(T) 415 { 416 /// 417 static if (is(T == struct)) 418 alias ChannelType = T.ChannelType; 419 else 420 alias ChannelType = T; 421 } 422 423 private template TransformSpec(alias Transformer, Spec...) 424 { 425 static if (Spec.length == 0) 426 alias TransformSpec = Spec; 427 else 428 static if (is(typeof(Spec[0]) == string)) 429 alias TransformSpec = AliasSeq!(Spec[0], TransformSpec!(Transformer, Spec[1 .. $])); 430 else 431 alias TransformSpec = AliasSeq!(Transformer!(Spec[0]), TransformSpec!(Transformer, Spec[1 .. $])); 432 } 433 434 /// Resolves to a Color instance with a different ChannelType. 435 template TransformChannelType(COLOR, alias Transformer) 436 if (isNumeric!COLOR) 437 { 438 alias TransformChannelType = Transformer!COLOR; 439 } 440 441 /// ditto 442 template TransformChannelType(COLOR, alias Transformer) 443 if (is(COLOR : Color!Spec, Spec...)) 444 { 445 static if (is(COLOR : Color!Spec, Spec...)) 446 alias TransformChannelType = Color!(TransformSpec!(Transformer, Spec)); 447 } 448 449 /// ditto 450 template ChangeChannelType(COLOR, T) 451 { 452 alias Transformer(_) = T; 453 alias ChangeChannelType = TransformChannelType!(COLOR, Transformer); 454 } 455 456 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16)); 457 static assert(is(ChangeChannelType!(int, ushort) == ushort)); 458 459 /// Wrapper around ExpandNumericType to only expand integer types. 460 template ExpandIntegerType(T, size_t bits) 461 { 462 /// 463 static if (is(T:real)) 464 alias ExpandIntegerType = T; 465 else 466 alias ExpandIntegerType = ExpandNumericType!(T, bits); 467 } 468 469 /// Resolves to a Color instance with its ChannelType expanded by BYTES bytes. 470 alias ExpandChannelType(COLOR, int BYTES) = 471 ChangeChannelType!(COLOR, 472 ExpandNumericType!(ChannelType!COLOR, BYTES * 8)); 473 474 /// Resolves to a Color instance with its ChannelType expanded by BYTES bytes and made signed. 475 alias ExpandChannelTypeSigned(COLOR, int BYTES) = 476 ChangeChannelType!(COLOR, 477 Signed!(ExpandNumericType!(ChannelType!COLOR, BYTES * 8))); 478 479 static assert(is(ExpandChannelType!(RGB, 1) == RGB16)); 480 481 unittest 482 { 483 alias RGBf = ChangeChannelType!(RGB, float); 484 auto rgb = RGB(1, 2, 3); 485 import std.conv : to; 486 auto rgbf = rgb.channelMap!RGBf(); 487 assert(rgbf.r == 1f); 488 assert(rgbf.g == 2f); 489 assert(rgbf.b == 3f); 490 } 491 492 /// Applies a transformation per-channel. 493 /// `expr` may be in the form of `a => ...`, or `(a, ref b) { b = ...; }` (only if `T` is explicitly specified). 494 template channelMap(T, alias expr = a => a) 495 if (is(T == struct)) 496 { 497 auto channelMap(COLOR)(COLOR color) 498 if (is(COLOR == struct)) 499 { 500 T result; 501 foreach (i, f; color.tupleof) 502 { 503 enum name = __traits(identifier, color.tupleof[i]); 504 static assert(__traits(hasMember, result, name), 505 "No matching field `" ~ name ~ "` in `" ~ T.stringof ~ "` when mapping from `" ~ COLOR.stringof ~ "`"); 506 507 static if (is(typeof(__traits(getMember, result, name) = expr(__traits(getMember, color, name))))) 508 __traits(getMember, result, name) = expr(__traits(getMember, color, name)); 509 else 510 static if (is(typeof(expr(__traits(getMember, color, name), __traits(getMember, result, name))))) 511 expr(__traits(getMember, color, name), __traits(getMember, result, name)); 512 else 513 { 514 pragma(msg, "channelMap expression does not compile with either 1 or 2 arguments:"); 515 expr(__traits(getMember, color, name), __traits(getMember, result, name)); 516 __traits(getMember, result, name) = expr(__traits(getMember, color, name)); 517 } 518 } 519 foreach (i, f; result.tupleof) 520 { 521 enum name = __traits(identifier, result.tupleof[i]); 522 static assert(__traits(hasMember, color, name), 523 "No matching field `" ~ name ~ "` in `" ~ COLOR.stringof ~ "` when mapping to `" ~ T.stringof ~ "`"); 524 } 525 return result; 526 } 527 } 528 529 /// ditto 530 template channelMap(alias expr) 531 if (!is(expr == struct)) 532 { 533 auto channelMap(COLOR)(COLOR c) 534 if (is(COLOR == struct) && !is(expr == struct)) 535 { 536 alias Transformer(C) = typeof({ C v = void; return expr(v); }()); 537 alias T = TransformChannelType!(typeof(c), Transformer); 538 return c.channelMap!(T, expr); 539 } 540 } 541 542 /// 543 unittest 544 { 545 // Effortlessly reordering channels with no modification. 546 assert(RGB(1, 2, 3).channelMap!BGR == BGR(3, 2, 1)); 547 548 // Perform per-channel transformations. 549 assert(RGB(1, 2, 3).channelMap!(v => cast(ubyte)(v + 1)) == RGB(2, 3, 4)); 550 551 // Perform per-channel transformations with a different resulting type, implicitly. 552 assert(RGB(1, 2, 3).channelMap!(v => cast(ushort)(v + 1)) == RGB16(2, 3, 4)); 553 554 // Perform per-channel transformations with a different resulting type, explicitly. 555 assert(RGB(1, 2, 3).channelMap!(RGB16, v => cast(ubyte)(v + 1)) == RGB16(2, 3, 4)); 556 557 // Ditto, with a ref parameter instead of return. 558 assert(RGB(1, 2, 3).channelMap!(RGB16, (a, ref b) { b = a; b++; }) == RGB16(2, 3, 4)); 559 560 // Effortlessly convert an image 561 import ae.utils.graphics.image : Image, colorMap, copy; 562 Image!BGR i = Image!RGB().colorMap!(channelMap!BGR).copy(); 563 } 564 565 // *************************************************************************** 566 567 /// Color storage unit for as-is storage. 568 alias PlainStorageUnit(Color) = Color[1]; 569 570 /// Color storage unit description for packed bit colors 571 /// (1-bit, 2-bit, 4-bit etc.) 572 struct BitStorageUnit(ValueType, size_t valueBits, StorageType, bool bigEndian) 573 { 574 StorageType storageValue; /// Raw value. 575 576 /// Array operations. 577 enum length = StorageType.sizeof * 8 / valueBits; 578 static assert(length * valueBits == StorageType.sizeof * 8, "Slack bits?"); 579 580 ValueType opIndex(size_t index) const 581 { 582 static if (bigEndian) 583 index = length - 1 - index; 584 auto shift = index * valueBits; 585 return cast(ValueType)((storageValue >> shift) & valueMask); 586 } /// ditto 587 588 ValueType opIndexAssign(ValueType value, size_t index) 589 { 590 static if (bigEndian) 591 index = length - 1 - index; 592 auto shift = index * valueBits; 593 StorageType mask = flipBits(cast(StorageType)(valueMask << shift)); 594 storageValue = (storageValue & mask) | cast(StorageType)(cast(StorageType)value << shift); 595 return value; 596 } /// ditto 597 private: 598 enum StorageType valueMask = ((cast(StorageType)1) << valueBits) - 1; 599 } 600 601 /// 8 monochrome bits packed into a byte, in the usual big-endian order. 602 alias OneBitStorageBE = BitStorageUnit!(bool, 1, ubyte, true); 603 /// As above, but in little-endian order. 604 alias OneBitStorageLE = BitStorageUnit!(bool, 1, ubyte, false); 605 606 /// Get the color value of a storage unit type. 607 alias StorageColor(StorageType) = typeof(StorageType.init[0]); 608 609 /// The number of bits that one individual color takes up. 610 enum size_t storageColorBits(StorageType) = StorageType.sizeof * 8 / StorageType.length; 611 612 /// True when we can take the address of an individual color within a storage unit. 613 enum bool isStorageColorLValue(StorageType) = is(typeof({ StorageType s = void; return &s[0]; }())); 614 615 /// Construct a `StorageType` with all colors set to the indicated value. 616 StorageType solidStorageUnit(StorageType)(StorageColor!StorageType color) 617 { 618 StorageType s; 619 foreach (i; 0 .. StorageType.length) 620 s[i] = color; 621 return s; 622 } 623 624 // *************************************************************************** 625 626 /// Calculate an interpolated color on a gradient with multiple points 627 struct Gradient(Value, Color) 628 { 629 /// Gradient points. 630 struct Point 631 { 632 Value value; /// Distance along the gradient. 633 Color color; /// Color at this point. 634 } 635 Point[] points; /// ditto 636 637 /// Obtain the value at the given position. 638 /// If `value` is before the first point, the first point's color is returned. 639 /// If `value` is after the last point, the last point's color is returned. 640 Color get(Value value) const 641 { 642 assert(points.length, "Gradient must have at least one point"); 643 644 if (value <= points[0].value) 645 return points[0].color; 646 647 for (size_t i = 1; i < points.length; i++) 648 { 649 assert(points[i-1].value <= points[i].value, 650 "Gradient values are not in ascending order"); 651 if (value < points[i].value) 652 return Color.itpl( 653 points[i-1].color, points[i].color, value, 654 points[i-1].value, points[i].value); 655 } 656 657 return points[$-1].color; 658 } 659 } 660 661 unittest 662 { 663 Gradient!(int, L8) grad; 664 grad.points = [ 665 grad.Point(0, L8(0)), 666 grad.Point(10, L8(100)), 667 ]; 668 669 assert(grad.get(-5) == L8( 0)); 670 assert(grad.get( 0) == L8( 0)); 671 assert(grad.get( 5) == L8( 50)); 672 assert(grad.get(10) == L8(100)); 673 assert(grad.get(15) == L8(100)); 674 } 675 676 unittest 677 { 678 Gradient!(float, L8) grad; 679 grad.points = [ 680 grad.Point(0.0f, L8( 0)), 681 grad.Point(0.5f, L8(10)), 682 grad.Point(1.0f, L8(30)), 683 ]; 684 685 assert(grad.get(0.00f) == L8( 0)); 686 assert(grad.get(0.25f) == L8( 5)); 687 assert(grad.get(0.50f) == L8( 10)); 688 assert(grad.get(0.75f) == L8( 20)); 689 assert(grad.get(1.00f) == L8( 30)); 690 } 691 692 // *************************************************************************** 693 694 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 ); } 695 deprecated alias blend = _blend;