From 6182db049e351ebeb95e0819d3c1c35353b2a2a4 Mon Sep 17 00:00:00 2001 From: That-One-Nerd Date: Tue, 18 Feb 2025 09:06:41 -0500 Subject: [PATCH] Some color format work and indexed colors. --- Nerd_STF/Graphics/ColorCMYK.cs | 6 +- Nerd_STF/Graphics/ColorChannel.cs | 3 +- Nerd_STF/Graphics/ColorPalette.cs | 158 ++++++++++++++++++++++ Nerd_STF/Graphics/ColorRGB.cs | 5 +- Nerd_STF/Graphics/Formats/IColorFormat.cs | 53 ++++++++ Nerd_STF/Graphics/Formats/IndexedColor.cs | 80 +++++++++++ Nerd_STF/Graphics/Formats/R8G8B8A8.cs | 141 +++++++++++++++++++ Nerd_STF/Graphics/IColorOperators.cs | 1 + Nerd_STF/ListTuple.cs | 2 +- 9 files changed, 442 insertions(+), 7 deletions(-) create mode 100644 Nerd_STF/Graphics/ColorPalette.cs create mode 100644 Nerd_STF/Graphics/Formats/IColorFormat.cs create mode 100644 Nerd_STF/Graphics/Formats/IndexedColor.cs create mode 100644 Nerd_STF/Graphics/Formats/R8G8B8A8.cs diff --git a/Nerd_STF/Graphics/ColorCMYK.cs b/Nerd_STF/Graphics/ColorCMYK.cs index 0f0bc4e..8e84943 100644 --- a/Nerd_STF/Graphics/ColorCMYK.cs +++ b/Nerd_STF/Graphics/ColorCMYK.cs @@ -271,8 +271,8 @@ namespace Nerd_STF.Graphics // Inlined version of AsRgb().AsHsv() double diffK = 1 - k; double r = (1 - c) * diffK, g = (1 - m) * diffK, b = (1 - y) * diffK; - double[] items = new double[] { r, g, b }; - double cMax = MathE.Max(items), cMin = MathE.Min(items), delta = cMax - cMin; + double[] group = new double[] { r, g, b }; + double cMax = MathE.Max(group), cMin = MathE.Min(group), delta = cMax - cMin; Angle h; if (delta == 0) h = Angle.Zero; @@ -343,7 +343,7 @@ namespace Nerd_STF.Graphics public static ColorCMYK operator +(ColorCMYK a, ColorCMYK b) => new ColorCMYK(a.c + b.c, a.m + b.m, a.y + b.y, a.k + b.k, 1 - (1 - a.a) * (1 - b.a)); public static ColorCMYK operator *(ColorCMYK a, ColorCMYK b) => new ColorCMYK(a.c * b.c, a.m * b.m, a.y * b.y, a.k * b.k, a.a * b.a); - public static ColorCMYK operator *(ColorCMYK a, double b) => new ColorCMYK(a.c * b, a.m * b, a.y * b, a.k * b, a.a); + public static ColorCMYK operator *(ColorCMYK a, double b) => new ColorCMYK(a.c, a.m, a.y, b == 0 ? 1 : MathE.Clamp(a.k / b, 0, 1), a.a); public static bool operator ==(ColorCMYK a, IColor b) => a.Equals(b.AsCmyk()); public static bool operator !=(ColorCMYK a, IColor b) => !a.Equals(b.AsCmyk()); public static bool operator ==(ColorCMYK a, ColorCMYK b) => a.Equals(b); diff --git a/Nerd_STF/Graphics/ColorChannel.cs b/Nerd_STF/Graphics/ColorChannel.cs index aba0fcd..c20d415 100644 --- a/Nerd_STF/Graphics/ColorChannel.cs +++ b/Nerd_STF/Graphics/ColorChannel.cs @@ -12,6 +12,7 @@ Cyan, Magenta, Yellow, - Key + Key, + Index } } diff --git a/Nerd_STF/Graphics/ColorPalette.cs b/Nerd_STF/Graphics/ColorPalette.cs new file mode 100644 index 0000000..8c16062 --- /dev/null +++ b/Nerd_STF/Graphics/ColorPalette.cs @@ -0,0 +1,158 @@ +using Nerd_STF.Graphics.Formats; +using Nerd_STF.Helpers; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nerd_STF.Graphics +{ + // TODO: Should this be a ref struct? + public class ColorPalette : IEnumerable, IEquatable> + where TColor : struct, IColor + { + public int BitDepth { get; private set; } + public int Length => colors.Length; + + private TColor[] colors; + private IndexedColor[] indexedColors; + +#pragma warning disable CS8618 + private ColorPalette() { } +#pragma warning restore CS8618 + public ColorPalette(int colors) + { + int size = GetSizeFor(colors, out int bits); + this.colors = new TColor[size]; + indexedColors = new IndexedColor[size]; + for (int i = 0; i < size; i++) indexedColors[i] = new IndexedColor(this, i); + BitDepth = bits; + } + public ColorPalette(ReadOnlySpan colors) + { + int size = GetSizeFor(colors.Length, out int bits); + this.colors = new TColor[size]; + colors.CopyTo(this.colors); + indexedColors = new IndexedColor[size]; + for (int i = 0; i < size; i++) indexedColors[i] = new IndexedColor(this, i); + BitDepth = bits; + } + + public static ColorPalette FromBitDepth(int bits) + { + int size = 1 << bits; + ColorPalette palette = new ColorPalette() + { + BitDepth = bits, + colors = new TColor[size], + indexedColors = new IndexedColor[size] + }; + for (int i = 0; i < size; i++) palette.indexedColors[i] = new IndexedColor(palette, i); + return palette; + } + + public IndexedColor this[int index] => indexedColors[index]; + public ref TColor Color(int index) => ref colors[index]; + + public void Clear() + { + for (int i = 0; i < colors.Length; i++) + { + colors[i] = default; + } + } + public bool Contains(TColor color) + { + for (int i = 0; i < Length; i++) + { + if (colors[i].Equals(color)) return true; + } + return false; + } + public bool Contains(Predicate predicate) + { + for (int i = 0; i < Length; i++) + { + if (predicate(colors[i])) return true; + } + return false; + } + public void CopyTo(Span destination) => CopyTo(0, destination, 0, Length); + public void CopyTo(int sourceIndex, Span destination, int destIndex, int count) + { + for (int i = 0; i < count; i++) + { + destination[destIndex + i] = colors[sourceIndex + i]; + } + } + public void Expand(int newSize) + { + int newLength = GetSizeFor(newSize, out int bits); + if (newLength <= Length) return; // Contraction not currently supported. + TColor[] newColors = new TColor[newLength]; + IndexedColor[] newIndexedColors = new IndexedColor[newLength]; + Array.Copy(colors, newColors, colors.Length); + Array.Copy(indexedColors, newIndexedColors, indexedColors.Length); + for (int i = Length; i < newLength; i++) newIndexedColors[i] = new IndexedColor(this, i); + colors = newColors; + indexedColors = newIndexedColors; + BitDepth = bits; + } + +#if CS8_OR_GREATER + public bool ReferenceEquals(ColorPalette? other) +#else + public bool ReferenceEquals(ColorPalette other) +#endif + { + return ReferenceEquals(this, other); + } +#if CS8_OR_GREATER + public bool Equals(ColorPalette? other) +#else + public bool Equals(ColorPalette other) +#endif + { + if (other is null) return false; + else if (Length != other.Length) return false; + + for (int i = 0; i < Length; i++) + { + if (!colors[i].Equals(other.colors[i])) return false; + } + + return true; + } +#if CS8_OR_GREATER + public override bool Equals(object? other) +#else + public override bool Equals(object other) +#endif + { + if (other is null) return false; + else if (other is ColorPalette otherColor) return Equals(otherColor); + else return false; + } + public override int GetHashCode() => base.GetHashCode(); + public override string ToString() => $"{BitDepth} BPP Palette: {typeof(TColor).Name}[{Length}]"; + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < colors.Length; i++) yield return colors[i]; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private static int GetSizeFor(int colors, out int bitDepth) + { + int maxSize = 1; + bitDepth = 0; + colors--; + while (colors > 0) + { + maxSize <<= 1; + colors >>= 1; + bitDepth++; + } + return maxSize; + } + } +} diff --git a/Nerd_STF/Graphics/ColorRGB.cs b/Nerd_STF/Graphics/ColorRGB.cs index fc85ff3..60a7feb 100644 --- a/Nerd_STF/Graphics/ColorRGB.cs +++ b/Nerd_STF/Graphics/ColorRGB.cs @@ -287,7 +287,8 @@ namespace Nerd_STF.Graphics public ColorHSV AsHsv() { // Thanks https://www.rapidtables.com/convert/color/rgb-to-hsv.html - double cMax = MathE.Max(this), cMin = MathE.Min(this), delta = cMax - cMin; + double[] group = new double[] { r, g, b }; + double cMax = MathE.Max(group), cMin = MathE.Min(group), delta = cMax - cMin; Angle h; if (delta == 0) h = Angle.Zero; @@ -302,7 +303,7 @@ namespace Nerd_STF.Graphics public ColorCMYK AsCmyk() { // Thanks https://www.rapidtables.com/convert/color/rgb-to-cmyk.html - double diffK = MathE.Max(this), invDiffK = 1 / diffK; + double diffK = MathE.Max(new double[] { r, g, b }), invDiffK = 1 / diffK; return new ColorCMYK((diffK - r) * invDiffK, (diffK - g) * invDiffK, (diffK - b) * invDiffK, diff --git a/Nerd_STF/Graphics/Formats/IColorFormat.cs b/Nerd_STF/Graphics/Formats/IColorFormat.cs new file mode 100644 index 0000000..cbe76ec --- /dev/null +++ b/Nerd_STF/Graphics/Formats/IColorFormat.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Nerd_STF.Graphics.Formats +{ + public interface IColorFormat + { + int ChannelCount { get; } + int BitLength { get; } + Dictionary BitsPerChannel { get; } + byte[] GetBitfield(ColorChannel channel); + + byte[] GetBits(); + IColor GetColor(); + + // TODO: Bitwriter? + // write to stream + } + + public interface IColorFormat : IColorFormat, + IEquatable + where TSelf : IColorFormat + where TColor : struct, IColor + { +#if CS11_OR_GREATER + new static abstract int BitLength { get; } + int IColorFormat.BitLength => TSelf.BitLength; + + new static abstract Dictionary BitsPerChannel { get; } + Dictionary IColorFormat.BitsPerChannel => TSelf.BitsPerChannel; + + new static abstract byte[] GetBitfield(ColorChannel channel); + byte[] IColorFormat.GetBitfield(ColorChannel channel) => TSelf.GetBitfield(channel); + + static abstract TSelf FromColor(IColor color); + static abstract TSelf FromColor(TColor color); +#endif + + new TColor GetColor(); +#if CS8_OR_GREATER + IColor IColorFormat.GetColor() => GetColor(); +#endif + void SetColor(TColor color); + +#if CS11_OR_GREATER + static abstract TSelf operator +(TSelf a, TSelf b); + static abstract TSelf operator *(TSelf a, TSelf b); + + static abstract bool operator ==(TSelf a, TSelf b); + static abstract bool operator !=(TSelf a, TSelf b); +#endif + } +} diff --git a/Nerd_STF/Graphics/Formats/IndexedColor.cs b/Nerd_STF/Graphics/Formats/IndexedColor.cs new file mode 100644 index 0000000..32dbd05 --- /dev/null +++ b/Nerd_STF/Graphics/Formats/IndexedColor.cs @@ -0,0 +1,80 @@ +using Nerd_STF.Helpers; +using Nerd_STF.Mathematics; +using System; +using System.Collections.Generic; + +namespace Nerd_STF.Graphics.Formats +{ + public class IndexedColor : IColorFormat, IEquatable> + where TColor : struct, IColor + { + public int BitLength => palette.BitDepth; + public int Index { get; } + + int IColorFormat.ChannelCount => 1; + Dictionary IColorFormat.BitsPerChannel => new Dictionary() + { + { ColorChannel.Index, palette.BitDepth } + }; + byte[] IColorFormat.GetBitfield(ColorChannel channel) + { + byte[] buf = new byte[MathE.Ceiling(palette.BitDepth / 8.0)]; + if (channel != ColorChannel.Index) return buf; // All zeroes. + int wholes = palette.BitDepth / 8, parts = palette.BitDepth % 8; + for (int i = 0; i < wholes; i++) buf[i] = 0xFF; + for (int i = 0; i < parts; i++) buf[wholes] = (byte)((buf[wholes] << 1) + 1); + return buf; + } + + private readonly ColorPalette palette; + + public IndexedColor(ColorPalette palette, int index) + { + this.palette = palette; + Index = index; + } + + public ColorPalette GetPalette() => palette; + + public ref TColor Color() => ref palette.Color(Index); + IColor IColorFormat.GetColor() => Color(); + public byte[] GetBits() + { + byte[] buf = new byte[MathE.Ceiling(palette.BitDepth / 8.0)]; + int bitIndex = 0, byteIndex = 0, remaining = Index; + while (remaining > 0) + { + buf[byteIndex] |= (byte)((remaining & 1) << bitIndex); + remaining >>= 1; + bitIndex++; + if (bitIndex == 8) + { + bitIndex = 0; + byteIndex++; + } + } + return buf; + } + + public bool ReferenceEquals(IndexedColor other) => ReferenceEquals(this, other); +#if CS8_OR_GREATER + public bool Equals(IndexedColor? other) +#else + public bool Equals(IndexedColor other) +#endif + => !(other is null) && Color().Equals(other.Color()); +#if CS8_OR_GREATER + public override bool Equals(object? other) +#else + public override bool Equals(object other) +#endif + { + if (other is null) return false; + else if (other is IndexedColor otherIndexed) return Equals(otherIndexed); + else if (other is TColor otherColor) return Color().Equals(otherColor); + else return false; + } + public override int GetHashCode() => base.GetHashCode(); + public override string ToString() => $"#0x{Index:X}: {Color()}"; + } +} diff --git a/Nerd_STF/Graphics/Formats/R8G8B8A8.cs b/Nerd_STF/Graphics/Formats/R8G8B8A8.cs new file mode 100644 index 0000000..e58d06c --- /dev/null +++ b/Nerd_STF/Graphics/Formats/R8G8B8A8.cs @@ -0,0 +1,141 @@ +using Nerd_STF.Mathematics; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Nerd_STF.Graphics.Formats +{ + public class R8G8B8A8 : IColorFormat + { + public static int ChannelCount => 4; + public static int BitLength => 32; + public static Dictionary BitsPerChannel { get; } = new Dictionary() + { + { ColorChannel.Red, 8 }, + { ColorChannel.Green, 8 }, + { ColorChannel.Blue, 8 }, + { ColorChannel.Alpha, 8 } + }; + + int IColorFormat.ChannelCount => ChannelCount; + int IColorFormat.BitLength => BitLength; + Dictionary IColorFormat.BitsPerChannel => BitsPerChannel; + + public byte R + { + get => r; + set => r = value; + } + public byte G + { + get => g; + set => g = value; + } + public byte B + { + get => b; + set => b = value; + } + public byte A + { + get => a; + set => a = value; + } + + private byte r, g, b, a; + + public R8G8B8A8(ColorRGB color) + { + r = (byte)MathE.Clamp(color.r * 255, 0, 255); + g = (byte)MathE.Clamp(color.g * 255, 0, 255); + b = (byte)MathE.Clamp(color.b * 255, 0, 255); + a = (byte)MathE.Clamp(color.a * 255, 0, 255); + } + public R8G8B8A8(byte r, byte g, byte b, byte a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + public static R8G8B8A8 FromColor(IColor color) => new R8G8B8A8(color.AsRgb()); + public static R8G8B8A8 FromColor(ColorRGB color) => new R8G8B8A8(color); + + public static byte[] GetBitfield(ColorChannel channel) + { + byte[] buf = new byte[4]; + switch (channel) + { + case ColorChannel.Red: buf[0] = 0xFF; break; + case ColorChannel.Green: buf[1] = 0xFF; break; + case ColorChannel.Blue: buf[2] = 0xFF; break; + case ColorChannel.Alpha: buf[3] = 0xFF; break; + } + return buf; + } + byte[] IColorFormat.GetBitfield(ColorChannel channel) => GetBitfield(channel); + +#if CS8_OR_GREATER + public bool Equals(R8G8B8A8? other) => +#else + public bool Equals(R8G8B8A8 other) => +#endif + !(other is null) && r == other.r && g == other.g && b == other.b && a == other.a; +#if CS8_OR_GREATER + public override bool Equals(object? obj) +#else + public override bool Equals(object obj) +#endif + { + if (obj is null) return false; + else if (obj is R8G8B8A8 formatObj) return Equals(formatObj); + else return false; + } + public override int GetHashCode() => base.GetHashCode(); + public override string ToString() => $"{{ r={r}, g={g}, b={b}, a={a} }}"; + + public byte[] GetBits() => new byte[] { r, g, b, a }; + + public ColorRGB GetColor() + { + const double inv255 = 0.00392156862745; // Constant for 1/255 + return new ColorRGB(r * inv255, + g * inv255, + b * inv255, + a * inv255); + } + IColor IColorFormat.GetColor() => GetColor(); + public void SetColor(ColorRGB color) + { + r = (byte)MathE.Clamp(color.r * 255, 0, 255); + g = (byte)MathE.Clamp(color.g * 255, 0, 255); + b = (byte)MathE.Clamp(color.b * 255, 0, 255); + a = (byte)MathE.Clamp(color.a * 255, 0, 255); + } + public void SetColor(byte r, byte g, byte b, byte a) + { + this.r = (byte)MathE.Clamp(r * 255, 0, 255); + this.g = (byte)MathE.Clamp(g * 255, 0, 255); + this.b = (byte)MathE.Clamp(b * 255, 0, 255); + this.a = (byte)MathE.Clamp(a * 255, 0, 255); + } + + public static R8G8B8A8 operator +(R8G8B8A8 a, R8G8B8A8 b) + { + return new R8G8B8A8((byte)MathE.Clamp(a.r + b.r, 0, 255), + (byte)MathE.Clamp(a.g + b.g, 0, 255), + (byte)MathE.Clamp(a.b + b.b, 0, 255), + (byte)MathE.Clamp(a.a + b.a, 0, 255)); + } + public static R8G8B8A8 operator *(R8G8B8A8 a, R8G8B8A8 b) + { + const double inv255 = 0.00392156862745; // Constant for 1/255 + return new R8G8B8A8((byte)MathE.Clamp(a.r * b.r * inv255, 0, 255), + (byte)MathE.Clamp(a.g * b.g * inv255, 0, 255), + (byte)MathE.Clamp(a.b * b.b * inv255, 0, 255), + (byte)MathE.Clamp(a.a * b.a * inv255, 0, 255)); + } + public static bool operator ==(R8G8B8A8 a, R8G8B8A8 b) => a.Equals(b); + public static bool operator !=(R8G8B8A8 a, R8G8B8A8 b) => !a.Equals(b); + } +} diff --git a/Nerd_STF/Graphics/IColorOperators.cs b/Nerd_STF/Graphics/IColorOperators.cs index 358ba57..f9502a5 100644 --- a/Nerd_STF/Graphics/IColorOperators.cs +++ b/Nerd_STF/Graphics/IColorOperators.cs @@ -4,6 +4,7 @@ namespace Nerd_STF.Graphics public interface IColorOperators where TSelf : IColorOperators { static abstract TSelf operator +(TSelf a, TSelf b); + static abstract TSelf operator *(TSelf a, TSelf b); static abstract TSelf operator *(TSelf a, double b); static abstract bool operator ==(TSelf a, IColor b); static abstract bool operator !=(TSelf a, IColor b); diff --git a/Nerd_STF/ListTuple.cs b/Nerd_STF/ListTuple.cs index 77d3273..5fdfd40 100644 --- a/Nerd_STF/ListTuple.cs +++ b/Nerd_STF/ListTuple.cs @@ -111,7 +111,7 @@ namespace Nerd_STF public static implicit operator ListTuple((T, T, T, T, T) tuple) => new ListTuple(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4, tuple.Item5); public static implicit operator ListTuple((T, T, T, T, T, T) tuple) => new ListTuple(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4, tuple.Item5, tuple.Item6); public static implicit operator ListTuple((T, T, T, T, T, T, T) tuple) => new ListTuple(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4, tuple.Item5, tuple.Item6, tuple.Item7); - public static implicit operator ListTuple(T[] array) => new ListTuple(array) + public static implicit operator ListTuple(T[] array) => new ListTuple(array); public struct Enumerator : IEnumerator {