-
-
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(); | |
} | |
} |
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.
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.