From f8fbae613082a1e0c9a85a37cd8931548a385d6d Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 26 Jul 2025 11:55:40 +0300 Subject: [PATCH] feat: added HSVA --- Engine.Core/Primitives/ColorHSV.cs | 36 +--- Engine.Core/Primitives/ColorHSVA.cs | 189 ++++++++++++++++++ Engine.Core/Primitives/ColorRGB.cs | 26 +-- Engine.Core/Primitives/ColorRGBA.cs | 26 ++- .../EngineExtensions/TweenColorExtensions.cs | 7 +- 5 files changed, 224 insertions(+), 60 deletions(-) create mode 100644 Engine.Core/Primitives/ColorHSVA.cs diff --git a/Engine.Core/Primitives/ColorHSV.cs b/Engine.Core/Primitives/ColorHSV.cs index cec3064..85e88b2 100644 --- a/Engine.Core/Primitives/ColorHSV.cs +++ b/Engine.Core/Primitives/ColorHSV.cs @@ -37,39 +37,9 @@ public readonly struct ColorHSV(float hue, float saturation, float value) public static bool operator ==(ColorHSV left, ColorHSV right) => left.Hue == right.Hue && left.Saturation == right.Saturation && left.Value == right.Value; public static bool operator !=(ColorHSV left, ColorHSV right) => left.Hue != right.Hue || left.Saturation != right.Saturation || left.Value != right.Value; - public static implicit operator ColorHSV(ColorRGBA rgba) => (ColorRGB)rgba; - public static implicit operator ColorHSV(ColorRGB rgb) - { - float hue; - float saturation; - float value; - - float rd = rgb.R / 255f; - float gd = rgb.G / 255f; - float bd = rgb.B / 255f; - - float max = Math.Max(rd, Math.Max(gd, bd)); - float min = Math.Min(rd, Math.Min(gd, bd)); - float delta = max - min; - - if (delta.ApproximatelyEquals(0)) - hue = 0f; - else if (max.ApproximatelyEquals(rd)) - hue = 60f * ((gd - bd) / delta % 6f); - else if (max.ApproximatelyEquals(gd)) - hue = 60f * (((bd - rd) / delta) + 2f); - else - hue = 60f * (((rd - gd) / delta) + 4f); - - if (hue < 0f) - hue += 360f; - - hue /= 360f; - saturation = max.ApproximatelyEquals(0f) ? 0f : delta / max; - value = max; - - return new(hue, saturation, value); - } + public static implicit operator ColorHSV(ColorHSVA hsva) => new(hsva.Hue, hsva.Saturation, hsva.Value); + public static implicit operator ColorHSV(ColorRGBA rgba) => (ColorHSVA)rgba; + public static implicit operator ColorHSV(ColorRGB rgb) => (ColorHSVA)rgb; /// /// Inverts the given . diff --git a/Engine.Core/Primitives/ColorHSVA.cs b/Engine.Core/Primitives/ColorHSVA.cs new file mode 100644 index 0000000..ee49c6a --- /dev/null +++ b/Engine.Core/Primitives/ColorHSVA.cs @@ -0,0 +1,189 @@ +namespace Syntriax.Engine.Core; + +/// +/// Represents an HSV color. +/// +/// Hue of the . +/// Saturation of the . +/// Value of the . +/// Alpha of the . +/// +/// Initializes a new instance of the struct with the specified values. +/// +[System.Diagnostics.DebuggerDisplay("{ToString(),nq}")] +public readonly struct ColorHSVA(float hue, float saturation, float value, float alpha = 1) +{ + /// + /// The Hue value of the . + /// + public readonly float Hue = hue.Clamp(0f, 1f); + + /// + /// The Saturation value of the . + /// + public readonly float Saturation = saturation.Clamp(0f, 1f); + + /// + /// The Value value of the . + /// + public readonly float Value = value.Clamp(0f, 1f); + + /// + /// The Alpha value of the . + /// + public readonly float Alpha = alpha; + + public static ColorHSVA operator -(ColorHSVA color) => new(color.Hue.OneMinus(), color.Saturation.OneMinus(), color.Value.OneMinus(), color.Alpha); + public static ColorHSVA operator +(ColorHSVA left, ColorHSVA right) => new(left.Hue + right.Hue, left.Saturation + right.Saturation, left.Value + right.Value, left.Alpha + right.Alpha); + public static ColorHSVA operator -(ColorHSVA left, ColorHSVA right) => new(left.Hue - right.Hue, left.Saturation - right.Saturation, left.Value - right.Value, left.Alpha - right.Alpha); + public static ColorHSVA operator *(ColorHSVA left, ColorHSVA right) => new(left.Hue * right.Hue, left.Saturation * right.Saturation, left.Value * right.Value, left.Alpha * right.Alpha); + public static ColorHSVA operator *(ColorHSVA color, float value) => new(color.Hue * value, color.Saturation * value, color.Value * value, color.Alpha * value); + public static ColorHSVA operator *(float value, ColorHSVA color) => new(color.Hue * value, color.Saturation * value, color.Value * value, color.Alpha * value); + public static ColorHSVA operator /(ColorHSVA color, float value) => new(color.Hue / value, color.Saturation / value, color.Value / value, color.Alpha / value); + public static bool operator ==(ColorHSVA left, ColorHSVA right) => left.Hue == right.Hue && left.Saturation == right.Saturation && left.Value == right.Value; + public static bool operator !=(ColorHSVA left, ColorHSVA right) => left.Hue != right.Hue || left.Saturation != right.Saturation || left.Value != right.Value; + + public static implicit operator ColorHSVA(ColorHSV hsv) => new(hsv.Hue, hsv.Saturation, hsv.Value, 1f); + public static implicit operator ColorHSVA(ColorRGB rgb) => (ColorRGBA)rgb; + public static implicit operator ColorHSVA(ColorRGBA rgba) + { + float hue; + float saturation; + float value; + + float rd = rgba.R / 255f; + float gd = rgba.G / 255f; + float bd = rgba.B / 255f; + + float max = Math.Max(rd, Math.Max(gd, bd)); + float min = Math.Min(rd, Math.Min(gd, bd)); + float delta = max - min; + + if (delta.ApproximatelyEquals(0)) + hue = 0f; + else if (max.ApproximatelyEquals(rd)) + hue = 60f * ((gd - bd) / delta % 6f); + else if (max.ApproximatelyEquals(gd)) + hue = 60f * (((bd - rd) / delta) + 2f); + else + hue = 60f * (((rd - gd) / delta) + 4f); + + if (hue < 0f) + hue += 360f; + + hue /= 360f; + saturation = max.ApproximatelyEquals(0f) ? 0f : delta / max; + value = max; + + return new(hue, saturation, value, rgba.A / 255f); + } + + /// + /// Inverts the given . + /// + /// The . + /// The inverted . + public static ColorHSVA Invert(ColorHSVA color) => -color; + + /// + /// Adds two s. + /// + /// The first . + /// The second . + /// The sum of the two s. + public static ColorHSVA Add(ColorHSVA left, ColorHSVA right) => left + right; + + /// + /// Subtracts one from another. + /// + /// The to subtract from. + /// The to subtract. + /// The result of subtracting the second from the first. + public static ColorHSVA Subtract(ColorHSVA left, ColorHSVA right) => left - right; + + /// + /// Multiplies a by a scalar value. + /// + /// The . + /// The scalar value. + /// The result of multiplying the by the scalar value. + public static ColorHSVA Multiply(ColorHSVA color, float value) => color * value; + + /// + /// Divides a by a scalar value. + /// + /// The . + /// The scalar value. + /// The result of dividing the by the scalar value. + public static ColorHSVA Divide(ColorHSVA color, float value) => color / value; + + /// + /// Performs linear interpolation between two s. + /// + /// The starting (t = 0). + /// The ending (t = 1). + /// The interpolation parameter. + /// The interpolated . + public static ColorHSVA Lerp(ColorHSVA from, ColorHSVA to, float t) + { + float hueDiff = to.Hue - from.Hue; + float saturationDiff = to.Saturation - from.Saturation; + float valueDiff = to.Value - from.Value; + float alphaDiff = to.Alpha - from.Alpha; + + return from + new ColorHSVA(hueDiff * t, saturationDiff * t, valueDiff * t, alphaDiff * t); + } + + /// + /// Checks if two s are approximately equal within a specified epsilon range. + /// + /// The first . + /// The second . + /// The epsilon range. + /// if the s are approximately equal; otherwise, . + public static bool ApproximatelyEquals(ColorHSVA left, ColorHSVA right, float epsilon = float.Epsilon) + => left.Hue.ApproximatelyEquals(right.Hue, epsilon) && left.Saturation.ApproximatelyEquals(right.Saturation, epsilon) && left.Value.ApproximatelyEquals(right.Value, epsilon); + + /// + /// Determines whether the specified object is equal to the current . + /// + /// The object to compare with the current . + /// if the specified object is equal to the current ; otherwise, . + public override bool Equals(object? obj) => obj is ColorHSVA colorHSVA && this == colorHSVA; + + /// + /// Generates a hash code for the . + /// + /// A hash code for the . + public override int GetHashCode() => System.HashCode.Combine(Hue, Saturation, Value); + + /// + /// Converts the to its string representation. + /// + /// A string representation of the . + public override string ToString() => $"{nameof(ColorHSVA)}({Hue}, {Saturation}, {Value})"; +} + +/// +/// Provides extension methods for type. +/// +public static class ColorHSVAExtensions +{ + /// + public static ColorHSVA Add(this ColorHSVA color, ColorHSVA value) => ColorHSVA.Add(color, value); + + /// + public static ColorHSVA Subtract(this ColorHSVA color, ColorHSVA value) => ColorHSVA.Subtract(color, value); + + /// + public static ColorHSVA Multiply(this ColorHSVA color, float value) => ColorHSVA.Multiply(color, value); + + /// + public static ColorHSVA Divide(this ColorHSVA color, float value) => ColorHSVA.Divide(color, value); + + /// + public static ColorHSVA Lerp(this ColorHSVA from, ColorHSVA to, float t) => ColorHSVA.Lerp(from, to, t); + + /// + public static bool ApproximatelyEquals(this ColorHSVA left, ColorHSVA right, float epsilon = float.Epsilon) => ColorHSVA.ApproximatelyEquals(left, right, epsilon); +} diff --git a/Engine.Core/Primitives/ColorRGB.cs b/Engine.Core/Primitives/ColorRGB.cs index ad52500..e5460f8 100644 --- a/Engine.Core/Primitives/ColorRGB.cs +++ b/Engine.Core/Primitives/ColorRGB.cs @@ -38,30 +38,8 @@ public readonly struct ColorRGB(byte r, byte g, byte b) public static bool operator !=(ColorRGB left, ColorRGB right) => left.R != right.R || left.G != right.G || left.B != right.B; public static implicit operator ColorRGB(ColorRGBA rgba) => new(rgba.R, rgba.G, rgba.B); - public static implicit operator ColorRGB(ColorHSV hsv) - { - float hue = hsv.Hue * 360f; - float chroma = hsv.Value * hsv.Saturation; - float x = chroma * (1f - Math.Abs(hue / 60f % 2f - 1f)); - float m = hsv.Value - chroma; - - float r1 = 0f; - float g1 = 0f; - float b1 = 0f; - - if (hue < 60) { r1 = chroma; g1 = x; b1 = 0; } - else if (hue < 120) { r1 = x; g1 = chroma; b1 = 0; } - else if (hue < 180) { r1 = 0; g1 = chroma; b1 = x; } - else if (hue < 240) { r1 = 0; g1 = x; b1 = chroma; } - else if (hue < 300) { r1 = x; g1 = 0; b1 = chroma; } - else if (hue <= 360) { r1 = chroma; g1 = 0; b1 = x; } - - byte r = (byte)Math.RoundToInt((r1 + m) * 255); - byte g = (byte)Math.RoundToInt((g1 + m) * 255); - byte b = (byte)Math.RoundToInt((b1 + m) * 255); - - return new(r, g, b); - } + public static implicit operator ColorRGB(ColorHSVA hsva) => (ColorRGBA)hsva; + public static implicit operator ColorRGB(ColorHSV hsv) => (ColorRGBA)hsv; /// /// Inverts the given . diff --git a/Engine.Core/Primitives/ColorRGBA.cs b/Engine.Core/Primitives/ColorRGBA.cs index 9a1b285..67b6464 100644 --- a/Engine.Core/Primitives/ColorRGBA.cs +++ b/Engine.Core/Primitives/ColorRGBA.cs @@ -44,7 +44,31 @@ public readonly struct ColorRGBA(byte r, byte g, byte b, byte a = 255) public static bool operator !=(ColorRGBA left, ColorRGBA right) => left.R != right.R || left.G != right.G || left.B != right.B || left.A != right.A; public static implicit operator ColorRGBA(ColorRGB rgb) => new(rgb.R, rgb.G, rgb.B, 255); - public static implicit operator ColorRGBA(ColorHSV hsv) => (ColorRGB)hsv; + public static implicit operator ColorRGBA(ColorHSV hsv) => (ColorHSVA)hsv; + public static implicit operator ColorRGBA(ColorHSVA hsva) + { + float hue = hsva.Hue * 360f; + float chroma = hsva.Value * hsva.Saturation; + float x = chroma * (1f - Math.Abs(hue / 60f % 2f - 1f)); + float m = hsva.Value - chroma; + + float r1 = 0f; + float g1 = 0f; + float b1 = 0f; + + if (hue < 60) { r1 = chroma; g1 = x; b1 = 0; } + else if (hue < 120) { r1 = x; g1 = chroma; b1 = 0; } + else if (hue < 180) { r1 = 0; g1 = chroma; b1 = x; } + else if (hue < 240) { r1 = 0; g1 = x; b1 = chroma; } + else if (hue < 300) { r1 = x; g1 = 0; b1 = chroma; } + else if (hue <= 360) { r1 = chroma; g1 = 0; b1 = x; } + + byte r = (byte)Math.RoundToInt((r1 + m) * 255); + byte g = (byte)Math.RoundToInt((g1 + m) * 255); + byte b = (byte)Math.RoundToInt((b1 + m) * 255); + + return new(r, g, b, (byte)(hsva.Alpha * 255)); + } /// /// Inverts the given . diff --git a/Engine.Systems/Tween/EngineExtensions/TweenColorExtensions.cs b/Engine.Systems/Tween/EngineExtensions/TweenColorExtensions.cs index 9a3b886..03b0e35 100644 --- a/Engine.Systems/Tween/EngineExtensions/TweenColorExtensions.cs +++ b/Engine.Systems/Tween/EngineExtensions/TweenColorExtensions.cs @@ -5,11 +5,14 @@ namespace Syntriax.Engine.Systems.Tween; public static class TweenColorExtensions { public static ITween TweenColor(this ColorRGB initialColorRGB, ITweenManager tweenManager, float duration, ColorRGB targetColorRGB, System.Action setMethod) - => tweenManager.StartTween(duration, t => setMethod?.Invoke(initialColorRGB.Lerp(targetColorRGB, t))); + => TweenColor((ColorHSV)initialColorRGB, tweenManager, duration, (ColorHSV)targetColorRGB, color => setMethod?.Invoke(color)); public static ITween TweenColor(this ColorRGBA initialColorRGBA, ITweenManager tweenManager, float duration, ColorRGBA targetColorRGBA, System.Action setMethod) - => tweenManager.StartTween(duration, t => setMethod?.Invoke(initialColorRGBA.Lerp(targetColorRGBA, t))); + => TweenColor((ColorHSVA)initialColorRGBA, tweenManager, duration, (ColorHSVA)targetColorRGBA, color => setMethod?.Invoke(color)); public static ITween TweenColor(this ColorHSV initialColorHSV, ITweenManager tweenManager, float duration, ColorHSV targetColorHSV, System.Action setMethod) => tweenManager.StartTween(duration, t => setMethod?.Invoke(initialColorHSV.Lerp(targetColorHSV, t))); + + public static ITween TweenColor(this ColorHSVA initialColorHSVA, ITweenManager tweenManager, float duration, ColorHSVA targetColorHSVA, System.Action setMethod) + => tweenManager.StartTween(duration, t => setMethod?.Invoke(initialColorHSVA.Lerp(targetColorHSVA, t))); }