Gamua / Starling-Framework

The Cross Platform Game Engine
http://www.starling-framework.org
Other
2.82k stars 821 forks source link

Color conversions (RGB <-> HSL & RGB <-> HSV) #1035

Closed Adolio closed 5 years ago

Adolio commented 6 years ago

Following 0744d51 I propose to integrate several color conversion algorithms into the Starling codebase. Below are few utility methods part of my ColorUtil class.

I hope this would be helpful for you guys đŸ™‚

/**
 * Convert HSL color to RGB color.
 * 
 * Theory: https://en.wikipedia.org/wiki/HSL_and_HSV
 * Implementation & tests: http://www.rapidtables.com/convert/color/hsl-to-rgb.htm
 * 
 * @param h Hue [0, 359]
 * @param s Saturation [0, 1.0]
 * @param l Lightness [0, 1.0]
 * @return RGB color
 */
public static function hslToRgb(h:Number, s:Number, l:Number):uint
{
    var c:Number = (1.0 - Math.abs(2.0*l - 1.0)) * s;
    var h2:Number = h / 60.0;
    var x:Number = c * (1.0 - Math.abs(h2 % 2 - 1.0));
    var m:Number = l - (c / 2.0);
    switch (int(h2 % 6))
    {
        case 0: return combineNormRgb(c + m, x + m, m);
        case 1: return combineNormRgb(x + m, c + m, m);
        case 2: return combineNormRgb(m, c + m, x + m);
        case 3: return combineNormRgb(m, x + m, c + m);
        case 4: return combineNormRgb(x + m, m, c + m);
        case 5: return combineNormRgb(c + m, m, x + m);
        default: return 0;
    }
}

/**
 * Convert RGB color to HSL color.
 * 
 * Theory: https://en.wikipedia.org/wiki/HSL_and_HSV
 * Implementation & tests: http://www.rapidtables.com/convert/color/rgb-to-hsl.htm
 * 
 * @param rgb Color
 * @param hsl output array[3], if null, an array will be instantiate
 * @return Array containing [0]=h, [1]=s, [2]=l
 */
public static function rgbToHsl(rgb:uint, hsl:Array=null):Array
{
    // Setup output container
    if (!hsl)
        hsl = new Array();

    var r:Number = extractRed(rgb) / 255.0;
    var g:Number = extractGreen(rgb) / 255.0;
    var b:Number = extractBlue(rgb) / 255.0;
    var cMax:Number = Math.max(r, g, b);
    var cMin:Number = Math.min(r, g, b);
    var delta:Number = cMax - cMin;

    // Hue
    if (delta == 0)
        hsl[0] = 0;
    else if (cMax == r)
        hsl[0] = 60.0 * (((g - b) / delta) % 6);
    else if (cMax == g)
        hsl[0] = 60.0 * ((b - r) / delta + 2);
    else if (cMax == b)
        hsl[0] = 60.0 * ((r - g) / delta + 4);

    // Normalize hue [0..359] - TODO better solution? Using a while statement? Or hsv[0] = ((hsv[0] % 360) + 360) % 360?
    if (hsl[0] < 0) hsl[0] += 360;
    if (hsl[0] >= 360) hsl[0] -= 360;

    // Lightness
    hsl[2] = (cMax + cMin) * 0.5;

    // Saturation
    hsl[1] = delta == 0 ? 0 : delta / (1.0 - Math.abs(2.0 * hsl[2] - 1.0));

    return hsl;
}

/**
 * Convert HSV / HSB color to RGB color.
 * 
 * Theory: https://en.wikipedia.org/wiki/HSL_and_HSV
 * Implementation & tests: http://www.rapidtables.com/convert/color/hsv-to-rgb.htm
 * 
 * @param h Hue [0, 359]
 * @param s Saturation [0, 1.0]
 * @param v Value or Brightness [0, 1.0]
 * @return RGB color
 */
public static function hsvToRgb(h:Number, s:Number, v:Number):uint
{
    var c:Number = v * s;
    var h2:Number = h / 60.0;
    var x:Number = c * (1.0 - Math.abs(h2 % 2 - 1.0));
    var m:Number = v - c;
    switch (int(h2 % 6))
    {
        case 0: return combineNormRgb(c + m, x + m, m);
        case 1: return combineNormRgb(x + m, c + m, m);
        case 2: return combineNormRgb(m, c + m, x + m);
        case 3: return combineNormRgb(m, x + m, c + m);
        case 4: return combineNormRgb(x + m, m, c + m);
        case 5: return combineNormRgb(c + m, m, x + m);
        default: return 0;
    }
}

/**
 * Convert RGB color to HSV / HSB color.
 * 
 * Theory: https://en.wikipedia.org/wiki/HSL_and_HSV
 * Implementation & tests: http://www.rapidtables.com/convert/color/rgb-to-hsv.htm
 * 
 * @param rgb Color
 * @param hsv output array[3], if null, an array will be instantiate
 * @return Array containing [0]=h, [1]=s, [2]=v
 */
public static function rgbToHsv(rgb:uint, hsv:Array=null):Array
{
    // Setup output container
    if (!hsv)
        hsv = new Array();

    var r:Number = extractRed(rgb) / 255.0;
    var g:Number = extractGreen(rgb) / 255.0;
    var b:Number = extractBlue(rgb) / 255.0;
    var cMax:Number = Math.max(r, g, b);
    var cMin:Number = Math.min(r, g, b);
    var delta:Number = cMax - cMin;

    // Hue
    if (delta == 0)
        hsv[0] = 0;
    else if (cMax == r)
        hsv[0] = 60.0 * (((g - b) / delta) % 6);
    else if (cMax == g)
        hsv[0] = 60.0 * ((b - r) / delta + 2);
    else if (cMax == b)
        hsv[0] = 60.0 * ((r - g) / delta + 4);

    // Normalize hue [0..359] - TODO better solution? Using a while statement? Or hsv[0] = ((hsv[0] % 360) + 360) % 360?
    if (hsv[0] < 0) hsv[0] += 360;
    if (hsv[0] >= 360) hsv[0] -= 360;

    // Value / brightness
    hsv[2] = cMax;

    // Saturation
    hsv[1] = cMax == 0 ? 0 : delta / cMax;

    return hsv;
}

/**
 * Combine normalized RBG components [0, 1.0] into RGB color (uint).
 * 
 * @param r Normalized red component [0, 1.0]
 * @param g Normalized green component [0, 1.0]
 * @param b Normalized blue component [0, 1.0]
 * @return RGB color
 */
private static function combineNormRgb(r:Number, g:Number, b:Number):uint
{
    return (Math.ceil(r * 255) << 16) | (Math.ceil(g * 255) << 8) | Math.ceil(b * 255);
}

public static function extractRed(c:uint):uint
{
    return (c >> 16) & 0xFF;
}

public static function extractGreen(c:uint):uint
{
    return (c >> 8) & 0xFF;
}

public static function extractBlue(c:uint):uint
{
    return c & 0xFF;
}

And here are my tests:

package
{
    import flash.display.Sprite;

    public class Main extends Sprite 
    {
        public function Main() 
        {
            testHslToRgb();
            trace("");
            testRgbToHsl();
            trace("");
            testHsvToRgb();
            trace("");
            testRgbToHsv();
        }

        public static function testHslToRgb():void
        {
            // Colors from: http://www.rapidtables.com/convert/color/hsl-to-rgb.htm
            trace("HSL to RGB tests:")
            checkColors(ColorUtil.hslToRgb(0, 0.5, 0), 0x000000, "Black");
            checkColors(ColorUtil.hslToRgb(0, 0.5, 1.0), 0xFFFFFF, "White");
            checkColors(ColorUtil.hslToRgb(0, 1.0, 0.5), 0xFF0000, "Red");
            checkColors(ColorUtil.hslToRgb(60, 1.0, 0.5), 0xFFFF00, "Yellow");
            checkColors(ColorUtil.hslToRgb(120, 1.0, 0.5), 0x00FF00, "Green");
            checkColors(ColorUtil.hslToRgb(180, 1.0, 0.5), 0x00FFFF, "Cyan");
            checkColors(ColorUtil.hslToRgb(240, 1.0, 0.5), 0x0000FF, "Blue");
            checkColors(ColorUtil.hslToRgb(300, 1.0, 0.5), 0xFF00FF, "Magenta");
            checkColors(ColorUtil.hslToRgb(0, 0, 0.75), 0xC0C0C0, "Silver");
            checkColors(ColorUtil.hslToRgb(0, 0, 0.5), 0x808080, "Gray");
            checkColors(ColorUtil.hslToRgb(0, 1.0, 0.25), 0x800000, "Maroon");
            checkColors(ColorUtil.hslToRgb(60, 1.0, 0.25), 0x808000, "Olive");
            checkColors(ColorUtil.hslToRgb(120, 1.0, 0.25), 0x008000, "Green");
            checkColors(ColorUtil.hslToRgb(300, 1.0, 0.25), 0x800080, "Purple");
            checkColors(ColorUtil.hslToRgb(180, 1.0, 0.25), 0x008080, "Teal");
            checkColors(ColorUtil.hslToRgb(240, 1.0, 0.25), 0x000080, "Navy");
        }

        public static function testRgbToHsl():void
        {
            // Colors from: http://www.rapidtables.com/convert/color/rgb-to-hsl.htm
            trace("RGB to HSL tests:")
            checkHsX(ColorUtil.rgbToHsl(0x000000), new Array(0, 0, 0), "Black");
            checkHsX(ColorUtil.rgbToHsl(0xFFFFFF), new Array(0, 0, 1.0), "White");
            checkHsX(ColorUtil.rgbToHsl(0xFF0000), new Array(0, 1.0, 0.5), "Red");
            checkHsX(ColorUtil.rgbToHsl(0xFFFF00), new Array(60, 1.0, 0.5), "Yellow");
            checkHsX(ColorUtil.rgbToHsl(0x00FF00), new Array(120, 1.0, 0.5), "Green");
            checkHsX(ColorUtil.rgbToHsl(0x00FFFF), new Array(180, 1.0, 0.5), "Cyan");
            checkHsX(ColorUtil.rgbToHsl(0x0000FF), new Array(240, 1.0, 0.5), "Blue");
            checkHsX(ColorUtil.rgbToHsl(0xFF00FF), new Array(300, 1.0, 0.5), "Magenta");
            checkHsX(ColorUtil.rgbToHsl(0xC0C0C0), new Array(0, 0, 0.75), "Silver");
            checkHsX(ColorUtil.rgbToHsl(0x808080), new Array(0, 0, 0.5), "Gray");
            checkHsX(ColorUtil.rgbToHsl(0x800000), new Array(0, 1.0, 0.25), "Maroon");
            checkHsX(ColorUtil.rgbToHsl(0x808000), new Array(60, 1.0, 0.25), "Olive");
            checkHsX(ColorUtil.rgbToHsl(0x008000), new Array(120, 1.0, 0.25), "Green");
            checkHsX(ColorUtil.rgbToHsl(0x800080), new Array(300, 1.0, 0.25), "Purple");
            checkHsX(ColorUtil.rgbToHsl(0x008080), new Array(180, 1.0, 0.25), "Teal");
            checkHsX(ColorUtil.rgbToHsl(0x000080), new Array(240, 1.0, 0.25), "Navy");

            checkHsX(ColorUtil.rgbToHsl(0x60B22A), new Array(96, 0.62, 0.43), "Custom");
        }

        public static function testHsvToRgb():void
        {
            // Colors from: http://www.rapidtables.com/convert/color/hsv-to-rgb.htm
            trace("HSV to RGB tests:")
            checkColors(ColorUtil.hsvToRgb(0, 0, 0), 0x000000, "Black");
            checkColors(ColorUtil.hsvToRgb(0, 0, 1.0), 0xFFFFFF, "White");
            checkColors(ColorUtil.hsvToRgb(0, 1.0, 1.0), 0xFF0000, "Red");
            checkColors(ColorUtil.hsvToRgb(60, 1.0, 1.0), 0xFFFF00, "Yellow");
            checkColors(ColorUtil.hsvToRgb(120, 1.0, 1.0), 0x00FF00, "Green");
            checkColors(ColorUtil.hsvToRgb(180, 1.0, 1.0), 0x00FFFF, "Cyan");
            checkColors(ColorUtil.hsvToRgb(240, 1.0, 1.0), 0x0000FF, "Blue");
            checkColors(ColorUtil.hsvToRgb(300, 1.0, 1.0), 0xFF00FF, "Magenta");
            checkColors(ColorUtil.hsvToRgb(0, 0, 0.75), 0xC0C0C0, "Silver");
            checkColors(ColorUtil.hsvToRgb(0, 0, 0.5), 0x808080, "Gray");
            checkColors(ColorUtil.hsvToRgb(0, 1.0, 0.5), 0x800000, "Maroon");
            checkColors(ColorUtil.hsvToRgb(60, 1.0, 0.5), 0x808000, "Olive");
            checkColors(ColorUtil.hsvToRgb(120, 1.0, 0.5), 0x008000, "Green");
            checkColors(ColorUtil.hsvToRgb(300, 1.0, 0.5), 0x800080, "Purple");
            checkColors(ColorUtil.hsvToRgb(180, 1.0, 0.5), 0x008080, "Teal");
            checkColors(ColorUtil.hsvToRgb(240, 1.0, 0.5), 0x000080, "Navy");
        }

        public static function testRgbToHsv():void
        {
            // Colors from: http://www.rapidtables.com/convert/color/rgb-to-hsv.htm
            trace("RGB to HSV tests:")
            checkHsX(ColorUtil.rgbToHsv(0x000000), new Array(0, 0, 0), "Black");
            checkHsX(ColorUtil.rgbToHsv(0xFFFFFF), new Array(0, 0, 1.0), "White");
            checkHsX(ColorUtil.rgbToHsv(0xFF0000), new Array(0, 1.0, 1.0), "Red");
            checkHsX(ColorUtil.rgbToHsv(0xFFFF00), new Array(60, 1.0, 1.0), "Yellow");
            checkHsX(ColorUtil.rgbToHsv(0x00FF00), new Array(120, 1.0, 1.0), "Green");
            checkHsX(ColorUtil.rgbToHsv(0x00FFFF), new Array(180, 1.0, 1.0), "Cyan");
            checkHsX(ColorUtil.rgbToHsv(0x0000FF), new Array(240, 1.0, 1.0), "Blue");
            checkHsX(ColorUtil.rgbToHsv(0xFF00FF), new Array(300, 1.0, 1.0), "Magenta");
            checkHsX(ColorUtil.rgbToHsv(0xC0C0C0), new Array(0, 0, 0.75), "Silver");
            checkHsX(ColorUtil.rgbToHsv(0x808080), new Array(0, 0, 0.5), "Gray");
            checkHsX(ColorUtil.rgbToHsv(0x800000), new Array(0, 1.0, 0.5), "Maroon");
            checkHsX(ColorUtil.rgbToHsv(0x808000), new Array(60, 1.0, 0.5), "Olive");
            checkHsX(ColorUtil.rgbToHsv(0x008000), new Array(120, 1.0, 0.5), "Green");
            checkHsX(ColorUtil.rgbToHsv(0x800080), new Array(300, 1.0, 0.5), "Purple");
            checkHsX(ColorUtil.rgbToHsv(0x008080), new Array(180, 1.0, 0.5), "Teal");
            checkHsX(ColorUtil.rgbToHsv(0x000080), new Array(240, 1.0, 0.5), "Navy");

            checkHsX(ColorUtil.rgbToHsv(0x60B22A), new Array(96, 0.764, 0.698), "Custom");
        }

        private static function checkColors(resultColor:uint, expectedColor:uint, title:String):void
        {
            if(resultColor != expectedColor)
                trace("[Error] " + title + ", result: 0x" + resultColor.toString(16) + ", expected 0x" + expectedColor.toString(16));
            else
                trace("[OK] " + title);
        }

        private static function checkHsX(resultHsX:Array, expectedHsX:Array, title:String, epsilon:Number = 0.005):void
        {
            var hOk:Boolean = Math.abs(resultHsX[0] - expectedHsX[0]) <= epsilon * 360.0;
            var sOk:Boolean = Math.abs(resultHsX[1] - expectedHsX[1]) <= epsilon;
            var lOk:Boolean = Math.abs(resultHsX[2] - expectedHsX[2]) <= epsilon;

            if(!hOk || !sOk || !lOk)
                trace("[Error] " + title + ", result: [" + resultHsX + "], expected [" + expectedHsX + "]");
            else
                trace("[OK] " + title);
        }
    }
}
PrimaryFeather commented 6 years ago

Awesome! Thanks a lot, Aurélien!

Adolio commented 6 years ago

I forgot to add the color component extraction methods. Now everything should be fine.

PrimaryFeather commented 5 years ago

Thanks again for sharing the code, Aurélien! I just added those methods to the Color class.

Beware, though, that I changed how hue is used: I put it into the range 0-1 instead of 0-360. For one, that's what I used in the existing method; and two, that range is easily converted to radian or degrees (simply by multiplying with 360 or 2 * Math.PI). :wink: