From eeff1e61744114f9076882ceaafda30c9c684c53 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 29 Apr 2020 09:42:11 +0100 Subject: [PATCH] Fixed hue wrap-around in HSV/HSL colours, added some more unit tests and added methods to distinguish between HSV and HSL saturation --- modules/juce_graphics/colour/juce_Colour.cpp | 208 +++++++++---------- modules/juce_graphics/colour/juce_Colour.h | 16 ++ 2 files changed, 109 insertions(+), 115 deletions(-) diff --git a/modules/juce_graphics/colour/juce_Colour.cpp b/modules/juce_graphics/colour/juce_Colour.cpp index 60db219787..e180a4bcda 100644 --- a/modules/juce_graphics/colour/juce_Colour.cpp +++ b/modules/juce_graphics/colour/juce_Colour.cpp @@ -96,7 +96,7 @@ namespace ColourHelpers auto min = (2.0f * l) - v; auto sv = (v - min) / v; - h = jlimit (0.0f, 360.0f, h * 360.0f) / 60.0f; + h = jlimit (0.0f, 360.0f, std::fmod (h, 1.0f) * 360.0f) / 60.0f; auto f = h - std::floor (h); auto vsf = v * sv * f; auto mid1 = min + vsf; @@ -152,7 +152,7 @@ namespace ColourHelpers return PixelARGB (alpha, intV, intV, intV); s = jmin (1.0f, s); - h = jlimit (0.0f, 360.0f, h * 360.0f) / 60.0f; + h = jlimit (0.0f, 360.0f, std::fmod (h, 1.0f) * 360.0f) / 60.0f; auto f = h - std::floor (h); auto x = (uint8) roundToInt (v * (1.0f - s)); @@ -301,14 +301,14 @@ bool Colour::isOpaque() const noexcept return getAlpha() == 0xff; } -Colour Colour::withAlpha (const uint8 newAlpha) const noexcept +Colour Colour::withAlpha (uint8 newAlpha) const noexcept { PixelARGB newCol (argb); newCol.setAlpha (newAlpha); return Colour (newCol); } -Colour Colour::withAlpha (const float newAlpha) const noexcept +Colour Colour::withAlpha (float newAlpha) const noexcept { jassert (newAlpha >= 0 && newAlpha <= 1.0f); @@ -317,7 +317,7 @@ Colour Colour::withAlpha (const float newAlpha) const noexcept return Colour (newCol); } -Colour Colour::withMultipliedAlpha (const float alphaMultiplier) const noexcept +Colour Colour::withMultipliedAlpha (float alphaMultiplier) const noexcept { jassert (alphaMultiplier >= 0); @@ -391,13 +391,15 @@ float Colour::getHue() const noexcept { return ColourHelpers::HSB (*th float Colour::getSaturation() const noexcept { return ColourHelpers::HSB (*this).saturation; } float Colour::getBrightness() const noexcept { return ColourHelpers::HSB (*this).brightness; } +float Colour::getSaturationHSL() const noexcept { return ColourHelpers::HSL (*this).saturation; } float Colour::getLightness() const noexcept { return ColourHelpers::HSL (*this).lightness; } Colour Colour::withHue (float h) const noexcept { ColourHelpers::HSB hsb (*this); hsb.hue = h; return hsb.toColour (*this); } Colour Colour::withSaturation (float s) const noexcept { ColourHelpers::HSB hsb (*this); hsb.saturation = s; return hsb.toColour (*this); } Colour Colour::withBrightness (float v) const noexcept { ColourHelpers::HSB hsb (*this); hsb.brightness = v; return hsb.toColour (*this); } -Colour Colour::withLightness (float l) const noexcept { ColourHelpers::HSL hsl (*this); hsl.lightness = l; return hsl.toColour (*this); } +Colour Colour::withSaturationHSL (float s) const noexcept { ColourHelpers::HSL hsl (*this); hsl.saturation = s; return hsl.toColour (*this); } +Colour Colour::withLightness (float l) const noexcept { ColourHelpers::HSL hsl (*this); hsl.lightness = l; return hsl.toColour (*this); } float Colour::getPerceivedBrightness() const noexcept { @@ -407,28 +409,35 @@ float Colour::getPerceivedBrightness() const noexcept } //============================================================================== -Colour Colour::withRotatedHue (const float amountToRotate) const noexcept +Colour Colour::withRotatedHue (float amountToRotate) const noexcept { ColourHelpers::HSB hsb (*this); hsb.hue += amountToRotate; return hsb.toColour (*this); } -Colour Colour::withMultipliedSaturation (const float amount) const noexcept +Colour Colour::withMultipliedSaturation (float amount) const noexcept { ColourHelpers::HSB hsb (*this); hsb.saturation = jmin (1.0f, hsb.saturation * amount); return hsb.toColour (*this); } -Colour Colour::withMultipliedBrightness (const float amount) const noexcept +Colour Colour::withMultipliedSaturationHSL (float amount) const noexcept +{ + ColourHelpers::HSL hsl (*this); + hsl.saturation = jmin (1.0f, hsl.saturation * amount); + return hsl.toColour (*this); +} + +Colour Colour::withMultipliedBrightness (float amount) const noexcept { ColourHelpers::HSB hsb (*this); hsb.brightness = jmin (1.0f, hsb.brightness * amount); return hsb.toColour (*this); } -Colour Colour::withMultipliedLightness (const float amount) const noexcept +Colour Colour::withMultipliedLightness (float amount) const noexcept { ColourHelpers::HSL hsl (*this); hsl.lightness = jmin (1.0f, hsl.lightness * amount); @@ -457,14 +466,14 @@ Colour Colour::darker (float amount) const noexcept } //============================================================================== -Colour Colour::greyLevel (const float brightness) noexcept +Colour Colour::greyLevel (float brightness) noexcept { auto level = ColourHelpers::floatToUInt8 (brightness); return Colour (level, level, level); } //============================================================================== -Colour Colour::contrasting (const float amount) const noexcept +Colour Colour::contrasting (float amount) const noexcept { return overlaidWith ((getPerceivedBrightness() >= 0.5f ? Colours::black @@ -542,168 +551,137 @@ public: void runTest() override { + auto testColour = [this] (Colour colour, + uint8 expectedRed, uint8 expectedGreen, uint8 expectedBlue, + uint8 expectedAlpha = 255, float expectedFloatAlpha = 1.0f) + { + expectEquals (colour.getRed(), expectedRed); + expectEquals (colour.getGreen(), expectedGreen); + expectEquals (colour.getBlue(), expectedBlue); + expectEquals (colour.getAlpha(), expectedAlpha); + expectEquals (colour.getFloatAlpha(), expectedFloatAlpha); + }; + beginTest ("Constructors"); { Colour c1; - expectEquals (c1.getRed(), (uint8) 0); - expectEquals (c1.getGreen(), (uint8) 0); - expectEquals (c1.getBlue(), (uint8) 0); - expectEquals (c1.getAlpha(), (uint8) 0); - expectEquals (c1.getFloatAlpha(), 0.0f); + testColour (c1, (uint8) 0, (uint8) 0, (uint8) 0, (uint8) 0, 0.0f); Colour c2 ((uint32) 0); - expectEquals (c2.getRed(), (uint8) 0); - expectEquals (c2.getGreen(), (uint8) 0); - expectEquals (c2.getBlue(), (uint8) 0); - expectEquals (c2.getAlpha(), (uint8) 0); - expectEquals (c2.getFloatAlpha(), 0.0f); + testColour (c2, (uint8) 0, (uint8) 0, (uint8) 0, (uint8) 0, 0.0f); Colour c3 ((uint32) 0xffffffff); - expectEquals (c3.getRed(), (uint8) 255); - expectEquals (c3.getGreen(), (uint8) 255); - expectEquals (c3.getBlue(), (uint8) 255); - expectEquals (c3.getAlpha(), (uint8) 255); - expectEquals (c3.getFloatAlpha(), 1.0f); + testColour (c3, (uint8) 255, (uint8) 255, (uint8) 255, (uint8) 255, 1.0f); Colour c4 (0, 0, 0); - expectEquals (c4.getRed(), (uint8) 0); - expectEquals (c4.getGreen(), (uint8) 0); - expectEquals (c4.getBlue(), (uint8) 0); - expectEquals (c4.getAlpha(), (uint8) 255); - expectEquals (c4.getFloatAlpha(), 1.0f); + testColour (c4, (uint8) 0, (uint8) 0, (uint8) 0, (uint8) 255, 1.0f); Colour c5 (255, 255, 255); - expectEquals (c5.getRed(), (uint8) 255); - expectEquals (c5.getGreen(), (uint8) 255); - expectEquals (c5.getBlue(), (uint8) 255); - expectEquals (c5.getAlpha(), (uint8) 255); - expectEquals (c5.getFloatAlpha(), 1.0f); + testColour (c5, (uint8) 255, (uint8) 255, (uint8) 255, (uint8) 255, 1.0f); Colour c6 ((uint8) 0, (uint8) 0, (uint8) 0, (uint8) 0); - expectEquals (c6.getRed(), (uint8) 0); - expectEquals (c6.getGreen(), (uint8) 0); - expectEquals (c6.getBlue(), (uint8) 0); - expectEquals (c6.getAlpha(), (uint8) 0); - expectEquals (c6.getFloatAlpha(), 0.0f); + testColour (c6, (uint8) 0, (uint8) 0, (uint8) 0, (uint8) 0, 0.0f); Colour c7 ((uint8) 255, (uint8) 255, (uint8) 255, (uint8) 255); - expectEquals (c7.getRed(), (uint8) 255); - expectEquals (c7.getGreen(), (uint8) 255); - expectEquals (c7.getBlue(), (uint8) 255); - expectEquals (c7.getAlpha(), (uint8) 255); - expectEquals (c7.getFloatAlpha(), 1.0f); + testColour (c7, (uint8) 255, (uint8) 255, (uint8) 255, (uint8) 255, 1.0f); Colour c8 ((uint8) 0, (uint8) 0, (uint8) 0, 0.0f); - expectEquals (c8.getRed(), (uint8) 0); - expectEquals (c8.getGreen(), (uint8) 0); - expectEquals (c8.getBlue(), (uint8) 0); - expectEquals (c8.getAlpha(), (uint8) 0); - expectEquals (c8.getFloatAlpha(), 0.0f); + testColour (c8, (uint8) 0, (uint8) 0, (uint8) 0, (uint8) 0, 0.0f); Colour c9 ((uint8) 255, (uint8) 255, (uint8) 255, 1.0f); - expectEquals (c9.getRed(), (uint8) 255); - expectEquals (c9.getGreen(), (uint8) 255); - expectEquals (c9.getBlue(), (uint8) 255); - expectEquals (c9.getAlpha(), (uint8) 255); - expectEquals (c9.getFloatAlpha(), 1.0f); + testColour (c9, (uint8) 255, (uint8) 255, (uint8) 255, (uint8) 255, 1.0f); } beginTest ("HSV"); { - auto testHSV = [this] (int hueDegrees, int saturationPercentage, int brightnessPercentage, - uint8 expectedRed, uint8 expectedGreen, uint8 expectedBlue) - { - auto testColour = Colour::fromHSV (hueDegrees / 360.0f, - saturationPercentage / 100.0f, - brightnessPercentage / 100.0f, - 1.0f); - - expectEquals (testColour.getRed(), expectedRed); - expectEquals (testColour.getGreen(), expectedGreen); - expectEquals (testColour.getBlue(), expectedBlue); - }; - // black - testHSV (0, 0, 0, 0, 0, 0); + testColour (Colour::fromHSV (0.0f, 0.0f, 0.0f, 1.0f), 0, 0, 0); // white - testHSV (0, 0, 100, 255, 255, 255); + testColour (Colour::fromHSV (0.0f, 0.0f, 1.0f, 1.0f), 255, 255, 255); // red - testHSV (0, 100, 100, 255, 0, 0); + testColour (Colour::fromHSV (0.0f, 1.0f, 1.0f, 1.0f), 255, 0, 0); + testColour (Colour::fromHSV (1.0f, 1.0f, 1.0f, 1.0f), 255, 0, 0); // lime - testHSV (120, 100, 100, 0, 255, 0); + testColour (Colour::fromHSV (120 / 360.0f, 1.0f, 1.0f, 1.0f), 0, 255, 0); // blue - testHSV (240, 100, 100, 0, 0, 255); + testColour (Colour::fromHSV (240 / 360.0f, 1.0f, 1.0f, 1.0f), 0, 0, 255); // yellow - testHSV (60, 100, 100, 255, 255, 0); + testColour (Colour::fromHSV (60 / 360.0f, 1.0f, 1.0f, 1.0f), 255, 255, 0); // cyan - testHSV (180, 100, 100, 0, 255, 255); + testColour (Colour::fromHSV (180 / 360.0f, 1.0f, 1.0f, 1.0f), 0, 255, 255); // magenta - testHSV (300, 100, 100, 255, 0, 255); + testColour (Colour::fromHSV (300 / 360.0f, 1.0f, 1.0f, 1.0f), 255, 0, 255); // silver - testHSV (0, 0, 75, 191, 191, 191); + testColour (Colour::fromHSV (0.0f, 0.0f, 0.75f, 1.0f), 191, 191, 191); // grey - testHSV (0, 0, 50, 128, 128, 128); + testColour (Colour::fromHSV (0.0f, 0.0f, 0.5f, 1.0f), 128, 128, 128); // maroon - testHSV (0, 100, 50, 128, 0, 0); + testColour (Colour::fromHSV (0.0f, 1.0f, 0.5f, 1.0f), 128, 0, 0); // olive - testHSV (60, 100, 50, 128, 128, 0); + testColour (Colour::fromHSV (60 / 360.0f, 1.0f, 0.5f, 1.0f), 128, 128, 0); // green - testHSV (120, 100, 50, 0, 128, 0); + testColour (Colour::fromHSV (120 / 360.0f, 1.0f, 0.5f, 1.0f), 0, 128, 0); // purple - testHSV (300, 100, 50, 128, 0, 128); + testColour (Colour::fromHSV (300 / 360.0f, 1.0f, 0.5f, 1.0f), 128, 0, 128); // teal - testHSV (180, 100, 50, 0, 128, 128); + testColour (Colour::fromHSV (180 / 360.0f, 1.0f, 0.5f, 1.0f), 0, 128, 128); // navy - testHSV (240, 100, 50, 0, 0, 128); + testColour (Colour::fromHSV (240 / 360.0f, 1.0f, 0.5f, 1.0f), 0, 0, 128); } beginTest ("HSL"); { - auto testHSL = [this] (int hueDegrees, int saturationPercentage, int lightnessPercentage, - uint8 expectedRed, uint8 expectedGreen, uint8 expectedBlue) - { - auto testColour = Colour::fromHSL (hueDegrees / 360.0f, - saturationPercentage / 100.0f, - lightnessPercentage / 100.0f, - 1.0f); - - expectEquals (testColour.getRed(), expectedRed); - expectEquals (testColour.getGreen(), expectedGreen); - expectEquals (testColour.getBlue(), expectedBlue); - }; - // black - testHSL (0, 0, 0, 0, 0, 0); + testColour (Colour::fromHSL (0.0f, 0.0f, 0.0f, 1.0f), 0, 0, 0); // white - testHSL (0, 0, 100, 255, 255, 255); + testColour (Colour::fromHSL (0.0f, 0.0f, 1.0f, 1.0f), 255, 255, 255); // red - testHSL (0, 100, 50, 255, 0, 0); + testColour (Colour::fromHSL (0.0f, 1.0f, 0.5f, 1.0f), 255, 0, 0); + testColour (Colour::fromHSL (1.0f, 1.0f, 0.5f, 1.0f), 255, 0, 0); // lime - testHSL (120, 100, 50, 0, 255, 0); + testColour (Colour::fromHSL (120 / 360.0f, 1.0f, 0.5f, 1.0f), 0, 255, 0); // blue - testHSL (240, 100, 50, 0, 0, 255); + testColour (Colour::fromHSL (240 / 360.0f, 1.0f, 0.5f, 1.0f), 0, 0, 255); // yellow - testHSL (60, 100, 50, 255, 255, 0); + testColour (Colour::fromHSL (60 / 360.0f, 1.0f, 0.5f, 1.0f), 255, 255, 0); // cyan - testHSL (180, 100, 50, 0, 255, 255); + testColour (Colour::fromHSL (180 / 360.0f, 1.0f, 0.5f, 1.0f), 0, 255, 255); // magenta - testHSL (300, 100, 50, 255, 0, 255); + testColour (Colour::fromHSL (300 / 360.0f, 1.0f, 0.5f, 1.0f), 255, 0, 255); // silver - testHSL (0, 0, 75, 191, 191, 191); + testColour (Colour::fromHSL (0.0f, 0.0f, 0.75f, 1.0f), 191, 191, 191); // grey - testHSL (0, 0, 50, 128, 128, 128); + testColour (Colour::fromHSL (0.0f, 0.0f, 0.5f, 1.0f), 128, 128, 128); // maroon - testHSL (0, 100, 25, 128, 0, 0); + testColour (Colour::fromHSL (0.0f, 1.0f, 0.25f, 1.0f), 128, 0, 0); // olive - testHSL (60, 100, 25, 128, 128, 0); + testColour (Colour::fromHSL (60 / 360.0f, 1.0f, 0.25f, 1.0f), 128, 128, 0); // green - testHSL (120, 100, 25, 0, 128, 0); + testColour (Colour::fromHSL (120 / 360.0f, 1.0f, 0.25f, 1.0f), 0, 128, 0); // purple - testHSL (300, 100, 25, 128, 0, 128); + testColour (Colour::fromHSL (300 / 360.0f, 1.0f, 0.25f, 1.0f), 128, 0, 128); // teal - testHSL (180, 100, 25, 0, 128, 128); + testColour (Colour::fromHSL (180 / 360.0f, 1.0f, 0.25f, 1.0f), 0, 128, 128); // navy - testHSL (240, 100, 25, 0, 0, 128); + testColour (Colour::fromHSL (240 / 360.0f, 1.0f, 0.25f, 1.0f), 0, 0, 128); + } + + beginTest ("Modifiers"); + { + Colour red (255, 0, 0); + testColour (red, 255, 0, 0); + + testColour (red.withHue (120.0f / 360.0f), 0, 255, 0); + testColour (red.withSaturation (0.5f), 255, 128, 128); + testColour (red.withSaturationHSL (0.5f), 191, 64, 64); + testColour (red.withBrightness (0.5f), 128, 0, 0); + testColour (red.withLightness (1.0f), 255, 255, 255); + testColour (red.withRotatedHue (120.0f / 360.0f), 0, 255, 0); + testColour (red.withRotatedHue (480.0f / 360.0f), 0, 255, 0); + testColour (red.withMultipliedSaturation (0.0f), 255, 255, 255); + testColour (red.withMultipliedSaturationHSL (0.0f), 128, 128, 128); + testColour (red.withMultipliedBrightness (0.5f), 128, 0, 0); + testColour (red.withMultipliedLightness (2.0f), 255, 255, 255); } } }; diff --git a/modules/juce_graphics/colour/juce_Colour.h b/modules/juce_graphics/colour/juce_Colour.h index 31694f9802..f90c0c4bb5 100644 --- a/modules/juce_graphics/colour/juce_Colour.h +++ b/modules/juce_graphics/colour/juce_Colour.h @@ -255,6 +255,11 @@ public: */ float getSaturation() const noexcept; + /** Returns the colour's saturation component as represented in the HSL colour space. + The value returned is in the range 0.0 to 1.0 + */ + float getSaturationHSL() const noexcept; + /** Returns the colour's brightness component. The value returned is in the range 0.0 to 1.0 */ @@ -292,6 +297,9 @@ public: /** Returns a copy of this colour with a different saturation. */ Colour withSaturation (float newSaturation) const noexcept; + /** Returns a copy of this colour with a different saturation in the HSL colour space. */ + Colour withSaturationHSL (float newSaturation) const noexcept; + /** Returns a copy of this colour with a different brightness. @see brighter, darker, withMultipliedBrightness */ @@ -314,6 +322,14 @@ public: */ Colour withMultipliedSaturation (float multiplier) const noexcept; + /** Returns a copy of this colour with its saturation multiplied by the given value. + The new colour's saturation is (this->getSaturation() * multiplier) + (the result is clipped to legal limits). + + This will be in the HSL colour space. + */ + Colour withMultipliedSaturationHSL (float multiplier) const noexcept; + /** Returns a copy of this colour with its brightness multiplied by the given value. The new colour's brightness is (this->getBrightness() * multiplier) (the result is clipped to legal limits).