diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index a18836e5e..984457f08 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -71,6 +71,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs index 631b9a87b..d7a4c5573 100644 --- a/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs @@ -1,67 +1,186 @@ -using SkiaSharp; -using System.Collections.Generic; +using System; +using SkiaSharp; using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Numerics; -namespace Artemis.Core.Services +namespace Artemis.Core.Services; + +internal class ColorCube { - internal class ColorCube + #region Properties & Fields + + private readonly int _from; + private readonly int _length; + private SortTarget _currentOrder = SortTarget.None; + + #endregion + + #region Constructors + + public ColorCube(in Span fullColorList, int from, int length, SortTarget preOrdered) { - private readonly List _colors; + this._from = from; + this._length = length; - internal ColorCube(IEnumerable colors) + OrderColors(fullColorList.Slice(from, length), preOrdered); + } + + #endregion + + #region Methods + + private void OrderColors(in Span colors, SortTarget preOrdered) + { + if (colors.Length < 2) return; + ColorRanges colorRanges = GetColorRanges(colors); + + if ((colorRanges.RedRange > colorRanges.GreenRange) && (colorRanges.RedRange > colorRanges.BlueRange)) { - if (colors.Count() < 2) - { - _colors = colors.ToList(); - return; - } + if (preOrdered != SortTarget.Red) + RadixLikeSortRed.Sort(colors); - int redRange = colors.Max(c => c.Red) - colors.Min(c => c.Red); - int greenRange = colors.Max(c => c.Green) - colors.Min(c => c.Green); - int blueRange = colors.Max(c => c.Blue) - colors.Min(c => c.Blue); - - if (redRange > greenRange && redRange > blueRange) - _colors = colors.OrderBy(a => a.Red).ToList(); - else if (greenRange > blueRange) - _colors = colors.OrderBy(a => a.Green).ToList(); - else - _colors = colors.OrderBy(a => a.Blue).ToList(); + _currentOrder = SortTarget.Red; } - - internal bool TrySplit([NotNullWhen(returnValue: true)] out ColorCube? a, [NotNullWhen(returnValue: true)] out ColorCube? b) + else if (colorRanges.GreenRange > colorRanges.BlueRange) { - if (_colors.Count < 2) - { - a = null; - b = null; - return false; - } + if (preOrdered != SortTarget.Green) + RadixLikeSortGreen.Sort(colors); - int median = _colors.Count / 2; - - a = new ColorCube(_colors.GetRange(0, median)); - b = new ColorCube(_colors.GetRange(median, _colors.Count - median)); - - return true; + _currentOrder = SortTarget.Green; } - - internal SKColor GetAverageColor() + else { - int r = 0, g = 0, b = 0; + if (preOrdered != SortTarget.Blue) + RadixLikeSortBlue.Sort(colors); - for (int i = 0; i < _colors.Count; i++) - { - r += _colors[i].Red; - g += _colors[i].Green; - b += _colors[i].Blue; - } - - return new SKColor( - (byte)(r / _colors.Count), - (byte)(g / _colors.Count), - (byte)(b / _colors.Count) - ); + _currentOrder = SortTarget.Blue; } } + + private ColorRanges GetColorRanges(in Span colors) + { + if (colors.Length < 70) + { + byte redMin = byte.MaxValue; + byte redMax = byte.MinValue; + byte greenMin = byte.MaxValue; + byte greenMax = byte.MinValue; + byte blueMin = byte.MaxValue; + byte blueMax = byte.MinValue; + + foreach (SKColor color in colors) + { + if (color.Red < redMin) redMin = color.Red; + if (color.Red > redMax) redMax = color.Red; + if (color.Green < greenMin) greenMin = color.Green; + if (color.Green > greenMax) greenMax = color.Green; + if (color.Blue < blueMin) blueMin = color.Blue; + if (color.Blue > blueMax) blueMax = color.Blue; + } + + return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin)); + } + else + { + int elementsPerVector = Vector.Count / 3; + int chunks = colors.Length / elementsPerVector; + int missingElements = colors.Length - (chunks * elementsPerVector); + + Vector max = Vector.Zero; + Vector min = new(byte.MaxValue); + + Span chunkData = stackalloc byte[Vector.Count]; + int dataIndex = 0; + for (int i = 0; i < chunks; i++) + { + int chunkDataIndex = 0; + for (int j = 0; j < elementsPerVector; j++) + { + SKColor color = colors[dataIndex]; + chunkData[chunkDataIndex] = color.Red; + ++chunkDataIndex; + chunkData[chunkDataIndex] = color.Green; + ++chunkDataIndex; + chunkData[chunkDataIndex] = color.Blue; + ++chunkDataIndex; + ++dataIndex; + } + + Vector chunkVector = new(chunkData); + max = Vector.Max(max, chunkVector); + min = Vector.Min(min, chunkVector); + } + + byte redMin = byte.MaxValue; + byte redMax = byte.MinValue; + byte greenMin = byte.MaxValue; + byte greenMax = byte.MinValue; + byte blueMin = byte.MaxValue; + byte blueMax = byte.MinValue; + + int vectorEntries = elementsPerVector * 3; + for (int i = 0; i < vectorEntries; i += 3) + { + if (min[i] < redMin) redMin = min[i]; + if (max[i] > redMax) redMax = max[i]; + if (min[i + 1] < greenMin) greenMin = min[i + 1]; + if (max[i + 1] > greenMax) greenMax = max[i + 1]; + if (min[i + 2] < blueMin) blueMin = min[i + 2]; + if (max[i + 2] > blueMax) blueMax = max[i + 2]; + } + + for (int i = 0; i < missingElements; i++) + { + SKColor color = colors[dataIndex]; + if (color.Red < redMin) redMin = color.Red; + if (color.Red > redMax) redMax = color.Red; + if (color.Green < greenMin) greenMin = color.Green; + if (color.Green > greenMax) greenMax = color.Green; + if (color.Blue < blueMin) blueMin = color.Blue; + if (color.Blue > blueMax) blueMax = color.Blue; + + ++dataIndex; + } + + return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin)); + } + } + + internal bool TrySplit(in Span fullColorList, [NotNullWhen(returnValue: true)] out ColorCube? a, [NotNullWhen(returnValue: true)] out ColorCube? b) + { + Span colors = fullColorList.Slice(_from, _length); + + if (colors.Length < 2) + { + a = null; + b = null; + return false; + } + + int median = colors.Length / 2; + + a = new ColorCube(fullColorList, _from, median, _currentOrder); + b = new ColorCube(fullColorList, _from + median, colors.Length - median, _currentOrder); + + return true; + } + + internal SKColor GetAverageColor(in Span fullColorList) + { + Span colors = fullColorList.Slice(_from, _length); + + int r = 0, g = 0, b = 0; + foreach (SKColor color in colors) + { + r += color.Red; + g += color.Green; + b += color.Blue; + } + + return new SKColor((byte)(r / colors.Length), + (byte)(g / colors.Length), + (byte)(b / colors.Length)); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizer.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizer.cs new file mode 100644 index 000000000..ca14f8eeb --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizer.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using SkiaSharp; + +namespace Artemis.Core.Services; + +/// +/// A service providing a pallette of colors in a bitmap based on vibrant.js +/// +public static class ColorQuantizer +{ + #region Properties & Fields + + /// + /// Target luma for dark color variants. (see ) + /// + public static float TargetDarkLuma { get; set; } = 0.26f; + + /// + /// Maximum luma for dark color variants. (see ) + /// + public static float MaxDarkLuma { get; set; } = 0.45f; + + /// + /// Minimum luma for light color variants. (see ) + /// + public static float MinLightLuma { get; set; } = 0.55f; + + /// + /// Target luma for light color variants. (see ) + /// + public static float TargetLightLuma { get; set; } = 0.74f; + + /// + /// Minimum luma for normal color variants. (see ) + /// + public static float MinNormalLuma { get; set; } = 0.3f; + + /// + /// Target luma for normal color variants. (see ) + /// + public static float TargetNormalLuma { get; set; } = 0.5f; + + /// + /// Maximum luma for normal color variants. (see ) + /// + public static float MaxNormalLuma { get; set; } = 0.7f; + + /// + /// Target saturation for muted color variants. (see ) + /// + public static float TargetMutesSaturation { get; set; } = 0.3f; + + /// + /// Maximum saturation for muted color variants. (see ) + /// + public static float MaxMutesSaturation { get; set; } = 0.3f; + + /// + /// Target saturation for vibrant color variants. (see ) + /// + public static float TargetVibrantSaturation { get; set; } = 1.0f; + + /// + /// Minimum saturation for vibrant color variants. (see ) + /// + public static float MinVibrantSaturation { get; set; } = 0.35f; + + /// + /// Weight of the saturation value. + /// + public static float WeightSaturation { get; set; } = 3f; + + /// + /// Weight of the luma value. + /// + public static float WeightLuma { get; set; } = 5f; + + #endregion + + #region Methods + + /// + /// Reduces an to a given amount of relevant colors. Based on the Median Cut algorithm. + /// + /// The image to quantize. + /// The number of colors that should be calculated. Must be a power of two. + /// The quantized colors. + public static SKColor[] Quantize(in SKImage image, int amount = 32) + { + using SKBitmap bitmap = SKBitmap.FromImage(image); + return Quantize(bitmap.Pixels, amount); + } + + /// + /// Reduces an to a given amount of relevant colors. Based on the Median Cut algorithm. + /// + /// The colors to quantize. + /// The number of colors that should be calculated. Must be a power of two. + /// The quantized colors. + public static SKColor[] Quantize(in Span colors, int amount = 32) + { + if ((amount & (amount - 1)) != 0) + throw new ArgumentException("Must be power of two", nameof(amount)); + + Queue cubes = new(amount); + cubes.Enqueue(new ColorCube(colors, 0, colors.Length, SortTarget.None)); + + while (cubes.Count < amount) + { + ColorCube cube = cubes.Dequeue(); + + if (cube.TrySplit(colors, out ColorCube? a, out ColorCube? b)) + { + cubes.Enqueue(a); + cubes.Enqueue(b); + } + } + + SKColor[] result = new SKColor[cubes.Count]; + int i = 0; + foreach (ColorCube colorCube in cubes) + result[i++] = colorCube.GetAverageColor(colors); + + return result; + } + + /// + /// Finds colors with certain characteristics in a given . + /// Vibrant variants are more saturated, while Muted colors are less. + /// Light and Dark colors have higher and lower lightness values, respectively. + /// + /// The colors to find the variations in + /// Which type of color to find + /// Ignore hard limits on whether a color is considered for each category. Result may be if this is false + /// The color found + public static SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false) + { + SKColor bestColor = SKColor.Empty; + float bestColorScore = 0; + + foreach (SKColor color in colors) + { + float score = GetScore(color, type, ignoreLimits); + if (score > bestColorScore) + { + bestColorScore = score; + bestColor = color; + } + } + + return bestColor; + } + + /// + /// Finds all the color variations available and returns a struct containing them all. + /// + /// The colors to find the variations in + /// Ignore hard limits on whether a color is considered for each category. Some colors may be if this is false + /// A swatch containing all color variations + public static ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false) + { + SKColor bestVibrantColor = SKColor.Empty; + SKColor bestLightVibrantColor = SKColor.Empty; + SKColor bestDarkVibrantColor = SKColor.Empty; + SKColor bestMutedColor = SKColor.Empty; + SKColor bestLightMutedColor = SKColor.Empty; + SKColor bestDarkMutedColor = SKColor.Empty; + float bestVibrantScore = float.MinValue; + float bestLightVibrantScore = float.MinValue; + float bestDarkVibrantScore = float.MinValue; + float bestMutedScore = float.MinValue; + float bestLightMutedScore = float.MinValue; + float bestDarkMutedScore = float.MinValue; + + //ugly but at least we only loop through the enumerable once ¯\_(ツ)_/¯ + foreach (SKColor color in colors) + { + static void SetIfBetterScore(ref float bestScore, ref SKColor bestColor, SKColor newColor, ColorType type, bool ignoreLimits) + { + float newScore = GetScore(newColor, type, ignoreLimits); + if (newScore > bestScore) + { + bestScore = newScore; + bestColor = newColor; + } + } + + SetIfBetterScore(ref bestVibrantScore, ref bestVibrantColor, color, ColorType.Vibrant, ignoreLimits); + SetIfBetterScore(ref bestLightVibrantScore, ref bestLightVibrantColor, color, ColorType.LightVibrant, ignoreLimits); + SetIfBetterScore(ref bestDarkVibrantScore, ref bestDarkVibrantColor, color, ColorType.DarkVibrant, ignoreLimits); + SetIfBetterScore(ref bestMutedScore, ref bestMutedColor, color, ColorType.Muted, ignoreLimits); + SetIfBetterScore(ref bestLightMutedScore, ref bestLightMutedColor, color, ColorType.LightMuted, ignoreLimits); + SetIfBetterScore(ref bestDarkMutedScore, ref bestDarkMutedColor, color, ColorType.DarkMuted, ignoreLimits); + } + + return new ColorSwatch + { + Vibrant = bestVibrantColor, + LightVibrant = bestLightVibrantColor, + DarkVibrant = bestDarkVibrantColor, + Muted = bestMutedColor, + LightMuted = bestLightMutedColor, + DarkMuted = bestDarkMutedColor, + }; + } + + /// + /// Quantizes the image, finds all the color variations available and returns a struct containing them all. + /// + /// The image to quantize. + /// The number of colors that should be calculated. Must be a power of two. + /// Ignore hard limits on whether a color is considered for each category. Some colors may be if this is false + /// + public static ColorSwatch GetColorVariations(in SKImage image, int amount = 32, bool ignoreLimits = false) => FindAllColorVariations(Quantize(image, amount), ignoreLimits); + + /// + /// Quantizes the colors, finds all the color variations available and returns a struct containing them all. + /// + /// The colors to quantize. + /// The number of colors that should be calculated. Must be a power of two. + /// Ignore hard limits on whether a color is considered for each category. Some colors may be if this is false + /// + public static ColorSwatch GetColorVariations(in Span colors, int amount = 32, bool ignoreLimits = false) => FindAllColorVariations(Quantize(colors, amount), ignoreLimits); + + private static float GetScore(SKColor color, ColorType type, bool ignoreLimits = false) + { + static float InvertDiff(float value, float target) => 1 - Math.Abs(value - target); + + color.ToHsl(out float _, out float saturation, out float luma); + saturation /= 100f; + luma /= 100f; + + if (!ignoreLimits && ((saturation <= GetMinSaturation(type)) || (saturation >= GetMaxSaturation(type)) || (luma <= GetMinLuma(type)) || (luma >= GetMaxLuma(type)))) + { + //if either saturation or luma falls outside the min-max, return the + //lowest score possible unless we're ignoring these limits. + return float.MinValue; + } + + float totalValue = (InvertDiff(saturation, GetTargetSaturation(type)) * WeightSaturation) + (InvertDiff(luma, GetTargetLuma(type)) * WeightLuma); + float totalWeight = WeightSaturation + WeightLuma; + + return totalValue / totalWeight; + } + + private static float GetTargetLuma(ColorType colorType) => colorType switch + { + ColorType.Vibrant => TargetNormalLuma, + ColorType.LightVibrant => TargetLightLuma, + ColorType.DarkVibrant => TargetDarkLuma, + ColorType.Muted => TargetNormalLuma, + ColorType.LightMuted => TargetLightLuma, + ColorType.DarkMuted => TargetDarkLuma, + _ => throw new ArgumentException(nameof(colorType)) + }; + + private static float GetMinLuma(ColorType colorType) => colorType switch + { + ColorType.Vibrant => MinNormalLuma, + ColorType.LightVibrant => MinLightLuma, + ColorType.DarkVibrant => 0f, + ColorType.Muted => MinNormalLuma, + ColorType.LightMuted => MinLightLuma, + ColorType.DarkMuted => 0, + _ => throw new ArgumentException(nameof(colorType)) + }; + + private static float GetMaxLuma(ColorType colorType) => colorType switch + { + ColorType.Vibrant => MaxNormalLuma, + ColorType.LightVibrant => 1f, + ColorType.DarkVibrant => MaxDarkLuma, + ColorType.Muted => MaxNormalLuma, + ColorType.LightMuted => 1f, + ColorType.DarkMuted => MaxDarkLuma, + _ => throw new ArgumentException(nameof(colorType)) + }; + + private static float GetTargetSaturation(ColorType colorType) => colorType switch + { + ColorType.Vibrant => TargetVibrantSaturation, + ColorType.LightVibrant => TargetVibrantSaturation, + ColorType.DarkVibrant => TargetVibrantSaturation, + ColorType.Muted => TargetMutesSaturation, + ColorType.LightMuted => TargetMutesSaturation, + ColorType.DarkMuted => TargetMutesSaturation, + _ => throw new ArgumentException(nameof(colorType)) + }; + + private static float GetMinSaturation(ColorType colorType) => colorType switch + { + ColorType.Vibrant => MinVibrantSaturation, + ColorType.LightVibrant => MinVibrantSaturation, + ColorType.DarkVibrant => MinVibrantSaturation, + ColorType.Muted => 0, + ColorType.LightMuted => 0, + ColorType.DarkMuted => 0, + _ => throw new ArgumentException(nameof(colorType)) + }; + + private static float GetMaxSaturation(ColorType colorType) => colorType switch + { + ColorType.Vibrant => 1f, + ColorType.LightVibrant => 1f, + ColorType.DarkVibrant => 1f, + ColorType.Muted => MaxMutesSaturation, + ColorType.LightMuted => MaxMutesSaturation, + ColorType.DarkMuted => MaxMutesSaturation, + _ => throw new ArgumentException(nameof(colorType)) + }; + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs deleted file mode 100644 index e3aaa86af..000000000 --- a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SkiaSharp; - -namespace Artemis.Core.Services -{ - /// - internal class ColorQuantizerService : IColorQuantizerService - { - /// - public SKColor[] Quantize(IEnumerable colors, int amount) - { - if ((amount & (amount - 1)) != 0) - throw new ArgumentException("Must be power of two", nameof(amount)); - - Queue cubes = new(amount); - cubes.Enqueue(new ColorCube(colors)); - - while (cubes.Count < amount) - { - ColorCube cube = cubes.Dequeue(); - if (cube.TrySplit(out ColorCube? a, out ColorCube? b)) - { - cubes.Enqueue(a); - cubes.Enqueue(b); - } - } - - return cubes.Select(c => c.GetAverageColor()).ToArray(); - } - - /// - public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false) - { - float bestColorScore = 0; - SKColor bestColor = SKColor.Empty; - - foreach (SKColor clr in colors) - { - float score = GetScore(clr, type, ignoreLimits); - if (score > bestColorScore) - { - bestColorScore = score; - bestColor = clr; - } - } - - return bestColor; - } - - /// - public ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false) - { - SKColor bestVibrantColor = SKColor.Empty; - SKColor bestLightVibrantColor = SKColor.Empty; - SKColor bestDarkVibrantColor = SKColor.Empty; - SKColor bestMutedColor = SKColor.Empty; - SKColor bestLightMutedColor = SKColor.Empty; - SKColor bestDarkMutedColor = SKColor.Empty; - float bestVibrantScore = 0; - float bestLightVibrantScore = 0; - float bestDarkVibrantScore = 0; - float bestMutedScore = 0; - float bestLightMutedScore = 0; - float bestDarkMutedScore = 0; - - //ugly but at least we only loop through the enumerable once ¯\_(ツ)_/¯ - foreach (SKColor color in colors) - { - static void SetIfBetterScore(ref float bestScore, ref SKColor bestColor, SKColor newColor, ColorType type, bool ignoreLimits) - { - float newScore = GetScore(newColor, type, ignoreLimits); - if (newScore > bestScore) - { - bestScore = newScore; - bestColor = newColor; - } - } - - SetIfBetterScore(ref bestVibrantScore, ref bestVibrantColor, color, ColorType.Vibrant, ignoreLimits); - SetIfBetterScore(ref bestLightVibrantScore, ref bestLightVibrantColor, color, ColorType.LightVibrant, ignoreLimits); - SetIfBetterScore(ref bestDarkVibrantScore, ref bestDarkVibrantColor, color, ColorType.DarkVibrant, ignoreLimits); - SetIfBetterScore(ref bestMutedScore, ref bestMutedColor, color, ColorType.Muted, ignoreLimits); - SetIfBetterScore(ref bestLightMutedScore, ref bestLightMutedColor, color, ColorType.LightMuted, ignoreLimits); - SetIfBetterScore(ref bestDarkMutedScore, ref bestDarkMutedColor, color, ColorType.DarkMuted, ignoreLimits); - } - - return new ColorSwatch - { - Vibrant = bestVibrantColor, - LightVibrant = bestLightVibrantColor, - DarkVibrant = bestDarkVibrantColor, - Muted = bestMutedColor, - LightMuted = bestLightMutedColor, - DarkMuted = bestDarkMutedColor, - }; - } - - private static float GetScore(SKColor color, ColorType type, bool ignoreLimits = false) - { - static float InvertDiff(float value, float target) - { - return 1 - Math.Abs(value - target); - } - - color.ToHsl(out float _, out float saturation, out float luma); - saturation /= 100f; - luma /= 100f; - - if (!ignoreLimits && - (saturation <= GetMinSaturation(type) || saturation >= GetMaxSaturation(type) - || luma <= GetMinLuma(type) || luma >= GetMaxLuma(type))) - { - //if either saturation or luma falls outside the min-max, return the - //lowest score possible unless we're ignoring these limits. - return float.MinValue; - } - - float totalValue = (InvertDiff(saturation, GetTargetSaturation(type)) * weightSaturation) + - (InvertDiff(luma, GetTargetLuma(type)) * weightLuma); - - const float totalWeight = weightSaturation + weightLuma; - - return totalValue / totalWeight; - } - - #region Constants - - private const float targetDarkLuma = 0.26f; - private const float maxDarkLuma = 0.45f; - private const float minLightLuma = 0.55f; - private const float targetLightLuma = 0.74f; - private const float minNormalLuma = 0.3f; - private const float targetNormalLuma = 0.5f; - private const float maxNormalLuma = 0.7f; - private const float targetMutesSaturation = 0.3f; - private const float maxMutesSaturation = 0.3f; - private const float targetVibrantSaturation = 1.0f; - private const float minVibrantSaturation = 0.35f; - private const float weightSaturation = 3f; - private const float weightLuma = 5f; - - private static float GetTargetLuma(ColorType colorType) => colorType switch - { - ColorType.Vibrant => targetNormalLuma, - ColorType.LightVibrant => targetLightLuma, - ColorType.DarkVibrant => targetDarkLuma, - ColorType.Muted => targetNormalLuma, - ColorType.LightMuted => targetLightLuma, - ColorType.DarkMuted => targetDarkLuma, - _ => throw new ArgumentException(nameof(colorType)) - }; - - private static float GetMinLuma(ColorType colorType) => colorType switch - { - ColorType.Vibrant => minNormalLuma, - ColorType.LightVibrant => minLightLuma, - ColorType.DarkVibrant => 0f, - ColorType.Muted => minNormalLuma, - ColorType.LightMuted => minLightLuma, - ColorType.DarkMuted => 0, - _ => throw new ArgumentException(nameof(colorType)) - }; - - private static float GetMaxLuma(ColorType colorType) => colorType switch - { - ColorType.Vibrant => maxNormalLuma, - ColorType.LightVibrant => 1f, - ColorType.DarkVibrant => maxDarkLuma, - ColorType.Muted => maxNormalLuma, - ColorType.LightMuted => 1f, - ColorType.DarkMuted => maxDarkLuma, - _ => throw new ArgumentException(nameof(colorType)) - }; - - private static float GetTargetSaturation(ColorType colorType) => colorType switch - { - ColorType.Vibrant => targetVibrantSaturation, - ColorType.LightVibrant => targetVibrantSaturation, - ColorType.DarkVibrant => targetVibrantSaturation, - ColorType.Muted => targetMutesSaturation, - ColorType.LightMuted => targetMutesSaturation, - ColorType.DarkMuted => targetMutesSaturation, - _ => throw new ArgumentException(nameof(colorType)) - }; - - private static float GetMinSaturation(ColorType colorType) => colorType switch - { - ColorType.Vibrant => minVibrantSaturation, - ColorType.LightVibrant => minVibrantSaturation, - ColorType.DarkVibrant => minVibrantSaturation, - ColorType.Muted => 0, - ColorType.LightMuted => 0, - ColorType.DarkMuted => 0, - _ => throw new ArgumentException(nameof(colorType)) - }; - - private static float GetMaxSaturation(ColorType colorType) => colorType switch - { - ColorType.Vibrant => 1f, - ColorType.LightVibrant => 1f, - ColorType.DarkVibrant => 1f, - ColorType.Muted => maxMutesSaturation, - ColorType.LightMuted => maxMutesSaturation, - ColorType.DarkMuted => maxMutesSaturation, - _ => throw new ArgumentException(nameof(colorType)) - }; - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorRanges.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorRanges.cs new file mode 100644 index 000000000..7caa08b5e --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorRanges.cs @@ -0,0 +1,23 @@ +namespace Artemis.Core.Services; + +internal readonly struct ColorRanges +{ + #region Properties & Fields + + public readonly byte RedRange; + public readonly byte GreenRange; + public readonly byte BlueRange; + + #endregion + + #region Constructors + + public ColorRanges(byte redRange, byte greenRange, byte blueRange) + { + this.RedRange = redRange; + this.GreenRange = greenRange; + this.BlueRange = blueRange; + } + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs index 9efab68e3..f14fdc690 100644 --- a/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs @@ -1,38 +1,37 @@ -namespace Artemis.Core.Services +namespace Artemis.Core.Services; + +/// +/// The types of relevant colors in an image. +/// +public enum ColorType { /// - /// The types of relevant colors in an image. + /// Represents a saturated color. /// - public enum ColorType - { - /// - /// Represents a saturated color. - /// - Vibrant, + Vibrant, - /// - /// Represents a saturated and light color. - /// - LightVibrant, + /// + /// Represents a saturated and light color. + /// + LightVibrant, - /// - /// Represents a saturated and dark color. - /// - DarkVibrant, + /// + /// Represents a saturated and dark color. + /// + DarkVibrant, - /// - /// Represents a desaturated color. - /// - Muted, + /// + /// Represents a desaturated color. + /// + Muted, - /// - /// Represents a desaturated and light color. - /// - LightMuted, + /// + /// Represents a desaturated and light color. + /// + LightMuted, - /// - /// Represents a desaturated and dark color. - /// - DarkMuted - } + /// + /// Represents a desaturated and dark color. + /// + DarkMuted } \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs deleted file mode 100644 index 82d70715a..000000000 --- a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using SkiaSharp; -using System.Collections.Generic; - -namespace Artemis.Core.Services -{ - /// - /// A service providing a pallette of colors in a bitmap based on vibrant.js - /// - public interface IColorQuantizerService : IArtemisService - { - /// - /// Reduces an to a given amount of relevant colors. Based on the Median Cut algorithm - /// - /// The colors to quantize. - /// The number of colors that should be calculated. Must be a power of two. - /// The quantized colors. - public SKColor[] Quantize(IEnumerable colors, int amount); - - /// - /// Finds colors with certain characteristics in a given . - /// Vibrant variants are more saturated, while Muted colors are less. - /// Light and Dark colors have higher and lower lightness values, respectively. - /// - /// The colors to find the variations in - /// Which type of color to find - /// Ignore hard limits on whether a color is considered for each category. Result may be if this is false - /// The color found - public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false); - - /// - /// Finds all the color variations available and returns a struct containing them all. - /// - /// The colors to find the variations in - /// Ignore hard limits on whether a color is considered for each category. Some colors may be if this is false - /// A swatch containing all color variations - public ColorSwatch FindAllColorVariations(IEnumerable colors, bool ignoreLimits = false); - } -} diff --git a/src/Artemis.Core/Services/ColorQuantizer/SortTarget.cs b/src/Artemis.Core/Services/ColorQuantizer/SortTarget.cs new file mode 100644 index 000000000..c5b887759 --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/SortTarget.cs @@ -0,0 +1,7 @@ +namespace Artemis.Core.Services; + +internal enum SortTarget +{ + None, Red, Green, Blue +} + diff --git a/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortBlue.cs b/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortBlue.cs new file mode 100644 index 000000000..bb3bf1a9a --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortBlue.cs @@ -0,0 +1,46 @@ +using System; +using System.Buffers; +using SkiaSharp; + +namespace Artemis.Core.Services; + +internal class RadixLikeSortBlue +{ + #region Methods + + public static void Sort(in Span span) + { + Span counts = stackalloc int[256]; + foreach (SKColor t in span) + counts[t.Blue]++; + + SKColor[][] bucketsArray = ArrayPool.Shared.Rent(256); + Span buckets = bucketsArray.AsSpan(0, 256); + for (int i = 0; i < counts.Length; i++) + buckets[i] = ArrayPool.Shared.Rent(counts[i]); + + Span currentBucketIndex = stackalloc int[256]; + foreach (SKColor color in span) + { + int index = color.Blue; + SKColor[] bucket = buckets[index]; + int bucketIndex = currentBucketIndex[index]; + currentBucketIndex[index]++; + bucket[bucketIndex] = color; + } + + int newIndex = 0; + for (int i = 0; i < buckets.Length; i++) + { + Span bucket = buckets[i].AsSpan(0, counts[i]); + bucket.CopyTo(span.Slice(newIndex)); + newIndex += bucket.Length; + + ArrayPool.Shared.Return(buckets[i]); + } + + ArrayPool.Shared.Return(bucketsArray); + } + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortGreen.cs b/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortGreen.cs new file mode 100644 index 000000000..35b05bd09 --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortGreen.cs @@ -0,0 +1,46 @@ +using System; +using System.Buffers; +using SkiaSharp; + +namespace Artemis.Core.Services; + +internal class RadixLikeSortGreen +{ + #region Methods + + public static void Sort(in Span span) + { + Span counts = stackalloc int[256]; + foreach (SKColor t in span) + counts[t.Green]++; + + SKColor[][] bucketsArray = ArrayPool.Shared.Rent(256); + Span buckets = bucketsArray.AsSpan(0, 256); + for (int i = 0; i < counts.Length; i++) + buckets[i] = ArrayPool.Shared.Rent(counts[i]); + + Span currentBucketIndex = stackalloc int[256]; + foreach (SKColor color in span) + { + int index = color.Green; + SKColor[] bucket = buckets[index]; + int bucketIndex = currentBucketIndex[index]; + currentBucketIndex[index]++; + bucket[bucketIndex] = color; + } + + int newIndex = 0; + for (int i = 0; i < buckets.Length; i++) + { + Span bucket = buckets[i].AsSpan(0, counts[i]); + bucket.CopyTo(span.Slice(newIndex)); + newIndex += bucket.Length; + + ArrayPool.Shared.Return(buckets[i]); + } + + ArrayPool.Shared.Return(bucketsArray); + } + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortRed.cs b/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortRed.cs new file mode 100644 index 000000000..ab325c93f --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/Sorting/RadixLikeSortRed.cs @@ -0,0 +1,46 @@ +using System; +using System.Buffers; +using SkiaSharp; + +namespace Artemis.Core.Services; + +internal class RadixLikeSortRed +{ + #region Methods + + public static void Sort(in Span span) + { + Span counts = stackalloc int[256]; + foreach (SKColor t in span) + counts[t.Red]++; + + SKColor[][] bucketsArray = ArrayPool.Shared.Rent(256); + Span buckets = bucketsArray.AsSpan(0, 256); + for (int i = 0; i < counts.Length; i++) + buckets[i] = ArrayPool.Shared.Rent(counts[i]); + + Span currentBucketIndex = stackalloc int[256]; + foreach (SKColor color in span) + { + int index = color.Red; + SKColor[] bucket = buckets[index]; + int bucketIndex = currentBucketIndex[index]; + currentBucketIndex[index]++; + bucket[bucketIndex] = color; + } + + int newIndex = 0; + for (int i = 0; i < buckets.Length; i++) + { + Span bucket = buckets[i].AsSpan(0, counts[i]); + bucket.CopyTo(span.Slice(newIndex)); + newIndex += bucket.Length; + + ArrayPool.Shared.Return(buckets[i]); + } + + ArrayPool.Shared.Return(bucketsArray); + } + + #endregion +} \ No newline at end of file