-
-
Save UweKeim/fb7f829b852c209557bc49c51ba14c8b to your computer and use it in GitHub Desktop.
namespace ZetaColorEditor.Colors; | |
using System; | |
using System.Drawing; | |
/// <summary> | |
/// Provides color conversion functionality. | |
/// </summary> | |
/// <remarks> | |
/// http://en.wikipedia.org/wiki/HSV_color_space | |
/// http://www.easyrgb.com/math.php?MATH=M19#text19 | |
/// </remarks> | |
internal static class ColorConverting | |
{ | |
public static RgbColor ColorToRgb( | |
Color color) | |
{ | |
return new(color.R, color.G, color.B, color.A); | |
} | |
public static Color RgbToColor( | |
RgbColor rgb) | |
{ | |
return Color.FromArgb(rgb.Alpha, rgb.Red, rgb.Green, rgb.Blue); | |
} | |
public static HsbColor RgbToHsb( | |
RgbColor rgb) | |
{ | |
// _NOTE #1: Even though we're dealing with a very small range of | |
// numbers, the accuracy of all calculations is fairly important. | |
// For this reason, I've opted to use double data types instead | |
// of float, which gives us a little bit extra precision (recall | |
// that precision is the number of significant digits with which | |
// the result is expressed). | |
var r = rgb.Red / 255d; | |
var g = rgb.Green / 255d; | |
var b = rgb.Blue / 255d; | |
var minValue = getMinimumValue(r, g, b); | |
var maxValue = getMaximumValue(r, g, b); | |
var delta = maxValue - minValue; | |
double hue = 0; | |
double saturation; | |
var brightness = maxValue * 100; | |
if (Math.Abs(maxValue - 0) < 0.00001 || Math.Abs(delta - 0) < 0.00001) | |
{ | |
hue = 0; | |
saturation = 0; | |
} | |
else | |
{ | |
// _NOTE #2: FXCop insists that we avoid testing for floating | |
// point equality (CA1902). Instead, we'll perform a series of | |
// tests with the help of 0.00001 that will provide | |
// a more accurate equality evaluation. | |
if (Math.Abs(minValue - 0) < 0.00001) | |
{ | |
saturation = 100; | |
} | |
else | |
{ | |
saturation = delta / maxValue * 100; | |
} | |
if (Math.Abs(r - maxValue) < 0.00001) | |
{ | |
hue = (g - b) / delta; | |
} | |
else if (Math.Abs(g - maxValue) < 0.00001) | |
{ | |
hue = 2 + (b - r) / delta; | |
} | |
else if (Math.Abs(b - maxValue) < 0.00001) | |
{ | |
hue = 4 + (r - g) / delta; | |
} | |
} | |
hue *= 60; | |
if (hue < 0) | |
{ | |
hue += 360; | |
} | |
return new( | |
hue, | |
saturation, | |
brightness, | |
rgb.Alpha); | |
} | |
public static HslColor RgbToHsl( | |
RgbColor rgb) | |
{ | |
var varR = rgb.Red / 255.0; //Where RGB values = 0 ÷ 255 | |
var varG = rgb.Green / 255.0; | |
var varB = rgb.Blue / 255.0; | |
var varMin = getMinimumValue(varR, varG, varB); //Min. value of RGB | |
var varMax = getMaximumValue(varR, varG, varB); //Max. value of RGB | |
var delMax = varMax - varMin; //Delta RGB value | |
double h; | |
double s; | |
var l = (varMax + varMin) / 2; | |
if (Math.Abs(delMax - 0) < 0.00001) //This is a gray, no chroma... | |
{ | |
h = 0; //HSL results = 0 ÷ 1 | |
s = 0; | |
// UK: | |
// s = 1.0; | |
} | |
else //Chromatic data... | |
{ | |
if (l < 0.5) | |
{ | |
s = delMax / (varMax + varMin); | |
} | |
else | |
{ | |
s = delMax / (2.0 - varMax - varMin); | |
} | |
var delR = ((varMax - varR) / 6.0 + delMax / 2.0) / delMax; | |
var delG = ((varMax - varG) / 6.0 + delMax / 2.0) / delMax; | |
var delB = ((varMax - varB) / 6.0 + delMax / 2.0) / delMax; | |
if (Math.Abs(varR - varMax) < 0.00001) | |
{ | |
h = delB - delG; | |
} | |
else if (Math.Abs(varG - varMax) < 0.00001) | |
{ | |
h = 1.0 / 3.0 + delR - delB; | |
} | |
else if (Math.Abs(varB - varMax) < 0.00001) | |
{ | |
h = 2.0 / 3.0 + delG - delR; | |
} | |
else | |
{ | |
// Uwe Keim. | |
h = 0.0; | |
} | |
if (h < 0.0) | |
{ | |
h += 1.0; | |
} | |
if (h > 1.0) | |
{ | |
h -= 1.0; | |
} | |
} | |
// -- | |
return new( | |
h * 360.0, | |
s * 100.0, | |
l * 100.0, | |
rgb.Alpha); | |
} | |
public static RgbColor HsbToRgb( | |
HsbColor hsb) | |
{ | |
double red = 0, green = 0, blue = 0; | |
double h = hsb.Hue; | |
var s = (double)hsb.Saturation / 100; | |
var b = (double)hsb.Brightness / 100; | |
if (Math.Abs(s - 0) < 0.00001) | |
{ | |
red = b; | |
green = b; | |
blue = b; | |
} | |
else | |
{ | |
// the color wheel has six sectors. | |
var sectorPosition = h / 60; | |
var sectorNumber = (int)Math.Floor(sectorPosition); | |
var fractionalSector = sectorPosition - sectorNumber; | |
var p = b * (1 - s); | |
var q = b * (1 - s * fractionalSector); | |
var t = b * (1 - s * (1 - fractionalSector)); | |
// Assign the fractional colors to r, g, and b | |
// based on the sector the angle is in. | |
switch (sectorNumber) | |
{ | |
case 0: | |
red = b; | |
green = t; | |
blue = p; | |
break; | |
case 1: | |
red = q; | |
green = b; | |
blue = p; | |
break; | |
case 2: | |
red = p; | |
green = b; | |
blue = t; | |
break; | |
case 3: | |
red = p; | |
green = q; | |
blue = b; | |
break; | |
case 4: | |
red = t; | |
green = p; | |
blue = b; | |
break; | |
case 5: | |
red = b; | |
green = p; | |
blue = q; | |
break; | |
} | |
} | |
var nRed = Convert.ToInt32(red * 255); | |
var nGreen = Convert.ToInt32(green * 255); | |
var nBlue = Convert.ToInt32(blue * 255); | |
return new(nRed, nGreen, nBlue, hsb.Alpha); | |
} | |
public static RgbColor HslToRgb( | |
HslColor hsl) | |
{ | |
double red, green, blue; | |
var h = hsl.PreciseHue / 360.0; | |
var s = hsl.PreciseSaturation / 100.0; | |
var l = hsl.PreciseLight / 100.0; | |
if (Math.Abs(s - 0.0) < 0.00001) | |
{ | |
red = l; | |
green = l; | |
blue = l; | |
} | |
else | |
{ | |
double var2; | |
if (l < 0.5) | |
{ | |
var2 = l * (1.0 + s); | |
} | |
else | |
{ | |
var2 = l + s - s * l; | |
} | |
var var1 = 2.0 * l - var2; | |
red = hue2Rgb(var1, var2, h + 1.0 / 3.0); | |
green = hue2Rgb(var1, var2, h); | |
blue = hue2Rgb(var1, var2, h - 1.0 / 3.0); | |
} | |
// -- | |
var nRed = Convert.ToInt32(red * 255.0); | |
var nGreen = Convert.ToInt32(green * 255.0); | |
var nBlue = Convert.ToInt32(blue * 255.0); | |
return new(nRed, nGreen, nBlue, hsl.Alpha); | |
} | |
private static double hue2Rgb( | |
double v1, | |
double v2, | |
double vH) | |
{ | |
if (vH < 0.0) | |
{ | |
vH += 1.0; | |
} | |
if (vH > 1.0) | |
{ | |
vH -= 1.0; | |
} | |
if (6.0 * vH < 1.0) | |
{ | |
return v1 + (v2 - v1) * 6.0 * vH; | |
} | |
if (2.0 * vH < 1.0) | |
{ | |
return v2; | |
} | |
if (3.0 * vH < 2.0) | |
{ | |
return v1 + (v2 - v1) * (2.0 / 3.0 - vH) * 6.0; | |
} | |
return v1; | |
} | |
/// <summary> | |
/// Determines the maximum value of all of the numbers provided in the | |
/// variable argument list. | |
/// </summary> | |
private static double getMaximumValue( | |
params double[] values) | |
{ | |
var maxValue = values[0]; | |
if (values.Length >= 2) | |
{ | |
for (var i = 1; i < values.Length; i++) | |
{ | |
var num = values[i]; | |
maxValue = Math.Max(maxValue, num); | |
} | |
} | |
return maxValue; | |
} | |
/// <summary> | |
/// Determines the minimum value of all of the numbers provided in the | |
/// variable argument list. | |
/// </summary> | |
private static double getMinimumValue( | |
params double[] values) | |
{ | |
var minValue = values[0]; | |
if (values.Length >= 2) | |
{ | |
for (var i = 1; i < values.Length; i++) | |
{ | |
var num = values[i]; | |
minValue = Math.Min(minValue, num); | |
} | |
} | |
return minValue; | |
} | |
} |
<Project> | |
<PropertyGroup> | |
<LangVersion>preview</LangVersion> | |
</PropertyGroup> | |
</Project> |
namespace ZetaColorEditor.Colors; | |
using System.Drawing; | |
/// <summary> | |
/// Represents a HSV (=HSB) color space. | |
/// http://en.wikipedia.org/wiki/HSV_color_space | |
/// </summary> | |
[PublicAPI] | |
public sealed class HsbColor(double hue, | |
double saturation, | |
double brightness, | |
int alpha) | |
{ | |
/// <summary> | |
/// Gets or sets the hue. Values from 0 to 360. | |
/// </summary> | |
public double PreciseHue { get; } = hue; | |
/// <summary> | |
/// Gets or sets the saturation. Values from 0 to 100. | |
/// </summary> | |
public double PreciseSaturation { get; } = saturation; | |
/// <summary> | |
/// Gets or sets the brightness. Values from 0 to 100. | |
/// </summary> | |
public double PreciseBrightness { get; } = brightness; | |
public int Hue => Convert.ToInt32(PreciseHue); | |
public int Saturation => Convert.ToInt32(PreciseSaturation); | |
public int Brightness => Convert.ToInt32(PreciseBrightness); | |
/// <summary> | |
/// Gets or sets the alpha. Values from 0 to 255. | |
/// </summary> | |
public int Alpha { get; } = alpha; | |
public static HsbColor FromColor( | |
Color color) | |
{ | |
return ColorConverting.ColorToRgb(color).ToHsbColor(); | |
} | |
public static HsbColor FromRgbColor( | |
RgbColor color) | |
{ | |
return color.ToHsbColor(); | |
} | |
public static HsbColor FromHsbColor( | |
HsbColor color) | |
{ | |
return new(color.PreciseHue, color.PreciseSaturation, color.PreciseBrightness, color.Alpha); | |
} | |
public static HsbColor FromHslColor( | |
HslColor color) | |
{ | |
return FromRgbColor(color.ToRgbColor()); | |
} | |
public override string? ToString() | |
{ | |
return $@"Hue: {Hue}; saturation: {Saturation}; brightness: {Brightness}."; | |
} | |
public Color ToColor() | |
{ | |
return ColorConverting.HsbToRgb(this).ToColor(); | |
} | |
public RgbColor ToRgbColor() | |
{ | |
return ColorConverting.HsbToRgb(this); | |
} | |
public HsbColor ToHsbColor() | |
{ | |
return new(PreciseHue, PreciseSaturation, PreciseBrightness, Alpha); | |
} | |
public HslColor ToHslColor() | |
{ | |
return ColorConverting.RgbToHsl(ToRgbColor()); | |
} | |
public override bool Equals(object? obj) | |
{ | |
var equal = false; | |
if (obj is HsbColor color) | |
{ | |
if (Math.Abs(PreciseHue - color.PreciseHue) < 0.00001 && | |
Math.Abs(PreciseSaturation - color.PreciseSaturation) < 0.00001 && | |
Math.Abs(PreciseBrightness - color.PreciseBrightness) < 0.00001 && | |
Alpha == color.Alpha) | |
{ | |
equal = true; | |
} | |
} | |
return equal; | |
} | |
public override int GetHashCode() | |
{ | |
return $@"H:{Hue}-S:{Saturation}-B:{Brightness}-A:{Alpha}".GetHashCode(); | |
} | |
} |
namespace ZetaColorEditor.Colors; | |
using System.Drawing; | |
/// <summary> | |
/// Represents a HSL color space. | |
/// http://en.wikipedia.org/wiki/HSV_color_space | |
/// </summary> | |
[PublicAPI] | |
public sealed class HslColor( | |
double hue, | |
double saturation, | |
double light, | |
int alpha) | |
{ | |
/// <summary> | |
/// Gets the hue. Values from 0 to 360. | |
/// </summary> | |
public int Hue => Convert.ToInt32(PreciseHue); | |
/// <summary> | |
/// Gets the precise hue. Values from 0 to 360. | |
/// </summary> | |
public double PreciseHue { get; } = hue; | |
/// <summary> | |
/// Gets the saturation. Values from 0 to 100. | |
/// </summary> | |
public int Saturation => Convert.ToInt32(PreciseSaturation); | |
/// <summary> | |
/// Gets the precise saturation. Values from 0 to 100. | |
/// </summary> | |
public double PreciseSaturation { get; } = saturation; | |
/// <summary> | |
/// Gets the light. Values from 0 to 100. | |
/// </summary> | |
public int Light => Convert.ToInt32(PreciseLight); | |
/// <summary> | |
/// Gets the precise light. Values from 0 to 100. | |
/// </summary> | |
public double PreciseLight { get; } = light; | |
/// <summary> | |
/// Gets the alpha. Values from 0 to 255 | |
/// </summary> | |
public int Alpha { get; } = alpha; | |
public static HslColor FromColor(Color color) | |
{ | |
return ColorConverting.RgbToHsl(ColorConverting.ColorToRgb(color)); | |
} | |
public static HslColor FromRgbColor(RgbColor color) | |
{ | |
return color.ToHslColor(); | |
} | |
public static HslColor FromHslColor(HslColor color) | |
{ | |
return new( | |
color.PreciseHue, | |
color.PreciseSaturation, | |
color.PreciseLight, | |
color.Alpha); | |
} | |
public static HslColor FromHsbColor(HsbColor color) | |
{ | |
return FromRgbColor(color.ToRgbColor()); | |
} | |
public override string? ToString() | |
{ | |
return Alpha < 255 | |
? $@"hsla({Hue}, {Saturation}%, {Light}%, {Alpha / 255f})" | |
: $@"hsl({Hue}, {Saturation}%, {Light}%)"; | |
} | |
public Color ToColor() | |
{ | |
return ColorConverting.HslToRgb(this).ToColor(); | |
} | |
public RgbColor ToRgbColor() | |
{ | |
return ColorConverting.HslToRgb(this); | |
} | |
public HslColor ToHslColor() | |
{ | |
return this; | |
} | |
public HsbColor ToHsbColor() | |
{ | |
return ColorConverting.RgbToHsb(ToRgbColor()); | |
} | |
public override bool Equals(object? obj) | |
{ | |
var equal = false; | |
if (obj is HslColor color) | |
{ | |
if (Math.Abs(Hue - color.PreciseHue) < 0.00001 && | |
Math.Abs(Saturation - color.PreciseSaturation) < 0.00001 && | |
Math.Abs(Light - color.PreciseLight) < 0.00001 && | |
Alpha == color.Alpha) | |
{ | |
equal = true; | |
} | |
} | |
return equal; | |
} | |
public override int GetHashCode() | |
{ | |
return $@"H:{PreciseHue}-S:{PreciseSaturation}-L:{PreciseLight}-A:{Alpha}".GetHashCode(); | |
} | |
} |
namespace ZetaColorEditor.Colors; | |
using System.Drawing; | |
/// <summary> | |
/// Represents a RGB color space. | |
/// http://en.wikipedia.org/wiki/HSV_color_space | |
/// </summary> | |
[PublicAPI] | |
public sealed class RgbColor( | |
int red, | |
int green, | |
int blue, | |
int alpha) | |
{ | |
/// <summary> | |
/// Gets or sets the red component. Values from 0 to 255. | |
/// </summary> | |
public int Red { get; } = red; | |
/// <summary> | |
/// Gets or sets the green component. Values from 0 to 255. | |
/// </summary> | |
public int Green { get; } = green; | |
/// <summary> | |
/// Gets or sets the blue component. Values from 0 to 255. | |
/// </summary> | |
public int Blue { get; } = blue; | |
/// <summary> | |
/// Gets or sets the alpha component. Values from 0 to 255. | |
/// </summary> | |
public int Alpha { get; } = alpha; | |
public static RgbColor FromColor( | |
Color color) | |
{ | |
return ColorConverting.ColorToRgb(color); | |
} | |
public static RgbColor FromRgbColor( | |
RgbColor color) | |
{ | |
return new(color.Red, color.Green, color.Blue, color.Alpha); | |
} | |
public static RgbColor FromHsbColor( | |
HsbColor color) | |
{ | |
return color.ToRgbColor(); | |
} | |
public static RgbColor FromHslColor( | |
HslColor color) | |
{ | |
return color.ToRgbColor(); | |
} | |
public override string? ToString() | |
{ | |
return Alpha < 255 ? $@"rgba({Red}, {Green}, {Blue}, {Alpha / 255d})" : $@"rgb({Red}, {Green}, {Blue})"; | |
} | |
public Color ToColor() | |
{ | |
return ColorConverting.RgbToColor(this); | |
} | |
public RgbColor ToRgbColor() | |
{ | |
return this; | |
} | |
public HsbColor ToHsbColor() | |
{ | |
return ColorConverting.RgbToHsb(this); | |
} | |
public HslColor ToHslColor() | |
{ | |
return ColorConverting.RgbToHsl(this); | |
} | |
public override bool Equals(object? obj) | |
{ | |
var equal = false; | |
if (obj is RgbColor color) | |
{ | |
if (Red == color.Red && Blue == color.Blue && Green == color.Green && Alpha == color.Alpha) | |
{ | |
equal = true; | |
} | |
} | |
return equal; | |
} | |
public override int GetHashCode() | |
{ | |
return $@"R:{Red}-G:{Green}-B:{Blue}-A:{Alpha}".GetHashCode(); | |
} | |
} |
This code is great. Is there a license?
Not that i am aware of. Use it for whatever you want
This is very handy piece of code.
Still, I think it is worth mentioning that you really shouldn't use Double.Epsilon
for deciding practical equality of two Doubles. See here for details and the official recommendation to not touch it for this purpose. TLDR: Double.Epsilon
is the smallest positive non-zero value, and is therefore of little help when you're comparing values of practically reasonable magnitudes.
Thanks, @hacklex, so what do you recommend how I should change my comparison code?
@UweKeim In short, you use 0.00001 (1e-5) rather than unreasonably small Double.Epsilon (5E-324).
As I tried using this code, I quickly noticed there are several logical flaws, one seemingly due to a overlooked copy/paste, another due to slightly inaccurate handling of angular quantity wrapping. So I took the liberty of rewriting some of the code. Now it uses certain modern C# features, takes way less vertical screen space, and is at least a bit better when wrapping around the hue value of 360 in the HSB color space.
You may take a look at this extracted gist -- or just straight copy it if you ever need it. What most probably were wrong in the initial code, is marked by comments, see lines 187 and 342. Refer to the documentation if unsure.
Also, I removed the hyperlinks to the site that appears to be already dead, and updated the Wiki links.
Also, take a look at the IsVisuallyEqualTo(IColor)
method, which might be more useful than the regular Equals()
.
That's incredible awesome, @hacklex. Thank you very much for your detailed reply and your code.
Glad you liked it. Also, be advised that your code ignores the alpha channel values in Equals
. This might eventually spell trouble, so I advise you to correct that even if you opt to stick with your old version.
I'm now thinking of making a proper github repo out of this. Such color space manipulations seem to be standard enough, and having them all in one place, neatly packaged and well documented, sounds like the right thing to do.
You won't mind my reusing parts of your code for that, right? The github repo will be public and MIT licensed of course.
Thanks again, @hacklex, I've updated my source code locally and pasted the new changes here as updated source files.
Hopefully I've fixed most of the issues.
Yes, some are fixed now. From quick review, this still needs fixing.
See my workaround, WrapAround
utility function being defined here. If you're not convinced, I can dig up my old code and find the exact example that breaks your version without this check.
You don't mind me creating a public repo for this particular task, do you? I am going to include several parts of your code, refactored to better suit the current C# standards. Naturally, I will mark your code as yours in the comments.
If you don't like the idea, just say so and I will not publish it, or make it private.
Thanks, @hacklex. You creating a public repository sounds like an awesome idea. I would be glad and honored 😊.
It has been long since I last touched this, but I finally had some time to make the code public. Will probably turn it into a nuget package later.
I like it very much, thank you, @hacklex.
@eelislynne I've just modified my sources and did some unit tests. They were successfully now.
I've updated the above code files (all of them).
These are much never versions of my the previous classes, with additional function. E.g. there is now not only
HslColor.Hue
as integer but alsoHslColor.PreciseHue
as double.I've just updated once more.
If you get compilation errors, please create a file "Directory.Build.props" in the same directory as your "*.csproj" file, fill it with the XML of the above same-named gist file, and include it into your project. This tells the build process to use the latest language features.