Skip to content

Commit

Permalink
Merge pull request #7 from NoahStolk/add-rgb-struct
Browse files Browse the repository at this point in the history
Add Rgb struct
  • Loading branch information
NoahStolk authored Jun 14, 2024
2 parents 6a919b5 + d4a96ba commit 72aa856
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 52 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This library uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.9.0

### Added

- Added `Rgb` struct.
- Added constructor to `Rgba` struct accepting `Rgb` and `byte` parameters.

## 0.8.0

### Added
Expand Down
24 changes: 24 additions & 0 deletions src/Detach.Tests/Tests/Numerics/RgbTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Detach.Numerics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Numerics;

namespace Detach.Tests.Tests.Numerics;

[TestClass]
public class RgbTests
{
[DataTestMethod]
[DataRow(0, 0, 0)]
[DataRow(1, 2, 3)]
[DataRow(5, 6, 7)]
[DataRow(255, 6, 255)]
[DataRow(255, 255, 255)]
public void RgbConversions(int r, int g, int b)
{
Rgb expectedRgb = new((byte)r, (byte)g, (byte)b);

Assert.AreEqual(expectedRgb, Rgb.FromVector4(new Vector4(r / 255f, g / 255f, b / 255f, 1)));
Assert.AreEqual(expectedRgb, Rgb.FromVector3(new Vector3(r / 255f, g / 255f, b / 255f)));
Assert.AreEqual(expectedRgb, Rgb.FromRgbInt(expectedRgb.ToRgbInt()));
}
}
2 changes: 1 addition & 1 deletion src/Detach/Detach.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<Copyright>Copyright © Noah Stolk</Copyright>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/NoahStolk/Detach</RepositoryUrl>
<Version>0.8.0</Version>
<Version>0.9.0</Version>
</PropertyGroup>

<ItemGroup Label="Static code analysis">
Expand Down
32 changes: 32 additions & 0 deletions src/Detach/Numerics/Colors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Detach.Numerics;

internal static class Colors
{
public static int GetPerceivedBrightness(byte r, byte g, byte b)
{
return (int)Math.Sqrt(r * r * 0.299 + g * g * 0.587 + b * b * 0.114);
}

public static int GetHue(byte r, byte g, byte b)
{
byte min = Math.Min(Math.Min(r, g), b);
byte max = Math.Max(Math.Max(r, g), b);

if (min == max)
return 0;

float hue;
if (max == r)
hue = (g - b) / (float)(max - min);
else if (max == g)
hue = 2f + (b - r) / (float)(max - min);
else
hue = 4f + (r - g) / (float)(max - min);

hue *= 60;
if (hue < 0)
hue += 360;

return (int)Math.Round(hue);
}
}
164 changes: 164 additions & 0 deletions src/Detach/Numerics/Rgb.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using Detach.Utils;
using System.Numerics;
using System.Runtime.CompilerServices;

namespace Detach.Numerics;

public readonly record struct Rgb(byte R, byte G, byte B)
{
public Rgb(Rgba rgba)
: this(rgba.R, rgba.G, rgba.B)
{
}

public static Rgb White => new(byte.MaxValue, byte.MaxValue, byte.MaxValue);
public static Rgb Black => new(byte.MinValue, byte.MinValue, byte.MinValue);

public static Rgb Red => new(byte.MaxValue, byte.MinValue, byte.MinValue);
public static Rgb Green => new(byte.MinValue, byte.MaxValue, byte.MinValue);
public static Rgb Blue => new(byte.MinValue, byte.MinValue, byte.MaxValue);

public static Rgb Yellow => new(byte.MaxValue, byte.MaxValue, byte.MinValue);
public static Rgb Aqua => new(byte.MinValue, byte.MaxValue, byte.MaxValue);
public static Rgb Purple => new(byte.MaxValue, byte.MinValue, byte.MaxValue);

public static Rgb Orange => new(byte.MaxValue, byte.MaxValue / 2, byte.MinValue);

public static implicit operator Vector3(Rgb rgb)
{
return new Vector3(rgb.R / (float)byte.MaxValue, rgb.G / (float)byte.MaxValue, rgb.B / (float)byte.MaxValue);
}

public static implicit operator Vector4(Rgb rgb)
{
return new Vector4(rgb.R / (float)byte.MaxValue, rgb.G / (float)byte.MaxValue, rgb.B / (float)byte.MaxValue, 1);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb operator +(Rgb left, Rgb right)
{
return new Rgb((byte)(left.R + right.R), (byte)(left.G + right.G), (byte)(left.B + right.B));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb operator -(Rgb left, Rgb right)
{
return new Rgb((byte)(left.R - right.R), (byte)(left.G - right.G), (byte)(left.B - right.B));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb operator *(Rgb left, Rgb right)
{
return new Rgb((byte)(left.R * right.R), (byte)(left.G * right.G), (byte)(left.B * right.B));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgb operator /(Rgb left, Rgb right)
{
return new Rgb((byte)(left.R / right.R), (byte)(left.G / right.G), (byte)(left.B / right.B));
}

public int GetPerceivedBrightness()
{
return Colors.GetPerceivedBrightness(R, G, B);
}

public Rgb Intensify(byte component)
{
byte r = (byte)Math.Min(byte.MaxValue, R + component);
byte g = (byte)Math.Min(byte.MaxValue, G + component);
byte b = (byte)Math.Min(byte.MaxValue, B + component);
return new Rgb(r, g, b);
}

public Rgb Desaturate(float f)
{
float r = R / (float)byte.MaxValue;
float g = G / (float)byte.MaxValue;
float b = B / (float)byte.MaxValue;

float l = 0.3f * r + 0.6f * g + 0.1f * b;
float newR = r + f * (l - r);
float newG = g + f * (l - g);
float newB = b + f * (l - b);

return new Rgb((byte)(newR * byte.MaxValue), (byte)(newG * byte.MaxValue), (byte)(newB * byte.MaxValue));
}

public Rgb Darken(float amount)
{
return new Rgb((byte)(R * (1 - amount)), (byte)(G * (1 - amount)), (byte)(B * (1 - amount)));
}

public int GetHue()
{
return Colors.GetHue(R, G, B);
}

public static Rgb Lerp(Rgb value1, Rgb value2, float amount)
{
float r = MathUtils.Lerp(value1.R, value2.R, amount);
float g = MathUtils.Lerp(value1.G, value2.G, amount);
float b = MathUtils.Lerp(value1.B, value2.B, amount);
return new Rgb((byte)r, (byte)g, (byte)b);
}

public static Rgb Invert(Rgb rgb)
{
return new Rgb((byte)(byte.MaxValue - rgb.R), (byte)(byte.MaxValue - rgb.G), (byte)(byte.MaxValue - rgb.B));
}

public static Rgb Gray(float value)
{
if (value is < 0 or > 1)
throw new ArgumentOutOfRangeException(nameof(value));

byte component = (byte)(value * byte.MaxValue);
return new Rgb(component, component, component);
}

public static Rgb FromHsv(float hue, float saturation, float value)
{
saturation = Math.Clamp(saturation, 0, 1);
value = Math.Clamp(value, 0, 1);

int hi = (int)MathF.Floor(hue / 60) % 6;
float f = hue / 60 - MathF.Floor(hue / 60);

value *= byte.MaxValue;
byte v = (byte)value;
byte p = (byte)(value * (1 - saturation));
byte q = (byte)(value * (1 - f * saturation));
byte t = (byte)(value * (1 - (1 - f) * saturation));

return hi switch
{
0 => new Rgb(v, t, p),
1 => new Rgb(q, v, p),
2 => new Rgb(p, v, t),
3 => new Rgb(p, q, v),
4 => new Rgb(t, p, v),
_ => new Rgb(v, p, q),
};
}

public static Rgb FromVector3(Vector3 vector)
{
return new Rgb((byte)(vector.X * byte.MaxValue), (byte)(vector.Y * byte.MaxValue), (byte)(vector.Z * byte.MaxValue));
}

public static Rgb FromVector4(Vector4 vector)
{
return new Rgb((byte)(vector.X * byte.MaxValue), (byte)(vector.Y * byte.MaxValue), (byte)(vector.Z * byte.MaxValue));
}

public int ToRgbInt()
{
return (R << 24) + (G << 16) + (B << 8);
}

public static Rgb FromRgbInt(int rgb)
{
return new Rgb((byte)(rgb >> 24), (byte)(rgb >> 16), (byte)(rgb >> 8));
}
}
60 changes: 9 additions & 51 deletions src/Detach/Numerics/Rgba.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ namespace Detach.Numerics;

public readonly record struct Rgba(byte R, byte G, byte B, byte A = byte.MaxValue)
{
public Rgba(Rgb rgb, byte a = byte.MaxValue)
: this(rgb.R, rgb.G, rgb.B, a)
{
}

public static Rgba Invisible => default;

public static Rgba White => new(byte.MaxValue, byte.MaxValue, byte.MaxValue);
Expand Down Expand Up @@ -60,7 +65,7 @@ public static implicit operator Vector4(Rgba rgba)

public int GetPerceivedBrightness()
{
return (int)Math.Sqrt(R * R * 0.299 + G * G * 0.587 + B * B * 0.114);
return Colors.GetPerceivedBrightness(R, G, B);
}

public Rgba Intensify(byte component)
Expand All @@ -74,16 +79,7 @@ public Rgba Intensify(byte component)

public Rgba Desaturate(float f)
{
float r = R / (float)byte.MaxValue;
float g = G / (float)byte.MaxValue;
float b = B / (float)byte.MaxValue;

float l = 0.3f * r + 0.6f * g + 0.1f * b;
float newR = r + f * (l - r);
float newG = g + f * (l - g);
float newB = b + f * (l - b);

return new Rgba((byte)(newR * byte.MaxValue), (byte)(newG * byte.MaxValue), (byte)(newB * byte.MaxValue), A);
return new Rgba(new Rgb(R, G, B).Desaturate(f), A);
}

public Rgba Darken(float amount)
Expand All @@ -93,25 +89,7 @@ public Rgba Darken(float amount)

public int GetHue()
{
byte min = Math.Min(Math.Min(R, G), B);
byte max = Math.Max(Math.Max(R, G), B);

if (min == max)
return 0;

float hue;
if (max == R)
hue = (G - B) / (float)(max - min);
else if (max == G)
hue = 2f + (B - R) / (float)(max - min);
else
hue = 4f + (R - G) / (float)(max - min);

hue *= 60;
if (hue < 0)
hue += 360;

return (int)Math.Round(hue);
return Colors.GetHue(R, G, B);
}

public static Rgba Lerp(Rgba value1, Rgba value2, float amount)
Expand Down Expand Up @@ -139,27 +117,7 @@ public static Rgba Gray(float value)

public static Rgba FromHsv(float hue, float saturation, float value)
{
saturation = Math.Clamp(saturation, 0, 1);
value = Math.Clamp(value, 0, 1);

int hi = (int)MathF.Floor(hue / 60) % 6;
float f = hue / 60 - MathF.Floor(hue / 60);

value *= byte.MaxValue;
byte v = (byte)value;
byte p = (byte)(value * (1 - saturation));
byte q = (byte)(value * (1 - f * saturation));
byte t = (byte)(value * (1 - (1 - f) * saturation));

return hi switch
{
0 => new Rgba(v, t, p),
1 => new Rgba(q, v, p),
2 => new Rgba(p, v, t),
3 => new Rgba(p, q, v),
4 => new Rgba(t, p, v),
_ => new Rgba(v, p, q),
};
return new Rgba(Rgb.FromHsv(hue, saturation, value));
}

public static Rgba FromVector3(Vector3 vector)
Expand Down

0 comments on commit 72aa856

Please sign in to comment.