Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@
<PackageVersion Include="System.Memory" Version="4.6.3" />
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
<PackageVersion Include="Polyfill" Version="8.8.1" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ public int GetArgb(DynamicScheme scheme)
return (argb & 0x00ffffff) | (alpha << 24);
}

public System.Windows.Media.Color GetColor(DynamicScheme scheme)
=> ColorUtils.ColorFromArgb(GetArgb(scheme));

public Hct GetHct(DynamicScheme scheme)
{
if (hctCache.TryGetValue(scheme, out var cached))
Expand Down Expand Up @@ -320,4 +323,4 @@ private void ValidateExtendedColor(SpecVersion specVersion, DynamicColor extende
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Windows.Media;

namespace MaterialColorUtilities;

/// <summary>
/// Factory for creating a dynamic color scheme.
/// </summary>
public static class DynamicSchemeFactory
{
/// <summary>
/// Factory method for creating a dynamic color scheme.
/// </summary>
/// <remarks>
/// The colors are optional. If any of them are null,
/// the color will be automatically generated based on the source color.
/// </remarks>
public static DynamicScheme Create(
Color sourceColor,
Variant variant,
bool isDark,
double contrastLevel,
Platform platform,
SpecVersion specVersion,
Color? primary,
Color? secondary,
Color? tertiary,
Color? neutral,
Color? neutralVariant,
Color? error)
{
var sourceColorHct = Hct.FromInt(ColorUtils.ArgbFromColor(sourceColor));

TonalPalette primaryPalette = primary == null
? ColorSpecs.Get(specVersion).GetPrimaryPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
: ColorSpecs.Get(specVersion).GetPrimaryPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(primary.Value)), isDark, platform, contrastLevel);

TonalPalette secondaryPalette = secondary == null
? ColorSpecs.Get(specVersion).GetSecondaryPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
: ColorSpecs.Get(specVersion).GetSecondaryPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(secondary.Value)), isDark, platform, contrastLevel);

TonalPalette tertiaryPalette = tertiary == null
? ColorSpecs.Get(specVersion).GetTertiaryPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
: ColorSpecs.Get(specVersion).GetTertiaryPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(tertiary.Value)), isDark, platform, contrastLevel);

TonalPalette neutralPalette = neutral == null
? ColorSpecs.Get(specVersion).GetNeutralPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
: ColorSpecs.Get(specVersion).GetNeutralPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(neutral.Value)), isDark, platform, contrastLevel);

TonalPalette neutralVariantPalette = neutralVariant == null
? ColorSpecs.Get(specVersion).GetNeutralVariantPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
: ColorSpecs.Get(specVersion).GetNeutralVariantPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(neutralVariant.Value)), isDark, platform, contrastLevel);

TonalPalette? errorPalette = error == null
? ColorSpecs.Get(specVersion).GetErrorPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
: ColorSpecs.Get(specVersion).GetErrorPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(error.Value)), isDark, platform, contrastLevel);

return new DynamicScheme(
sourceColorHct,
variant,
isDark,
contrastLevel,
platform,
specVersion,
primaryPalette,
secondaryPalette,
tertiaryPalette,
neutralPalette,
neutralVariantPalette,
errorPalette);
}
}
16 changes: 16 additions & 0 deletions src/MaterialDesign3.MaterialColorUtilities/Utils/ColorUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ public static class ColorUtils
/// </summary>
public static int ArgbFromRgb(int red, int green, int blue) => (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);

/// <summary>
/// Converts a color in ARGB format to a <see cref="System.Windows.Media.Color"/>.
/// </summary>
public static System.Windows.Media.Color ColorFromArgb(int argb) =>
System.Windows.Media.Color.FromArgb(
(byte)AlphaFromArgb(argb),
(byte)RedFromArgb(argb),
(byte)GreenFromArgb(argb),
(byte)BlueFromArgb(argb));

/// <summary>
/// Converts a <see cref="System.Windows.Media.Color"/> to ARGB format.
/// </summary>
public static int ArgbFromColor(System.Windows.Media.Color color) =>
(color.A << 24) | (color.R << 16) | (color.G << 8) | color.B;

/// <summary>
/// Converts a color from linear RGB components to ARGB format.
/// </summary>
Expand Down
41 changes: 40 additions & 1 deletion tests/MaterialColorUtilities.Tests/ColorUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace MaterialColorUtilities.Tests;
using System.Windows.Media;

namespace MaterialColorUtilities.Tests;

public sealed class ColorUtilsTests
{
Expand Down Expand Up @@ -264,4 +266,41 @@ public async Task Linearize_Delinearize_RoundTrip()
await Assert.That(converted).IsEqualTo(c);
}
}

public static IEnumerable<Func<(int argb, Color color)>> TestColors()
{
yield return () => (unchecked((int)0xFFFF0000), Colors.Red);
yield return () => (unchecked((int)0xFF00FF00), Colors.Lime);
yield return () => (unchecked((int)0xFF0000FF), Colors.Blue);
yield return () => (unchecked((int)0xFFFF00FF), Colors.Magenta);
yield return () => (unchecked((int)0xFFFFFF00), Colors.Yellow);
yield return () => (unchecked((int)0xFF00FFFF), Colors.Cyan);
yield return () => (unchecked((int)0xFFFFFFFF), Colors.White);
yield return () => (unchecked((int)0xFF000000), Colors.Black);
yield return () => (unchecked((int)0x00FFFFFF), Colors.Transparent);
}

[Test]
[DisplayName("colorFromArgb returns known Colors")]
[MethodDataSource(nameof(TestColors))]
public async Task ColorFromArgb_KnownColors(int argb, Color color)
{
var converted = ColorUtils.ColorFromArgb(argb);

string result = $"{converted.A:x}{converted.R:x}{converted.G:x}{converted.B:X}";
string expected = $"{color.A:x}{color.R:x}{color.G:x}{color.B:X}";

await Assert.That(result).IsEqualTo(expected);
}

[Test]
[DisplayName("argbFromColor returns known ints")]
[MethodDataSource(nameof(TestColors))]
public async Task ArgbFromColor_KnownColors(int argb, Color color)
{
string result = ColorUtils.ArgbFromColor(color).ToString("X");
string expected = argb.ToString("X");

await Assert.That(result).IsEqualTo(expected);
}
}
105 changes: 104 additions & 1 deletion tests/MaterialColorUtilities.Tests/DynamicSchemeTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Runtime.Serialization;
using System.Windows.Media;

namespace MaterialColorUtilities.Tests;

Expand Down Expand Up @@ -65,4 +65,107 @@ public async Task RotationGreaterThan360_Wraps()
// 43 + 480 = 523 -> 163 after sanitize/wrap
await Assert.That(hue).IsEqualTo(163.0).Within(1.0);
}

/// <summary>
/// Shows how te crate a theme from a primary color.
/// </summary>
[Test]
public async Task CreateThemeFromColor()
{
var mdc = new MaterialDynamicColors();
var primaryColorHct = Hct.FromInt(ColorUtils.ArgbFromColor(Color.FromArgb(0xff, 0x6a, 0x9c, 0x59)));
var scheme = new SchemeContent(
primaryColorHct,
isDark: true,
contrastLevel : 0.5,
SpecVersion.Spec2025,
Platform.Phone);

scheme.ShouldSatisfyAllConditions(
// Main Palettes
() => mdc.PrimaryPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x52, 0x83, 0x43)),
() => mdc.SecondaryPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x68, 0x7D, 0x5E)),
() => mdc.TertiaryPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x2B, 0x9F, 0x94)),
() => mdc.NeutralPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x75, 0x78, 0x71)),
() => mdc.NeutralVariantPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x72, 0x79, 0x6C)),
() => mdc.ErrorPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xDE, 0x37, 0x30)),

// Surfaces [S]
() => mdc.Background.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x11, 0x14, 0x0F)),
() => mdc.OnBackground.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xE1, 0xE3, 0xDB)),
() => mdc.Surface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x11, 0x14, 0x0F)),
() => mdc.SurfaceDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x11, 0x14, 0x0F)),
() => mdc.SurfaceBright.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x42, 0x45, 0x3F)),
() => mdc.SurfaceContainerLowest.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x06, 0x08, 0x05)),
() => mdc.SurfaceContainerLow.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x1B, 0x1E, 0x19)),
() => mdc.SurfaceContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x26, 0x29, 0x23)),
() => mdc.SurfaceContainerHigh.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x30, 0x33, 0x2E)),
() => mdc.SurfaceContainerHighest.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x3C, 0x3F, 0x39)),
() => mdc.OnSurface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)),
() => mdc.SurfaceVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x42, 0x49, 0x3E)),
() => mdc.OnSurfaceVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xD8, 0xDF, 0xCF)),
() => mdc.Outline.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xAD, 0xB4, 0xA6)),
() => mdc.OutlineVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x8B, 0x93, 0x85)),
() => mdc.InverseSurface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xE1, 0xE3, 0xDB)),
() => mdc.InverseOnSurface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x28, 0x2B, 0x25)),
() => mdc.Shadow.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
() => mdc.Scrim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
() => mdc.SurfaceTint.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x9F, 0xD5, 0x8B)),

// Primaries [P]
() => mdc.Primary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xB5, 0xEB, 0x9F)),
() => mdc.PrimaryDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
() => mdc.OnPrimary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x04, 0x2D, 0x00)),
() => mdc.PrimaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x6B, 0x9D, 0x5A)),
() => mdc.OnPrimaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
() => mdc.PrimaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xBB, 0xF1, 0xA5)),
() => mdc.PrimaryFixedDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x9F, 0xD5, 0x8B)),
() => mdc.OnPrimaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x01, 0x16, 0x00)),
() => mdc.OnPrimaryFixedVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x10, 0x3F, 0x06)),
() => mdc.InversePrimary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x24, 0x52, 0x18)),

// Secondaries [Q]
() => mdc.Secondary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xCC, 0xE3, 0xBF)),
() => mdc.SecondaryDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
() => mdc.OnSecondary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x18, 0x2A, 0x12)),
() => mdc.SecondaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x81, 0x97, 0x77)),
() => mdc.OnSecondaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),

// Secondary Fixed [QF]
() => mdc.SecondaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xD2, 0xE9, 0xC5)),
() => mdc.SecondaryFixedDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xB7, 0xCD, 0xAA)),
() => mdc.OnSecondaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x05, 0x15, 0x02)),
() => mdc.OnSecondaryFixedVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x28, 0x3B, 0x22)),

// Tertiaries [T]
() => mdc.Tertiary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x85, 0xEE, 0xE1)),
() => mdc.TertiaryDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
() => mdc.OnTertiary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x2B, 0x27)),
() => mdc.TertiaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x2D, 0xA1, 0x96)),
() => mdc.OnTertiaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),

// Tertiary Fixed [TF]
() => mdc.TertiaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x8B, 0xF5, 0xE8)),
() => mdc.TertiaryFixedDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x6E, 0xD8, 0xCC)),
() => mdc.OnTertiaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x15, 0x12)),
() => mdc.OnTertiaryFixedVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x3E, 0x38)),

// Errors [E]
() => mdc.Error.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xFF, 0xD2, 0xCC)),
() => mdc.ErrorDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
() => mdc.OnError.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x54, 0x00, 0x03)),
() => mdc.ErrorContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xFF, 0x54, 0x49)),
() => mdc.OnErrorContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),

// Android-only
() => mdc.ControlActivated.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x23, 0x51, 0x17)),
() => mdc.ControlNormal.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xC2, 0xC9, 0xBA)),
() => mdc.ControlHighlight.GetColor(scheme).ShouldBe(Color.FromArgb(0x33, 0xFF, 0xFF, 0xFF)),
() => mdc.TextPrimaryInverse.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)),
() => mdc.TextSecondaryAndTertiaryInverse.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x42, 0x49, 0x3E)),
() => mdc.TextPrimaryInverseDisableOnly.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)),
() => mdc.TextSecondaryAndTertiaryInverseDisabled.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)),
() => mdc.TextHintInverse.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)));
await Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-windows</TargetFrameworks>
<TargetFramework>net8.0-windows</TargetFramework>
<AssemblyTitle>MaterialColorUtilities.Tests</AssemblyTitle>
<Product>MaterialColorUtilities.Tests</Product>
<OutputType>Exe</OutputType>
Expand All @@ -13,6 +13,7 @@
<EnableSourceLink>false</EnableSourceLink>
<SuppressImplicitGitSourceLink>true</SuppressImplicitGitSourceLink>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\MaterialDesign3.MaterialColorUtilities\MaterialColorUtilities.csproj" />
</ItemGroup>
Expand All @@ -25,10 +26,12 @@
<PackageReference Include="System.Net.Http" />
<PackageReference Include="System.Text.RegularExpressions" />
<PackageReference Include="TUnit" />
<PackageReference Include="Shouldly" />
</ItemGroup>
<ItemGroup>
<Using Include="TUnit.Assertions.AssertConditions.Throws" />
<Using Include="TUnit.Core.Executors" />
<Using Include="TUnit.Assertions" />
<Using Include="Shouldly" />
</ItemGroup>
</Project>