feat: add cover background

This commit is contained in:
Zhe Fang
2025-12-14 07:58:41 -05:00
parent 3d7e6061e9
commit adb02658f4
6 changed files with 256 additions and 83 deletions

View File

@@ -47,6 +47,47 @@
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageAlbumArtLayer" IsExpanded="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageOpacity" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="100"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsBackgroundSettings.CoverOverlayOpacity, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageSpeed" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="50"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsBackgroundSettings.CoverOverlaySpeed, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageBlurAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="100"
Maximum="100"
Minimum="0"
Value="{x:Bind LyricsBackgroundSettings.CoverOverlayBlurAmount, Mode=TwoWay}" />
</dev:SettingsCard>
<!--<dev:SettingsCard x:Uid="SettingsPageBackgroundAcrylicEffectAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="0"
Maximum="10"
Minimum="0"
Value="{x:Bind LyricsBackgroundSettings.CoverAcrylicEffectAmount, Mode=TwoWay}" />
</dev:SettingsCard>-->
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander
x:Uid="SettingsPageFluidLayer"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},

View File

@@ -13,15 +13,21 @@ using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.UI;
using static Vanara.PInvoke.Ole32;
namespace BetterLyrics.WinUI3.Controls
{
@@ -34,7 +40,8 @@ namespace BetterLyrics.WinUI3.Controls
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<TextAlignmentType>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<string>>
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<IRandomAccessStream?>>
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
@@ -42,6 +49,7 @@ namespace BetterLyrics.WinUI3.Controls
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
private readonly CoverBackgroundRenderer _coverRenderer = new();
private readonly PureColorBackgroundRenderer _pureColorRenderer = new();
private readonly SnowRenderer _snowRenderer = new();
private readonly FogRenderer _fogRenderer = new();
@@ -368,8 +376,8 @@ namespace BetterLyrics.WinUI3.Controls
lyricsBg.IsPureColorOverlayEnabled
);
_fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity / 100.0;
_fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
_coverRenderer.Draw(sender, args.DrawingSession);
_fluidRenderer.Draw(sender, args.DrawingSession);
_snowRenderer.Draw(sender, args.DrawingSession);
@@ -549,16 +557,21 @@ namespace BetterLyrics.WinUI3.Controls
_isLayoutChanged = false;
if (_fluidRenderer.IsEnabled)
{
_fluidRenderer.UpdateColors(
_accentColor1Transition.Value,
_accentColor2Transition.Value,
_accentColor3Transition.Value,
_accentColor4Transition.Value
);
_fluidRenderer.Update(elapsedTime);
}
_fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
_fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity / 100.0;
_fluidRenderer.UpdateColors(
_accentColor1Transition.Value,
_accentColor2Transition.Value,
_accentColor3Transition.Value,
_accentColor4Transition.Value
);
_fluidRenderer.Update(elapsedTime);
_coverRenderer.IsEnabled = lyricsBg.IsCoverOverlayEnabled;
_coverRenderer.Opacity = lyricsBg.CoverOverlayOpacity;
_coverRenderer.BlurAmount = lyricsBg.CoverOverlayBlurAmount;
_coverRenderer.Speed = lyricsBg.CoverOverlaySpeed;
_coverRenderer.Update(elapsedTime);
_snowRenderer.IsEnabled = lyricsBg.IsSnowFlakeOverlayEnabled;
_snowRenderer.Amount = lyricsBg.SnowFlakeOverlayAmount / 100f;
@@ -600,7 +613,13 @@ namespace BetterLyrics.WinUI3.Controls
private async void Canvas_CreateResources(CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
{
args.TrackAsyncAction(_fluidRenderer.LoadResourcesAsync().AsAsyncAction());
var tasks = new Task[]
{
_fluidRenderer.LoadResourcesAsync(),
ReloadCoverBackgroundResourcesAsync()
};
args.TrackAsyncAction(tasks.WhenAll().AsAsyncAction());
_snowRenderer.LoadResources();
_fogRenderer.LoadResources();
@@ -682,6 +701,16 @@ namespace BetterLyrics.WinUI3.Controls
}).ToList();
}
private async Task ReloadCoverBackgroundResourcesAsync()
{
if (_mediaSessionsService.AlbumArtBitmapStream is IRandomAccessStream stream)
{
stream.Seek(0);
CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(Canvas, stream);
_coverRenderer.SetCoverBitmap(bitmap);
}
}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
@@ -874,5 +903,15 @@ namespace BetterLyrics.WinUI3.Controls
}
}
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapStream))
{
_ = ReloadCoverBackgroundResourcesAsync();
}
}
}
}
}

View File

@@ -12,6 +12,11 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPureColorOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PureColorOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsCoverOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlaySpeed { get; set; } = 50;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayBlurAmount { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFluidOverlayEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int FluidOverlayOpacity { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial PaletteGeneratorType PaletteGeneratorType { get; set; } = PaletteGeneratorType.MedianCut;

View File

@@ -5,6 +5,7 @@ using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Numerics;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Renderer
{
@@ -13,17 +14,47 @@ namespace BetterLyrics.WinUI3.Renderer
private CanvasBitmap? _currentBitmap;
private CanvasBitmap? _previousBitmap;
private readonly ValueTransition<double> _crossfadeTransition;
private CanvasRenderTarget? _currentTargetCache;
private CanvasRenderTarget? _previousTargetCache;
private Size _lastScreenSize;
private bool _lastWasRotating = false;
private readonly ValueTransition<double> _crossfadeTransition;
private float _rotationAngle = 0f;
public bool IsEnabled { get; set; } = false;
public int Opacity { get; set; } = 100;
public int BlurAmount { get; set; } = 100;
private bool _needsCacheUpdate = false;
public int Speed { get; set; } = 100;
private int _blurAmount = 100;
public int BlurAmount
{
get => _blurAmount;
set
{
if (_blurAmount != value)
{
_blurAmount = value;
_needsCacheUpdate = true;
}
}
}
private int _speed = 100;
public int Speed
{
get => _speed;
set
{
if (_speed != value)
{
_speed = value;
_needsCacheUpdate = true;
}
}
}
public CoverBackgroundRenderer()
{
@@ -34,26 +65,30 @@ namespace BetterLyrics.WinUI3.Renderer
{
if (_currentBitmap == newBitmap) return;
if (_currentBitmap == null)
{
_currentBitmap = newBitmap;
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
return;
}
_previousBitmap = _currentBitmap;
_previousTargetCache = _currentTargetCache;
_currentTargetCache = null;
_currentBitmap = newBitmap;
if (newBitmap != null)
if (_currentBitmap == null)
{
_crossfadeTransition.Reset(0.0);
_crossfadeTransition.StartTransition(1.0);
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
}
else
{
_previousBitmap = null;
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
if (_previousBitmap == null)
{
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
}
else
{
_crossfadeTransition.Reset(0.0);
_crossfadeTransition.StartTransition(1.0);
}
}
_needsCacheUpdate = true;
}
public void Update(TimeSpan deltaTime)
@@ -64,17 +99,17 @@ namespace BetterLyrics.WinUI3.Renderer
if (Speed > 0)
{
float baseSpeed = 0.6f; // 弧度/秒
float baseSpeed = 0.6f;
float currentSpeed = (Speed / 100.0f) * baseSpeed;
_rotationAngle += currentSpeed * (float)deltaTime.TotalSeconds;
_rotationAngle %= (float)(2 * Math.PI);
}
if (_crossfadeTransition.Value >= 1.0 && _previousBitmap != null)
{
_previousBitmap = null;
_previousTargetCache?.Dispose();
_previousTargetCache = null;
}
}
@@ -82,84 +117,133 @@ namespace BetterLyrics.WinUI3.Renderer
{
if (!IsEnabled || Opacity <= 0) return;
if (_lastScreenSize != control.Size)
{
_lastScreenSize = control.Size;
_needsCacheUpdate = true;
}
bool isRotating = Speed > 0;
if (_lastWasRotating != isRotating)
{
_lastWasRotating = isRotating;
_needsCacheUpdate = true;
}
EnsureCachedLayer(control, _currentBitmap, ref _currentTargetCache);
float baseAlpha = Opacity / 100.0f;
float currentBlur = BlurAmount;
float angle = Speed > 0 ? _rotationAngle : 0f;
float angle = isRotating ? _rotationAngle : 0f;
double fadeProgress = _crossfadeTransition.Value;
bool isCrossfading = fadeProgress < 1.0 && _previousBitmap != null;
bool isCrossfading = fadeProgress < 1.0 && _previousTargetCache != null;
Vector2 screenCenter = new Vector2((float)control.Size.Width / 2f, (float)control.Size.Height / 2f);
if (isCrossfading)
{
DrawLayer(ds, control.Size, _previousBitmap, angle, currentBlur, baseAlpha);
DrawCachedLayer(ds, _previousTargetCache, screenCenter, angle, baseAlpha);
float newLayerAlpha = baseAlpha * (float)fadeProgress;
if (newLayerAlpha > 0.005f)
{
DrawLayer(ds, control.Size, _currentBitmap, angle, currentBlur, newLayerAlpha);
}
DrawCachedLayer(ds, _currentTargetCache, screenCenter, angle, newLayerAlpha);
}
else if (_currentBitmap != null)
else if (_currentTargetCache != null)
{
DrawLayer(ds, control.Size, _currentBitmap, angle, currentBlur, baseAlpha);
DrawCachedLayer(ds, _currentTargetCache, screenCenter, angle, baseAlpha);
}
}
private void DrawLayer(CanvasDrawingSession ds, Windows.Foundation.Size screenSize, CanvasBitmap? bitmap, float rotationRadians, float blurAmount, float alpha)
private void EnsureCachedLayer(ICanvasResourceCreator resourceCreator, CanvasBitmap? sourceBitmap, ref CanvasRenderTarget? targetCache)
{
if (bitmap == null) return;
float imgW = bitmap.SizeInPixels.Width;
float imgH = bitmap.SizeInPixels.Height;
Vector2 screenCenter = new Vector2((float)screenSize.Width / 2f, (float)screenSize.Height / 2f);
float scale;
if (Speed > 0 && Math.Abs(rotationRadians) > 0.001f)
if (sourceBitmap == null)
{
float screenDiagonal = (float)Math.Sqrt(screenSize.Width * screenSize.Width + screenSize.Height * screenSize.Height);
float scaleX = screenDiagonal / imgW;
float scaleY = screenDiagonal / imgH;
scale = Math.Max(scaleX, scaleY);
}
else
{
float scaleX = (float)screenSize.Width / imgW;
float scaleY = (float)screenSize.Height / imgH;
scale = Math.Max(scaleX, scaleY);
targetCache?.Dispose();
targetCache = null;
return;
}
// 缩放图片 -> 将图片中心移动到 (0,0) 以便旋转 -> 旋转 ->将图片移回屏幕中心
Vector2 imgCenterOffset = new Vector2(
((float)screenSize.Width - imgW * scale) / 2.0f,
((float)screenSize.Height - imgH * scale) / 2.0f
);
bool deviceMismatch = targetCache != null && targetCache.Device != resourceCreator.Device;
if (_needsCacheUpdate || targetCache == null || deviceMismatch)
{
targetCache?.Dispose();
float imgW = sourceBitmap.SizeInPixels.Width;
float imgH = sourceBitmap.SizeInPixels.Height;
Size screenSize = _lastScreenSize;
float scale;
if (_lastWasRotating) // Speed > 0
{
float screenDiagonal = (float)Math.Sqrt(screenSize.Width * screenSize.Width + screenSize.Height * screenSize.Height);
scale = Math.Max(screenDiagonal / imgW, screenDiagonal / imgH);
}
else
{
float scaleX = (float)screenSize.Width / imgW;
float scaleY = (float)screenSize.Height / imgH;
scale = Math.Max(scaleX, scaleY);
}
float targetW = imgW * scale;
float targetH = imgH * scale;
targetCache = new CanvasRenderTarget(resourceCreator, targetW, targetH, sourceBitmap.Dpi);
using (var ds = targetCache.CreateDrawingSession())
{
ds.Clear(Windows.UI.Color.FromArgb(0, 0, 0, 0));
using (var transformEffect = new Transform2DEffect())
using (var blurEffect = new GaussianBlurEffect())
{
transformEffect.Source = sourceBitmap;
transformEffect.TransformMatrix = Matrix3x2.CreateScale(scale);
transformEffect.InterpolationMode = CanvasImageInterpolation.Linear;
blurEffect.Source = transformEffect;
blurEffect.BlurAmount = BlurAmount;
blurEffect.BorderMode = EffectBorderMode.Hard;
ds.DrawImage(blurEffect);
}
}
if (sourceBitmap == _currentBitmap)
{
_needsCacheUpdate = false;
}
}
}
private void DrawCachedLayer(CanvasDrawingSession ds, CanvasRenderTarget? cachedTexture, Vector2 screenCenter, float rotationRadians, float alpha)
{
if (cachedTexture == null) return;
Vector2 textureCenter = new Vector2((float)cachedTexture.Size.Width / 2f, (float)cachedTexture.Size.Height / 2f);
Matrix3x2 transform =
Matrix3x2.CreateScale(scale) * Matrix3x2.CreateTranslation(imgCenterOffset) * Matrix3x2.CreateRotation(rotationRadians, screenCenter);
Matrix3x2.CreateTranslation(-textureCenter) * Matrix3x2.CreateRotation(rotationRadians) * Matrix3x2.CreateTranslation(screenCenter);
using (var transformEffect = new Transform2DEffect())
using (var blurEffect = new GaussianBlurEffect())
{
transformEffect.Source = bitmap;
transformEffect.TransformMatrix = transform;
transformEffect.InterpolationMode = CanvasImageInterpolation.Linear;
Matrix3x2 previousTransform = ds.Transform;
blurEffect.Source = transformEffect;
blurEffect.BlurAmount = blurAmount > 0 ? (blurAmount / 2.0f) : 0f;
blurEffect.BorderMode = EffectBorderMode.Hard;
ds.Transform = transform * previousTransform;
ds.DrawImage(cachedTexture, 0, 0, new Rect(0, 0, cachedTexture.Size.Width, cachedTexture.Size.Height), alpha);
ds.DrawImage(blurEffect, 0, 0, new Windows.Foundation.Rect(0, 0, screenSize.Width, screenSize.Height), alpha);
}
ds.Transform = previousTransform;
}
public void Dispose()
{
_currentBitmap?.Dispose();
_currentBitmap = null;
_previousBitmap?.Dispose();
_currentTargetCache?.Dispose();
_previousTargetCache?.Dispose();
_currentBitmap = null;
_previousBitmap = null;
_currentTargetCache = null;
_previousTargetCache = null;
}
}
}

View File

@@ -5,6 +5,7 @@ using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
@@ -28,6 +29,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
LyricsData? CurrentLyricsData { get; }
BitmapImage? AlbumArtBitmapImage { get; }
IRandomAccessStream? AlbumArtBitmapStream { get; }
AlbumArtThemeColors CalculateAlbumArtThemeColors(LyricsWindowStatus lyricsWindowStatus, Color backdropAccentColor);

View File

@@ -30,6 +30,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private BitmapDecoder? _albumArtBitmapDecoder = null;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial BitmapImage? AlbumArtBitmapImage { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial IRandomAccessStream? AlbumArtBitmapStream { get; set; }
private void UpdateAlbumArt()
{
@@ -80,6 +81,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (token.IsCancellationRequested) return;
AlbumArtBitmapImage = bitmapImage;
AlbumArtBitmapStream = ImageHelper.ToIRandomAccessStream(buffer);
}
public AlbumArtThemeColors CalculateAlbumArtThemeColors(LyricsWindowStatus lyricsWindowStatus, Color backdropAccentColor)