Skip to content

Commit

Permalink
Refactor CorePalette, add support for more strategies (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
albi005 authored Oct 28, 2022
1 parent b444306 commit 46bf7bf
Show file tree
Hide file tree
Showing 15 changed files with 331 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public void DisableTheming()
dynamicColorService.Initialize(null);

Assert.IsFalse(dynamicColorService.EnableTheming);
Assert.IsNull(dynamicColorService.CorePalette);
Assert.IsNull(dynamicColorService.SchemeMaui);
Assert.IsTrue(_application.Resources.Count == 0);
}
Expand Down
18 changes: 3 additions & 15 deletions MaterialColorUtilities.Maui/DynamicColorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class DynamicColorService<
TLightSchemeMapper,
TDarkSchemeMapper>
: IMauiInitializeService
where TCorePalette : CorePalette
where TCorePalette : CorePalette, new()
where TSchemeInt : Scheme<uint>, new()
where TSchemeMaui : Scheme<Color>, new()
where TLightSchemeMapper : ISchemeMapper<TCorePalette, TSchemeInt>, new()
Expand Down Expand Up @@ -125,7 +125,7 @@ public uint Seed
}
}

public TCorePalette CorePalette { get; protected set; } = null!;
public TCorePalette CorePalette { get; } = new();
public TSchemeInt SchemeInt { get; protected set; } = null!;
public TSchemeMaui SchemeMaui { get; protected set; } = null!;

Expand Down Expand Up @@ -183,7 +183,7 @@ private void Update()
_seed = (uint)_seedColorService.SeedColor;

if (Seed != _prevSeed)
CorePalette = CreateCorePalette(Seed);
CorePalette.Fill(Seed);

if (Seed == _prevSeed && IsDark == _prevIsDark) return;
_prevSeed = Seed;
Expand Down Expand Up @@ -227,16 +227,4 @@ protected virtual void Apply()
_appResources[key + "Brush"] = new SolidColorBrush(value);
}
}

/// <summary>Constructs a <typeparamref name="TCorePalette"/>.</summary>
/// <remarks>
/// C# doesn't support constructor with parameters as a generic constraint,
/// so reflection is required to access the constructor. <see href="https://github.com/dotnet/csharplang/discussions/769">Discussion here.</see>
/// If you replace CorePalette, make sure it has a constructor with the following parameters: <c>int seed, bool isContent</c>
/// </remarks>
// TODO: Replace with using empty constructor and method call
private static TCorePalette CreateCorePalette(uint seed)
{
return (TCorePalette)Activator.CreateInstance(typeof(TCorePalette), seed, false)!;
}
}
12 changes: 6 additions & 6 deletions MaterialColorUtilities.Tests/SchemeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ public class SchemeTests
[TestMethod]
public void BlueLight()
{
Scheme<uint> scheme = new LightSchemeMapper().Map(new(0xff0000ff));
Scheme<uint> scheme = new LightSchemeMapper().Map(CorePalette.Of(0xff0000ff));
Assert.AreEqual(0xff343DFF, scheme.Primary);
}

[TestMethod]
public void BlueDark()
{
Scheme<uint> scheme = new DarkSchemeMapper().Map(new(0xff0000ff));
Scheme<uint> scheme = new DarkSchemeMapper().Map(CorePalette.Of(0xff0000ff));
Assert.AreEqual(0xffBEC2FF, scheme.Primary);
}

[TestMethod]
public void PurpleishLight()
{
Scheme<uint> scheme = new LightSchemeMapper().Map(new(0xff6750A4));
Scheme<uint> scheme = new LightSchemeMapper().Map(CorePalette.Of(0xff6750A4));
Assert.AreEqual(0xff6750A4, scheme.Primary);
Assert.AreEqual(0xff625B71, scheme.Secondary);
Assert.AreEqual(0xff7E5260, scheme.Tertiary);
Expand All @@ -50,7 +50,7 @@ public void PurpleishLight()
[TestMethod]
public void PurpleishDark()
{
Scheme<uint> scheme = new DarkSchemeMapper().Map(new(0xff6750A4));
Scheme<uint> scheme = new DarkSchemeMapper().Map(CorePalette.Of(0xff6750A4));
Assert.AreEqual(0xffCFBCFF, scheme.Primary);
Assert.AreEqual(0xffCBC2DB, scheme.Secondary);
Assert.AreEqual(0xffEFB8C8, scheme.Tertiary);
Expand All @@ -61,7 +61,7 @@ public void PurpleishDark()
[TestMethod]
public void LightSchemeFromHighChromaColor()
{
Scheme<uint> scheme = new LightSchemeMapper().Map(new(0xfffa2bec));
Scheme<uint> scheme = new LightSchemeMapper().Map(CorePalette.Of(0xfffa2bec));
Assert.AreEqual(0xffab00a2, scheme.Primary);
Assert.AreEqual(0xffffffff, scheme.OnPrimary);
Assert.AreEqual(0xffffd7f3, scheme.PrimaryContainer);
Expand Down Expand Up @@ -94,7 +94,7 @@ public void LightSchemeFromHighChromaColor()
[TestMethod]
public void DarkSchemeFromHighChromaColor()
{
Scheme<uint> scheme = new DarkSchemeMapper().Map(new(0xfffa2bec));
Scheme<uint> scheme = new DarkSchemeMapper().Map(CorePalette.Of(0xfffa2bec));
Assert.AreEqual(0xffffabee, scheme.Primary);
Assert.AreEqual(0xff5c0057, scheme.OnPrimary);
Assert.AreEqual(0xff83007b, scheme.PrimaryContainer);
Expand Down
157 changes: 125 additions & 32 deletions MaterialColorUtilities/Palettes/CorePalette.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,51 +18,144 @@
namespace MaterialColorUtilities.Palettes;

/// <summary>
/// An intermediate concept between the key color for a UI theme, and a full color scheme.
/// 5 sets of tones are generated, all except one use the same hue as the key color, and
/// all vary in chroma.
/// Contains tonal palettes for the key colors.
/// </summary>
public class CorePalette
{
public TonalPalette Primary { get; set; }
public TonalPalette Secondary { get; set; }
public TonalPalette Tertiary { get; set; }
public TonalPalette Neutral { get; set; }
public TonalPalette NeutralVariant { get; set; }
public TonalPalette Error { get; set; }
public TonalPalette Primary { get; set; } = null!;
public TonalPalette Secondary { get; set; } = null!;
public TonalPalette Tertiary { get; set; } = null!;
public TonalPalette Neutral { get; set; } = null!;
public TonalPalette NeutralVariant { get; set; } = null!;
public TonalPalette Error { get; set; } = null!;

/// <summary>Create key tones from a color.</summary>
/// <param name="argb">ARGB representation of a color.</param>
public static CorePalette Of(uint argb) => new(argb);
/// <summary>Creates an empty core palette.</summary>
public CorePalette()
{
}

/// <summary>Create key tones from a color using the default strategy.</summary>
/// <param name="seed">ARGB representation of a color.</param>
public static CorePalette Of(uint seed) => Of(seed, Strategy.Default);

/// <summary>Create content key tones from a color.</summary>
/// <param name="argb">ARGB representation of a color.</param>
public static CorePalette ContentOf(uint argb) => new(argb, true);
/// <param name="seed">ARGB representation of a color.</param>
public static CorePalette ContentOf(uint seed) => Of(seed, Strategy.Content);

/// <summary>Create key tones from a color.</summary>
/// <param name="argb">ARGB representation of a color.</param>
/// <param name="isContent"></param>
public CorePalette(uint argb, bool isContent = false)
/// <param name="seed">ARGB representation of a color.</param>
/// <param name="strategy">
/// The strategy that decides what hue and chroma the created tonal palettes should have.
/// </param>
public static CorePalette Of(uint seed, Strategy strategy)
{
CorePalette corePalette = new();
corePalette.Fill(seed, strategy);
return corePalette;
}

public virtual void Fill(uint seed, Strategy strategy = Strategy.Default)
{
Hct hct = Hct.FromInt(argb);
Hct hct = Hct.FromInt(seed);
double hue = hct.Hue;
double chroma = hct.Chroma;
if (isContent)
{
Primary = TonalPalette.FromHueAndChroma(hue, chroma);
Secondary = TonalPalette.FromHueAndChroma(hue, chroma / 3);
Tertiary = TonalPalette.FromHueAndChroma(hue + 60, chroma / 2);
Neutral = TonalPalette.FromHueAndChroma(hue, Math.Min(chroma / 12, 4));
NeutralVariant = TonalPalette.FromHueAndChroma(hue, Math.Min(chroma / 6, 8));
}
else

switch (strategy)
{
Primary = TonalPalette.FromHueAndChroma(hue, Math.Max(48, chroma));
Secondary = TonalPalette.FromHueAndChroma(hue, 16);
Tertiary = TonalPalette.FromHueAndChroma(hue + 60, 24);
Neutral = TonalPalette.FromHueAndChroma(hue, 4);
NeutralVariant = TonalPalette.FromHueAndChroma(hue, 8);
case Strategy.Default:
Primary = TonalPalette.FromHueAndChroma(hue, Math.Max(48, chroma));
Secondary = TonalPalette.FromHueAndChroma(hue, 16);
Tertiary = TonalPalette.FromHueAndChroma(hue + 60, 24);
Neutral = TonalPalette.FromHueAndChroma(hue, 4);
NeutralVariant = TonalPalette.FromHueAndChroma(hue, 8);
break;
case Strategy.Content:
Primary = TonalPalette.FromHueAndChroma(hue, chroma);
Secondary = TonalPalette.FromHueAndChroma(hue, chroma / 3);
Tertiary = TonalPalette.FromHueAndChroma(hue + 60, chroma / 2);
Neutral = TonalPalette.FromHueAndChroma(hue, Math.Min(chroma / 12, 4));
NeutralVariant = TonalPalette.FromHueAndChroma(hue, Math.Min(chroma / 6, 8));
break;
case Strategy.A:
Primary = TonalPalette.FromHueAndChroma(hue, 12);
Secondary = TonalPalette.FromHueAndChroma(hue, 8);
Tertiary = TonalPalette.FromHueAndChroma(hue, 16);
Neutral = TonalPalette.FromHueAndChroma(hue, 2);
NeutralVariant = TonalPalette.FromHueAndChroma(hue, 2);
break;
case Strategy.B:
Primary = TonalPalette.FromHueAndChroma(hue, 150);
Secondary = TonalPalette.FromHueAndChroma(hue + 15, 24);
Tertiary = TonalPalette.FromHueAndChroma(hue + 30, 32);
Neutral = TonalPalette.FromHueAndChroma(hue, 10);
NeutralVariant = TonalPalette.FromHueAndChroma(hue, 12);
break;
case Strategy.C:
Primary = TonalPalette.FromHueAndChroma(hue - 120, 40);
Secondary = TonalPalette.FromHueAndChroma(hue switch
{
> 30 and < 50 => hue + 95,
> 130 and < 150 => hue + 20,
> 200 and < 265 => hue + 90,
_ => hue + 45
}, 24);
Tertiary = TonalPalette.FromHueAndChroma(hue + 135, 32);
Neutral = TonalPalette.FromHueAndChroma(hue + 15, 8);
NeutralVariant = TonalPalette.FromHueAndChroma(hue + 15, 12);
break;
default:
throw new ArgumentOutOfRangeException(nameof(strategy), strategy, null);
}
Error = TonalPalette.FromHueAndChroma(25, 84);
}

/// <summary>
/// Decides what hue and chroma the different tonal palettes should have.
/// </summary>
public enum Strategy
{
/// <summary>
/// The default strategy. Use when theming based on the user's wallpaper.
/// </summary>
/// <remarks>
/// All tonal palettes except tertiary use the same
/// hue as the seed color, and all vary in chroma. <br/>
/// More on <a href="https://m3.material.io/styles/color/dynamic-color/user-generated-color#35bc06c5-35d9-4559-9f5d-07ea734cbcb1">m3.material.io</a>
/// </remarks>
Default,

/// <summary>
/// Use when theming based on in-app content.
/// </summary>
/// <remarks>
/// More on <a href="https://m3.material.io/styles/color/dynamic-color/user-generated-color#8af550b9-a19e-4e9f-bb0a-7f611fed5d0f">m3.material.io</a>
/// </remarks>
Content,

/// <summary>
/// All of the tonal palettes use the same hue as the seed and with low chroma.
/// </summary>
/// <remarks>
/// Approximation of the 2. option available on Google Pixel phones.
/// </remarks>
A,

/// <summary>
/// All of the tonal palettes use the same hue as the seed other than secondary and tertiary.
/// Secondary is 15 higher and tertiary is 30 higher.
/// They all have relatively high chroma.
/// </summary>
/// <remarks>
/// Approximation of the 3. option available on Google Pixel phones.
/// </remarks>
B,

/// <summary>
/// Produces vibrant palettes using a diverse set of hues.
/// </summary>
/// <remarks>
/// Approximation of the 4. option available on Google Pixel phones.
/// </remarks>
C
}
}
14 changes: 7 additions & 7 deletions MaterialColorUtilities/Palettes/TonalPalette.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ namespace MaterialColorUtilities.Palettes;
/// </summary>
public class TonalPalette
{
private readonly Dictionary<uint, uint> cache = new();
private readonly double hue;
private readonly double chroma;
private readonly Dictionary<uint, uint> _cache = new();
private readonly double _hue;
private readonly double _chroma;

/// <summary>Creates tones using the HCT hue and chroma from a color.</summary>
/// <param name="argb">ARGB representation of a color.</param>
Expand All @@ -44,17 +44,17 @@ public static TonalPalette FromInt(uint argb)

private TonalPalette(double hue, double chroma)
{
this.hue = hue;
this.chroma = chroma;
_hue = hue;
_chroma = chroma;
}

/// <summary>Creates an ARGB color with HCT hue and chroma of this TonalPalette instance, and the provided HCT tone.</summary>
/// <param name="tone">HCT tone, measured from 0 to 100.</param>
/// <returns>ARGB representation of a color with that tone.</returns>
public uint Tone(uint tone)
=> cache.TryGetValue(tone, out uint value)
=> _cache.TryGetValue(tone, out uint value)
? value
: cache[tone] = Hct.From(hue, chroma, tone).ToInt();
: _cache[tone] = Hct.From(_hue, _chroma, tone).ToInt();

/// <summary>Creates an ARGB color with HCT hue and chroma of this TonalPalette instance, and the provided HCT tone.</summary>
/// <param name="tone">HCT tone, measured from 0 to 100.</param>
Expand Down
13 changes: 8 additions & 5 deletions Playground/Playground.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
Console.WriteLine($"Seed: {StringUtils.HexFromArgb(seedColor)}");

// CorePalette gives you access to every tone of the key colors
CorePalette corePalette = new(seedColor);
CorePalette corePalette = CorePalette.Of(seedColor);

// Map the core palette to color schemes
// A Scheme contains the named colors, like Primary or OnTertiaryContainer
Expand All @@ -41,8 +41,9 @@
// - EXTENSION -
// Adding your own colors:

// 4. Use your new colors (this is should be at the end, but you can't add top-level statements after type declarations)
MyCorePalette myCorePalette = new(seedColor);
// 4. Use your new colors (this part should be at the end, but you can't add top-level statements after type declarations)
MyCorePalette myCorePalette = new();
corePalette.Fill(seedColor);
MyScheme<string> myDarkScheme = new MyDarkSchemeMapper()
.Map(myCorePalette)
.ConvertTo(StringUtils.HexFromArgb);
Expand All @@ -52,9 +53,11 @@
public class MyCorePalette : CorePalette
{
public TonalPalette Orange { get; set; }
public MyCorePalette(uint seed) : base(seed)

public override void Fill(uint seed, Strategy strategy = Strategy.Default)
{
base.Fill(seed, strategy);

// You can harmonize a color to make it closer to the seed color
uint harmonizedOrange = Blender.Harmonize(0xFFA500, seed);
Orange = TonalPalette.FromInt(harmonizedOrange);
Expand Down
Loading

0 comments on commit 46bf7bf

Please sign in to comment.