diff --git a/Nerd_STF/Graphics/ColorCMYK.cs b/Nerd_STF/Graphics/ColorCMYK.cs new file mode 100644 index 0000000..0f0bc4e --- /dev/null +++ b/Nerd_STF/Graphics/ColorCMYK.cs @@ -0,0 +1,365 @@ +using Nerd_STF.Mathematics; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Nerd_STF.Graphics +{ + public struct ColorCMYK : IColor, + IEnumerable +#if CS11_OR_GREATER + ,IFromTuple, + IFromTuple, + ISplittable +#endif + { + public static int ChannelCount => 5; + + public static ColorCMYK Black => new ColorCMYK(0 , 0 , 0 , 1 , 1); + public static ColorCMYK Blue => new ColorCMYK(1 , 1 , 0 , 0 , 1); + public static ColorCMYK Clear => new ColorCMYK(0 , 0 , 0 , 0 , 0); + public static ColorCMYK Cyan => new ColorCMYK(1 , 0 , 0 , 0 , 1); + public static ColorCMYK Gray => new ColorCMYK(0 , 0 , 0 , 0.5, 1); + public static ColorCMYK Green => new ColorCMYK(1 , 0 , 1 , 0 , 1); + public static ColorCMYK Magenta => new ColorCMYK(0 , 1 , 0 , 0 , 1); + public static ColorCMYK Orange => new ColorCMYK(0 , 0.5, 1 , 0 , 1); + public static ColorCMYK Purple => new ColorCMYK(0.5, 1 , 0 , 0 , 1); + public static ColorCMYK Red => new ColorCMYK(0 , 1 , 1 , 0 , 1); + public static ColorCMYK White => new ColorCMYK(0 , 0 , 0 , 0 , 1); + public static ColorCMYK Yellow => new ColorCMYK(0 , 0 , 1 , 0 , 1); + + public double c, m, y, k, a; + + public ColorCMYK(double cyan, double magenta, double yellow, double black) + { + c = cyan; + m = magenta; + y = yellow; + k = black; + a = 1; + } + public ColorCMYK(double cyan, double magenta, double yellow, double black, double alpha) + { + c = cyan; + m = magenta; + y = yellow; + k = black; + a = alpha; + } + public ColorCMYK(IEnumerable nums) + { + c = 0; + m = 0; + y = 0; + k = 0; + a = 1; + + int index = 0; + foreach (double item in nums) + { + this[index] = item; + index++; + if (index == 5) break; + } + } + public ColorCMYK(Fill fill) + { + c = fill(0); + m = fill(1); + y = fill(2); + k = fill(3); + a = fill(4); + } + + public double this[int index] + { + get + { + switch (index) + { + case 0: return c; + case 1: return m; + case 2: return y; + case 3: return k; + case 4: return a; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + set + { + switch (index) + { + case 0: c = value; break; + case 1: m = value; break; + case 2: y = value; break; + case 3: k = value; break; + case 4: a = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } + public ListTuple this[string key] + { + get + { + double[] items = new double[key.Length]; + for (int i = 0; i < key.Length; i++) + { + char c = key[i]; + switch (c) + { + case 'c': items[i] = c; break; + case 'm': items[i] = m; break; + case 'y': items[i] = y; break; + case 'k': items[i] = k; break; + case 'a': items[i] = a; break; + default: throw new ArgumentException("Invalid key.", nameof(key)); + } + } + return new ListTuple(items); + } + set + { + IEnumerator stepper = value.GetEnumerator(); + for (int i = 0; i < key.Length; i++) + { + char c = key[i]; + stepper.MoveNext(); + switch (c) + { + case 'c': this.c = stepper.Current; break; + case 'm': m = stepper.Current; break; + case 'y': y = stepper.Current; break; + case 'k': k = stepper.Current; break; + case 'a': a = stepper.Current; break; + default: throw new ArgumentException("Invalid key.", nameof(key)); + } + } + } + } + + public static ColorCMYK Average(double gamma, IEnumerable colors) + { + double avgC = 0, avgM = 0, avgY = 0, avgK = 0, avgA = 0; + int count = 0; + foreach (ColorCMYK color in colors) + { + double correctC = MathE.Pow(color.c, gamma), + correctM = MathE.Pow(color.m, gamma), + correctY = MathE.Pow(color.y, gamma), + correctK = MathE.Pow(color.k, gamma); + // Gamma doesn't apply to the alpha channel. + + avgC += correctC; + avgM += correctM; + avgY += correctY; + avgK += correctK; + avgA += color.a; + count++; + } + avgC /= count; + avgM /= count; + avgY /= count; + avgK /= count; + avgA /= count; + double invGamma = 1 / gamma; + return new ColorCMYK(MathE.Pow(avgC, invGamma), + MathE.Pow(avgM, invGamma), + MathE.Pow(avgY, invGamma), + MathE.Pow(avgK, invGamma), + avgA); + } +#if CS11_OR_GREATER + static ColorCMYK IColor.Average(IEnumerable colors) => Average(1, colors); +#endif + public static ColorCMYK Clamp(ColorCMYK color, ColorCMYK min, ColorCMYK max) => + new ColorCMYK(MathE.Clamp(color.c, min.c, max.c), + MathE.Clamp(color.m, min.m, max.m), + MathE.Clamp(color.y, min.y, max.y), + MathE.Clamp(color.k, min.k, max.k), + MathE.Clamp(color.a, min.a, max.a)); + public static ColorCMYK Lerp(double gamma, ColorCMYK a, ColorCMYK b, double t, bool clamp = true) + { + double aCorrectedC = MathE.Pow(a.c, gamma), bCorrectedC = MathE.Pow(b.c, gamma), + aCorrectedM = MathE.Pow(a.m, gamma), bCorrectedM = MathE.Pow(b.m, gamma), + aCorrectedY = MathE.Pow(a.y, gamma), bCorrectedY = MathE.Pow(b.y, gamma), + aCorrectedK = MathE.Pow(a.k, gamma), bCorrectedK = MathE.Pow(b.k, gamma); + + double newC = MathE.Lerp(aCorrectedC, bCorrectedC, t, clamp), + newM = MathE.Lerp(aCorrectedM, bCorrectedM, t, clamp), + newY = MathE.Lerp(aCorrectedY, bCorrectedY, t, clamp), + newK = MathE.Lerp(aCorrectedK, bCorrectedK, t, clamp), + newA = MathE.Lerp(a.a, b.a, t, clamp); + + double invGamma = 1 / gamma; + return new ColorCMYK(MathE.Pow(newC, invGamma), + MathE.Pow(newM, invGamma), + MathE.Pow(newY, invGamma), + MathE.Pow(newK, invGamma), + newA); + } +#if CS11_OR_GREATER + static ColorCMYK IInterpolable.Lerp(ColorCMYK a, ColorCMYK b, double t, bool clamp) => Lerp(1, a, b, t, clamp); +#endif + public static ColorCMYK Product(IEnumerable colors) + { + bool any = false; + ColorCMYK result = new ColorCMYK(1, 1, 1, 1, 1); + foreach (ColorCMYK color in colors) + { + any = true; + result *= color; + } + return any ? result : Black; + } + public static ColorCMYK Sum(IEnumerable colors) + { + bool any = false; + ColorCMYK result = new ColorCMYK(0, 0, 0, 0); + foreach (ColorCMYK color in colors) + { + any = true; + result += color; + } + return any ? result : Black; + } + public static (double[] Cs, double[] Ms, double[] Ys, double[] Ks, double[] As) SplitArray(IEnumerable colors) + { + int count = colors.Count(); + double[] Cs = new double[count], Ms = new double[count], Ys = new double[count], Ks = new double[count], As = new double[count]; + int index = 0; + foreach (ColorCMYK c in colors) + { + Cs[index] = c.c; + Ms[index] = c.m; + Ys[index] = c.y; + Ks[index] = c.k; + As[index] = c.a; + index++; + } + return (Cs, Ms, Ys, Ks, As); + } + + public Dictionary GetChannels() => new Dictionary() + { + { ColorChannel.Cyan, c }, + { ColorChannel.Magenta, m }, + { ColorChannel.Yellow, y }, + { ColorChannel.Key, k }, + { ColorChannel.Alpha, a } + }; + + public TColor AsColor() where TColor : struct, IColor + { + Type type = typeof(TColor); + if (type == typeof(ColorRGB)) return (TColor)(object)AsRgb(); + else if (type == typeof(ColorHSV)) return (TColor)(object)AsHsv(); + else if (type == typeof(ColorCMYK)) return (TColor)(object)this; + else throw new InvalidCastException(); + } + public ColorRGB AsRgb() + { + // Thanks https://www.rapidtables.com/convert/color/cmyk-to-rgb.html + double diffK = 1 - k; + return new ColorRGB((1 - c) * diffK, + (1 - m) * diffK, + (1 - y) * diffK); + } + public ColorHSV AsHsv() + { + // 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; + Angle h; + + if (delta == 0) h = Angle.Zero; + else if (cMax == r) h = Angle.FromDegrees(60 * MathE.ModAbs((g - b) / delta, 6)); + else if (cMax == g) h = Angle.FromDegrees(60 * ((b - r) / delta + 2)); + else if (cMax == b) h = Angle.FromDegrees(60 * ((r - g) / delta + 4)); + else h = Angle.Zero; + + double s = cMax == 0 ? 0 : delta / cMax; + return new ColorHSV(h, s, cMax); + } + public ColorCMYK AsCmyk() => this; + + public string HexCode() => AsRgb().HexCode(); + + public IEnumerator GetEnumerator() + { + yield return c; + yield return m; + yield return y; + yield return k; + yield return a; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Deconstruct(out double c, out double m, out double y, out double k) + { + c = this.c; + m = this.m; + y = this.y; + k = this.k; + } + public void Deconstruct(out double c, out double m, out double y, out double k, out double a) + { + c = this.c; + m = this.m; + y = this.y; + k = this.k; + a = this.a; + } + + public bool Equals(ColorCMYK other) + { + if (a <= 0 && other.a <= 0) return true; + else if (k >= 1 && other.k >= 1) return true; + else return c == other.c && m == other.m && y == other.y && k == other.k && a == other.a; + } + public bool Equals(IColor other) => Equals(other.AsCmyk()); +#if CS8_OR_GREATER + public override bool Equals(object? other) +#else + public override bool Equals(object other) +#endif + { + if (other is IColor color) return Equals(color.AsRgb()); + else return false; + } + public override int GetHashCode() => base.GetHashCode(); + public override string ToString() => $"{{ c={c:0.00}, m={m:0.00}, y={y:0.00}, k={k:0.00}, a={a:0.00} }}"; + + public double[] ToArray() => new double[] { c, m, y, k, a }; + public Fill ToFill() + { + ColorCMYK copy = this; + return i => copy[i]; + } + public List ToList() => new List { c, m, y, k, 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, 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 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); + public static bool operator !=(ColorCMYK a, ColorCMYK b) => !a.Equals(b); + + public static implicit operator ColorCMYK(ListTuple tuple) + { + if (tuple.Length == 4) return new ColorCMYK(tuple[0], tuple[1], tuple[2], tuple[3]); + else if (tuple.Length == 5) return new ColorCMYK(tuple[0], tuple[1], tuple[2], tuple[3], tuple[4]); + else throw new InvalidCastException(); + } + public static implicit operator ColorCMYK((double, double, double, double) tuple) => new ColorCMYK(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4); + public static implicit operator ColorCMYK((double, double, double, double, double) tuple) => new ColorCMYK(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4, tuple.Item5); + + public static implicit operator ListTuple(ColorCMYK color) => new ListTuple(color.c, color.m, color.y, color.k, color.a); + public static implicit operator ValueTuple(ColorCMYK color) => (color.c, color.m, color.y, color.k); + public static implicit operator ValueTuple(ColorCMYK color) => (color.c, color.m, color.y, color.k, color.a); + } +} diff --git a/Nerd_STF/Graphics/ColorChannel.cs b/Nerd_STF/Graphics/ColorChannel.cs index 86f3892..aba0fcd 100644 --- a/Nerd_STF/Graphics/ColorChannel.cs +++ b/Nerd_STF/Graphics/ColorChannel.cs @@ -5,6 +5,13 @@ Red, Green, Blue, - Alpha + Alpha, + Hue, + Saturation, + Value, + Cyan, + Magenta, + Yellow, + Key } } diff --git a/Nerd_STF/Graphics/ColorHSV.cs b/Nerd_STF/Graphics/ColorHSV.cs new file mode 100644 index 0000000..c12fa58 --- /dev/null +++ b/Nerd_STF/Graphics/ColorHSV.cs @@ -0,0 +1,350 @@ +using Nerd_STF.Helpers; +using Nerd_STF.Mathematics; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Nerd_STF.Graphics +{ + public struct ColorHSV : IColor, + IEnumerable +#if CS11_OR_GREATER + ,IFromTuple, + IFromTuple, + IFromTuple, + IFromTuple, + ISplittable +#endif + { + public static int ChannelCount => 4; + + public static ColorHSV Black => new ColorHSV(Angle.FromDegrees( 0), 0, 0 , 1); + public static ColorHSV Blue => new ColorHSV(Angle.FromDegrees(240), 1, 1 , 1); + public static ColorHSV Clear => new ColorHSV(Angle.FromDegrees( 0), 0, 0 , 0); + public static ColorHSV Cyan => new ColorHSV(Angle.FromDegrees(180), 1, 1 , 1); + public static ColorHSV Gray => new ColorHSV(Angle.FromDegrees( 0), 0, 0.5, 1); + public static ColorHSV Green => new ColorHSV(Angle.FromDegrees(120), 1, 1 , 1); + public static ColorHSV Magenta => new ColorHSV(Angle.FromDegrees(300), 1, 1 , 1); + public static ColorHSV Orange => new ColorHSV(Angle.FromDegrees( 30), 1, 1 , 1); + public static ColorHSV Purple => new ColorHSV(Angle.FromDegrees(270), 1, 1 , 1); + public static ColorHSV Red => new ColorHSV(Angle.FromDegrees( 0), 1, 1 , 1); + public static ColorHSV White => new ColorHSV(Angle.FromDegrees( 0), 0, 1 , 1); + public static ColorHSV Yellow => new ColorHSV(Angle.FromDegrees( 60), 0, 1 , 1); + + public Angle h; + public double s, v, a; + + public ColorHSV(Angle hue) + { + h = hue.Normalized; + s = 1; + v = 1; + a = 1; + } + public ColorHSV(double hue) + { + h = Angle.FromRevolutions(hue); + s = 1; + v = 1; + a = 1; + } + public ColorHSV(Angle hue, double saturation, double value) + { + h = hue.Normalized; + s = saturation; + v = value; + a = 1; + } + public ColorHSV(double hue, double saturation, double value) + { + h = Angle.FromRevolutions(hue); + s = saturation; + v = value; + a = 1; + } + public ColorHSV(Angle hue, double saturation, double value, double alpha) + { + h = hue.Normalized; + s = saturation; + v = value; + a = alpha; + } + public ColorHSV(double hue, double saturation, double value, double alpha) + { + h = Angle.FromRevolutions(hue); + s = saturation; + v = value; + a = alpha; + } + public ColorHSV(IEnumerable nums) + { + h = Angle.Zero; + s = 0; + v = 0; + a = 1; + + int index = 0; + foreach (double item in nums) + { + this[index] = item; + index++; + if (index == 4) break; + } + } + public ColorHSV(Fill fill) + { + h = Angle.FromRevolutions(fill(0) % 1); + s = fill(1); + v = fill(2); + a = fill(3); + } + + public double this[int index] + { + get + { + switch (index) + { + case 0: return h.Revolutions; + case 1: return s; + case 2: return v; + case 3: return a; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + set + { + switch (index) + { + case 0: h = Angle.FromRevolutions(value); break; + case 1: s = value; break; + case 2: v = value; break; + case 3: a = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } + public ListTuple this[string key] + { + get + { + double[] items = new double[key.Length]; + for (int i = 0; i < key.Length; i++) + { + char c = key[i]; + switch (c) + { + case 'h': items[i] = h.Revolutions; break; + case 'H': items[i] = h.Degrees; break; + case 's': items[i] = s; break; + case 'v': items[i] = v; break; + case 'a': items[i] = a; break; + default: throw new ArgumentException("Invalid key.", nameof(key)); + } + } + return new ListTuple(items); + } + set + { + IEnumerator stepper = value.GetEnumerator(); + for (int i = 0; i < key.Length; i++) + { + char c = key[i]; + stepper.MoveNext(); + switch (c) + { + case 'h': h = Angle.FromRevolutions(stepper.Current); break; + case 'H': h = Angle.FromDegrees(stepper.Current); break; + case 's': s = stepper.Current; break; + case 'v': v = stepper.Current; break; + case 'a': a = stepper.Current; break; + default: throw new ArgumentException("Invalid key.", nameof(key)); + } + } + } + } + + public static ColorHSV Average(IEnumerable colors) + { + Angle avgH = Angle.Zero; + double avgS = 0, avgV = 0, avgA = 0; + int count = 0; + foreach (ColorHSV color in colors) + { + avgH += color.h; + avgS += color.s; + avgV += color.v; + avgA += color.a; + } + return new ColorHSV(avgH / count, avgS / count, avgV / count, avgA / count); + } + public static ColorHSV Clamp(ColorHSV color, ColorHSV min, ColorHSV max) => + new ColorHSV(Angle.Clamp(color.h, min.h, max.h), + MathE.Clamp(color.s, min.s, max.s), + MathE.Clamp(color.v, min.v, max.v), + MathE.Clamp(color.a, min.a, max.a)); + public static ColorHSV Lerp(ColorHSV a, ColorHSV b, double t, bool clamp = true) => + new ColorHSV(Angle.Lerp(a.h, b.h, t, clamp), + MathE.Lerp(a.s, b.s, t, clamp), + MathE.Lerp(a.v, b.v, t, clamp), + MathE.Lerp(a.a, b.a, t, clamp)); + public static ColorHSV Product(IEnumerable colors) + { + bool any = false; + ColorHSV result = new ColorHSV(Angle.Full, 1, 1, 1); + foreach (ColorHSV color in colors) + { + any = true; + result *= color; + } + return any ? result : Black; + } + public static ColorHSV Sum(IEnumerable colors) + { + bool any = false; + ColorHSV result = new ColorHSV(Angle.Zero, 0, 0, 0); + foreach (ColorHSV color in colors) + { + any = true; + result += color; + } + return any ? result : Black; + } + public static (Angle[] Hs, double[] Ss, double[] Vs, double[] As) SplitArray(IEnumerable colors) + { + int count = colors.Count(); + Angle[] Hs = new Angle[count]; + double[] Ss = new double[count], Vs = new double[count], As = new double[count]; + int index = 0; + foreach (ColorHSV c in colors) + { + Hs[index] = c.h; + Ss[index] = c.s; + Vs[index] = c.v; + As[index] = c.a; + index++; + } + return (Hs, Ss, Vs, As); + } + + public Dictionary GetChannels() => new Dictionary() + { + { ColorChannel.Hue, h.Revolutions }, + { ColorChannel.Saturation, s }, + { ColorChannel.Value, v }, + { ColorChannel.Alpha, a } + }; + + public TColor AsColor() where TColor : struct, IColor + { + Type type = typeof(TColor); + if (type == typeof(ColorRGB)) return (TColor)(object)AsRgb(); + else if (type == typeof(ColorHSV)) return (TColor)(object)this; + else if (type == typeof(ColorCMYK)) return (TColor)(object)AsCmyk(); + else throw new InvalidCastException(); + } + public ColorRGB AsRgb() + { + // Thanks https://www.rapidtables.com/convert/color/hsv-to-rgb.html + double H = h.Normalized.Degrees, + c = v * s, + x = c * (1 - MathE.Abs(MathE.ModAbs(H / 60, 2) - 1)), + m = v - c; + + ColorRGB color; + if (H < 60) color = new ColorRGB(c, x, 0); + else if (H < 120) color = new ColorRGB(x, c, 0); + else if (H < 180) color = new ColorRGB(0, c, x); + else if (H < 240) color = new ColorRGB(0, x, c); + else if (H < 300) color = new ColorRGB(x, 0, c); + else if (H < 360) color = new ColorRGB(c, 0, x); + else throw new ArgumentOutOfRangeException(); + + color.r += m; + color.g += m; + color.b += m; + return color; + } + public ColorHSV AsHsv() => this; + public ColorCMYK AsCmyk() => AsRgb().AsCmyk(); + + public string HexCode() => AsRgb().HexCode(); + + public IEnumerator GetEnumerator() + { + yield return h.Revolutions; + yield return s; + yield return v; + yield return a; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Deconstruct(out Angle h, out double s, out double v) + { + h = this.h; + s = this.s; + v = this.v; + } + public void Deconstruct(out Angle h, out double s, out double v, out double a) + { + h = this.h; + s = this.s; + v = this.v; + a = this.a; + } + + public bool Equals(ColorHSV other) + { + if (a <= 0 && other.a <= 0) return true; + else if (v <= 0 && other.v <= 0) return true; + else if (s <= 0 && other.s <= 0) return true; + else return h == other.h && s == other.s && v == other.v && a == other.a; + } + public bool Equals(IColor other) => Equals(other.AsHsv()); +#if CS8_OR_GREATER + public override bool Equals(object? other) +#else + public override bool Equals(object other) +#endif + { + if (other is IColor color) return Equals(color.AsHsv()); + else return false; + } + public override int GetHashCode() => base.GetHashCode(); + public override string ToString() => $"{{ h={h.Degrees:0}°, s={s:0.00}, v={v:0.00}, a={a:0.00} }}"; + + public double[] ToArray() => new double[] { h.Revolutions, s, v, a }; + public Fill ToFill() + { + ColorHSV copy = this; + return i => copy[i]; + } + public List ToList() => new List { h.Revolutions, s, v, a }; + + public static ColorHSV operator +(ColorHSV a, ColorHSV b) => new ColorHSV(a.h + b.h, a.s + b.s, a.v + b.v, 1 - (1 - a.a) * (1 - b.a)); + public static ColorHSV operator *(ColorHSV a, ColorHSV b) => new ColorHSV(Angle.FromRevolutions(a.h.Revolutions * b.h.Revolutions), a.s * b.s, a.v * b.v, a.a * b.a); + public static ColorHSV operator *(ColorHSV a, double b) => new ColorHSV(a.h, a.s * b, a.v, a.a); + public static bool operator ==(ColorHSV a, IColor b) => a.Equals(b.AsHsv()); + public static bool operator !=(ColorHSV a, IColor b) => !a.Equals(b.AsHsv()); + public static bool operator ==(ColorHSV a, ColorHSV b) => a.Equals(b); + public static bool operator !=(ColorHSV a, ColorHSV b) => !a.Equals(b); + + public static implicit operator ColorHSV(ListTuple tuple) + { + if (tuple.Length == 3) return new ColorHSV(tuple[0], tuple[1], tuple[2]); + else if (tuple.Length == 4) return new ColorHSV(tuple[0], tuple[1], tuple[2], tuple[3]); + else throw new InvalidCastException(); + } + public static implicit operator ColorHSV((Angle, double, double) tuple) => new ColorHSV(tuple.Item1, tuple.Item2, tuple.Item3); + public static implicit operator ColorHSV((double, double, double) tuple) => new ColorHSV(tuple.Item1, tuple.Item2, tuple.Item3); + public static implicit operator ColorHSV((Angle, double, double, double) tuple) => new ColorHSV(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4); + public static implicit operator ColorHSV((double, double, double, double) tuple) => new ColorHSV(tuple.Item1, tuple.Item2, tuple.Item3, tuple.Item4); + + public static implicit operator ListTuple(ColorHSV color) => new ListTuple(color.h.Revolutions, color.s, color.v, color.a); + public static implicit operator ValueTuple(ColorHSV color) => (color.h, color.s, color.v); + public static implicit operator ValueTuple(ColorHSV color) => (color.h.Revolutions, color.s, color.v); + public static implicit operator ValueTuple(ColorHSV color) => (color.h, color.s, color.v, color.a); + public static implicit operator ValueTuple(ColorHSV color) => (color.h.Revolutions, color.s, color.v, color.a); + } +} diff --git a/Nerd_STF/Graphics/ColorRGB.cs b/Nerd_STF/Graphics/ColorRGB.cs index a348619..fc85ff3 100644 --- a/Nerd_STF/Graphics/ColorRGB.cs +++ b/Nerd_STF/Graphics/ColorRGB.cs @@ -7,7 +7,8 @@ using System.Linq; namespace Nerd_STF.Graphics { - public struct ColorRGB : IColor + public struct ColorRGB : IColor, + INumberGroup #if CS11_OR_GREATER ,IFromTuple, IFromTuple, @@ -16,36 +17,36 @@ namespace Nerd_STF.Graphics { public static int ChannelCount => 4; - public static ColorRGB Black => new ColorRGB(0, 0, 0, 1); - public static ColorRGB Blue => new ColorRGB(0, 0, 1, 1); - public static ColorRGB Clear => new ColorRGB(0, 0, 0, 0); - public static ColorRGB Cyan => new ColorRGB(0, 1, 1, 1); - public static ColorRGB Gray => new ColorRGB(0.5, 0.5, 0.5, 1); - public static ColorRGB Green => new ColorRGB(0, 1, 0, 1); - public static ColorRGB Magenta => new ColorRGB(1, 0, 1, 1); - public static ColorRGB Orange => new ColorRGB(1, 0.5, 0, 1); - public static ColorRGB Purple => new ColorRGB(0.5, 0, 1, 1); - public static ColorRGB Red => new ColorRGB(1, 0, 0, 1); - public static ColorRGB White => new ColorRGB(1, 1, 1, 1); - public static ColorRGB Yellow => new ColorRGB(1, 1, 0, 1); + public static ColorRGB Black => new ColorRGB(0 , 0 , 0 , 1); + public static ColorRGB Blue => new ColorRGB(0 , 0 , 1 , 1); + public static ColorRGB Clear => new ColorRGB(0 , 0 , 0 , 0); + public static ColorRGB Cyan => new ColorRGB(0 , 1 , 1 , 1); + public static ColorRGB Gray => new ColorRGB(0.5, 0.5, 0.5, 1); + public static ColorRGB Green => new ColorRGB(0 , 1 , 0 , 1); + public static ColorRGB Magenta => new ColorRGB(1 , 0 , 1 , 1); + public static ColorRGB Orange => new ColorRGB(1 , 0.5, 0 , 1); + public static ColorRGB Purple => new ColorRGB(0.5, 0 , 1 , 1); + public static ColorRGB Red => new ColorRGB(1 , 0 , 0 , 1); + public static ColorRGB White => new ColorRGB(1 , 1 , 1 , 1); + public static ColorRGB Yellow => new ColorRGB(1 , 1 , 0 , 1); public double Magnitude => MathE.Sqrt(r * r + g * g + b * b); public double r, g, b, a; - public ColorRGB(double r, double g, double b) + public ColorRGB(double red, double green, double blue) { - this.r = r; - this.g = g; - this.b = b; + r = red; + g = green; + b = blue; a = 1; } - public ColorRGB(double r, double g, double b, double a) + public ColorRGB(double red, double green, double blue, double alpha) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; + r = red; + g = green; + b = blue; + a = alpha; } public ColorRGB(IEnumerable nums) { @@ -160,6 +161,9 @@ namespace Nerd_STF.Graphics MathE.Pow(avgB, invGamma), avgA); } +#if CS11_OR_GREATER + static ColorRGB IColor.Average(IEnumerable colors) => Average(1, colors); +#endif public static ColorRGB Clamp(ColorRGB color, ColorRGB min, ColorRGB max) => new ColorRGB(MathE.Clamp(color.r, min.r, max.r), MathE.Clamp(color.g, min.g, max.g), @@ -206,12 +210,9 @@ namespace Nerd_STF.Graphics } public static ColorRGB Lerp(double gamma, ColorRGB a, ColorRGB b, double t, bool clamp = true) { - double aCorrectedR = MathE.Pow(a.r, gamma), - aCorrectedG = MathE.Pow(a.g, gamma), - aCorrectedB = MathE.Pow(a.b, gamma), - bCorrectedR = MathE.Pow(b.r, gamma), - bCorrectedG = MathE.Pow(b.g, gamma), - bCorrectedB = MathE.Pow(b.b, gamma); + double aCorrectedR = MathE.Pow(a.r, gamma), bCorrectedR = MathE.Pow(b.r, gamma), + aCorrectedG = MathE.Pow(a.g, gamma), bCorrectedG = MathE.Pow(b.g, gamma), + aCorrectedB = MathE.Pow(a.b, gamma), bCorrectedB = MathE.Pow(b.b, gamma); // Gamma doesn't apply to the alpha channel. double newR = MathE.Lerp(aCorrectedR, bCorrectedR, t, clamp), @@ -242,7 +243,7 @@ namespace Nerd_STF.Graphics public static ColorRGB Sum(IEnumerable colors) { bool any = false; - ColorRGB result = new ColorRGB(0, 0, 0, 1); + ColorRGB result = new ColorRGB(0, 0, 0, 0); foreach (ColorRGB color in colors) { any = true; @@ -278,9 +279,37 @@ namespace Nerd_STF.Graphics { Type type = typeof(TColor); if (type == typeof(ColorRGB)) return (TColor)(object)this; + else if (type == typeof(ColorHSV)) return (TColor)(object)AsHsv(); + else if (type == typeof(ColorCMYK)) return (TColor)(object)AsCmyk(); else throw new InvalidCastException(); } public ColorRGB AsRgb() => this; + 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; + Angle h; + + if (delta == 0) h = Angle.Zero; + else if (cMax == r) h = Angle.FromDegrees(60 * MathE.ModAbs((g - b) / delta, 6)); + else if (cMax == g) h = Angle.FromDegrees(60 * ((b - r) / delta + 2)); + else if (cMax == b) h = Angle.FromDegrees(60 * ((r - g) / delta + 4)); + else h = Angle.Zero; + + double s = cMax == 0 ? 0 : delta / cMax; + return new ColorHSV(h, s, cMax); + } + public ColorCMYK AsCmyk() + { + // Thanks https://www.rapidtables.com/convert/color/rgb-to-cmyk.html + double diffK = MathE.Max(this), invDiffK = 1 / diffK; + return new ColorCMYK((diffK - r) * invDiffK, + (diffK - g) * invDiffK, + (diffK - b) * invDiffK, + 1 - diffK); + } + + public string HexCode() => $"#{(int)(r * 255):X2}{(int)(g * 255):X2}{(int)(b * 255):X2}"; public IEnumerator GetEnumerator() { @@ -307,8 +336,8 @@ namespace Nerd_STF.Graphics public bool Equals(ColorRGB other) { - if (a <= 0 && b <= 0) return true; - else return r == other.r && g == other.g && b == other.b; + if (a <= 0 && other.a <= 0) return true; + else return r == other.r && g == other.g && b == other.b && a == other.a; } public bool Equals(IColor other) => Equals(other.AsRgb()); #if CS8_OR_GREATER @@ -329,13 +358,11 @@ namespace Nerd_STF.Graphics ColorRGB copy = this; return i => copy[i]; } - public List ToList() => new List() { r, g, b, a }; + public List ToList() => new List { r, g, b, a }; - public static ColorRGB operator +(ColorRGB a) => a; - public static ColorRGB operator +(ColorRGB a, ColorRGB b) => new ColorRGB(a.r + b.r, a.g + b.g, a.b + b.b, 1 - (1 - a.a) * (1 - b.b)); - public static ColorRGB operator -(ColorRGB a) => new ColorRGB(1 - a.r, 1 - a.g, 1 - a.b); + public static ColorRGB operator +(ColorRGB a, ColorRGB b) => new ColorRGB(a.r + b.r, a.g + b.g, a.b + b.b, 1 - (1 - a.a) * (1 - b.a)); public static ColorRGB operator *(ColorRGB a, ColorRGB b) => new ColorRGB(a.r * b.r, a.g * b.g, a.b * b.b, a.a * b.a); - public static ColorRGB operator *(ColorRGB a, double b) => new ColorRGB(a.r * b, a.g * b, a.b * b); + public static ColorRGB operator *(ColorRGB a, double b) => new ColorRGB(a.r * b, a.g * b, a.b * b, a.a); public static bool operator ==(ColorRGB a, IColor b) => a.Equals(b.AsRgb()); public static bool operator !=(ColorRGB a, IColor b) => !a.Equals(b.AsRgb()); public static bool operator ==(ColorRGB a, ColorRGB b) => a.Equals(b); diff --git a/Nerd_STF/Graphics/IColor.cs b/Nerd_STF/Graphics/IColor.cs index 15529ac..d615de9 100644 --- a/Nerd_STF/Graphics/IColor.cs +++ b/Nerd_STF/Graphics/IColor.cs @@ -10,24 +10,27 @@ namespace Nerd_STF.Graphics TColor AsColor() where TColor : struct, IColor; ColorRGB AsRgb(); + ColorHSV AsHsv(); + ColorCMYK AsCmyk(); + + string HexCode(); bool Equals(IColor other); } public interface IColor : IColor, - IEquatable, - INumberGroup + IEquatable #if CS11_OR_GREATER - ,IColorPresets + ,IColorOperators, + IColorPresets, + IInterpolable #endif where TSelf : struct, IColor { #if CS11_OR_GREATER static abstract int ChannelCount { get; } - // TODO: Do all color formats have a gamma value? - static abstract TSelf Average(double gamma, IEnumerable colors); - static abstract TSelf Lerp(double gamma, TSelf a, TSelf b, double t, bool clamp = true); + static abstract TSelf Average(IEnumerable colors); #endif } } diff --git a/Nerd_STF/Graphics/IColorOperators.cs b/Nerd_STF/Graphics/IColorOperators.cs new file mode 100644 index 0000000..358ba57 --- /dev/null +++ b/Nerd_STF/Graphics/IColorOperators.cs @@ -0,0 +1,14 @@ +#if CS11_OR_GREATER +namespace Nerd_STF.Graphics +{ + public interface IColorOperators where TSelf : IColorOperators + { + 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); + static abstract bool operator ==(TSelf a, TSelf b); + static abstract bool operator !=(TSelf a, TSelf b); + } +} +#endif diff --git a/Nerd_STF/Graphics/IColorPresets.cs b/Nerd_STF/Graphics/IColorPresets.cs index afaba3d..a270a22 100644 --- a/Nerd_STF/Graphics/IColorPresets.cs +++ b/Nerd_STF/Graphics/IColorPresets.cs @@ -1,11 +1,7 @@ #if CS11_OR_GREATER -using System; -using System.Collections.Generic; -using System.Text; - namespace Nerd_STF.Graphics { - public interface IColorPresets where TSelf : struct, IColor, IColorPresets + public interface IColorPresets where TSelf : IColorPresets { static abstract TSelf Black { get; } static abstract TSelf Blue { get; } diff --git a/Nerd_STF/Helpers/TargetHelper.cs b/Nerd_STF/Helpers/TargetHelper.cs index a14afb7..45c5e1e 100644 --- a/Nerd_STF/Helpers/TargetHelper.cs +++ b/Nerd_STF/Helpers/TargetHelper.cs @@ -4,6 +4,14 @@ namespace Nerd_STF.Helpers { internal static class TargetHelper { + public static void WriteLine(string content) + { +#if NETSTANDARD1_1 +#else + Console.WriteLine(content); +#endif + } + public static T[] EmptyArray() { #if NETSTANDARD1_1 diff --git a/Nerd_STF/ListTuple.cs b/Nerd_STF/ListTuple.cs index 85994b4..77d3273 100644 --- a/Nerd_STF/ListTuple.cs +++ b/Nerd_STF/ListTuple.cs @@ -86,6 +86,12 @@ namespace Nerd_STF return builder.ToString(); } + public Fill ToFill() + { + T[] items = this.items; + return i => items[i]; + } + public static bool operator ==(ListTuple a, ListTuple b) => a.Equals(b); public static bool operator !=(ListTuple a, ListTuple b) => !a.Equals(b); @@ -105,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 { diff --git a/Nerd_STF/Mathematics/Angle.cs b/Nerd_STF/Mathematics/Angle.cs index 1ebd1d7..6049e15 100644 --- a/Nerd_STF/Mathematics/Angle.cs +++ b/Nerd_STF/Mathematics/Angle.cs @@ -9,6 +9,7 @@ namespace Nerd_STF.Mathematics IEquatable #if CS11_OR_GREATER ,IFromTuple, + IInterpolable, IPresets2d #endif { @@ -38,6 +39,11 @@ namespace Nerd_STF.Mathematics private readonly double revTheta; + public static Angle FromDegrees(double deg) => new Angle(deg, Units.Degrees); + public static Angle FromRadians(double rad) => new Angle(rad, Units.Radians); + public static Angle FromGradians(double grade) => new Angle(grade, Units.Gradians); + public static Angle FromRevolutions(double turns) => new Angle(turns, Units.Revolutions); + public Angle(double theta, Units unit) { switch (unit) @@ -127,6 +133,8 @@ namespace Nerd_STF.Mathematics } public static Angle Clamp(Angle value, Angle min, Angle max) => new Angle(MathE.Clamp(value.revTheta, min.revTheta, max.revTheta)); + public static Angle Lerp(Angle a, Angle b, double t, bool clamp = true) => + new Angle(MathE.Lerp(a.revTheta, b.revTheta, t, clamp)); public static Angle Max(IEnumerable values) => Max(false, values); public static Angle Max(bool normalize, IEnumerable values) { diff --git a/Nerd_STF/Mathematics/Float3.cs b/Nerd_STF/Mathematics/Float3.cs index 058122e..67dbf41 100644 --- a/Nerd_STF/Mathematics/Float3.cs +++ b/Nerd_STF/Mathematics/Float3.cs @@ -1,4 +1,5 @@ using Nerd_STF.Exceptions; +using Nerd_STF.Graphics; using System; using System.Collections; using System.Collections.Generic; @@ -127,7 +128,7 @@ namespace Nerd_STF.Mathematics total += val; count++; } - return total; + return total / count; } public static Int3 Ceiling(Float3 val) => new Int3(MathE.Ceiling(val.x), @@ -314,6 +315,7 @@ namespace Nerd_STF.Mathematics public static bool operator ==(Float3 a, Float3 b) => a.Equals(b); public static bool operator !=(Float3 a, Float3 b) => !a.Equals(b); + public static explicit operator Float3(ColorRGB color) => new Float3(color.r, color.g, color.b); public static implicit operator Float3(Float2 floats) => new Float3(floats.x, floats.y, 0); public static explicit operator Float3(Float4 floats) => new Float3(floats.x, floats.y, floats.z); public static implicit operator Float3(Int2 ints) => new Float3(ints.x, ints.y, 0); diff --git a/Nerd_STF/Mathematics/Float4.cs b/Nerd_STF/Mathematics/Float4.cs index a46513c..c56832a 100644 --- a/Nerd_STF/Mathematics/Float4.cs +++ b/Nerd_STF/Mathematics/Float4.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using Nerd_STF.Exceptions; +using Nerd_STF.Graphics; namespace Nerd_STF.Mathematics { @@ -136,7 +137,7 @@ namespace Nerd_STF.Mathematics total += val; count++; } - return total; + return total / count; } public static Int4 Ceiling(Float4 val) => new Int4(MathE.Ceiling(val.w), @@ -336,6 +337,7 @@ namespace Nerd_STF.Mathematics public static bool operator ==(Float4 a, Float4 b) => a.Equals(b); public static bool operator !=(Float4 a, Float4 b) => !a.Equals(b); + public static explicit operator Float4(ColorRGB color) => new Float4(color.a, color.r, color.g, color.b); public static implicit operator Float4(Int2 ints) => new Float4(0, ints.x, ints.y, 0); public static implicit operator Float4(Int3 ints) => new Float4(0, ints.x, ints.y, ints.z); public static implicit operator Float4(Int4 ints) => new Float4(ints.w, ints.x, ints.y, ints.z);