Skip to content

Instantly share code, notes, and snippets.

@profexorgeek
Created September 5, 2018 15:39
Show Gist options
  • Save profexorgeek/a407c0c96f69a37a2f2554b43491e247 to your computer and use it in GitHub Desktop.
Save profexorgeek/a407c0c96f69a37a2f2554b43491e247 to your computer and use it in GitHub Desktop.
This struct represents a Color using the HSL color space. It provides convenient conversions into the XNA/MonoGame RGB Color struct. HSL offers an easier way to work with color artistically by separating color into Hue, Saturation and Luminance.
public struct HSLColor
{
// HSL stands for Hue, Saturation and Luminance. HSL
// color space makes it easier to do calculations
// that operate on these channels
// Helpful color math can be found here:
// https://www.easyrgb.com/en/math.php
/// <summary>
/// Hue: the 'color' of the color!
/// </summary>
public float H;
/// <summary>
/// Saturation: How grey or vivid/colorful a color is
/// </summary>
public float S;
/// <summary>
/// Luminance: The brightness or lightness of the color
/// </summary>
public float L;
public HSLColor(float h, float s, float l)
{
H = h;
S = s;
L = l;
}
public static HSLColor FromColor(Color color)
{
return FromRgb(color.R, color.G, color.B);
}
public static HSLColor FromRgb(byte R, byte G, byte B)
{
var hsl = new HSLColor();
hsl.H = 0;
hsl.S = 0;
hsl.L = 0;
float r = R / 255f;
float g = G / 255f;
float b = B / 255f;
float min = Math.Min(Math.Min(r, g), b);
float max = Math.Max(Math.Max(r, g), b);
float delta = max - min;
// luminance is the ave of max and min
hsl.L = (max + min) / 2f;
if (delta > 0)
{
if (hsl.L < 0.5f)
{
hsl.S = delta / (max + min);
}
else
{
hsl.S = delta / (2 - max - min);
}
float deltaR = (((max - r) / 6f) + (delta / 2f)) / delta;
float deltaG = (((max - g) / 6f) + (delta / 2f)) / delta;
float deltaB = (((max - b) / 6f) + (delta / 2f)) / delta;
if (r == max)
{
hsl.H = deltaB - deltaG;
}
else if (g == max)
{
hsl.H = (1f / 3f) + deltaR - deltaB;
}
else if (b == max)
{
hsl.H = (2f / 3f) + deltaG - deltaR;
}
if (hsl.H < 0)
{
hsl.H += 1;
}
if (hsl.H > 1)
{
hsl.H -= 1;
}
}
return hsl;
}
public HSLColor GetComplement()
{
// complementary colors are across the color wheel
// which is 180 degrees or 50% of the way around the
// wheel. Add 50% to our hue and wrap large/small values
var h = H + 0.5f;
if (h > 1)
{
h -= 1;
}
return new HSLColor(h, S, L);
}
public Color ToRgbColor()
{
var c = new Color();
if (S == 0)
{
c.R = (byte)(L * 255f);
c.G = (byte)(L * 255f);
c.B = (byte)(L * 255f);
}
else
{
float v2 = (L + S) - (S * L);
if (L < 0.5f)
{
v2 = L * (1 + S);
}
float v1 = 2f * L - v2;
c.R = (byte)(255f * HueToRgb(v1, v2, H + (1f / 3f)));
c.G = (byte)(255f * HueToRgb(v1, v2, H));
c.B = (byte)(255f * HueToRgb(v1, v2, H - (1f / 3f)));
}
return c;
}
private static float HueToRgb(float v1, float v2, float vH)
{
vH += (vH < 0) ? 1 : 0;
vH -= (vH > 1) ? 1 : 0;
float ret = v1;
if ((6 * vH) < 1)
{
ret = (v1 + (v2 - v1) * 6 * vH);
}
else if ((2 * vH) < 1)
{
ret = (v2);
}
else if ((3 * vH) < 2)
{
ret = (v1 + (v2 - v1) * ((2f / 3f) - vH) * 6f);
}
return ret.Clamp(0, 1);
}
}
@profexorgeek
Copy link
Author

Can you show me how you are using it, and are you using this with MonoGame? This should work (may be minor syntax errors since I just wrote this in the browser):

var hslRed = HSLColor.FromColor(Color.Red);
var hslGreen = hslRed.GetComplement();
var greenColor = hslGreen.ToRgbColor();

I use this class extensively in my Steam game Masteroid so I know it works well with MonoGame. The Color object this is designed to work with is specifically the Microsoft.Framework.Xna.Color, not a standard .NET framework color!

@Soraiko
Copy link

Soraiko commented Dec 29, 2019 via email

@profexorgeek
Copy link
Author

Can you give me some sample values you used to initialize the HSLColor object? There are two things that could be happening here:

  1. You are initializing the HSLColor with a Luminosity value of 1: this will always result in white.
  2. You are using/casting to a different type of color object or other settings in your project are changing how premultiplied alpha works. Premultiplied alpha can cause a variety of unexpected conversion problems.

I have double checked the initialization and conversion and they seem to match what I've been using in my project for a few years now. It's working fine in my project so I suspect it's one of these two things. I'd love to figure this out if you have time to test or provide any more specific information. If I get a chance and remember, I'll try creating a small sample project and pushing it to github for demonstration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment