chores: Support Multiple Palette Generation Method

This commit is contained in:
Raspberry-Monster
2025-10-23 18:16:23 +08:00
parent d348a30237
commit 7ab833a53a
12 changed files with 160 additions and 24 deletions

View File

@@ -5,6 +5,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:enums="using:BetterLyrics.WinUI3.Enums"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:BetterLyrics.WinUI3.Controls" xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
@@ -115,6 +116,13 @@
</controls:SettingsExpander.Items> </controls:SettingsExpander.Items>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPagePaletteGeneratorType" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE790;}">
<ComboBox SelectedIndex="{x:Bind LyricsBackgroundSettings.PaletteGeneratorType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem Content="MedianCut" />
<ComboBoxItem Content="OctTree" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander <controls:SettingsExpander
x:Uid="SettingsPageSpectrumLayer" x:Uid="SettingsPageSpectrumLayer"

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum PaletteGeneratorType
{
MedianCut,
OctTree
}
}

View File

@@ -1,5 +1,7 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models.Settings;
using CommunityToolkit.WinUI.Helpers; using CommunityToolkit.WinUI.Helpers;
using Impressionist.Abstractions; using Impressionist.Abstractions;
using Impressionist.Implementations; using Impressionist.Implementations;
@@ -91,26 +93,24 @@ namespace BetterLyrics.WinUI3.Helper
return buffer; return buffer;
} }
public static async Task<PaletteResult> GetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null)
public static Task<ThemeColorResult> GetAccentColorFromByteAsync(byte[] bytes, PaletteGeneratorType generatorType)
{ {
using var stream = new InMemoryRandomAccessStream(); return generatorType switch
await stream.WriteAsync(bytes.AsBuffer()); {
stream.Seek(0); PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorFromByteAsync(bytes),
var decoder = await BitmapDecoder.CreateAsync(stream); PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorFromByteAsync(bytes),
var colors = await GetPixelColor(decoder); _ => throw new ArgumentOutOfRangeException("generatorType"),
var palette = await PaletteGenerators.OctTreePaletteGenerator.CreatePalette(colors, count, false, isDark); };
return palette;
} }
public static Task<PaletteResult> GetAccentColorsFromByteAsync(byte[] bytes, int count, PaletteGeneratorType generatorType, bool? isDark = null)
public static async Task<ThemeColorResult> GetAccentColorFromByteAsync(byte[] bytes)
{ {
using var stream = new InMemoryRandomAccessStream(); return generatorType switch
await stream.WriteAsync(bytes.AsBuffer()); {
stream.Seek(0); PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorsFromByteAsync(bytes, count, isDark),
var decoder = await BitmapDecoder.CreateAsync(stream); PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorsFromByteAsync(bytes, count, isDark),
var colors = await GetPixelColor(decoder); _ => throw new ArgumentOutOfRangeException("generatorType"),
var theme = await PaletteGenerators.OctTreePaletteGenerator.CreateThemeColor(colors, false); };
return theme;
} }
public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder) public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
@@ -193,7 +193,7 @@ namespace BetterLyrics.WinUI3.Helper
return (double)(sum / (pixels.Length / 4)); return (double)(sum / (pixels.Length / 4));
} }
public static async Task<byte[]> MakeSquareWithThemeColor(byte[] imageBytes) public static async Task<byte[]> MakeSquareWithThemeColor(byte[] imageBytes, PaletteGeneratorType generatorType)
{ {
using var image = Image.Load<Rgba32>(imageBytes); using var image = Image.Load<Rgba32>(imageBytes);
@@ -205,7 +205,7 @@ namespace BetterLyrics.WinUI3.Helper
int size = Math.Max(image.Width, image.Height); int size = Math.Max(image.Width, image.Height);
var result = await GetAccentColorFromByteAsync(imageBytes); var result = await GetAccentColorFromByteAsync(imageBytes, generatorType);
var color = Windows.UI.Color.FromArgb(255, (byte)result.Color.X, (byte)result.Color.Y, (byte)result.Color.Z); var color = Windows.UI.Color.FromArgb(255, (byte)result.Color.X, (byte)result.Color.Y, (byte)result.Color.Z);
var themeColor = Rgba32.ParseHex(color.ToHex()); var themeColor = Rgba32.ParseHex(color.ToHex());

View File

@@ -0,0 +1,95 @@
using Impressionist.Abstractions;
using Impressionist.Implementations;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Helper
{
public static class PaletteHelper
{
public static async Task<PaletteResult> OctTreeGetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null)
{
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
stream.Seek(0);
var decoder = await BitmapDecoder.CreateAsync(stream);
var colors = await GetPixelColor(decoder);
var palette = await PaletteGenerators.OctTreePaletteGenerator.CreatePalette(colors, count, false, isDark);
return palette;
}
public static async Task<ThemeColorResult> OctTreeGetAccentColorFromByteAsync(byte[] bytes)
{
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
stream.Seek(0);
var decoder = await BitmapDecoder.CreateAsync(stream);
var colors = await GetPixelColor(decoder);
var theme = await PaletteGenerators.OctTreePaletteGenerator.CreateThemeColor(colors, false);
return theme;
}
public static Task<ThemeColorResult> MedianCutGetAccentColorFromByteAsync(byte[] bytes)
{
using var image = Image.Load<Rgba32>(bytes);
var colorThief = new ColorThief.ImageSharp.ColorThief();
var mainColor = colorThief.GetColor(image, 10, false);
var theme = new ThemeColorResult(new Vector3(mainColor.Color.R, mainColor.Color.G, mainColor.Color.B), mainColor.IsDark);
return Task.FromResult(theme);
}
public static Task<PaletteResult> MedianCutGetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null)
{
using var image = Image.Load<Rgba32>(bytes);
var colorThief = new ColorThief.ImageSharp.ColorThief();
var mainColor = colorThief.GetColor(image, 10, false);
var theme = new ThemeColorResult(new Vector3(mainColor.Color.R, mainColor.Color.G, mainColor.Color.B), mainColor.IsDark);
var palette = colorThief.GetPalette(image, 255, 10, false);
var topColors = palette
.Where(x => x.IsDark == (isDark ?? mainColor.IsDark))
.OrderByDescending(x => x.Population)
.Select(x => new Vector3(x.Color.R, x.Color.G, x.Color.B))
.Take(count)
.ToList();
var paletteResult = new PaletteResult(topColors, mainColor.IsDark, theme);
return Task.FromResult(paletteResult);
}
public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
{
var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync();
var pixels = pixelDataProvider.DetachPixelData();
var count = bitmapDecoder.PixelWidth * bitmapDecoder.PixelHeight;
var vector = new Dictionary<Vector3, int>();
for (int i = 0; i < count; i += 10)
{
var offset = i * 4;
var b = pixels[offset];
var g = pixels[offset + 1];
var r = pixels[offset + 2];
var a = pixels[offset + 3];
if (a == 0) continue;
var color = new Vector3(r, g, b);
if (vector.ContainsKey(color))
{
vector[color]++;
}
else
{
vector[color] = 1;
}
}
return vector;
}
}
}

View File

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using BetterLyrics.WinUI3.Helper;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -27,6 +28,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsSpectrumOverlayEnabled { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsSpectrumOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial PaletteGeneratorType PaletteGeneratorType { get; set; } = PaletteGeneratorType.OctTree;
public LyricsBackgroundSettings() { } public LyricsBackgroundSettings() { }
public object Clone() public object Clone()
@@ -39,6 +42,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
PureColorOverlayOpacity = this.PureColorOverlayOpacity, PureColorOverlayOpacity = this.PureColorOverlayOpacity,
CoverOverlaySpeed = this.CoverOverlaySpeed, CoverOverlaySpeed = this.CoverOverlaySpeed,
CoverAcrylicEffectAmount = this.CoverAcrylicEffectAmount, CoverAcrylicEffectAmount = this.CoverAcrylicEffectAmount,
PaletteGeneratorType = this.PaletteGeneratorType
}; };
} }
} }

View File

@@ -49,7 +49,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
} }
bytes = await ImageHelper.MakeSquareWithThemeColor(bytes); bytes = await ImageHelper.MakeSquareWithThemeColor(bytes, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType);
using var stream = new InMemoryRandomAccessStream(); using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer()); await stream.WriteAsync(bytes.AsBuffer());
@@ -62,9 +62,9 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
albumArtSwBitmap = SoftwareBitmap.Copy(albumArtSwBitmap); albumArtSwBitmap = SoftwareBitmap.Copy(albumArtSwBitmap);
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var albumArtLightAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, false); var albumArtLightAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, false);
var lightColorBytes = albumArtLightAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList(); var lightColorBytes = albumArtLightAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList();
var albumArtDarkAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, true); var albumArtDarkAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, true);
var darkColorBytes = albumArtDarkAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList(); var darkColorBytes = albumArtDarkAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList();
AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, lightColorBytes, darkColorBytes)); AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, lightColorBytes, darkColorBytes));
} }

View File

@@ -1012,6 +1012,9 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve"> <data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>Open in file explorer</value> <value>Open in file explorer</value>
</data> </data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>Palette Generator Type</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve"> <data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>This folder is already included in the existing folder and does not need to be added again</value> <value>This folder is already included in the existing folder and does not need to be added again</value>
</data> </data>

View File

@@ -1012,6 +1012,9 @@
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve"> <data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>ファイルエクスプローラーで開きます</value> <value>ファイルエクスプローラーで開きます</value>
</data> </data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>パレット生成器タイプ</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve"> <data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>このフォルダーは既存のフォルダーに既に含まれており、再度追加する必要はありません</value> <value>このフォルダーは既存のフォルダーに既に含まれており、再度追加する必要はありません</value>
</data> </data>

View File

@@ -1012,6 +1012,9 @@
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve"> <data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>파일 탐색기에서 열립니다</value> <value>파일 탐색기에서 열립니다</value>
</data> </data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>팔레트 생성기 유형</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve"> <data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>이 폴더는 이미 기존 폴더에 포함되어 있으며 다시 추가 할 필요가 없습니다.</value> <value>이 폴더는 이미 기존 폴더에 포함되어 있으며 다시 추가 할 필요가 없습니다.</value>
</data> </data>

View File

@@ -1012,6 +1012,9 @@
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve"> <data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>在文件资源管理器中打开</value> <value>在文件资源管理器中打开</value>
</data> </data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>取色算法</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve"> <data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>该文件夹已包含在已有文件夹中,无需再次添加</value> <value>该文件夹已包含在已有文件夹中,无需再次添加</value>
</data> </data>

View File

@@ -1012,6 +1012,9 @@
<data name="SettingsPageOpenFolderButton.Content" xml:space="preserve"> <data name="SettingsPageOpenFolderButton.Content" xml:space="preserve">
<value>在檔案總管中開啟</value> <value>在檔案總管中開啟</value>
</data> </data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>取色算法</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve"> <data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>該資料夾已包含在已有資料夾中,無需再次添加</value> <value>該資料夾已包含在已有資料夾中,無需再次添加</value>
</data> </data>

View File

@@ -8,7 +8,7 @@ namespace Impressionist.Abstractions
public List<Vector3> Palette { get; } = new List<Vector3>(); public List<Vector3> Palette { get; } = new List<Vector3>();
public bool PaletteIsDark { get; } public bool PaletteIsDark { get; }
public ThemeColorResult ThemeColor { get; } public ThemeColorResult ThemeColor { get; }
internal PaletteResult(List<Vector3> palette, bool paletteIsDark, ThemeColorResult themeColor) public PaletteResult(List<Vector3> palette, bool paletteIsDark, ThemeColorResult themeColor)
{ {
Palette = palette; Palette = palette;
PaletteIsDark = paletteIsDark; PaletteIsDark = paletteIsDark;
@@ -19,7 +19,7 @@ namespace Impressionist.Abstractions
{ {
public Vector3 Color { get; } public Vector3 Color { get; }
public bool ColorIsDark { get; } public bool ColorIsDark { get; }
internal ThemeColorResult(Vector3 color, bool colorIsDark) public ThemeColorResult(Vector3 color, bool colorIsDark)
{ {
Color = color; Color = color;
ColorIsDark = colorIsDark; ColorIsDark = colorIsDark;