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
24 changes: 24 additions & 0 deletions WallpaperMaker.Avalonia/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@

<Separator Margin="4,0" Width="1" />

<TextBlock Text="Style:" VerticalAlignment="Center" />
<ComboBox
SelectionChanged="CbStyle_SelectionChanged"
Width="180"
x:Name="CbStyle">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

<Separator Margin="4,0" Width="1" />

<TextBlock Text="Palette:" VerticalAlignment="Center" />
<ComboBox
SelectionChanged="CbPalette_SelectionChanged"
Expand Down Expand Up @@ -67,6 +81,16 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

<CheckBox
x:Name="ChkUsePresetColors"
Content="Use preset colors"
IsEnabled="False"
IsChecked="False"
Checked="ChkUsePresetColors_Checked"
Unchecked="ChkUsePresetColors_Unchecked"
VerticalAlignment="Center"
Margin="8,0,0,0" />
</StackPanel>

<!-- Resolution / multisampling bar -->
Expand Down
97 changes: 95 additions & 2 deletions WallpaperMaker.Avalonia/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public partial class MainWindow : Window
private Pallet? _activePalette;
private SKBitmap? _lastBitmap;
private WallpaperConfig _config = WallpaperConfig.CreateDefault();
private Pallet? _presetGeneratedPalette; // Cached preset-matched palette
private bool _usePresetColors = false; // Toggle state

private const string DefaultPalletJson = """
{
Expand Down Expand Up @@ -92,10 +94,21 @@ private void InitFormElements()
_pallets.AddRange(Utilities.UnpackExternalColorPallets());

RefreshPaletteComboBox();
RefreshStylePresetComboBox();
DetectResolution();
UpdateMultisamplingList();
}

private void RefreshStylePresetComboBox()
{
CbStyle.Items.Clear();
foreach (var preset in StylePresets.All)
CbStyle.Items.Add(preset);

if (StylePresets.All.Count > 0)
CbStyle.SelectedIndex = 0;
}

private void DetectResolution()
{
var screen = Screens.ScreenFromWindow(this) ?? Screens.Primary;
Expand Down Expand Up @@ -182,12 +195,31 @@ private async void BtnGenerate_Click(object? sender, RoutedEventArgs e)
_lastBitmap?.Dispose();
_lastBitmap = await Task.Run(() =>
{
using var gen = new Generator(_activePalette, w, h, ms, _config);
// Select the appropriate palette based on toggle state
Pallet paletteToUse = (_usePresetColors && _presetGeneratedPalette != null)
? _presetGeneratedPalette
: _activePalette!;

using var gen = new Generator(paletteToUse, w, h, ms, _config);
return gen.Generate(_config);
});

ImgPreview.Source = SKBitmapToAvaloniaBitmap(_lastBitmap);
TxtStatus.Text = $"Generated {w}x{h} wallpaper.";

// Score the generated wallpaper
int qualityScore = QualityScorer.ScoreWallpaper(_lastBitmap, _activePalette, _config);
var report = QualityScorer.GenerateReport(_lastBitmap, _activePalette, _config);

// Format status with quality score
string scoreIndicator = qualityScore switch
{
>= 75 => "Excellent",
>= 60 => "Good",
>= 50 => "Fair",
_ => "Below Average"
};

TxtStatus.Text = $"Generated {w}x{h} wallpaper. Quality: {scoreIndicator} ({qualityScore}/100)";
}
catch (Exception ex)
{
Expand Down Expand Up @@ -296,6 +328,67 @@ private void CbPalette_SelectionChanged(object? sender, SelectionChangedEventArg
_activePalette = item.Pallet;
}

private void CbStyle_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (CbStyle.SelectedItem is StylePreset preset)
{
// Apply the preset's configuration
_config = new WallpaperConfig
{
Background = preset.Config.Background,
ShapeFill = preset.Config.ShapeFill,
MinOpacity = preset.Config.MinOpacity,
MaxOpacity = preset.Config.MaxOpacity,
EnableStrokes = preset.Config.EnableStrokes,
StrokeWidth = preset.Config.StrokeWidth,
SaturationLevel = preset.RecommendedSaturation,
ApplyQualityConstraints = preset.Config.ApplyQualityConstraints,
MinimumBackgroundShapeContrast = preset.Config.MinimumBackgroundShapeContrast,
BackgroundTone = preset.Config.BackgroundTone,
Shapes = new List<ShapeConfig>(preset.Config.Shapes)
};

// Auto-generate a harmonically appropriate palette
_presetGeneratedPalette = preset.Config.BackgroundTone == BackgroundTone.PreferDark
? ColorTheory.GenerateDarkAccentPalette($"Auto: {preset.Name}")
: ColorTheory.GeneratePaletteWithConstraints(
preset.RecommendedHarmony,
$"Auto: {preset.Name}",
preset.RecommendedSaturation);

// Set default to use preset colors
_activePalette = _presetGeneratedPalette;
_usePresetColors = true;

// Enable and check the toggle
if (ChkUsePresetColors != null)
{
ChkUsePresetColors.IsEnabled = true;
ChkUsePresetColors.IsChecked = true;
}

TxtStatus.Text = $"Loaded style: {preset.Name} — using preset-matched colors";
}
else
{
// No preset selected, disable the toggle
if (ChkUsePresetColors != null)
ChkUsePresetColors.IsEnabled = false;
}
}

private void ChkUsePresetColors_Checked(object? sender, RoutedEventArgs e)
{
_usePresetColors = true;
TxtStatus.Text = "Using preset-matched colors for generation";
}

private void ChkUsePresetColors_Unchecked(object? sender, RoutedEventArgs e)
{
_usePresetColors = false;
TxtStatus.Text = "Using selected palette (preset shapes only)";
}

private static Bitmap SKBitmapToAvaloniaBitmap(SKBitmap skBitmap)
{
using var image = SKImage.FromBitmap(skBitmap);
Expand Down
6 changes: 0 additions & 6 deletions WallpaperMaker.Avalonia/SettingsWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@
<Slider x:Name="SlStrokeWidth" Minimum="1" Maximum="10" Value="2" Width="120" IsSnapToTickEnabled="True" TickFrequency="1" />
</StackPanel>

<!-- Seed display -->
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Seed:" VerticalAlignment="Center" />
<TextBox x:Name="TbSeed" IsReadOnly="True" Width="260" />
</StackPanel>

<!-- Buttons -->
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
<Button Content="Cancel" Width="80" Click="BtnCancel_Click" />
Expand Down
15 changes: 1 addition & 14 deletions WallpaperMaker.Avalonia/SettingsWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
public partial class SettingsWindow : Window
{
public WallpaperConfig? ResultConfig { get; private set; }
public string? ResultSeed { get; private set; }

private readonly List<ShapeRow> _rows = new();
private bool _initialized;

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · win-x64

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · osx-x64

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · win-x86

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · win-arm64

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · osx-arm64

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · linux-musl-x64

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · linux-x64

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · linux-musl-arm64

The field 'SettingsWindow._initialized' is assigned but its value is never used

Check warning on line 14 in WallpaperMaker.Avalonia/SettingsWindow.axaml.cs

View workflow job for this annotation

GitHub Actions / Build · linux-arm64

The field 'SettingsWindow._initialized' is assigned but its value is never used

private class ShapeRow
{
Expand All @@ -24,8 +23,6 @@

public SettingsWindow() : this(WallpaperConfig.CreateDefault()) { }

public SettingsWindow(string currentSeed) : this(WallpaperConfig.FromSeed(currentSeed)) { }

public SettingsWindow(WallpaperConfig config)
{
InitializeComponent();
Expand All @@ -34,7 +31,6 @@
BuildShapeRows(config);
LoadConfigSettings(config);
_initialized = true;
UpdateSeed();
}

private void PopulateFillModes()
Expand Down Expand Up @@ -112,7 +108,6 @@
bool enabled = checkBox.IsChecked == true;
amountSlider.IsEnabled = enabled;
sizeSlider.IsEnabled = enabled;
if (_initialized) UpdateSeed();
};

var grid = new Grid
Expand Down Expand Up @@ -184,15 +179,9 @@
return config;
}

private void UpdateSeed()
{
var config = BuildConfig();
TbSeed.Text = config.ToSeed();
}

private void AnySlider_Changed(object? sender, RangeBaseValueChangedEventArgs e)
{
if (_initialized) UpdateSeed();
// Slider changed - configuration updated automatically
}

private void BtnDone_Click(object? sender, RoutedEventArgs e)
Expand All @@ -201,14 +190,12 @@
if (!anyEnabled) return;

ResultConfig = BuildConfig();
ResultSeed = ResultConfig.ToSeed();
Close();
}

private void BtnCancel_Click(object? sender, RoutedEventArgs e)
{
ResultConfig = null;
ResultSeed = null;
Close();
}
}
145 changes: 145 additions & 0 deletions WallpaperMaker.Domain/ColorContrast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using SkiaSharp;

namespace WallpaperMaker.Domain;

/// <summary>
/// WCAG 2.0 contrast calculations for ensuring visual clarity and accessibility.
/// Provides tools for analyzing color contrast ratios and selecting colors with sufficient contrast.
/// </summary>
public static class ColorContrast
{
/// <summary>
/// WCAG contrast level classifications based on contrast ratio.
/// </summary>
public enum ContrastLevel
{
/// <summary>Enhanced contrast (7:1 or higher) - AAA level, best for small text</summary>
AAA,
/// <summary>Acceptable contrast (4.5:1 or higher) - AA level, standard for text</summary>
AA,
/// <summary>Low contrast (below 4.5:1) - may be hard to distinguish</summary>
Low
}

/// <summary>
/// Calculates the relative luminance of a color according to WCAG 2.0.
/// Used as the basis for contrast ratio calculations.
/// </summary>
/// <param name="color">The SKColor to analyze</param>
/// <returns>Relative luminance (0.0 = darkest, 1.0 = brightest)</returns>
public static float GetRelativeLuminance(SKColor color)
{
float r = RgbToLinear(color.Red / 255f);
float g = RgbToLinear(color.Green / 255f);
float b = RgbToLinear(color.Blue / 255f);

return 0.2126f * r + 0.7152f * g + 0.0722f * b;
}

/// <summary>
/// Converts an sRGB value to linear RGB for luminance calculation.
/// </summary>
private static float RgbToLinear(float value)
{
if (value <= 0.03928f)
return value / 12.92f;
return (float)Math.Pow((value + 0.055f) / 1.055f, 2.4f);
}

/// <summary>
/// Calculates the contrast ratio between two colors according to WCAG 2.0.
/// </summary>
/// <param name="color1">First color</param>
/// <param name="color2">Second color</param>
/// <returns>Contrast ratio (1.0 = no contrast, 21.0 = maximum contrast)</returns>
public static float GetContrastRatio(SKColor color1, SKColor color2)
{
float lum1 = GetRelativeLuminance(color1);
float lum2 = GetRelativeLuminance(color2);

float lighter = Math.Max(lum1, lum2);
float darker = Math.Min(lum1, lum2);

return (lighter + 0.05f) / (darker + 0.05f);
}

/// <summary>
/// Classifies the contrast level between two colors.
/// </summary>
/// <param name="contrastRatio">The contrast ratio to classify</param>
/// <returns>ContrastLevel classification</returns>
public static ContrastLevel ClassifyContrast(float contrastRatio)
{
if (contrastRatio >= 7.0f)
return ContrastLevel.AAA;
if (contrastRatio >= 4.5f)
return ContrastLevel.AA;
return ContrastLevel.Low;
}

/// <summary>
/// Finds the color from a palette that has the highest contrast with a background color.
/// Ensures shapes will visually stand out from the background.
/// </summary>
/// <param name="background">The background color to contrast against</param>
/// <param name="palette">List of colors to choose from</param>
/// <param name="minContrast">Minimum acceptable contrast ratio (default 3.0 for visibility)</param>
/// <returns>The color from palette with highest contrast, or first valid color if none meet minimum</returns>
public static SKColor FindContrastingColor(SKColor background, List<SKColor> palette, float minContrast = 3.0f)
{
if (palette.Count == 0)
return SKColors.Black;

SKColor bestColor = palette[0];
float bestContrast = GetContrastRatio(background, bestColor);

foreach (var color in palette)
{
float contrast = GetContrastRatio(background, color);
if (contrast > bestContrast)
{
bestContrast = contrast;
bestColor = color;
}
}

// Return best color even if it doesn't meet minimum
// (palette colors should be chosen by user/preset to work together)
return bestColor;
}

/// <summary>
/// Filters palette colors to only those with sufficient contrast against a background.
/// </summary>
/// <param name="background">The background color</param>
/// <param name="palette">List of candidate colors</param>
/// <param name="minContrast">Minimum required contrast ratio</param>
/// <returns>Filtered list of colors meeting the contrast threshold</returns>
public static List<SKColor> FilterByContrast(SKColor background, List<SKColor> palette, float minContrast = 3.0f)
{
return palette
.Where(color => GetContrastRatio(background, color) >= minContrast)
.ToList();
}

/// <summary>
/// Calculates average contrast between a color and multiple background colors.
/// Useful for evaluating color suitability across varied compositions.
/// </summary>
/// <param name="color">The color to evaluate</param>
/// <param name="backgroundColors">List of background colors to test against</param>
/// <returns>Average contrast ratio</returns>
public static float GetAverageContrast(SKColor color, List<SKColor> backgroundColors)
{
if (backgroundColors.Count == 0)
return 0f;

float totalContrast = 0f;
foreach (var bg in backgroundColors)
{
totalContrast += GetContrastRatio(color, bg);
}

return totalContrast / backgroundColors.Count;
}
}
Loading