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.");
}
}