diff --git a/components/ColorAnalyzer/samples/ColorAnalyzer.Samples.csproj b/components/ColorAnalyzer/samples/ColorAnalyzer.Samples.csproj index 57dfffe41..724def1ee 100644 --- a/components/ColorAnalyzer/samples/ColorAnalyzer.Samples.csproj +++ b/components/ColorAnalyzer/samples/ColorAnalyzer.Samples.csproj @@ -18,4 +18,7 @@ PreserveNewest + + + diff --git a/components/ColorAnalyzer/samples/AccentAnalyzerSample.xaml b/components/ColorAnalyzer/samples/ColorPaletteSampler/AccentAnalyzerSample.xaml similarity index 65% rename from components/ColorAnalyzer/samples/AccentAnalyzerSample.xaml rename to components/ColorAnalyzer/samples/ColorPaletteSampler/AccentAnalyzerSample.xaml index 05628bc19..2b7f841c0 100644 --- a/components/ColorAnalyzer/samples/AccentAnalyzerSample.xaml +++ b/components/ColorAnalyzer/samples/ColorPaletteSampler/AccentAnalyzerSample.xaml @@ -1,4 +1,4 @@ - + - + + + + + + @@ -23,19 +34,17 @@ - - + - - + - @@ -72,14 +81,14 @@ Margin="4" Padding="2"> - + - + + helpers:ContrastHelper.Opponent="{x:Bind BasePalette.SelectedColors[0], FallbackValue=Transparent, Mode=OneWay}" + Color="{x:Bind AccentPalette.SelectedColors[0], FallbackValue=Transparent, Mode=OneWay}" /> @@ -90,9 +99,9 @@ Margin="4" Padding="2"> - + - @@ -101,9 +110,9 @@ Margin="4" Padding="2"> - + - @@ -112,9 +121,9 @@ Margin="4" Padding="2"> - + - @@ -125,9 +134,9 @@ - - - + + + diff --git a/components/ColorAnalyzer/samples/AccentAnalyzerSample.xaml.cs b/components/ColorAnalyzer/samples/ColorPaletteSampler/AccentAnalyzerSample.xaml.cs similarity index 100% rename from components/ColorAnalyzer/samples/AccentAnalyzerSample.xaml.cs rename to components/ColorAnalyzer/samples/ColorPaletteSampler/AccentAnalyzerSample.xaml.cs diff --git a/components/ColorAnalyzer/samples/ImageOptionsPane.xaml b/components/ColorAnalyzer/samples/ColorPaletteSampler/ImageOptionsPane.xaml similarity index 100% rename from components/ColorAnalyzer/samples/ImageOptionsPane.xaml rename to components/ColorAnalyzer/samples/ColorPaletteSampler/ImageOptionsPane.xaml diff --git a/components/ColorAnalyzer/samples/ImageOptionsPane.xaml.cs b/components/ColorAnalyzer/samples/ColorPaletteSampler/ImageOptionsPane.xaml.cs similarity index 95% rename from components/ColorAnalyzer/samples/ImageOptionsPane.xaml.cs rename to components/ColorAnalyzer/samples/ColorPaletteSampler/ImageOptionsPane.xaml.cs index f54f1eedc..04a29d7c3 100644 --- a/components/ColorAnalyzer/samples/ImageOptionsPane.xaml.cs +++ b/components/ColorAnalyzer/samples/ColorPaletteSampler/ImageOptionsPane.xaml.cs @@ -39,6 +39,6 @@ private void GridView_ItemClick(object sender, ItemClickEventArgs e) private void SetImage(Uri uri) { - _sample.AccentedImage.Source = new BitmapImage(uri); + _sample.SampledImage.Source = new BitmapImage(uri); } } diff --git a/components/ColorAnalyzer/samples/ContrastHelperSample.xaml b/components/ColorAnalyzer/samples/ContrastHelper/ContrastHelperSample.xaml similarity index 100% rename from components/ColorAnalyzer/samples/ContrastHelperSample.xaml rename to components/ColorAnalyzer/samples/ContrastHelper/ContrastHelperSample.xaml diff --git a/components/ColorAnalyzer/samples/ContrastHelperSample.xaml.cs b/components/ColorAnalyzer/samples/ContrastHelper/ContrastHelperSample.xaml.cs similarity index 100% rename from components/ColorAnalyzer/samples/ContrastHelperSample.xaml.cs rename to components/ColorAnalyzer/samples/ContrastHelper/ContrastHelperSample.xaml.cs diff --git a/components/ColorAnalyzer/samples/ContrastOptionsPane.xaml b/components/ColorAnalyzer/samples/ContrastHelper/ContrastOptionsPane.xaml similarity index 100% rename from components/ColorAnalyzer/samples/ContrastOptionsPane.xaml rename to components/ColorAnalyzer/samples/ContrastHelper/ContrastOptionsPane.xaml diff --git a/components/ColorAnalyzer/samples/ContrastOptionsPane.xaml.cs b/components/ColorAnalyzer/samples/ContrastHelper/ContrastOptionsPane.xaml.cs similarity index 100% rename from components/ColorAnalyzer/samples/ContrastOptionsPane.xaml.cs rename to components/ColorAnalyzer/samples/ContrastHelper/ContrastOptionsPane.xaml.cs diff --git a/components/ColorAnalyzer/src/AccentAnalyzer.Properties.cs b/components/ColorAnalyzer/src/AccentAnalyzer.Properties.cs deleted file mode 100644 index 1e48b16f9..000000000 --- a/components/ColorAnalyzer/src/AccentAnalyzer.Properties.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Windows.Input; -using Windows.UI; - -namespace CommunityToolkit.WinUI.Helpers; - -public partial class AccentAnalyzer -{ - /// - /// Gets the for the property. - /// - public static readonly DependencyProperty SourceProperty = - DependencyProperty.Register(nameof(Source), typeof(UIElement), typeof(AccentAnalyzer), new PropertyMetadata(null, OnSourceChanged)); - - /// - /// Gets the for the property. - /// - public static readonly DependencyProperty PrimaryAccentColorProperty = - DependencyProperty.Register(nameof(PrimaryAccentColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent)); - - /// - /// Gets the for the property. - /// - public static readonly DependencyProperty SecondaryAccentColorProperty = - DependencyProperty.Register(nameof(SecondaryAccentColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent)); - - /// - /// Gets the for the property. - /// - public static readonly DependencyProperty TertiaryAccentColorProperty = - DependencyProperty.Register(nameof(TertiaryAccentColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent)); - - /// - /// Gets the for the property. - /// - public static readonly DependencyProperty BaseColorProperty = - DependencyProperty.Register(nameof(BaseColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent)); - - /// - /// Gets the for the property. - /// - public static readonly DependencyProperty DominantColorProperty = - DependencyProperty.Register(nameof(DominantColor), typeof(Color), typeof(AccentAnalyzer), new PropertyMetadata(Colors.Transparent)); - - /// - /// Gets the for the property. - /// - public static readonly DependencyProperty ColorfulnessProperty = - DependencyProperty.Register(nameof(Colorfulness), typeof(float), typeof(AccentAnalyzer), new PropertyMetadata(0f)); - - /// - /// An event fired when the accent properties are updated. - /// - public event EventHandler? AccentsUpdated; - - /// - /// Gets or sets the source for accent color analysis. - /// - public UIElement? Source - { - get => (UIElement)GetValue(SourceProperty); - set => SetValue(SourceProperty, value); - } - - /// - /// Gets the primary accent color as extracted from the . - /// - /// - /// The most "colorful" found in the image. - /// - public Color PrimaryAccentColor - { - get => (Color)GetValue(PrimaryAccentColorProperty); - protected set => SetValue(PrimaryAccentColorProperty, value); - } - - /// - /// Gets the secondary accent color as extracted from the . - /// - /// - /// The second most "colorful" color found in the image. - /// - public Color SecondaryAccentColor - { - get => (Color)GetValue(SecondaryAccentColorProperty); - protected set => SetValue(SecondaryAccentColorProperty, value); - } - - /// - /// Gets the tertiary accent color as extracted from the . - /// - /// - /// The third most "colorful" color found in the image. - /// - public Color TertiaryAccentColor - { - get => (Color)GetValue(TertiaryAccentColorProperty); - protected set => SetValue(TertiaryAccentColorProperty, value); - } - - /// - /// Gets the base color as extracted from the . - /// - /// - /// The least "colorful" color found in the image. - /// - public Color BaseColor - { - get => (Color)GetValue(BaseColorProperty); - protected set => SetValue(BaseColorProperty, value); - } - - /// - /// Gets the dominant color as extracted from the . - /// - /// - /// The color that takes up the most of the image. - /// - public Color DominantColor - { - get => (Color)GetValue(DominantColorProperty); - protected set => SetValue(DominantColorProperty, value); - } - - /// - /// Gets the "colorfulness" of the . - /// - /// - /// Colorfulness is defined by David Hasler and Sabine Susstrunk's paper on measuring colorfulness - /// . - /// - /// An image with colors of high saturation and value will have a high colorfulness (around 1), - /// meanwhile images that are mostly gray or white will have a low colorfulness (around 0). - /// - public float Colorfulness - { - get => (float)GetValue(ColorfulnessProperty); - private set => SetValue(ColorfulnessProperty, value); - } - - /// - /// Gets the set of extracted on last update. - /// - public IReadOnlyList? AccentColors { get; private set; } - - private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not AccentAnalyzer analyzer) - return; - - _ = analyzer.UpdateAccentAsync(); - } -} diff --git a/components/ColorAnalyzer/src/AccentColorInfo.cs b/components/ColorAnalyzer/src/AccentColorInfo.cs deleted file mode 100644 index a67f0ed8e..000000000 --- a/components/ColorAnalyzer/src/AccentColorInfo.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Numerics; -using Windows.UI; - -namespace CommunityToolkit.WinUI.Helpers; - -/// -/// A struct containing accent color info. -/// -public readonly struct AccentColorInfo -{ - internal AccentColorInfo(Vector3 rgb, float prominence) - { - Colorfulness = AccentAnalyzer.FindColorfulness(rgb); - - rgb *= byte.MaxValue; - Color = Color.FromArgb(byte.MaxValue, (byte)rgb.X, (byte)rgb.Y, (byte)rgb.Z); - Prominence = prominence; - } - - /// - /// Gets the of the accent color. - /// - public Color Color { get; } - - /// - /// Gets the colorfulness index of the accent color. - /// - /// - /// The exact definition of colorfulness is defined by David Hasler and Sabine Susstrunk's paper on measuring colorfulness - /// . - /// - /// Colors of high saturation and value will have a high colorfulness (around 1), - /// while colors that are mostly gray or white will have a low colorfulness (around 0). - /// - public float Colorfulness { get; } - - /// - /// Gets the prominence of the accent color in the sampled image. - /// - public float Prominence { get; } -} diff --git a/components/ColorAnalyzer/src/ColorExtensions.cs b/components/ColorAnalyzer/src/ColorExtensions.cs new file mode 100644 index 000000000..2f6cc1db7 --- /dev/null +++ b/components/ColorAnalyzer/src/ColorExtensions.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +internal static class ColorExtensions +{ + internal static Color ToColor(this Vector3 color) + { + color *= 255; + return Color.FromArgb(255, (byte)(color.X), (byte)(color.Y), (byte)(color.Z)); + } + + internal static Vector3 ToVector3(this Color color) + { + var vector = new Vector3(color.R, color.G, color.B); + return vector / 255; + } + + /// + /// Get WCAG contrast ratio between two colors. + /// + internal static double ContrastRatio(this Color color1, Color color2) + { + // Using the formula for contrast ratio + // Source WCAG guidelines: https://www.w3.org/TR/WCAG20/#contrast-ratiodef + + // Calculate perceived luminance for both colors + double luminance1 = color1.PerceivedLuminance(); + double luminance2 = color2.PerceivedLuminance(); + + // Determine lighter and darker luminance + double lighter = Math.Max(luminance1, luminance2); + double darker = Math.Min(luminance1, luminance2); + + // Calculate contrast ratio + return (lighter + 0.05f) / (darker + 0.05f); + } + + internal static double PerceivedLuminance(this Color color) + { + // Color theory is a massive iceberg. Here's a peek at the tippy top: + + // There's two (main) standards for calculating luminance from RGB values. + + // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + + // | Standard | Formula | Ref. Section | Ref. Link | + // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + + // | ITU Rec. 709 | Y = 0.2126 R + 0.7152 G + 0.0722 B | Page 4/Item 3.2 | https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf | + // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + + // | ITU Rec. 601 | Y = 0.299 R + 0.587 G + 0.114 B | Page 2/Item 2.5.1 | https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf | + // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + + + // They're based on the standard ability of the human eye to perceive brightness, + // from different colors, as well as the average monitor's ability to produce them. + // Both standards produce similar results, but Rec. 709 is more accurate for modern displays. + + // NOTE: If we for whatever reason we ever need to optimize this code, + // we can make approximations using integer math instead of floating point math. + // The precise values are not critical, as long as the relative luminance is accurate. + // Like so: return (2 * color.R + 7 * color.G + color.B); + + // TLDR: We use ITU Rec. 709 standard formula for perceived luminance. + return (0.2126f * color.R + 0.7152f * color.G + 0.0722 * color.B) / 255; + } + + internal static float FindColorfulness(this Color color) + { + var vectorColor = color.ToVector3(); + var rg = vectorColor.X - vectorColor.Y; + var yb = ((vectorColor.X + vectorColor.Y) / 2) - vectorColor.Z; + return 0.3f * new Vector2(rg, yb).Length(); + } + + internal static float FindColorfulness(this Color[] colors) + { + var vectorColors = colors.Select(ToVector3); + + // Isolate rg and yb + var rg = vectorColors.Select(x => Math.Abs(x.X - x.Y)); + var yb = vectorColors.Select(x => Math.Abs(0.5f * (x.X + x.Y) - x.Z)); + + // Evaluate rg and yb mean and std + var rg_std = FindStandardDeviation(rg, out var rg_mean); + var yb_std = FindStandardDeviation(yb, out var yb_mean); + + // Combine means and standard deviations + var std = new Vector2(rg_mean, yb_mean).Length(); + var mean = new Vector2(rg_std, yb_std).Length(); + + // Return colorfulness + return std + (0.3f * mean); + } + + private static float FindStandardDeviation(IEnumerable data, out float avg) + { + var average = data.Average(); + avg = average; + var sumOfSquares = data.Select(x => (x - average) * (x - average)).Sum(); + return (float)Math.Sqrt(sumOfSquares / data.Count()); + } +} diff --git a/components/ColorAnalyzer/src/AccentAnalyzer.Clustering.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.Clustering.cs similarity index 79% rename from components/ColorAnalyzer/src/AccentAnalyzer.Clustering.cs rename to components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.Clustering.cs index e0c4cb370..e6a17bfe0 100644 --- a/components/ColorAnalyzer/src/AccentAnalyzer.Clustering.cs +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.Clustering.cs @@ -6,7 +6,7 @@ namespace CommunityToolkit.WinUI.Helpers; -public partial class AccentAnalyzer +public partial class ColorPaletteSampler { private static Vector3[] KMeansCluster(Span points, int k, out int[] counts) { @@ -121,7 +121,7 @@ private static void CalculateCentroidsAndPrune(ref Span centroids, ref } /// - /// Finds the index of the centroid nearest the point + /// Finds the index of the centroid nearest the point. /// private static int FindNearestClusterIndex(Vector3 point, Span centroids) { @@ -146,37 +146,4 @@ private static int FindNearestClusterIndex(Vector3 point, Span centroid return nearestIndex; } - - internal static float FindColorfulness(Vector3 color) - { - var rg = color.X - color.Y; - var yb = ((color.X + color.Y) / 2) - color.Z; - return 0.3f * new Vector2(rg, yb).Length(); - } - - internal static float FindColorfulness(Vector3[] colors) - { - // Isolate rg and yb - var rg = colors.Select(x => Math.Abs(x.X - x.Y)); - var yb = colors.Select(x => Math.Abs(0.5f * (x.X + x.Y) - x.Z)); - - // Evaluate rg and yb mean and std - var rg_std = FindStandardDeviation(rg, out var rg_mean); - var yb_std = FindStandardDeviation(yb, out var yb_mean); - - // Combine means and standard deviations - var std = new Vector2(rg_mean, yb_mean).Length(); - var mean = new Vector2(rg_std, yb_std).Length(); - - // Return colorfulness - return std + (0.3f * mean); - } - - private static float FindStandardDeviation(IEnumerable data, out float avg) - { - var average = data.Average(); - avg = average; - var sumOfSquares = data.Select(x => (x - average) * (x - average)).Sum(); - return (float)Math.Sqrt(sumOfSquares / data.Count()); - } } diff --git a/components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.Properties.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.Properties.cs new file mode 100644 index 000000000..488d46365 --- /dev/null +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.Properties.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Helpers; + +public partial class ColorPaletteSampler +{ + /// + /// Gets the for the property. + /// + public static readonly DependencyProperty SourceProperty = + DependencyProperty.Register(nameof(Source), typeof(UIElement), typeof(ColorPaletteSampler), new PropertyMetadata(null, OnSourceChanged)); + + /// + /// An event fired when the and are updated. + /// + public event EventHandler? PaletteUpdated; + + /// + /// Gets or sets the source sampled for a color palette. + /// + public UIElement? Source + { + get => (UIElement)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + /// + /// The list of to update when the is set or changed. + /// + public IList PaletteSelectors { get; set; } + + /// + /// Gets the set of extracted on last update. + /// + /// + /// The palette is the set of colors extracted from the element, and + /// the fraction of the image that each covers. + /// + public IReadOnlyList? Palette { get; private set; } + + private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not ColorPaletteSampler analyzer) + return; + + _ = analyzer.UpdatePaletteAsync(); + } +} diff --git a/components/ColorAnalyzer/src/AccentAnalyzer.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.cs similarity index 56% rename from components/ColorAnalyzer/src/AccentAnalyzer.cs rename to components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.cs index d5a22f2a1..bfa4b68ab 100644 --- a/components/ColorAnalyzer/src/AccentAnalyzer.cs +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/ColorPaletteSampler.cs @@ -13,31 +13,43 @@ using System.Numerics; using System.Windows.Input; +using Windows.UI; namespace CommunityToolkit.WinUI.Helpers; /// /// A resource that can be used to extract color palettes out of any . /// -public partial class AccentAnalyzer : DependencyObject +[ContentProperty(Name = nameof(PaletteSelectors))] +public partial class ColorPaletteSampler : DependencyObject { /// - /// Initialize an instance of the class. + /// Initialize an instance of the class. /// - public AccentAnalyzer() + public ColorPaletteSampler() { + PaletteSelectors = []; } - /// - /// Update the accent - /// - public void UpdateAccent() + /// + /// + /// Runs the async palette update method, without awaiting it. + /// + public void UpdatePalette() { - _ = UpdateAccentAsync(); + _ = UpdatePaletteAsync(); } - private async Task UpdateAccentAsync() + /// + /// Updates the and by sampling the element. + /// + public async Task UpdatePaletteAsync() { + // No palettes to update. + // Skip a lot of unnecessary computation + if (PaletteSelectors.Count is 0) + return; + const int sampleCount = 4096; const int k = 8; @@ -49,80 +61,31 @@ private async Task UpdateAccentAsync() return; // Cluster samples in RGB floating-point color space - // With Euclidean Squared distance function - // The accumulate accent color infos + // With Euclidean Squared distance function, then construct palette data. var clusters = KMeansCluster(samples, k, out var sizes); - var colorData = clusters - .Select((color, i) => new AccentColorInfo(color, (float)sizes[i] / samples.Length)); - - // Evaluate colorfulness - // TODO: Should this be weighted by cluster sizes? - var overallColorfulness = FindColorfulness(clusters); + var colorData = clusters.Select((vectorColor, i) => new PaletteColor(vectorColor.ToColor(), (float)sizes[i] / samples.Length)); - // Select accent colors - SelectAccentColors(colorData, overallColorfulness); + // Update palettes on the UI thread + foreach (var palette in PaletteSelectors) + { + DispatcherQueue.GetForCurrentThread().TryEnqueue(() => + { + palette.SelectColors(colorData); + }); + } - // Update accent colors property + // Update palette property // Not a dependency property, so no need to update from the UI Thread #if !WINDOWS_UWP - AccentColors = [..colorData]; -#else - AccentColors = colorData.ToList(); -#endif - - // Update the colorfulness and invoke accents updated event, - // both from the UI thread - DispatcherQueue.GetForCurrentThread().TryEnqueue(() => - { - Colorfulness = overallColorfulness; - AccentsUpdated?.Invoke(this, EventArgs.Empty); - }); - } - - /// - /// This method takes the processed color information and selects the accent colors from it. - /// - /// - /// There is no guarentee that this method will be called from the UI Thread. - /// Dependency properties should be updated using a dispatcher. - /// - /// The analyzed accent color info from the image. - /// The overall colorfulness of the image. - protected virtual void SelectAccentColors(IEnumerable colorData, float imageColorfulness) - { - // Select accent colors - var accentColors = colorData - .OrderByDescending(x => x.Colorfulness) - .Select(x => x.Color); - - // Get primary/secondary/tertiary accents - var primary = accentColors.First(); - var secondary = accentColors.ElementAtOrDefault(1); - secondary = secondary != default ? secondary : primary; - var tertiary = accentColors.ElementAtOrDefault(2); - tertiary = tertiary != default ? tertiary : secondary; - - // Get base color - var baseColor = accentColors.Last(); - - // Get dominant color by prominence -#if NET6_0_OR_GREATER - var dominantColor = colorData - .MaxBy(x => x.Prominence).Color; + Palette = [..colorData]; #else - var dominantColor = colorData - .OrderByDescending((x) => x.Prominence) - .First().Color; + Palette = colorData.ToList(); #endif - // Batch update the dependency properties in the UI Thread + // Invoke palette updated event from the UI thread DispatcherQueue.GetForCurrentThread().TryEnqueue(() => { - PrimaryAccentColor = primary; - SecondaryAccentColor = secondary; - TertiaryAccentColor = tertiary; - BaseColor = baseColor; - DominantColor = dominantColor; + PaletteUpdated?.Invoke(this, EventArgs.Empty); }); } diff --git a/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/AccentColorPaletteSelector.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/AccentColorPaletteSelector.cs new file mode 100644 index 000000000..c9acedb8c --- /dev/null +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/AccentColorPaletteSelector.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// A based on the three most "colorful" colors. +/// +public class AccentColorPaletteSelector : ColorPaletteSelector +{ + /// + public override void SelectColors(IEnumerable palette) + { + // Select accent colors + SelectedColors = palette + .Select(x => x.Color) + .OrderByDescending(ColorExtensions.FindColorfulness) + .ToList() + .EnsureMinColorCount(MinColorCount); + } +} diff --git a/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/BaseColorPaletteSelector.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/BaseColorPaletteSelector.cs new file mode 100644 index 000000000..81ee9e3ff --- /dev/null +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/BaseColorPaletteSelector.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// A based on the least "colorful" color. +/// +public class BaseColorPaletteSelector : ColorPaletteSelector +{ + /// + public override void SelectColors(IEnumerable palettes) + { + // Get base color + SelectedColors = palettes + .Select(x => x.Color) + .OrderBy(ColorExtensions.FindColorfulness) + .ToList() + .EnsureMinColorCount(MinColorCount); + } +} diff --git a/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorPaletteSelector.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorPaletteSelector.cs new file mode 100644 index 000000000..13015d8f8 --- /dev/null +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorPaletteSelector.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// A base class for selecting colors from a palette extracted by the . +/// +public abstract class ColorPaletteSelector : DependencyObject +{ + private IEnumerable? _palette; + + /// + /// An attached property that defines the of colors selected from the palette. + /// + public static readonly DependencyProperty SelectedColorsProperty = + DependencyProperty.Register( + nameof(SelectedColors), + typeof(IList), + typeof(ColorPaletteSelector), + new PropertyMetadata(null)); + + /// + /// An attached property that defines the minimum number of colors permitted to select from the palette. + /// + public static readonly DependencyProperty MinColorCountProperty = + DependencyProperty.Register( + nameof(MinColorCount), + typeof(int), + typeof(ColorPaletteSelector), + new PropertyMetadata(1, OnMinColorCountChanged)); + + /// + /// Gets the list of colors selected from the palette. + /// + public IList? SelectedColors + { + get => (IList?)GetValue(SelectedColorsProperty); + protected set => SetValue(SelectedColorsProperty, value); + } + + /// + /// Gets or sets the minimum number of colors permitted to select from the palette. + /// + public int MinColorCount + { + get => (int)GetValue(MinColorCountProperty); + set => SetValue(MinColorCountProperty, value); + } + + /// + /// Selects a set of colors from a palette to create a sub-group. + /// + /// The color info extracted by the . + public virtual void SelectColors(IEnumerable palette) + { + _palette = palette; + } + + private static void OnMinColorCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not ColorPaletteSelector selector || selector._palette is null) + return; + + selector.SelectColors(selector._palette); + } +} diff --git a/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorPaletteSelectorExtensions.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorPaletteSelectorExtensions.cs new file mode 100644 index 000000000..97c6a5db5 --- /dev/null +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorPaletteSelectorExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// Extension methods for . +/// +public static class ColorPaletteSelectorExtensions +{ + /// + /// Extends the list of colors to ensure it meets the minimum count by repeating the th color. + /// + /// The list of colors to extend + /// The minimum number of colors required + /// The index of the item to repeat + public static IList EnsureMinColorCount(this IList colors, int minCount, int index = 0) + { + // If we already have enough colors, do nothing. + if (colors.Count >= minCount) + return colors; + + var nthColor = colors[index]; + while (colors.Count < minCount) + { + colors.Add(nthColor); + } + + return colors; + } +} diff --git a/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorWeightPaletteSelector.cs b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorWeightPaletteSelector.cs new file mode 100644 index 000000000..40926dcf5 --- /dev/null +++ b/components/ColorAnalyzer/src/ColorPaletteSampler/PaletteSelectors/ColorWeightPaletteSelector.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// A based on the three most prominent colors. +/// +public class ColorWeightPaletteSelector : ColorPaletteSelector +{ + /// + public override void SelectColors(IEnumerable colors) + { + // Order by weight and ensure we have at least MinColorCount colors + SelectedColors = colors + .OrderByDescending(x => x.Weight) + .Select(x => x.Color) + .ToList() + .EnsureMinColorCount(MinColorCount); + } +} diff --git a/components/ColorAnalyzer/src/Contrast/ContrastHelper.Callbacks.cs b/components/ColorAnalyzer/src/ContrastHelper/ContrastHelper.Callbacks.cs similarity index 100% rename from components/ColorAnalyzer/src/Contrast/ContrastHelper.Callbacks.cs rename to components/ColorAnalyzer/src/ContrastHelper/ContrastHelper.Callbacks.cs diff --git a/components/ColorAnalyzer/src/Contrast/ContrastHelper.Properties.cs b/components/ColorAnalyzer/src/ContrastHelper/ContrastHelper.Properties.cs similarity index 100% rename from components/ColorAnalyzer/src/Contrast/ContrastHelper.Properties.cs rename to components/ColorAnalyzer/src/ContrastHelper/ContrastHelper.Properties.cs diff --git a/components/ColorAnalyzer/src/Contrast/ContrastHelper.cs b/components/ColorAnalyzer/src/ContrastHelper/ContrastHelper.cs similarity index 52% rename from components/ColorAnalyzer/src/Contrast/ContrastHelper.cs rename to components/ColorAnalyzer/src/ContrastHelper/ContrastHelper.cs index 323b32c98..7943822ac 100644 --- a/components/ColorAnalyzer/src/Contrast/ContrastHelper.cs +++ b/components/ColorAnalyzer/src/ContrastHelper/ContrastHelper.cs @@ -31,7 +31,7 @@ private static void ApplyContrastCheck(DependencyObject d) if (@base != Colors.Transparent) { // Calculate the WCAG contrast ratio - var ratio = CalculateWCAGContrastRatio(@base, opponent); + var ratio = @base.ContrastRatio(opponent); SetOriginalContrastRatio(d, ratio); // Use original color if the contrast is in the acceptable range @@ -44,7 +44,7 @@ private static void ApplyContrastCheck(DependencyObject d) // Current contrast is too small. // Select either black or white backed on the opponent luminance - var luminance = CalculatePerceivedLuminance(opponent); + var luminance = opponent.PerceivedLuminance(); var contrastingColor = luminance < 0.5f ? Colors.White : Colors.Black; UpdateContrastedProperties(d, contrastingColor); } @@ -91,54 +91,12 @@ private static void UpdateContrastedProperties(DependencyObject d, Color color) } // Calculate the actual ratio, between the opponent and the actual color - var actualRatio = CalculateWCAGContrastRatio(color, GetOpponent(d)); + var opponent = GetOpponent(d); + var actualRatio = color.ContrastRatio(opponent); SetContrastRatio(d, actualRatio); // Unlock the original color updates _selfUpdate = false; } - private static double CalculateWCAGContrastRatio(Color color1, Color color2) - { - // Using the formula for contrast ratio - // Source WCAG guidelines: https://www.w3.org/TR/WCAG20/#contrast-ratiodef - - // Calculate perceived luminance for both colors - double luminance1 = CalculatePerceivedLuminance(color1); - double luminance2 = CalculatePerceivedLuminance(color2); - - // Determine lighter and darker luminance - double lighter = Math.Max(luminance1, luminance2); - double darker = Math.Min(luminance1, luminance2); - - // Calculate contrast ratio - return (lighter + 0.05f) / (darker + 0.05f); - } - - private static double CalculatePerceivedLuminance(Color color) - { - // Color theory is a massive iceberg. Here's a peek at the tippy top: - - // There's two (main) standards for calculating luminance from RGB values. - - // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + - // | Standard | Formula | Ref. Section | Ref. Link | - // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + - // | ITU Rec. 709 | Y = 0.2126 R + 0.7152 G + 0.0722 B | Page 4/Item 3.2 | https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf | - // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + - // | ITU Rec. 601 | Y = 0.299 R + 0.587 G + 0.114 B | Page 2/Item 2.5.1 | https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf | - // + ------------- + ------------------------------------ + ------------------ + ------------------------------------------------------------------------------- + - - // They're based on the standard ability of the human eye to perceive brightness, - // from different colors, as well as the average monitor's ability to produce them. - // Both standards produce similar results, but Rec. 709 is more accurate for modern displays. - - // NOTE: If we for whatever reason we ever need to optimize this code, - // we can make approximations using integer math instead of floating point math. - // The precise values are not critical, as long as the relative luminance is accurate. - // Like so: return (2 * color.R + 7 * color.G + color.B); - - // TLDR: We use ITU Rec. 709 standard formula for perceived luminance. - return (0.2126f * color.R + 0.7152f * color.G + 0.0722 * color.B) / 255; - } } diff --git a/components/ColorAnalyzer/src/PaletteColor.cs b/components/ColorAnalyzer/src/PaletteColor.cs new file mode 100644 index 000000000..c2947d8dc --- /dev/null +++ b/components/ColorAnalyzer/src/PaletteColor.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// A struct containing palettized color info. +/// +public readonly struct PaletteColor +{ + internal PaletteColor(Color color, float sampleFraction) + { + Color = color; + Weight = sampleFraction; + } + + /// + /// Gets the color of the . + /// + public Color Color { get; } + + /// + /// Gets the fraction of the image the color covers. + /// + /// + /// Multiply by 100 to get the percentage of the image the color represents. + /// + public float Weight { get; } +} diff --git a/components/ColorAnalyzer/tests/ExampleAccentAnalyzerTestClass.cs b/components/ColorAnalyzer/tests/ExampleAccentAnalyzerTestClass.cs index 9757153ca..0428eab54 100644 --- a/components/ColorAnalyzer/tests/ExampleAccentAnalyzerTestClass.cs +++ b/components/ColorAnalyzer/tests/ExampleAccentAnalyzerTestClass.cs @@ -14,10 +14,10 @@ public partial class ExampleAccentAnalyzerTestClass : VisualUITestBase [TestMethod] public void SimpleSynchronousExampleTest() { - var assembly = typeof(AccentAnalyzer).Assembly; - var type = assembly.GetType(typeof(AccentAnalyzer).FullName ?? string.Empty); + var assembly = typeof(ColorPaletteSampler).Assembly; + var type = assembly.GetType(typeof(ColorPaletteSampler).FullName ?? string.Empty); - Assert.IsNotNull(type, "Could not find AccentAnalyzer type."); - Assert.AreEqual(typeof(AccentAnalyzer), type, "Type of AccentAnalyzer does not match expected type."); + Assert.IsNotNull(type, "Could not find ColorPaletteSampler type."); + Assert.AreEqual(typeof(ColorPaletteSampler), type, "Type of ColorPaletteSampler does not match expected type."); } }