Skip to content

Commit d071fa9

Browse files
committed
Added MinRatio property for ContrastAnalyzer to specify threshold WCAG rato to enable color override
1 parent 225404e commit d071fa9

File tree

2 files changed

+77
-27
lines changed

2 files changed

+77
-27
lines changed

components/ColorAnalyzer/samples/AccentAnalyzerSample.xaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- 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. -->
1+
<!-- 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. -->
22
<Page x:Class="ColorAnalyzerExperiment.Samples.AccentAnalyzerSample"
33
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -74,8 +74,13 @@
7474
<Border.Background>
7575
<SolidColorBrush Color="{x:Bind AccentAnalyzer.BaseColor, Mode=OneWay}" />
7676
</Border.Background>
77-
<TextBlock helpers:ContrastAnalyzer.Opponent="{x:Bind AccentAnalyzer.BaseColor, Mode=OneWay}"
78-
Text="Base" />
77+
<TextBlock Text="Base">
78+
<TextBlock.Foreground>
79+
<SolidColorBrush helpers:ContrastAnalyzer.MinRatio="5"
80+
helpers:ContrastAnalyzer.Opponent="{x:Bind AccentAnalyzer.BaseColor, Mode=OneWay}"
81+
Color="{x:Bind AccentAnalyzer.PrimaryAccentColor, Mode=OneWay}" />
82+
</TextBlock.Foreground>
83+
</TextBlock>
7984
</Border>
8085

8186
<!-- Primary -->

components/ColorAnalyzer/src/ContrastAnalyzer.AttachedProperties.cs

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,43 @@ public partial class ContrastAnalyzer
2121
typeof(ContrastAnalyzer),
2222
new PropertyMetadata(Colors.Transparent, OnOpponentChanged));
2323

24+
/// <summary>
25+
/// An attached property that defines the minimum acceptable contrast ratio against the opponent color.
26+
/// </summary>
27+
/// <remarks>
28+
/// Range: 1 to 21 (inclusive). Default is 21 (maximum contrast).
29+
/// </remarks>
30+
public static readonly DependencyProperty MinRatioProperty =
31+
DependencyProperty.RegisterAttached(
32+
"MinRatio",
33+
typeof(double),
34+
typeof(ContrastAnalyzer),
35+
new PropertyMetadata(21d));
36+
2437
/// <summary>
2538
/// Get the opponent color to compare against.
2639
/// </summary>
2740
/// <returns>The opponent color.</returns>
28-
public static Color GetOpponent(DependencyObject obj)
29-
{
30-
return (Color)obj.GetValue(OpponentProperty);
31-
}
41+
public static Color GetOpponent(DependencyObject obj) => (Color)obj.GetValue(OpponentProperty);
3242

3343
/// <summary>
3444
/// Set the opponent color to compare against.
3545
/// </summary>
36-
public static void SetOpponent(DependencyObject obj, Color value)
37-
{
38-
obj.SetValue(OpponentProperty, value);
39-
}
46+
public static void SetOpponent(DependencyObject obj, Color value) => obj.SetValue(OpponentProperty, value);
47+
48+
/// <summary>
49+
/// Get the minimum acceptable contrast ratio against the opponent color.
50+
/// </summary>
51+
public static double GetMinRatio(DependencyObject obj) => (double)obj.GetValue(MinRatioProperty);
52+
53+
/// <summary>
54+
/// Set the minimum acceptable contrast ratio against the opponent color.
55+
/// </summary>
56+
public static void SetMinRatio(DependencyObject obj, double value) => obj.SetValue(MinRatioProperty, value);
4057

4158
private static void OnOpponentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
4259
{
43-
SolidColorBrush? brush = d switch
44-
{
45-
SolidColorBrush b => b,
46-
TextBlock t => t.Foreground as SolidColorBrush,
47-
Control c => c.Foreground as SolidColorBrush,
48-
_ => null,
49-
};
60+
var brush = FindBrush(d, out var dp);
5061

5162
// Could not find a brush to modify.
5263
if (brush is null)
@@ -55,32 +66,66 @@ private static void OnOpponentChanged(DependencyObject d, DependencyPropertyChan
5566
Color @base = brush.Color;
5667
Color opponent = (Color)e.NewValue;
5768

58-
// TODO: Cache original color
59-
// TODO: Adjust only if contrast is insufficient
69+
// If the colors already meet the minimum ratio, no adjustment is needed
70+
if (CalculateWCAGContrastRatio(@base, opponent) >= GetMinRatio(d))
71+
return;
6072

6173
// In the meantime, adjust by percieved luminance regardless
6274
var luminance = CalculatePerceivedLuminance(opponent);
6375
var contrastingColor = luminance < 0.5f ? Colors.White : Colors.Black;
6476

65-
// Assign back
77+
// Assign color
78+
AssignColor(d, contrastingColor);
79+
}
80+
81+
private static SolidColorBrush? FindBrush(DependencyObject d, out DependencyProperty? dp)
82+
{
83+
(SolidColorBrush? brush, dp) = d switch
84+
{
85+
SolidColorBrush b => (b, SolidColorBrush.ColorProperty),
86+
TextBlock t => (t.Foreground as SolidColorBrush, TextBlock.ForegroundProperty),
87+
Control c => (c.Foreground as SolidColorBrush, Control.ForegroundProperty),
88+
_ => (null, null),
89+
};
90+
91+
return brush;
92+
}
93+
94+
private static void AssignColor(DependencyObject d, Color color)
95+
{
6696
switch (d)
6797
{
6898
case SolidColorBrush b:
69-
b.Color = contrastingColor;
99+
b.Color = color;
70100
break;
71101
case TextBlock t:
72-
t.Foreground = new SolidColorBrush(contrastingColor);
102+
t.Foreground = new SolidColorBrush(color);
73103
break;
74104
case Control c:
75-
c.Foreground = new SolidColorBrush(contrastingColor);
105+
c.Foreground = new SolidColorBrush(color);
76106
break;
77107
}
78108
}
79109

80-
private static float CalculatePerceivedLuminance(Color color)
110+
private static double CalculateWCAGContrastRatio(Color color1, Color color2)
81111
{
112+
// Using the formula for contrast ratio
113+
// Source WCAG guidelines: https://www.w3.org/TR/WCAG20/#contrast-ratiodef
114+
115+
// Calculate perceived luminance for both colors
116+
double luminance1 = CalculatePerceivedLuminance(color1);
117+
double luminance2 = CalculatePerceivedLuminance(color2);
118+
119+
// Determine lighter and darker luminance
120+
double lighter = Math.Max(luminance1, luminance2);
121+
double darker = Math.Min(luminance1, luminance2);
122+
123+
// Calculate contrast ratio
124+
return (lighter + 0.05f) / (darker + 0.05f);
125+
}
126+
127+
private static double CalculatePerceivedLuminance(Color color) =>
82128
// Using the formula for perceived luminance
83129
// Source WCAG guidelines: https://www.w3.org/TR/AERT/#color-contrast
84-
return (0.299f * color.R + 0.587f * color.G + 0.114f * color.B) / 255f;
85-
}
130+
(0.299f * color.R + 0.587f * color.G + 0.114f * color.B) / 255f;
86131
}

0 commit comments

Comments
 (0)