From c08aea88637a97588168502a1587dfa669537a0d Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Sat, 7 Aug 2021 22:30:25 +0100 Subject: [PATCH] ColorQuantizer - Added FindAllColorVariations method --- .../ColorQuantizer/ColorQuantizerService.cs | 180 ++++++++++++++---- .../Services/ColorQuantizer/ColorSwatch.cs | 40 ++++ .../Interfaces/IColorQuantizerService.cs | 8 + 3 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs index 4fe7d22c4..f61b70dd5 100644 --- a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs @@ -8,21 +8,6 @@ namespace Artemis.Core.Services /// internal class ColorQuantizerService : IColorQuantizerService { - private static float GetComparisonValue(float sat, float targetSaturation, float luma, float targetLuma) - { - static float InvertDiff(float value, float target) - { - return 1 - Math.Abs(value - target); - } - - const float totalWeight = weightSaturation + weightLuma; - - float totalValue = InvertDiff(sat, targetSaturation) * weightSaturation + - InvertDiff(luma, targetLuma) * weightLuma; - - return totalValue / totalWeight; - } - /// public SKColor[] Quantize(IEnumerable colors, int amount) { @@ -48,29 +33,12 @@ namespace Artemis.Core.Services /// public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false) { - (float targetLuma, float minLuma, float maxLuma, float targetSaturation, float minSaturation, float maxSaturation) = type switch - { - ColorType.Vibrant => (targetNormalLuma, minNormalLuma, maxNormalLuma, targetVibrantSaturation, minVibrantSaturation, 1f), - ColorType.LightVibrant => (targetLightLuma, minLightLuma, 1f, targetVibrantSaturation, minVibrantSaturation, 1f), - ColorType.DarkVibrant => (targetDarkLuma, 0f, maxDarkLuma, targetVibrantSaturation, minVibrantSaturation, 1f), - ColorType.Muted => (targetNormalLuma, minNormalLuma, maxNormalLuma, targetMutesSaturation, 0, maxMutesSaturation), - ColorType.LightMuted => (targetLightLuma, minLightLuma, 1f, targetMutesSaturation, 0, maxMutesSaturation), - ColorType.DarkMuted => (targetDarkLuma, 0, maxDarkLuma, targetMutesSaturation, 0, maxMutesSaturation), - _ => (0.5f, 0f, 1f, 0.5f, 0f, 1f) - }; - - float bestColorScore = float.MinValue; + float bestColorScore = 0; SKColor bestColor = SKColor.Empty; + foreach (SKColor clr in colors) { - clr.ToHsl(out float _, out float sat, out float luma); - sat /= 100f; - luma /= 100f; - - if (!ignoreLimits && (sat <= minSaturation || sat >= maxSaturation || luma <= minLuma || luma >= maxLuma)) - continue; - - float score = GetComparisonValue(sat, targetSaturation, luma, targetLuma); + float score = GetScore(clr, type, ignoreLimits); if (score > bestColorScore) { bestColorScore = score; @@ -81,6 +49,82 @@ namespace Artemis.Core.Services 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 (var 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() + { + 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; @@ -97,6 +141,72 @@ namespace Artemis.Core.Services 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/ColorSwatch.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs new file mode 100644 index 000000000..81abb7cc2 --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorSwatch.cs @@ -0,0 +1,40 @@ +using SkiaSharp; + +namespace Artemis.Core.Services +{ + /// + /// Swatch containing the known useful color variations. + /// + public struct ColorSwatch + { + /// + /// The component. + /// + public SKColor Vibrant { get; init; } + + /// + /// The component. + /// + public SKColor LightVibrant { get; init; } + + /// + /// The component. + /// + public SKColor DarkVibrant { get; init; } + + /// + /// The component. + /// + public SKColor Muted { get; init; } + + /// + /// The component. + /// + public SKColor LightMuted { get; init; } + + /// + /// The component. + /// + public SKColor DarkMuted { get; init; } + } +} diff --git a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs index 6b0971684..82d70715a 100644 --- a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs +++ b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs @@ -26,5 +26,13 @@ namespace Artemis.Core.Services /// 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); } }