Skip to content

Commit c5d5759

Browse files
committed
Improved Color blending
1 parent ce163b5 commit c5d5759

File tree

7 files changed

+354
-92
lines changed

7 files changed

+354
-92
lines changed

examples/graphics/source/examples/ColorLab.h

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -656,18 +656,18 @@ class ColorLabDemo : public yup::Component
656656
const auto& colorA = blendStartColor;
657657
const auto& colorB = blendEndColor;
658658

659-
const int steps = 10;
660-
const float gap = 6.0f;
661-
const float swatchWidth = (area.getWidth() - gap * (steps - 1)) / static_cast<float> (steps);
662-
const float swatchHeight = area.getHeight() - 6.0f;
663-
664-
for (int i = 0; i < steps; ++i)
665-
{
666-
const float t = static_cast<float> (i) / static_cast<float> (steps - 1);
667-
auto swatch = yup::Rectangle<float> (area.getX() + i * (swatchWidth + gap), area.getY(), swatchWidth, swatchHeight);
668-
g.setFillColor (blendColors (colorA, colorB, t, mode));
669-
g.fillRoundedRect (swatch, 8.0f);
670-
}
659+
const auto start = yup::Point<float> (area.getX(), area.getCenterY());
660+
const auto end = yup::Point<float> (area.getRight(), area.getCenterY());
661+
const auto colorSpace = mode == BlendMode::Spectral
662+
? yup::ColorSpace::Spectral
663+
: (mode == BlendMode::Srgb ? yup::ColorSpace::SRGB : yup::ColorSpace::RGB);
664+
const size_t steps = colorSpace == yup::ColorSpace::RGB ? 2 : (colorSpace == yup::ColorSpace::SRGB ? 24 : 48);
665+
auto gradient = yup::ColorGradient::fromLinearColors (colorA, start, colorB, end, steps, yup::ColorGradient::Type::Linear, colorSpace);
666+
g.setFillColorGradient (gradient);
667+
g.fillRoundedRect (area, 8.0f);
668+
g.setStrokeColor (yup::Colors::white.withAlpha (0.2f));
669+
g.setStrokeWidth (1.0f);
670+
g.strokeRoundedRect (area, 8.0f);
671671
}
672672

673673
void drawHsluvPalette (yup::Graphics& g, yup::Rectangle<float> area)

modules/yup_graphics/graphics/yup_Color.cpp

Lines changed: 252 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222
namespace yup
2323
{
2424

25-
//==============================================================================
26-
2725
namespace
2826
{
27+
28+
//==============================================================================
29+
2930
int hexCharToInt (yup_wchar c) noexcept
3031
{
3132
return CharacterFunctions::getHexDigitValue (c);
@@ -197,6 +198,7 @@ Color parseNamedColor (const String& name)
197198
}
198199

199200
//==============================================================================
201+
200202
using DoubleRgb = std::array<double, 3>;
201203
using IntRgb = std::array<int, 3>;
202204

@@ -375,40 +377,11 @@ void hsluvGetBounds (double l, std::array<HsluvBounds, 6>& bounds) noexcept
375377
}
376378
}
377379

378-
double hsluvIntersectLineLine (const HsluvBounds& line1, const HsluvBounds& line2) noexcept
379-
{
380-
return (line1.b - line2.b) / (line2.a - line1.a);
381-
}
382-
383-
double hsluvDistanceFromPoleSquared (double x, double y) noexcept
384-
{
385-
return x * x + y * y;
386-
}
387-
388380
double hsluvRayLengthUntilIntersect (double theta, const HsluvBounds& line) noexcept
389381
{
390382
return line.b / (std::sin (theta) - line.a * std::cos (theta));
391383
}
392384

393-
double hsluvMaxSafeChromaForL (double l) noexcept
394-
{
395-
double minLenSquared = std::numeric_limits<double>::max();
396-
std::array<HsluvBounds, 6> bounds;
397-
398-
hsluvGetBounds (l, bounds);
399-
for (const auto& bound : bounds)
400-
{
401-
const HsluvBounds line2 = { -1.0 / bound.a, 0.0 };
402-
const double x = hsluvIntersectLineLine (bound, line2);
403-
const double distance = hsluvDistanceFromPoleSquared (x, bound.b + x * bound.a);
404-
405-
if (distance < minLenSquared)
406-
minLenSquared = distance;
407-
}
408-
409-
return std::sqrt (minLenSquared);
410-
}
411-
412385
double hsluvMaxChromaForLH (double l, double h) noexcept
413386
{
414387
double minLen = std::numeric_limits<double>::max();
@@ -500,6 +473,7 @@ DoubleRgb hsluvFromRgb (double r, double g, double b) noexcept
500473
}
501474

502475
//==============================================================================
476+
503477
constexpr int spectralSampleCount = 38;
504478
constexpr double spectralGamma = 2.4;
505479
constexpr double spectralEpsilon = 0.00000001;
@@ -646,24 +620,265 @@ IntRgb spectralMix (const DoubleRgb& lrgb1, const DoubleRgb& lrgb2, double t) no
646620

647621
return xyzToSrgb (reflectanceToXyz (reflectance));
648622
}
623+
624+
//==============================================================================
625+
626+
struct DoubleHsl
627+
{
628+
double h = 0.0;
629+
double s = 0.0;
630+
double l = 0.0;
631+
};
632+
633+
DoubleHsl rgbToHsl (const DoubleRgb& rgb) noexcept
634+
{
635+
const double maxValue = jmax (rgb[0], rgb[1], rgb[2]);
636+
const double minValue = jmin (rgb[0], rgb[1], rgb[2]);
637+
const double l = (maxValue + minValue) * 0.5;
638+
639+
if (maxValue == minValue)
640+
return { 0.0, 0.0, l };
641+
642+
const double d = maxValue - minValue;
643+
const double s = l > 0.5 ? d / (2.0 - maxValue - minValue) : d / (maxValue + minValue);
644+
double h = 0.0;
645+
646+
if (maxValue == rgb[0])
647+
h = (rgb[1] - rgb[2]) / d + (rgb[1] < rgb[2] ? 6.0 : 0.0);
648+
else if (maxValue == rgb[1])
649+
h = (rgb[2] - rgb[0]) / d + 2.0;
650+
else
651+
h = (rgb[0] - rgb[1]) / d + 4.0;
652+
653+
h /= 6.0;
654+
return { h, s, l };
655+
}
656+
657+
DoubleRgb hslToRgb (const DoubleHsl& hsl) noexcept
658+
{
659+
auto hueToRgb = [] (double p, double q, double t)
660+
{
661+
if (t < 0.0)
662+
t += 1.0;
663+
if (t > 1.0)
664+
t -= 1.0;
665+
if (t < 1.0 / 6.0)
666+
return p + (q - p) * 6.0 * t;
667+
if (t < 1.0 / 2.0)
668+
return q;
669+
if (t < 2.0 / 3.0)
670+
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
671+
return p;
672+
};
673+
674+
double r = hsl.l;
675+
double g = hsl.l;
676+
double b = hsl.l;
677+
678+
if (hsl.s != 0.0)
679+
{
680+
const double q = hsl.l < 0.5 ? hsl.l * (1.0 + hsl.s) : hsl.l + hsl.s - hsl.l * hsl.s;
681+
const double p = 2.0 * hsl.l - q;
682+
r = hueToRgb (p, q, hsl.h + 1.0 / 3.0);
683+
g = hueToRgb (p, q, hsl.h);
684+
b = hueToRgb (p, q, hsl.h - 1.0 / 3.0);
685+
}
686+
687+
return { r, g, b };
688+
}
689+
690+
double blendChannel (BlendMode mode, double backdrop, double source) noexcept
691+
{
692+
switch (mode)
693+
{
694+
case BlendMode::SrcOver:
695+
return source;
696+
case BlendMode::Screen:
697+
return 1.0 - (1.0 - backdrop) * (1.0 - source);
698+
case BlendMode::Overlay:
699+
return backdrop <= 0.5 ? 2.0 * backdrop * source : 1.0 - 2.0 * (1.0 - backdrop) * (1.0 - source);
700+
case BlendMode::Darken:
701+
return jmin (backdrop, source);
702+
case BlendMode::Lighten:
703+
return jmax (backdrop, source);
704+
case BlendMode::ColorDodge:
705+
return source >= 1.0 ? 1.0 : jmin (1.0, backdrop / (1.0 - source));
706+
case BlendMode::ColorBurn:
707+
return source <= 0.0 ? 0.0 : 1.0 - jmin (1.0, (1.0 - backdrop) / source);
708+
case BlendMode::HardLight:
709+
return source <= 0.5 ? 2.0 * backdrop * source : 1.0 - 2.0 * (1.0 - backdrop) * (1.0 - source);
710+
case BlendMode::SoftLight:
711+
{
712+
if (source <= 0.5)
713+
return backdrop - (1.0 - 2.0 * source) * backdrop * (1.0 - backdrop);
714+
715+
const double d = backdrop <= 0.25 ? ((16.0 * backdrop - 12.0) * backdrop + 4.0) * backdrop : std::sqrt (backdrop);
716+
return backdrop + (2.0 * source - 1.0) * (d - backdrop);
717+
}
718+
case BlendMode::Difference:
719+
return std::abs (backdrop - source);
720+
case BlendMode::Exclusion:
721+
return backdrop + source - 2.0 * backdrop * source;
722+
case BlendMode::Multiply:
723+
return backdrop * source;
724+
case BlendMode::Hue:
725+
case BlendMode::Saturation:
726+
case BlendMode::Color:
727+
case BlendMode::Luminosity:
728+
return source;
729+
default:
730+
return source;
731+
}
732+
}
733+
734+
DoubleRgb blendRgb (BlendMode mode, const DoubleRgb& backdrop, const DoubleRgb& source) noexcept
735+
{
736+
if (mode == BlendMode::Hue || mode == BlendMode::Saturation || mode == BlendMode::Color || mode == BlendMode::Luminosity)
737+
{
738+
const auto hslBackdrop = rgbToHsl (backdrop);
739+
const auto hslSource = rgbToHsl (source);
740+
DoubleHsl blended = hslBackdrop;
741+
742+
if (mode == BlendMode::Hue)
743+
blended.h = hslSource.h;
744+
else if (mode == BlendMode::Saturation)
745+
blended.s = hslSource.s;
746+
else if (mode == BlendMode::Color)
747+
{
748+
blended.h = hslSource.h;
749+
blended.s = hslSource.s;
750+
}
751+
else if (mode == BlendMode::Luminosity)
752+
blended.l = hslSource.l;
753+
754+
return hslToRgb (blended);
755+
}
756+
757+
return {
758+
blendChannel (mode, backdrop[0], source[0]),
759+
blendChannel (mode, backdrop[1], source[1]),
760+
blendChannel (mode, backdrop[2], source[2])
761+
};
762+
}
649763
} // namespace
650764

651765
//==============================================================================
652766

653-
Color Color::mixedWith (Color other, float amount) const noexcept
767+
Color& Color::mixWith (Color other, float amount, ColorSpace space) noexcept
654768
{
655769
const float clamped = jlimit (0.0f, 1.0f, amount);
770+
771+
if (space == ColorSpace::RGB)
772+
{
773+
if (clamped <= 0.0f)
774+
return *this;
775+
776+
if (clamped >= 1.0f)
777+
{
778+
*this = other;
779+
return *this;
780+
}
781+
782+
return blendWith (other.withMultipliedAlpha (clamped), BlendMode::SrcOver);
783+
}
784+
785+
if (space == ColorSpace::SRGB)
786+
{
787+
const double ra = getRedFloat();
788+
const double ga = getGreenFloat();
789+
const double ba = getBlueFloat();
790+
const double rb = other.getRedFloat();
791+
const double gb = other.getGreenFloat();
792+
const double bb = other.getBlueFloat();
793+
794+
const float outA = getAlphaFloat() + (other.getAlphaFloat() - getAlphaFloat()) * clamped;
795+
const float outR = static_cast<float> (ra + (rb - ra) * clamped);
796+
const float outG = static_cast<float> (ga + (gb - ga) * clamped);
797+
const float outB = static_cast<float> (ba + (bb - ba) * clamped);
798+
799+
a = normalizedToComponent (outA);
800+
r = normalizedToComponent (outR);
801+
g = normalizedToComponent (outG);
802+
b = normalizedToComponent (outB);
803+
return *this;
804+
}
805+
656806
const auto mixed = spectralMix (srgbToLinear (*this), srgbToLinear (other), static_cast<double> (clamped));
657807
const float alpha = getAlphaFloat() + (other.getAlphaFloat() - getAlphaFloat()) * clamped;
658808

659-
return {
660-
normalizedToComponent (alpha),
661-
static_cast<uint8> (mixed[0]),
662-
static_cast<uint8> (mixed[1]),
663-
static_cast<uint8> (mixed[2])
809+
a = normalizedToComponent (alpha);
810+
r = static_cast<uint8> (mixed[0]);
811+
g = static_cast<uint8> (mixed[1]);
812+
b = static_cast<uint8> (mixed[2]);
813+
return *this;
814+
}
815+
816+
Color Color::mixedWith (Color other, float amount, ColorSpace space) const noexcept
817+
{
818+
Color result (*this);
819+
result.mixWith (other, amount, space);
820+
return result;
821+
}
822+
823+
//==============================================================================
824+
825+
Color& Color::blendWith (Color src, BlendMode mode) noexcept
826+
{
827+
const double srcAlpha = src.getAlphaFloat();
828+
const double destAlpha = getAlphaFloat();
829+
830+
if (destAlpha <= 0.0)
831+
{
832+
*this = src;
833+
return *this;
834+
}
835+
836+
if (srcAlpha <= 0.0)
837+
return *this;
838+
839+
const DoubleRgb dest = { getRedFloat(), getGreenFloat(), getBlueFloat() };
840+
const DoubleRgb source = { src.getRedFloat(), src.getGreenFloat(), src.getBlueFloat() };
841+
const DoubleRgb blended = blendRgb (mode, dest, source);
842+
843+
const double outAlpha = srcAlpha + destAlpha - srcAlpha * destAlpha;
844+
if (outAlpha <= 0.0)
845+
{
846+
a = 0;
847+
r = 0;
848+
g = 0;
849+
b = 0;
850+
return *this;
851+
}
852+
853+
auto compositeChannel = [srcAlpha, destAlpha, outAlpha] (double cb, double cs, double bb)
854+
{
855+
const double co = (1.0 - srcAlpha) * cb * destAlpha
856+
+ (1.0 - destAlpha) * cs * srcAlpha
857+
+ destAlpha * srcAlpha * bb;
858+
return jlimit (0.0, 1.0, co / outAlpha);
664859
};
860+
861+
const float outR = static_cast<float> (compositeChannel (dest[0], source[0], blended[0]));
862+
const float outG = static_cast<float> (compositeChannel (dest[1], source[1], blended[1]));
863+
const float outB = static_cast<float> (compositeChannel (dest[2], source[2], blended[2]));
864+
const float outA = static_cast<float> (jlimit (0.0, 1.0, outAlpha));
865+
866+
r = normalizedToComponent (outR);
867+
g = normalizedToComponent (outG);
868+
b = normalizedToComponent (outB);
869+
a = normalizedToComponent (outA);
870+
return *this;
665871
}
666872

873+
Color Color::blendedWith (Color src, BlendMode mode) const noexcept
874+
{
875+
Color result (*this);
876+
result.blendWith (src, mode);
877+
return result;
878+
}
879+
880+
//==============================================================================
881+
667882
std::tuple<float, float, float> Color::toHSLuv() const noexcept
668883
{
669884
const auto hsluv = hsluvFromRgb (getRedFloat(), getGreenFloat(), getBlueFloat());

0 commit comments

Comments
 (0)