Skip to content

Commit 4637f4f

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

File tree

2 files changed

+78
-26
lines changed

2 files changed

+78
-26
lines changed

components/ColorAnalyzer/samples/AccentAnalyzerSample.xaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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 Color="{x:Bind AccentAnalyzer.PrimaryAccentColor, Mode=OneWay}"
80+
helpers:ContrastAnalyzer.Opponent="{x:Bind AccentAnalyzer.BaseColor, Mode=OneWay}"
81+
helpers:ContrastAnalyzer.MinRatio="5"/>
82+
</TextBlock.Foreground>
83+
</TextBlock>
7984
</Border>
8085

8186
<!-- Primary -->

components/ColorAnalyzer/src/ContrastAnalyzer.AttachedProperties.cs

Lines changed: 71 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,68 @@ 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+
_selfUpdate = true;
79+
AssignColor(d, contrastingColor);
80+
_selfUpdate = false;
81+
}
82+
83+
private static SolidColorBrush? FindBrush(DependencyObject d, out DependencyProperty? dp)
84+
{
85+
(SolidColorBrush? brush, dp) = d switch
86+
{
87+
SolidColorBrush b => (b, SolidColorBrush.ColorProperty),
88+
TextBlock t => (t.Foreground as SolidColorBrush, TextBlock.ForegroundProperty),
89+
Control c => (c.Foreground as SolidColorBrush, Control.ForegroundProperty),
90+
_ => (null, null),
91+
};
92+
93+
return brush;
94+
}
95+
96+
private static void AssignColor(DependencyObject d, Color color)
97+
{
6698
switch (d)
6799
{
68100
case SolidColorBrush b:
69-
b.Color = contrastingColor;
101+
b.Color = color;
70102
break;
71103
case TextBlock t:
72-
t.Foreground = new SolidColorBrush(contrastingColor);
104+
t.Foreground = new SolidColorBrush(color);
73105
break;
74106
case Control c:
75-
c.Foreground = new SolidColorBrush(contrastingColor);
107+
c.Foreground = new SolidColorBrush(color);
76108
break;
77109
}
78110
}
79111

80-
private static float CalculatePerceivedLuminance(Color color)
112+
private static double CalculateWCAGContrastRatio(Color color1, Color color2)
81113
{
114+
// Using the formula for contrast ratio
115+
// Source WCAG guidelines: https://www.w3.org/TR/WCAG20/#contrast-ratiodef
116+
117+
// Calculate perceived luminance for both colors
118+
double luminance1 = CalculatePerceivedLuminance(color1);
119+
double luminance2 = CalculatePerceivedLuminance(color2);
120+
121+
// Determine lighter and darker luminance
122+
double lighter = Math.Max(luminance1, luminance2);
123+
double darker = Math.Min(luminance1, luminance2);
124+
125+
// Calculate contrast ratio
126+
return (lighter + 0.05f) / (darker + 0.05f);
127+
}
128+
129+
private static double CalculatePerceivedLuminance(Color color) =>
82130
// Using the formula for perceived luminance
83131
// 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-
}
132+
(0.299f * color.R + 0.587f * color.G + 0.114f * color.B) / 255f;
86133
}

0 commit comments

Comments
 (0)