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