Skip to content

Commit 33e1ebd

Browse files
author
Johann Dirry
committed
Porting material-color-utilities to dotnet #2475
1 parent 46e5193 commit 33e1ebd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+6929
-0
lines changed

MaterialDesignToolkit.Full.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaterialDesign3Demo", "src\
6363
EndProject
6464
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialDesignDemo.Shared", "src\MaterialDesignDemo.Shared\MaterialDesignDemo.Shared.csproj", "{B39795A7-D66A-4F2F-9F41-050838D14048}"
6565
EndProject
66+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialColorUtilities", "src\MaterialDesign3.MaterialColorUtilities\MaterialColorUtilities.csproj", "{2C29B80E-1689-43CE-85AC-71799666B4AC}"
67+
EndProject
6668
Global
6769
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6870
Debug|Any CPU = Debug|Any CPU
@@ -251,6 +253,22 @@ Global
251253
{B39795A7-D66A-4F2F-9F41-050838D14048}.Release|x64.Build.0 = Release|Any CPU
252254
{B39795A7-D66A-4F2F-9F41-050838D14048}.Release|x86.ActiveCfg = Release|Any CPU
253255
{B39795A7-D66A-4F2F-9F41-050838D14048}.Release|x86.Build.0 = Release|Any CPU
256+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
257+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
258+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|ARM.ActiveCfg = Debug|Any CPU
259+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|ARM.Build.0 = Debug|Any CPU
260+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|x64.ActiveCfg = Debug|Any CPU
261+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|x64.Build.0 = Debug|Any CPU
262+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|x86.ActiveCfg = Debug|Any CPU
263+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Debug|x86.Build.0 = Debug|Any CPU
264+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
265+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|Any CPU.Build.0 = Release|Any CPU
266+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|ARM.ActiveCfg = Release|Any CPU
267+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|ARM.Build.0 = Release|Any CPU
268+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|x64.ActiveCfg = Release|Any CPU
269+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|x64.Build.0 = Release|Any CPU
270+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|x86.ActiveCfg = Release|Any CPU
271+
{2C29B80E-1689-43CE-85AC-71799666B4AC}.Release|x86.Build.0 = Release|Any CPU
254272
EndGlobalSection
255273
GlobalSection(SolutionProperties) = preSolution
256274
HideSolutionNode = FALSE
@@ -262,6 +280,7 @@ Global
262280
{18401177-A30E-4330-8F6B-101DF876CE99} = {55087897-5F09-45CA-8E12-12B36B45F262}
263281
{98627CBE-F009-482E-97E9-C69C7135E91F} = {D34BE232-DE51-43C1-ABDC-B69003BB50FF}
264282
{B39795A7-D66A-4F2F-9F41-050838D14048} = {D34BE232-DE51-43C1-ABDC-B69003BB50FF}
283+
{2C29B80E-1689-43CE-85AC-71799666B4AC} = {9E303A4A-3712-44B9-91EE-830FDC087795}
265284
EndGlobalSection
266285
GlobalSection(ExtensibilityGlobals) = postSolution
267286
SolutionGuid = {730B2F9E-74AE-46CE-9E61-89AA5C6D5DD3}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using static System.Math;
2+
using static MaterialColorUtilities.MathUtils;
3+
4+
namespace MaterialColorUtilities;
5+
6+
/// <summary>
7+
/// Functions for blending in HCT and CAM16.
8+
/// </summary>
9+
public static class Blend
10+
{
11+
/// <summary>
12+
/// Blend the design color's HCT hue towards the key color's HCT hue, in a way that
13+
/// leaves the original color recognizable and recognizably shifted towards the key color.
14+
/// </summary>
15+
/// <param name="designColor">ARGB representation of an arbitrary color.</param>
16+
/// <param name="sourceColor">ARGB representation of the main theme color.</param>
17+
/// <returns>
18+
/// The design color with a hue shifted towards the system's color, a slightly
19+
/// warmer/cooler variant of the design color's hue.
20+
/// </returns>
21+
public static int Harmonize(int designColor, int sourceColor)
22+
{
23+
var fromHct = Hct.FromInt(designColor);
24+
var toHct = Hct.FromInt(sourceColor);
25+
var differenceDegrees = DifferenceDegrees(fromHct.Hue, toHct.Hue);
26+
var rotationDegrees = Min(differenceDegrees * 0.5, 15.0);
27+
var outputHue = SanitizeDegreesDouble(
28+
fromHct.Hue + rotationDegrees * RotationDirection(fromHct.Hue, toHct.Hue));
29+
return Hct.From(outputHue, fromHct.Chroma, fromHct.Tone).Argb;
30+
}
31+
32+
/// <summary>
33+
/// Blends hue from one color into another. The chroma and tone of the original color are maintained.
34+
/// </summary>
35+
/// <param name="from">ARGB representation of color.</param>
36+
/// <param name="to">ARGB representation of color.</param>
37+
/// <param name="amount">How much blending to perform; 0.0 &gt;= and &lt;= 1.0.</param>
38+
/// <returns>
39+
/// <paramref name="from"/> with a hue blended towards <paramref name="to"/>.
40+
/// Chroma and tone are constant.
41+
/// </returns>
42+
public static int HctHue(int from, int to, double amount)
43+
{
44+
var ucs = Cam16Ucs(from, to, amount);
45+
var ucsCam = Cam16.FromInt(ucs);
46+
var fromCam = Cam16.FromInt(from);
47+
var blended = Hct.From(ucsCam.GetHue(), fromCam.GetChroma(), ColorUtils.LstarFromArgb(from));
48+
return blended.Argb;
49+
}
50+
51+
/// <summary>
52+
/// Blend in CAM16-UCS space.
53+
/// </summary>
54+
/// <param name="from">ARGB representation of color.</param>
55+
/// <param name="to">ARGB representation of color.</param>
56+
/// <param name="amount">How much blending to perform; 0.0 &gt;= and &lt;= 1.0.</param>
57+
/// <returns>
58+
/// <paramref name="from"/>, blended towards <paramref name="to"/>. Hue, chroma, and tone will change.
59+
/// </returns>
60+
public static int Cam16Ucs(int from, int to, double amount)
61+
{
62+
var fromCam = Cam16.FromInt(from);
63+
var toCam = Cam16.FromInt(to);
64+
var fromJ = fromCam.GetJstar();
65+
var fromA = fromCam.GetAstar();
66+
var fromB = fromCam.GetBstar();
67+
var toJ = toCam.GetJstar();
68+
var toA = toCam.GetAstar();
69+
var toB = toCam.GetBstar();
70+
var jstar = fromJ + (toJ - fromJ) * amount;
71+
var astar = fromA + (toA - fromA) * amount;
72+
var bstar = fromB + (toB - fromB) * amount;
73+
return Cam16.FromUcs(jstar, astar, bstar).ToInt();
74+
}
75+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
using static System.Math;
2+
3+
namespace MaterialColorUtilities;
4+
5+
/// <summary>
6+
/// Color science for contrast utilities.
7+
///
8+
/// Utility methods for calculating contrast given two colors, or calculating a color given one
9+
/// color and a contrast ratio.
10+
///
11+
/// Contrast ratio is calculated using XYZ's Y. When linearized to match human perception, Y
12+
/// becomes HCT's tone and L*a*b*'s' L*.
13+
/// </summary>
14+
public static class Contrast
15+
{
16+
// The minimum contrast ratio of two colors.
17+
// Contrast ratio equation = lighter + 5 / darker + 5, if lighter == darker, ratio == 1.
18+
public const double RatioMin = 1.0;
19+
20+
// The maximum contrast ratio of two colors.
21+
// Contrast ratio equation = lighter + 5 / darker + 5. Lighter and darker scale from 0 to 100.
22+
// If lighter == 100, darker = 0, ratio == 21.
23+
public const double RatioMax = 21.0;
24+
25+
public const double Ratio30 = 3.0;
26+
public const double Ratio45 = 4.5;
27+
public const double Ratio70 = 7.0;
28+
29+
// Given a color and a contrast ratio to reach, the luminance of a color that reaches that ratio
30+
// with the color can be calculated. However, that luminance may not contrast as desired, i.e. the
31+
// contrast ratio of the input color and the returned luminance may not reach the contrast ratio
32+
// asked for.
33+
//
34+
// When the desired contrast ratio and the result contrast ratio differ by more than this amount,
35+
// an error value should be returned, or the method should be documented as 'unsafe', meaning,
36+
// it will return a valid luminance but that luminance may not meet the requested contrast ratio.
37+
//
38+
// 0.04 selected because it ensures the resulting ratio rounds to the same tenth.
39+
private const double ContrastRatioEpsilon = 0.04;
40+
41+
// Color spaces that measure luminance, such as Y in XYZ, L* in L*a*b*, or T in HCT, are known as
42+
// perceptually accurate color spaces.
43+
//
44+
// To be displayed, they must gamut map to a "display space", one that has a defined limit on the
45+
// number of colors. Display spaces include sRGB, more commonly understood as RGB/HSL/HSV/HSB.
46+
// Gamut mapping is undefined and not defined by the color space. Any gamut mapping algorithm must
47+
// choose how to sacrifice accuracy in hue, saturation, and/or lightness.
48+
//
49+
// A principled solution is to maintain lightness, thus maintaining contrast/a11y, maintain hue,
50+
// thus maintaining aesthetic intent, and reduce chroma until the color is in gamut.
51+
//
52+
// HCT chooses this solution, but, that doesn't mean it will _exactly_ matched desired lightness,
53+
// if only because RGB is quantized: RGB is expressed as a set of integers: there may be an RGB
54+
// color with, for example, 47.892 lightness, but not 47.891.
55+
//
56+
// To allow for this inherent incompatibility between perceptually accurate color spaces and
57+
// display color spaces, methods that take a contrast ratio and luminance, and return a luminance
58+
// that reaches that contrast ratio for the input luminance, purposefully darken/lighten their
59+
// result such that the desired contrast ratio will be reached even if inaccuracy is introduced.
60+
//
61+
// 0.4 is generous, ex. HCT requires much less delta. It was chosen because it provides a rough
62+
// guarantee that as long as a perceptual color space gamut maps lightness such that the resulting
63+
// lightness rounds to the same as the requested, the desired contrast ratio will be reached.
64+
private const double LuminanceGamutMapTolerance = 0.4;
65+
66+
/// <summary>
67+
/// Contrast ratio is a measure of legibility, its used to compare the lightness of two colors.
68+
/// This method is used commonly in industry due to its use by WCAG.
69+
///
70+
/// To compare lightness, the colors are expressed in the XYZ color space, where Y is lightness,
71+
/// also known as relative luminance.
72+
///
73+
/// The equation is ratio = lighter Y + 5 / darker Y + 5.
74+
/// </summary>
75+
public static double RatioOfYs(double y1, double y2)
76+
{
77+
var lighter = y1 > y2 ? y1 : y2;
78+
var darker = (lighter == y2) ? y1 : y2;
79+
return (lighter + 5.0) / (darker + 5.0);
80+
}
81+
82+
/// <summary>
83+
/// Contrast ratio of two tones. T in HCT, L* in L*a*b*. Also known as luminance or perceptual
84+
/// luminance.
85+
///
86+
/// Contrast ratio is defined using Y in XYZ, relative luminance. However, relative luminance is
87+
/// linear to number of photons, not to perception of lightness. Perceptual luminance, L* in
88+
/// L*a*b*, T in HCT, is. Designers prefer color spaces with perceptual luminance since they're
89+
/// accurate to the eye.
90+
///
91+
/// Y and L* are pure functions of each other, so it possible to use perceptually accurate color
92+
/// spaces, and measure contrast, and measure contrast in a much more understandable way: instead
93+
/// of a ratio, a linear difference. This allows a designer to determine what they need to adjust a
94+
/// color's lightness to in order to reach their desired contrast, instead of guessing &amp; checking
95+
/// with hex codes.
96+
/// </summary>
97+
public static double RatioOfTones(double t1, double t2)
98+
{
99+
return RatioOfYs(ColorUtils.YFromLstar(t1), ColorUtils.YFromLstar(t2));
100+
}
101+
102+
/// <summary>
103+
/// Returns T in HCT, L* in L*a*b* &gt;= tone parameter that ensures ratio with input T/L*.
104+
/// Returns -1 if ratio cannot be achieved.
105+
/// </summary>
106+
/// <param name="tone">Tone return value must contrast with.</param>
107+
/// <param name="ratio">Desired contrast ratio of return value and tone parameter.</param>
108+
public static double Lighter(double tone, double ratio)
109+
{
110+
if (tone is < 0.0 or > 100.0)
111+
{
112+
return -1.0;
113+
}
114+
var darkY = ColorUtils.YFromLstar(tone);
115+
var lightY = ratio * (darkY + 5.0) - 5.0;
116+
if (lightY is < 0.0 or > 100.0)
117+
{
118+
return -1.0;
119+
}
120+
var realContrast = RatioOfYs(lightY, darkY);
121+
var delta = Abs(realContrast - ratio);
122+
if (realContrast < ratio && delta > ContrastRatioEpsilon)
123+
{
124+
return -1.0;
125+
}
126+
127+
var returnValue = ColorUtils.LstarFromY(lightY) + LuminanceGamutMapTolerance;
128+
if (returnValue is < 0.0 or > 100.0)
129+
{
130+
return -1.0;
131+
}
132+
return returnValue;
133+
}
134+
135+
/// <summary>
136+
/// Tone &gt;= tone parameter that ensures ratio. 100 if ratio cannot be achieved.
137+
///
138+
/// This method is unsafe because the returned value is guaranteed to be in bounds, but, the in
139+
/// bounds return value may not reach the desired ratio.
140+
/// </summary>
141+
/// <param name="tone">Tone return value must contrast with.</param>
142+
/// <param name="ratio">Desired contrast ratio of return value and tone parameter.</param>
143+
public static double LighterUnsafe(double tone, double ratio)
144+
{
145+
var lighterSafe = Lighter(tone, ratio);
146+
return lighterSafe < 0.0 ? 100.0 : lighterSafe;
147+
}
148+
149+
/// <summary>
150+
/// Returns T in HCT, L* in L*a*b* &lt;= tone parameter that ensures ratio with input T/L*.
151+
/// Returns -1 if ratio cannot be achieved.
152+
/// </summary>
153+
/// <param name="tone">Tone return value must contrast with.</param>
154+
/// <param name="ratio">Desired contrast ratio of return value and tone parameter.</param>
155+
public static double Darker(double tone, double ratio)
156+
{
157+
if (tone is < 0.0 or > 100.0)
158+
{
159+
return -1.0;
160+
}
161+
var lightY = ColorUtils.YFromLstar(tone);
162+
var darkY = ((lightY + 5.0) / ratio) - 5.0;
163+
if (darkY is < 0.0 or > 100.0)
164+
{
165+
return -1.0;
166+
}
167+
var realContrast = RatioOfYs(lightY, darkY);
168+
var delta = Abs(realContrast - ratio);
169+
if (realContrast < ratio && delta > ContrastRatioEpsilon)
170+
{
171+
return -1.0;
172+
}
173+
var returnValue = ColorUtils.LstarFromY(darkY) - LuminanceGamutMapTolerance;
174+
if (returnValue is < 0.0 or > 100.0)
175+
{
176+
return -1.0;
177+
}
178+
return returnValue;
179+
}
180+
181+
/// <summary>
182+
/// Tone &lt;= tone parameter that ensures ratio. 0 if ratio cannot be achieved.
183+
///
184+
/// This method is unsafe because the returned value is guaranteed to be in bounds, but, the in
185+
/// bounds return value may not reach the desired ratio.
186+
/// </summary>
187+
/// <param name="tone">Tone return value must contrast with.</param>
188+
/// <param name="ratio">Desired contrast ratio of return value and tone parameter.</param>
189+
public static double DarkerUnsafe(double tone, double ratio)
190+
{
191+
var darkerSafe = Darker(tone, ratio);
192+
return Max(0.0, darkerSafe);
193+
}
194+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using static System.Math;
2+
3+
namespace MaterialColorUtilities;
4+
5+
/// <summary>
6+
/// Check and/or fix universally disliked colors.
7+
///
8+
/// Color science studies of color preference indicate universal distaste for dark yellow-greens,
9+
/// and also show this is correlated to distaste for biological waste and rotting food.
10+
///
11+
/// See Palmer and Schloss, 2010 or Schloss and Palmer's Chapter 21 in Handbook of Color
12+
/// Psychology (2015).
13+
/// </summary>
14+
public static class DislikeAnalyzer
15+
{
16+
/// <summary>
17+
/// Returns true if color is disliked.
18+
///
19+
/// Disliked is defined as a dark yellow-green that is not neutral.
20+
/// </summary>
21+
public static bool IsDisliked(Hct hct)
22+
{
23+
var roundedHue = Round(hct.Hue);
24+
var roundedChroma = Round(hct.Chroma);
25+
var roundedTone = Round(hct.Tone);
26+
27+
var huePasses = roundedHue is >= 90.0 and <= 111.0;
28+
var chromaPasses = roundedChroma > 16.0;
29+
var tonePasses = roundedTone < 65.0;
30+
31+
return huePasses && chromaPasses && tonePasses;
32+
}
33+
34+
/// <summary>
35+
/// If color is disliked, lighten it to make it likable.
36+
/// </summary>
37+
public static Hct FixIfDisliked(Hct hct)
38+
{
39+
return IsDisliked(hct)
40+
? Hct.From(hct.Hue, hct.Chroma, 70.0)
41+
: hct;
42+
}
43+
}

0 commit comments

Comments
 (0)