This commit is contained in:
Zhe Fang
2025-11-27 14:36:10 -05:00
parent 016d9a626f
commit 2099332f02
37 changed files with 2828 additions and 2845 deletions

View File

@@ -13,7 +13,6 @@ using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslateService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
@@ -116,7 +115,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<SettingsPageViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<MusicGalleryViewModel>()
.AddSingleton<LyricsRendererViewModel>()
//.AddSingleton<LyricsRendererViewModel>()
.AddSingleton<AboutControlViewModel>()
.BuildServiceProvider()
);

View File

@@ -65,7 +65,7 @@
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
<PackageReference Include="csharp-kana" Version="1.0.2" />
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="DevWinUI.Controls" Version="9.5.0" />
<PackageReference Include="DevWinUI.Controls" Version="9.6.0" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="F23.StringSimilarity" Version="7.0.0" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.2" />
@@ -80,7 +80,7 @@
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="NTextCat" Version="0.3.65" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.3-dev-02320" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.0" />
@@ -93,7 +93,7 @@
<PackageReference Include="Vanara.Windows.Shell" Version="4.2.1" />
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
<PackageReference Include="z440.atl.core" Version="7.8.0" />
<PackageReference Include="z440.atl.core" Version="7.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />

View File

@@ -218,13 +218,6 @@
Value="{x:Bind LyricsStyleSettings.LyricsLineSpacingFactor, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLyricsTranslationSeparator" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF464;}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBox AcceptsReturn="True" Text="{x:Bind LyricsStyleSettings.LyricsTranslationSeparator, Mode=TwoWay}" />
<Button Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, FontSize=12, Glyph=&#xE8FB;}" Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</dev:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Renderer.LyricsRenderer"
x:Class="BetterLyrics.WinUI3.Controls.NowPlayingCanvas"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Renderer"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Unloaded="LyricsCanvas_Unloaded"
Unloaded="Canvas_Unloaded"
mc:Ignorable="d">
<Grid>
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
CreateResources="LyricsCanvas_CreateResources"
Draw="LyricsCanvas_Draw"
Update="LyricsCanvas_Update" />
x:Name="Canvas"
CreateResources="Canvas_CreateResources"
Draw="Canvas_Draw"
Update="Canvas_Update" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,607 @@
// 2025/6/23 by Zhe Fang
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Logic;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Renderer;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
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.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Diagnostics;
using System.Linq;
using Windows.Foundation;
using Windows.UI;
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class NowPlayingCanvas : UserControl,
IRecipient<PropertyChangedMessage<Color>>,
IRecipient<PropertyChangedMessage<BitmapImage?>>,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<LyricsData?>>,
IRecipient<PropertyChangedMessage<LyricsWindowStatus>>
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly ILiveStatesService _liveStatesService = Ioc.Default.GetRequiredService<ILiveStatesService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
private readonly PureColorBackgroundRenderer _pureColorRenderer = new();
private readonly SnowRenderer _snowRenderer = new();
private readonly FogRenderer _fogRenderer = new();
private readonly SpectrumRenderer _spectrumRenderer = new();
private readonly LyricsSynchronizer _synchronizer = new();
private readonly LyricsLayoutManager _layoutManager = new();
private readonly LyricsAnimator _animator = new();
private readonly LyricsThemeManager _themeManager;
private readonly SpectrumAnalyzer _spectrumAnalyzer = new();
private Color _environmentalColor = Colors.Transparent;
private readonly ValueTransition<Color> _immersiveBgColorTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f,
durationSeconds: 0.3f
);
private readonly ValueTransition<Color> _accentColor1Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor2Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor3Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor4Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutQuad
);
private TimeSpan _songPosition; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1>
private TimeSpan _totalPlayedTime; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD>ŵ<EFBFBD>ʱ<EFBFBD>
private bool _isLastFMTracked = false;
private double _renderLyricsStartX = 0;
private double _renderLyricsStartY = 0;
private double _renderLyricsWidth = 0;
private double _renderLyricsHeight = 0;
private double _renderLyricsOpacity = 0;
private bool _isLayoutChanged = true;
private int _playingLineIndex;
private (int Start, int End) _visibleRange;
private double _canvasTargetScrollOffset;
private LyricsThemeColors _currentThemeColors;
public TimeSpan SongPosition => _songPosition;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><CABC> X <20><><EFBFBD><EFBFBD>
public double LyricsStartX
{
get { return (double)GetValue(LyricsStartXProperty); }
set { SetValue(LyricsStartXProperty, value); }
}
public static readonly DependencyProperty LyricsStartXProperty =
DependencyProperty.Register(nameof(LyricsStartX), typeof(double), typeof(NowPlayingCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʼ Y <20><><EFBFBD><EFBFBD>
public double LyricsStartY
{
get { return (double)GetValue(LyricsStartYProperty); }
set { SetValue(LyricsStartYProperty, value); }
}
public static readonly DependencyProperty LyricsStartYProperty =
DependencyProperty.Register(nameof(LyricsStartY), typeof(double), typeof(NowPlayingCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
public double LyricsWidth
{
get { return (double)GetValue(LyricsWidthProperty); }
set { SetValue(LyricsWidthProperty, value); }
}
public static readonly DependencyProperty LyricsWidthProperty =
DependencyProperty.Register(nameof(LyricsWidth), typeof(double), typeof(NowPlayingCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߶<EFBFBD>
public double LyricsHeight
{
get { return (double)GetValue(LyricsHeightProperty); }
set { SetValue(LyricsHeightProperty, value); }
}
public static readonly DependencyProperty LyricsHeightProperty =
DependencyProperty.Register(nameof(LyricsHeight), typeof(double), typeof(NowPlayingCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD><CDB8><EFBFBD><EFBFBD>
public double LyricsOpacity
{
get { return (double)GetValue(LyricsOpacityProperty); }
set { SetValue(LyricsOpacityProperty, value); }
}
public static readonly DependencyProperty LyricsOpacityProperty =
DependencyProperty.Register(nameof(LyricsOpacity), typeof(double), typeof(NowPlayingCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
public NowPlayingCanvas()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<Color>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<BitmapImage?>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<TimeSpan>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<LyricsData?>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<LyricsWindowStatus>>(this);
_themeManager = new(_mediaSessionsService);
}
private static void OnLayoutPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is NowPlayingCanvas canvas)
{
if (e.Property == LyricsStartXProperty)
{
canvas._renderLyricsStartX = Convert.ToDouble(e.NewValue);
}
else if (e.Property == LyricsStartYProperty)
{
canvas._renderLyricsStartY = Convert.ToDouble(e.NewValue);
}
else if (e.Property == LyricsWidthProperty)
{
canvas._renderLyricsWidth = Convert.ToDouble(e.NewValue);
}
else if (e.Property == LyricsHeightProperty)
{
canvas._renderLyricsHeight = Convert.ToDouble(e.NewValue);
}
else if (e.Property == LyricsOpacityProperty)
{
canvas._renderLyricsOpacity = Convert.ToDouble(e.NewValue);
}
canvas.TriggerRelayout();
}
}
// ====
private void Canvas_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
var bounds = new Rect(0, 0, sender.Size.Width, sender.Size.Height);
var status = _liveStatesService.LiveStates.LyricsWindowStatus;
var albumArtLayout = status.AlbumArtLayoutSettings;
var lyricsBg = status.LyricsBackgroundSettings;
var lyricsStyle = status.LyricsStyleSettings;
var lyricsEffect = status.LyricsEffectSettings;
var lyricsData = _mediaSessionsService.CurrentLyricsData;
double songDuration = _mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
double fixedSongPositionMs = _songPosition.TotalMilliseconds + (_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
Color overlayColor;
double finalOpacity;
if (status.IsAdaptToEnvironment)
{
// <20><><EFBFBD><EFBFBD>Ӧɫ
overlayColor = _immersiveBgColorTransition.Value;
finalOpacity = _immersiveBgOpacityTransition.Value * lyricsBg.PureColorOverlayOpacity / 100.0;
}
else
{
// ר<><D7A8>ɫ
overlayColor = _accentColor1Transition.Value;
finalOpacity = lyricsBg.PureColorOverlayOpacity / 100.0;
}
_pureColorRenderer.Draw(
args.DrawingSession,
bounds,
overlayColor,
finalOpacity,
lyricsBg.IsPureColorOverlayEnabled
);
_fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity;
_fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
_fluidRenderer.Draw(sender, args.DrawingSession);
_snowRenderer.Draw(sender, args.DrawingSession);
_fogRenderer.Draw(sender, args.DrawingSession);
_lyricsRenderer.Draw(
control: sender,
ds: args.DrawingSession,
lyricsData: _mediaSessionsService.CurrentLyricsData,
playingLineIndex: _playingLineIndex,
startVisibleIndex: _visibleRange.Start,
endVisibleIndex: _visibleRange.End,
canvasHeight: sender.Size.Height,
lyricsX: _renderLyricsStartX,
lyricsWidth: _renderLyricsWidth,
windowStatus: status,
strokeColor: _currentThemeColors.StrokeFontColor,
bgColor: _currentThemeColors.BgFontColor,
getPlaybackState: (lineIndex) =>
{
if (lyricsData == null) return new LinePlaybackState();
var line = lyricsData.LyricsLines.ElementAtOrDefault(lineIndex);
if (line == null) return new LinePlaybackState();
var nextLine = lyricsData.LyricsLines.ElementAtOrDefault(lineIndex + 1);
return _synchronizer.GetLinePlayingProgress(
fixedSongPositionMs,
line,
nextLine,
songDuration,
isForceWordByWord
);
}
);
if (_spectrumAnalyzer.IsCapturing)
{
_spectrumRenderer.Draw(
resourceCreator: sender,
ds: args.DrawingSession,
spectrumData: _spectrumAnalyzer?.SmoothSpectrum,
barCount: _spectrumAnalyzer?.BarCount ?? 0,
isEnabled: lyricsBg.IsSpectrumOverlayEnabled,
placement: lyricsBg.SpectrumPlacement,
canvasWidth: sender.Size.Width,
canvasHeight: sender.Size.Height,
fillColor: _currentThemeColors.BgFontColor
);
}
}
private void Canvas_Update(ICanvasAnimatedControl sender, CanvasAnimatedUpdateEventArgs args)
{
var lyricsBg = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings;
var lyricsEffect = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings;
TimeSpan elapsedTime = args.Timing.ElapsedTime;
_accentColor1Transition.Update(elapsedTime);
_accentColor2Transition.Update(elapsedTime);
_accentColor3Transition.Update(elapsedTime);
_accentColor4Transition.Update(elapsedTime);
_immersiveBgOpacityTransition.Update(elapsedTime);
_immersiveBgColorTransition.Update(elapsedTime);
UpdatePlaybackState(elapsedTime);
#region UpdatePlayingLineIndex
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPosition.TotalMilliseconds, _mediaSessionsService.CurrentLyricsData);
bool isPlayingLineChanged = newPlayingIndex != _playingLineIndex;
_playingLineIndex = newPlayingIndex;
#endregion
#region UpdateTargetScrollOffset
var targetScroll = _layoutManager.CalculateTargetScrollOffset(_mediaSessionsService.CurrentLyricsData, _playingLineIndex);
if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value;
_canvasYScrollTransition.StartTransition(_canvasTargetScrollOffset);
_canvasYScrollTransition.Update(elapsedTime);
#endregion
_visibleRange = _layoutManager.CalculateVisibleRange(
_mediaSessionsService.CurrentLyricsData?.LyricsLines,
_canvasYScrollTransition.Value, // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>λ<EFBFBD><CEBB>
sender.Size.Height
);
_animator.UpdateVisibleLines(
_mediaSessionsService.CurrentLyricsData,
_visibleRange.Start,
_visibleRange.End,
_playingLineIndex,
sender.Size.Height,
_canvasTargetScrollOffset,
_liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings,
_canvasYScrollTransition,
elapsedTime,
isPlayingLineChanged || _isLayoutChanged
);
_lyricsRenderer.CalculateLyrics3DMatrix(
lyricsEffect: lyricsEffect,
lyricsX: _renderLyricsStartX,
lyricsY: _renderLyricsStartY,
lyricsWidth: _renderLyricsWidth,
canvasHeight: sender.Size.Height
);
_isLayoutChanged = false;
if (_fluidRenderer.IsEnabled)
{
_fluidRenderer.UpdateColors(
_accentColor1Transition.Value,
_accentColor2Transition.Value,
_accentColor3Transition.Value,
_accentColor4Transition.Value
);
_fluidRenderer.Update(elapsedTime);
}
_snowRenderer.IsEnabled = lyricsBg.IsSnowFlakeOverlayEnabled;
_snowRenderer.Amount = lyricsBg.SnowFlakeOverlayAmount / 100f;
_snowRenderer.Speed = lyricsBg.SnowFlakeOverlaySpeed;
_snowRenderer.Update(elapsedTime.TotalSeconds);
_fogRenderer.IsEnabled = lyricsBg.IsFogOverlayEnabled;
_fogRenderer.Update(elapsedTime.TotalSeconds);
if (lyricsBg.IsSpectrumOverlayEnabled && !_spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.BarCount = 64;
_spectrumAnalyzer.StartCapture();
}
else if (!lyricsBg.IsSpectrumOverlayEnabled && _spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.StopCapture();
}
if (_spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.UpdateSmoothSpectrum();
}
}
private void Canvas_Unloaded(object sender, RoutedEventArgs e)
{
Canvas.RemoveFromVisualTree();
Canvas = null;
_fluidRenderer.Dispose();
_snowRenderer.Dispose();
_fogRenderer.Dispose();
_spectrumRenderer.Dispose();
DisposeAnalyzer();
}
private async void Canvas_CreateResources(CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
{
args.TrackAsyncAction(_fluidRenderer.LoadResourcesAsync().AsAsyncAction());
_snowRenderer.LoadResources();
_fogRenderer.LoadResources();
TriggerRelayout();
}
// ====
private void UpdateColorConfig()
{
var themeColors = _themeManager.UpdateColors(
_liveStatesService.LiveStates.LyricsWindowStatus,
_environmentalColor,
_accentColor1Transition,
_accentColor2Transition,
_accentColor3Transition,
_accentColor4Transition
);
_currentThemeColors = themeColors;
_accentColor1Transition.StartTransition(_currentThemeColors.AccentColor1);
_accentColor2Transition.StartTransition(_currentThemeColors.AccentColor2);
_accentColor3Transition.StartTransition(_currentThemeColors.AccentColor3);
_accentColor4Transition.StartTransition(_currentThemeColors.AccentColor4);
}
private void DisposeAnalyzer()
{
if (_spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.StopCapture();
}
_spectrumAnalyzer.Dispose();
}
private void TriggerRelayout()
{
if (_layoutManager == null || _mediaSessionsService.CurrentLyricsData == null) return;
_layoutManager.MeasureAndArrange(
resourceCreator: Canvas,
lyricsData: _mediaSessionsService.CurrentLyricsData,
status: _liveStatesService.LiveStates.LyricsWindowStatus,
appSettings: _settingsService.AppSettings,
canvasWidth: Canvas.Size.Width,
canvasHeight: Canvas.Size.Height,
lyricsWidth: _renderLyricsWidth
);
_isLayoutChanged = true;
}
private void UpdatePlaybackState(TimeSpan elapsedTime)
{
if (_mediaSessionsService.CurrentIsPlaying)
{
_songPosition += elapsedTime;
_totalPlayedTime += elapsedTime;
CheckAndScrobbleLastFM();
}
}
private void CheckAndScrobbleLastFM()
{
bool isEnabled = _mediaSessionsService.CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (!isEnabled || _isLastFMTracked) return;
var songInfo = _mediaSessionsService.CurrentSongInfo;
if (songInfo == null || songInfo.Duration <= 0) return;
if (_totalPlayedTime.TotalSeconds >= songInfo.Duration * 0.5)
{
_isLastFMTracked = true;
_lastFMService.TrackAsync(songInfo);
}
}
private void ResetPlaybackState()
{
_totalPlayedTime = TimeSpan.Zero;
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (_mediaSessionsService.CurrentSongInfo == null
|| _mediaSessionsService.CurrentLyricsData == null
|| _mediaSessionsService.CurrentLyricsData.LyricsLines.Count == 0)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _mediaSessionsService.CurrentLyricsData.LyricsLines.Count - 1);
}
public void Receive(PropertyChangedMessage<Color> message)
{
if (message.Sender is LyricsWindowViewModel)
{
if (message.PropertyName == nameof(LyricsWindowViewModel.BackdropAccentColor))
{
var newColor = message.NewValue;
_immersiveBgColorTransition.StartTransition(newColor);
_environmentalColor = newColor;
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
{
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{
var realPosition = message.NewValue;
var diff = Math.Abs(_songPosition.TotalMilliseconds - realPosition.TotalMilliseconds);
var timelineSyncThreshold = _mediaSessionsService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
// ƫ<><C6AB> or seek
if (diff >= timelineSyncThreshold)
{
_songPosition = realPosition;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˿<EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD> LastFM ͳ<><CDB3>״̬
if (_songPosition.TotalSeconds <= 1)
{
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
// <20>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD>
if (diff >= timelineSyncThreshold + 5000)
{
_isLayoutChanged = true;
}
}
}
}
public void Receive(PropertyChangedMessage<LyricsData?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentLyricsData))
{
DispatcherQueue.TryEnqueue(() =>
{
if (IsLoaded)
{
TriggerRelayout();
}
});
}
}
}
public void Receive(PropertyChangedMessage<LyricsWindowStatus> message)
{
if (message.Sender is LiveStates)
{
if (message.PropertyName == nameof(LiveStates.LyricsWindowStatus))
{
DispatcherQueue.TryEnqueue(() =>
{
if (IsLoaded)
{
TriggerRelayout();
}
});
}
}
}
}
}

View File

@@ -6,6 +6,7 @@ namespace BetterLyrics.WinUI3.Helper
{
public partial class SpectrumAnalyzer : IDisposable
{
private readonly object _lock = new();
private WasapiLoopbackCapture? _capture;
private int _sampleRate = 48000;
@@ -121,12 +122,15 @@ namespace BetterLyrics.WinUI3.Helper
Array.Copy(_spectrumRightData, 0, _spectrumData, _spectrumLeftData.Length, _spectrumRightData.Length);
}
for (int i = 0; i < BarCount; i++)
lock (_lock)
{
int index = (int)((float)i / BarCount * _spectrumData.Length);
if (index < _spectrumData.Length)
for (int i = 0; i < BarCount; i++)
{
_currentSpectrum[i] = _spectrumData[index] * 250f * Sensitivity;
int index = (int)((float)i / BarCount * _spectrumData.Length);
if (index < _spectrumData.Length)
{
_currentSpectrum[i] = _spectrumData[index] * 250f * Sensitivity;
}
}
}
@@ -139,10 +143,13 @@ namespace BetterLyrics.WinUI3.Helper
return;
}
for (int i = 0; i < BarCount; i++)
lock (_lock)
{
SmoothSpectrum[i] = SmoothSpectrum[i] * SmoothingFactor +
_currentSpectrum[i] * (1 - SmoothingFactor);
for (int i = 0; i < BarCount; i++)
{
SmoothSpectrum[i] = SmoothSpectrum[i] * SmoothingFactor +
_currentSpectrum[i] * (1 - SmoothingFactor);
}
}
}

View File

@@ -0,0 +1,105 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.UI.Xaml.Media.Animation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BetterLyrics.WinUI3.Logic
{
public class LyricsAnimator
{
private readonly double _defaultScale = 0.75f;
private readonly double _highlightedScale = 1.0f;
public void UpdateVisibleLines(
LyricsData? lyricsData,
int startVisibleIndex,
int endVisibleIndex,
int playingLineIndex,
double canvasHeight,
double targetYScrollOffset,
LyricsEffectSettings lyricsEffect,
ValueTransition<double> canvasYScrollTransition,
TimeSpan elapsedTime,
bool isForceUpdate) // 对应 _isLayoutChanged || _isPlayingLineChanged
{
if (lyricsData == null) return;
var currentPlayingLine = lyricsData.LyricsLines.ElementAtOrDefault(playingLineIndex);
if (currentPlayingLine == null) return;
// 循环更新可见行
for (int i = startVisibleIndex; i <= endVisibleIndex + 1; i++)
{
var line = lyricsData.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
if (isForceUpdate)
{
int lineCountDelta = i - playingLineIndex;
int absLineCountDelta = Math.Abs(lineCountDelta);
double distanceFromPlayingLine = Math.Abs(line.OriginalPosition.Y - currentPlayingLine.OriginalPosition.Y);
double distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight / 2), 0, 1);
line.AngleTransition.StartTransition(lyricsEffect.IsFanLyricsEnabled
? Math.PI
* (lyricsEffect.FanLyricsAngle / 180.0)
* distanceFactor
* (i > playingLineIndex ? 1 : -1)
: 0
);
line.BlurAmountTransition.StartTransition(5 * distanceFactor);
line.ScaleTransition.StartTransition(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale));
line.PhoneticTextOpacityTransition.StartTransition(absLineCountDelta == 0 ? 0.3 : (1 - distanceFactor) * 0.3);
line.OriginalTextOpacityTransition.StartTransition(absLineCountDelta == 0 ? 1 : (1 - distanceFactor) * 0.3);
line.TranslatedTextOpacityTransition.StartTransition(absLineCountDelta == 0 ? 0.3 : (1 - distanceFactor) * 0.3);
double yScrollDuration;
double yScrollDelay;
if (lineCountDelta < 0)
{
yScrollDuration =
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollTopDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollTopDelay / 1000.0;
}
else if (lineCountDelta == 0)
{
yScrollDuration = canvasYScrollTransition.DurationSeconds;
yScrollDelay = 0;
}
else
{
yScrollDuration =
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollBottomDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollBottomDelay / 1000.0;
}
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.YOffsetTransition.SetDuration(yScrollDuration);
line.YOffsetTransition.SetDelay(yScrollDelay);
//line.YOffsetTransition.StartTransition(_canvasTargetYScrollOffset, _isLayoutChanged);
line.YOffsetTransition.StartTransition(targetYScrollOffset);
}
line.AngleTransition.Update(elapsedTime);
line.ScaleTransition.Update(elapsedTime);
line.BlurAmountTransition.Update(elapsedTime);
line.PhoneticTextOpacityTransition.Update(elapsedTime);
line.OriginalTextOpacityTransition.Update(elapsedTime);
line.TranslatedTextOpacityTransition.Update(elapsedTime);
line.YOffsetTransition.Update(elapsedTime);
}
}
}
}

View File

@@ -0,0 +1,179 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
namespace BetterLyrics.WinUI3.Logic
{
public class LyricsLayoutManager
{
public void MeasureAndArrange(
ICanvasAnimatedControl resourceCreator,
LyricsData? lyricsData,
LyricsWindowStatus status,
AppSettings appSettings,
double canvasWidth,
double canvasHeight,
double lyricsWidth)
{
if (lyricsData == null || resourceCreator == null) return;
// 计算字体大小
int originalFontSize, phoneticFontSize, translatedFontSize;
var style = status.LyricsStyleSettings;
if (style.IsDynamicLyricsFontSize)
{
originalFontSize = (int)Math.Clamp(Math.Min(canvasHeight, canvasWidth) / 15, 18, 96);
translatedFontSize = phoneticFontSize = (int)(originalFontSize * 2.0 / 3.0);
}
else
{
originalFontSize = style.OriginalLyricsFontSize;
phoneticFontSize = style.PhoneticLyricsFontSize;
translatedFontSize = style.TranslatedLyricsFontSize;
}
var fontWeight = style.LyricsFontWeight;
// 排版
double currentY = 0;
foreach (var line in lyricsData.LyricsLines)
{
if (line == null) continue;
line.RecreateTextLayout(
resourceCreator,
appSettings.TranslationSettings.IsChineseRomanizationEnabled || appSettings.TranslationSettings.IsJapaneseRomanizationEnabled,
appSettings.TranslationSettings.IsTranslationEnabled,
phoneticFontSize, originalFontSize, translatedFontSize,
fontWeight,
style.LyricsCJKFontFamily, style.LyricsWesternFontFamily,
lyricsWidth, canvasHeight, style.LyricsAlignmentType
);
line.RecreateTextGeometry();
// 注音层
line.PhoneticPosition = new Vector2(0, (float)currentY);
if (line.PhoneticCanvasTextLayout != null)
{
currentY += line.PhoneticCanvasTextLayout.LayoutBounds.Height;
// 间距
currentY += (line.PhoneticCanvasTextLayout.LayoutBounds.Height / line.PhoneticCanvasTextLayout.LineCount) * 0.1;
}
// 原文层
line.OriginalPosition = new Vector2(0, (float)currentY);
if (line.OriginalCanvasTextLayout != null)
{
currentY += line.OriginalCanvasTextLayout.LayoutBounds.Height;
}
// 翻译层
if (line.TranslatedCanvasTextLayout != null)
{
// 间距
currentY += (line.TranslatedCanvasTextLayout.LayoutBounds.Height / line.TranslatedCanvasTextLayout.LineCount) * 0.1;
}
line.TranslatedPosition = new Vector2(0, (float)currentY);
if (line.TranslatedCanvasTextLayout != null)
{
currentY += line.TranslatedCanvasTextLayout.LayoutBounds.Height;
}
// 行间距
if (line.OriginalCanvasTextLayout != null)
{
currentY += (line.OriginalCanvasTextLayout.LayoutBounds.Height / line.OriginalCanvasTextLayout.LineCount) * style.LyricsLineSpacingFactor;
}
// 更新中心点
line.UpdateCenterPosition(lyricsWidth, style.LyricsAlignmentType);
}
}
/// <summary>
/// 计算当前应该滚动到的目标 Y 轴偏移量
/// </summary>
public double? CalculateTargetScrollOffset(
LyricsData? lyricsData,
int playingLineIndex)
{
var lines = lyricsData?.LyricsLines;
if (lines == null || lines.Count == 0) return null;
var currentLine = lines.ElementAtOrDefault(playingLineIndex);
var firstLine = lines.FirstOrDefault();
if (currentLine?.OriginalCanvasTextLayout == null || firstLine == null) return null;
return -currentLine.OriginalPosition.Y
+ firstLine.OriginalPosition.Y
- (currentLine.TranslatedPosition.Y
+ (currentLine.TranslatedCanvasTextLayout?.LayoutBounds.Height ?? 0)
- currentLine.PhoneticPosition.Y) / 2.0;
}
/// <summary>
/// 计算当前屏幕可见的行范围
/// 返回值: (StartVisibleIndex, EndVisibleIndex)
/// </summary>
public (int Start, int End) CalculateVisibleRange(
IList<LyricsLine>? lines,
double currentScrollOffset,
double canvasHeight)
{
if (lines == null || lines.Count == 0) return (-1, -1);
double offset = currentScrollOffset + canvasHeight / 2;
int start = FindFirstVisibleLine(lines, offset);
int end = FindLastVisibleLine(lines, offset, canvasHeight);
// 修正边界情况
if (start != -1 && end == -1)
{
end = lines.Count - 1;
}
return (start, end);
}
private int FindFirstVisibleLine(IList<LyricsLine> lines, double offset)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.OriginalPosition.Y + (double)line.OriginalCanvasTextLayout.LayoutBounds.Height;
if (value >= 0) { result = mid; right = mid - 1; }
else { left = mid + 1; }
}
return result;
}
private int FindLastVisibleLine(IList<LyricsLine> lines, double offset, double canvasHeight)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.OriginalPosition.Y + (double)line.OriginalCanvasTextLayout.LayoutBounds.Height;
if (value >= canvasHeight) { result = mid; right = mid - 1; }
else { left = mid + 1; }
}
return result;
}
}
}

View File

@@ -0,0 +1,154 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Logic
{
public class LyricsSynchronizer
{
private int _lastFoundIndex = 0;
public void Reset()
{
_lastFoundIndex = 0;
}
public int GetCurrentLineIndex(double currentTimeMs, LyricsData? lyricsData)
{
if (lyricsData == null || lyricsData.LyricsLines.Count == 0) return 0;
var lines = lyricsData.LyricsLines;
//// Cache hit
//if (IsTimeInLine(currentTimeMs, lines, _lastFoundIndex)) return _lastFoundIndex;
//if (_lastFoundIndex + 1 < lines.Count && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex + 1))
//{
// _lastFoundIndex++;
// return _lastFoundIndex;
//}
// Cache miss
for (int i = 0; i < lines.Count; i++)
{
if (IsTimeInLine(currentTimeMs, lines, i))
{
_lastFoundIndex = i;
return i;
}
}
// Default
return Math.Min(_lastFoundIndex, lines.Count - 1);
}
public LinePlaybackState GetLinePlayingProgress(
double currentTimeMs,
LyricsLine line,
LyricsLine? nextLine,
double songDurationMs,
bool isForceWordByWord)
{
var state = new LinePlaybackState { SyllableStartIndex = 0, SyllableLength = 0, SyllableProgress = 0 };
if (line == null) return state;
double lineEndMs;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = songDurationMs;
// 还没到
if (currentTimeMs < line.StartMs) return state;
// 过了
if (currentTimeMs > lineEndMs)
{
state.SyllableProgress = 1f;
state.SyllableStartIndex = Math.Max(0, line.OriginalText.Length - 1);
state.SyllableLength = 1;
return state;
}
// 逐字
if (line.LyricsSyllables != null && line.LyricsSyllables.Count > 1)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
// 强制逐字
if (isForceWordByWord && line.OriginalText.Length > 0)
{
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs);
}
else
{
// 普通行
state.SyllableStartIndex = line.OriginalText.Length;
state.SyllableProgress = 1f;
return state;
}
}
private LinePlaybackState CalculateSyllableProgress(double time, LyricsLine line, double lineEndMs)
{
var state = new LinePlaybackState();
int count = line.LyricsSyllables.Count;
for (int i = 0; i < count; i++)
{
var timing = line.LyricsSyllables[i];
var nextTiming = (i + 1 < count) ? line.LyricsSyllables[i + 1] : null;
double timingEndMs = timing.EndMs ?? nextTiming?.StartMs ?? lineEndMs;
// 在当前字范围内
if (time >= timing.StartMs && time <= timingEndMs)
{
state.SyllableStartIndex = timing.StartIndex;
state.SyllableLength = timing.Text.Length;
state.SyllableProgress = (timingEndMs > timing.StartMs)
? (time - timing.StartMs) / (timingEndMs - timing.StartMs)
: 0;
return state;
}
// 在空隙中 (已过当前字,未到下个字)
else if (time > timingEndMs && (nextTiming == null || time < nextTiming.StartMs))
{
state.SyllableProgress = 1f; // 保持上个字满进度
state.SyllableStartIndex = timing.StartIndex;
state.SyllableLength = timing.Text.Length;
return state;
}
}
return state;
}
private LinePlaybackState CalculateSimulatedProgress(double time, LyricsLine line, double lineEndMs)
{
var state = new LinePlaybackState();
int textLength = line.OriginalText.Length;
double progress = (time - line.StartMs) / (lineEndMs - line.StartMs);
progress = Math.Clamp(progress, 0, 1);
double charFloatIndex = progress * textLength;
int charIndex = (int)charFloatIndex;
state.SyllableStartIndex = Math.Clamp(charIndex, 0, textLength - 1);
state.SyllableLength = 1;
state.SyllableProgress = charFloatIndex - charIndex;
return state;
}
private bool IsTimeInLine(double time, IList<LyricsLine> lines, int index)
{
if (index < 0 || index >= lines.Count) return false;
var line = lines[index];
var nextLine = (index + 1 < lines.Count) ? lines[index + 1] : null;
if (time < line.StartMs) return false;
if (nextLine != null && time >= nextLine.StartMs) return false;
return true;
}
}
}

View File

@@ -0,0 +1,152 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Windows.UI;
namespace BetterLyrics.WinUI3.Logic
{
public class LyricsThemeManager
{
private readonly IMediaSessionsService _mediaSessionsService;
public LyricsThemeManager(IMediaSessionsService mediaSessionsService)
{
_mediaSessionsService = mediaSessionsService;
}
public LyricsThemeColors UpdateColors(
LyricsWindowStatus status,
Color environmentalColor,
ValueTransition<Color> accentColor1Transition,
ValueTransition<Color> accentColor2Transition,
ValueTransition<Color> accentColor3Transition,
ValueTransition<Color> accentColor4Transition
)
{
var result = new LyricsThemeColors();
ElementTheme themeTypeSent;
if (status.IsAdaptToEnvironment)
{
themeTypeSent = Helper.ColorHelper.GetElementThemeFromBackgroundColor(environmentalColor);
}
else
{
themeTypeSent = status.LyricsBackgroundSettings.LyricsBackgroundTheme;
}
bool isLight = themeTypeSent switch
{
ElementTheme.Default => Application.Current.RequestedTheme == ApplicationTheme.Light,
ElementTheme.Light => true,
ElementTheme.Dark => false,
_ => false
};
Color adaptiveGrayedFontColor;
Color grayedEnvironmentalColor;
Color? adaptiveColoredFontColor;
Color darkColor = Colors.Black;
Color lightColor = Colors.White;
if (isLight)
{
adaptiveGrayedFontColor = darkColor;
// brightness = 0.7f;
grayedEnvironmentalColor = lightColor;
result.AccentColor1 = _mediaSessionsService.LightAccentColors.ElementAtOrDefault(0);
result.AccentColor2 = _mediaSessionsService.LightAccentColors.ElementAtOrDefault(1);
result.AccentColor3 = _mediaSessionsService.LightAccentColors.ElementAtOrDefault(2);
result.AccentColor4 = _mediaSessionsService.LightAccentColors.ElementAtOrDefault(3);
}
else
{
adaptiveGrayedFontColor = lightColor;
// brightness = 0.3f;
grayedEnvironmentalColor = darkColor;
result.AccentColor1 = _mediaSessionsService.DarkAccentColors.ElementAtOrDefault(0);
result.AccentColor2 = _mediaSessionsService.DarkAccentColors.ElementAtOrDefault(1);
result.AccentColor3 = _mediaSessionsService.DarkAccentColors.ElementAtOrDefault(2);
result.AccentColor4 = _mediaSessionsService.DarkAccentColors.ElementAtOrDefault(3);
}
if (status.IsAdaptToEnvironment)
{
adaptiveColoredFontColor = Helper.ColorHelper.GetForegroundColor(environmentalColor);
}
else
{
if (isLight)
adaptiveColoredFontColor = _mediaSessionsService.DarkAccentColors.ElementAtOrDefault(0);
else
adaptiveColoredFontColor = _mediaSessionsService.LightAccentColors.ElementAtOrDefault(0);
}
result.ThemeType = themeTypeSent;
// 背景字色
switch (status.LyricsStyleSettings.LyricsBgFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
result.BgFontColor = adaptiveGrayedFontColor;
break;
case LyricsFontColorType.AdaptiveColored:
result.BgFontColor = adaptiveColoredFontColor ?? adaptiveGrayedFontColor;
break;
case LyricsFontColorType.Custom:
result.BgFontColor = status.LyricsStyleSettings.LyricsCustomBgFontColor;
break;
default:
result.BgFontColor = adaptiveGrayedFontColor;
break;
}
// 前景字色
switch (status.LyricsStyleSettings.LyricsFgFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
result.FgFontColor = adaptiveGrayedFontColor;
break;
case LyricsFontColorType.AdaptiveColored:
result.FgFontColor = adaptiveColoredFontColor ?? adaptiveGrayedFontColor;
break;
case LyricsFontColorType.Custom:
result.FgFontColor = status.LyricsStyleSettings.LyricsCustomFgFontColor;
break;
default:
result.FgFontColor = adaptiveGrayedFontColor;
break;
}
// 描边颜色
switch (status.LyricsStyleSettings.LyricsStrokeFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
result.StrokeFontColor = grayedEnvironmentalColor.WithBrightness(0.7);
break;
case LyricsFontColorType.AdaptiveColored:
result.StrokeFontColor = environmentalColor.WithBrightness(0.7);
break;
case LyricsFontColorType.Custom:
result.StrokeFontColor = status.LyricsStyleSettings.LyricsCustomStrokeFontColor;
break;
default:
result.StrokeFontColor = Colors.Transparent;
break;
}
return result;
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public struct LinePlaybackState
{
public int SyllableStartIndex;
public int SyllableLength;
public double SyllableProgress;
}
}

View File

@@ -47,7 +47,7 @@ namespace BetterLyrics.WinUI3.Models
}
}
public void SetTranslatedText(LyricsData translationData, string separator, int toleranceMs = 0)
public void SetTranslatedText(LyricsData translationData, int toleranceMs = 0)
{
foreach (var line in LyricsLines)
{
@@ -68,7 +68,7 @@ namespace BetterLyrics.WinUI3.Models
}
}
public void SetPhoneticText(LyricsData phoneticData, string separator, int toleranceMs = 0)
public void SetPhoneticText(LyricsData phoneticData, int toleranceMs = 0)
{
foreach (var line in LyricsLines)
{
@@ -89,7 +89,7 @@ namespace BetterLyrics.WinUI3.Models
}
}
public void SetTranslation(string translation, string separator)
public void SetTranslation(string translation)
{
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;

View File

@@ -0,0 +1,19 @@
using Microsoft.UI.Xaml;
using Windows.UI;
namespace BetterLyrics.WinUI3.Models
{
public struct LyricsThemeColors
{
public Color BgFontColor;
public Color FgFontColor;
public Color StrokeFontColor;
public Color AccentColor1;
public Color AccentColor2;
public Color AccentColor3;
public Color AccentColor4;
public ElementTheme ThemeType;
}
}

View File

@@ -24,7 +24,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsFontColorType LyricsStrokeFontColorType { get; set; } = LyricsFontColorType.AdaptiveGrayed;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsFontWeight LyricsFontWeight { get; set; } = LyricsFontWeight.Bold;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double LyricsLineSpacingFactor { get; set; } = 0.5;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsTranslationSeparator { get; set; } = StringHelper.NewLine;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsCJKFontFamily { get; set; } = FontHelper.SystemFontFamilies.FirstOrDefault() ?? "";
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsWesternFontFamily { get; set; } = FontHelper.SystemFontFamilies.FirstOrDefault() ?? "";
@@ -48,7 +47,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
LyricsStrokeFontColorType = this.LyricsStrokeFontColorType,
LyricsFontWeight = this.LyricsFontWeight,
LyricsLineSpacingFactor = this.LyricsLineSpacingFactor,
LyricsTranslationSeparator = this.LyricsTranslationSeparator,
LyricsCJKFontFamily = this.LyricsCJKFontFamily,
LyricsWesternFontFamily = this.LyricsWesternFontFamily,
};

View File

@@ -0,0 +1,112 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class FluidBackgroundRenderer : IDisposable
{
private PixelShaderEffect? _fluidEffect;
private float _timeAccumulator = 0f;
private Vector3 _c1, _c2, _c3, _c4;
public bool IsEnabled { get; set; } = false;
public double Opacity { get; set; } = 1.0;
public bool EnableLightWave { get; set; } = false;
public async Task LoadResourcesAsync()
{
Dispose();
try
{
var uri = new Uri("ms-appx:///Assets/FluidEffect.bin");
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(uri);
using (var stream = await file.OpenReadAsync())
{
var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size);
await stream.ReadAsync(buffer, (uint)stream.Size, Windows.Storage.Streams.InputStreamOptions.None);
byte[] bytes = buffer.ToArray();
_fluidEffect = new PixelShaderEffect(bytes);
_fluidEffect.Properties["EnableLightWave"] = EnableLightWave;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[FluidRenderer] Load Failed: {ex.Message}");
_fluidEffect = null;
}
}
public void UpdateColors(Color c1, Color c2, Color c3, Color c4)
{
_c1 = c1.ToVector3RGB();
_c2 = c2.ToVector3RGB();
_c3 = c3.ToVector3RGB();
_c4 = c4.ToVector3RGB();
}
public void Update(TimeSpan deltaTime)
{
if (_fluidEffect == null || !IsEnabled) return;
_timeAccumulator += (float)deltaTime.TotalSeconds;
_fluidEffect.Properties["iTime"] = _timeAccumulator;
_fluidEffect.Properties["color1"] = _c1;
_fluidEffect.Properties["color2"] = _c2;
_fluidEffect.Properties["color3"] = _c3;
_fluidEffect.Properties["color4"] = _c4;
_fluidEffect.Properties["EnableLightWave"] = EnableLightWave;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_fluidEffect == null || !IsEnabled || Opacity <= 0) return;
float pixelWidth = control.ConvertDipsToPixels((float)control.Size.Width, CanvasDpiRounding.Round);
float pixelHeight = control.ConvertDipsToPixels((float)control.Size.Height, CanvasDpiRounding.Round);
_fluidEffect.Properties["Width"] = pixelWidth;
_fluidEffect.Properties["Height"] = pixelHeight;
if (Opacity >= 1.0)
{
ds.DrawImage(_fluidEffect);
}
else
{
using (var opacityEffect = new OpacityEffect
{
Source = _fluidEffect,
Opacity = (float)Opacity
})
{
ds.DrawImage(opacityEffect);
}
}
}
public void Dispose()
{
_fluidEffect?.Dispose();
_fluidEffect = null;
}
}
}

View File

@@ -0,0 +1,49 @@
using BetterLyrics.WinUI3.Shaders;
using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
namespace BetterLyrics.WinUI3.Renderer
{
public class FogRenderer : IDisposable
{
private PixelShaderEffect<FogEffect>? _fogEffect;
private float _timeAccumulator = 0f;
public bool IsEnabled { get; set; } = false;
public void LoadResources()
{
Dispose();
_fogEffect = new PixelShaderEffect<FogEffect>();
}
public void Update(double deltaTime)
{
if (_fogEffect == null || !IsEnabled) return;
_timeAccumulator += (float)deltaTime;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_fogEffect == null || !IsEnabled) return;
float width = control.ConvertDipsToPixels((float)control.Size.Width, CanvasDpiRounding.Round);
float height = control.ConvertDipsToPixels((float)control.Size.Height, CanvasDpiRounding.Round);
_fogEffect.ConstantBuffer = new FogEffect(
_timeAccumulator,
new float2(width, height)
);
ds.DrawImage(_fogEffect);
}
public void Dispose()
{
_fogEffect?.Dispose();
_fogEffect = null;
}
}
}

View File

@@ -0,0 +1,226 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Lyricify.Lyrics.Providers.Web.Netease;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Linq;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class LyricsRenderer
{
private readonly PlayingLineRenderer _playingRenderer = new();
private readonly UnplayingLineRenderer _unplayingRenderer = new();
private Matrix4x4 _threeDimMatrix = Matrix4x4.Identity;
public void Draw(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
LyricsData? lyricsData,
int playingLineIndex,
int startVisibleIndex,
int endVisibleIndex,
double canvasHeight,
double lyricsX,
double lyricsWidth,
LyricsWindowStatus windowStatus,
Color strokeColor,
Color bgColor,
Func<int, LinePlaybackState> getPlaybackState)
{
if (windowStatus.LyricsEffectSettings.Is3DLyricsEnabled)
{
using (var layer = new CanvasCommandList(control))
{
using (var layerDs = layer.CreateDrawingSession())
{
DrawLyrics(
control,
layerDs,
lyricsData,
playingLineIndex,
startVisibleIndex,
endVisibleIndex,
canvasHeight,
lyricsX,
lyricsWidth,
windowStatus,
strokeColor,
bgColor,
getPlaybackState);
}
ds.DrawImage(new Transform3DEffect
{
Source = layer,
TransformMatrix = _threeDimMatrix
});
}
}
else
{
DrawLyrics(
control,
ds,
lyricsData,
playingLineIndex,
startVisibleIndex,
endVisibleIndex,
canvasHeight,
lyricsX,
lyricsWidth,
windowStatus,
strokeColor,
bgColor,
getPlaybackState);
}
}
private void DrawLyrics(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
LyricsData? lyricsData,
int playingLineIndex,
int startVisibleIndex,
int endVisibleIndex,
double canvasHeight,
double lyricsX,
double lyricsWidth,
LyricsWindowStatus windowStatus,
Color strokeColor,
Color bgColor,
Func<int, LinePlaybackState> getPlaybackState)
{
if (lyricsData == null) return;
var currentPlayingLine = lyricsData.LyricsLines.ElementAtOrDefault(playingLineIndex);
if (currentPlayingLine == null) return;
var effectSettings = windowStatus.LyricsEffectSettings;
var styleSettings = windowStatus.LyricsStyleSettings;
var rotationY = currentPlayingLine.OriginalPosition.WithX(effectSettings.FanLyricsAngle < 0 ? (float)lyricsWidth : 0);
for (int i = startVisibleIndex; i <= endVisibleIndex; i++)
{
var line = lyricsData.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
if (line.OriginalCanvasTextLayout == null) continue;
if (line.OriginalCanvasTextLayout.LayoutBounds.Width <= 0) continue;
double yOffset = line.YOffsetTransition.Value + canvasHeight / 2;
var transform =
Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition) *
Matrix3x2.CreateRotation((float)line.AngleTransition.Value, rotationY) *
Matrix3x2.CreateTranslation((float)lyricsX, (float)yOffset);
ds.Transform = transform;
using (var textOnlyLayer = RenderBaseTextLayer(control, line, styleSettings.LyricsFontStrokeWidth, strokeColor, bgColor))
{
if (i == playingLineIndex)
{
var state = getPlaybackState(i);
_playingRenderer.Draw(control, ds, textOnlyLayer, line, state, effectSettings);
}
else
{
_unplayingRenderer.Draw(ds, textOnlyLayer, line);
}
}
ds.Transform = Matrix3x2.Identity;
}
}
private CanvasCommandList RenderBaseTextLayer(
ICanvasResourceCreator resourceCreator,
LyricsLine line,
double strokeWidth,
Color strokeColor,
Color fillColor)
{
var commandList = new CanvasCommandList(resourceCreator);
using (var clds = commandList.CreateDrawingSession())
{
if (strokeWidth > 0)
{
DrawGeometrySafely(clds, line.PhoneticCanvasGeometry, line.PhoneticPosition, strokeColor, strokeWidth);
DrawGeometrySafely(clds, line.OriginalCanvasGeometry, line.OriginalPosition, strokeColor, strokeWidth);
DrawGeometrySafely(clds, line.TranslatedCanvasGeometry, line.TranslatedPosition, strokeColor, strokeWidth);
}
DrawTextLayoutSafely(clds, line.PhoneticCanvasTextLayout, line.PhoneticPosition, fillColor);
DrawTextLayoutSafely(clds, line.OriginalCanvasTextLayout, line.OriginalPosition, fillColor);
DrawTextLayoutSafely(clds, line.TranslatedCanvasTextLayout, line.TranslatedPosition, fillColor);
}
return commandList;
}
private void DrawGeometrySafely(CanvasDrawingSession ds, CanvasGeometry? geo, Vector2 pos, Color color, double width)
{
if (geo == null) return;
try
{
ds.DrawGeometry(geo, pos, color, (float)width);
}
catch (Exception) { }
}
private void DrawTextLayoutSafely(CanvasDrawingSession ds, CanvasTextLayout? layout, Vector2 pos, Color color)
{
if (layout == null) return;
try
{
ds.DrawTextLayout(layout, pos, color);
}
catch (Exception) { }
}
public void CalculateLyrics3DMatrix(LyricsEffectSettings lyricsEffect, double lyricsX, double lyricsY, double lyricsWidth, double canvasHeight)
{
if (!lyricsEffect.Is3DLyricsEnabled) return;
Vector3 center = new(
(float)(lyricsX + lyricsWidth / 2),
(float)(lyricsY + canvasHeight / 2),
0);
float rotationX = (float)(Math.PI * lyricsEffect.Lyrics3DXAngle / 180.0);
float rotationY = (float)(Math.PI * lyricsEffect.Lyrics3DYAngle / 180.0);
float rotationZ = (float)(Math.PI * lyricsEffect.Lyrics3DZAngle / 180.0);
Matrix4x4 rotation =
Matrix4x4.CreateRotationX(rotationX) *
Matrix4x4.CreateRotationY(rotationY) *
Matrix4x4.CreateRotationZ(rotationZ);
Matrix4x4 perspective = Matrix4x4.Identity;
perspective.M34 = 1.0f / lyricsEffect.Lyrics3DDepth;
// 组合变换:
// 1. 将中心移到原点
// 2. 旋转
// 3. 应用透视
// 4. 将中心移回原位
_threeDimMatrix =
Matrix4x4.CreateTranslation(-center) *
rotation *
perspective *
Matrix4x4.CreateTranslation(center);
}
}
}

View File

@@ -1,41 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Renderer
{
public sealed partial class LyricsRenderer : UserControl
{
public LyricsRendererViewModel ViewModel { get; set; }
public LyricsRenderer()
{
InitializeComponent();
ViewModel = Ioc.Default.GetRequiredService<LyricsRendererViewModel>();
}
private void LyricsCanvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedDrawEventArgs args)
{
using var ds = args.DrawingSession;
ViewModel.Draw(sender, ds);
}
private void LyricsCanvas_Update(Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedUpdateEventArgs args)
{
ViewModel.Update(sender, args);
}
private void LyricsCanvas_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
LyricsCanvas.RemoveFromVisualTree();
LyricsCanvas = null;
}
private void LyricsCanvas_CreateResources(Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
{
ViewModel.CreateResources(sender, args);
}
}
}

View File

@@ -0,0 +1,254 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Windows.Foundation;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class PlayingLineRenderer
{
public void Draw(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
ICanvasImage textOnlyLayer,
LyricsLine line,
LinePlaybackState playbackState,
LyricsEffectSettings settings)
{
DrawPhonetic(ds, textOnlyLayer, line);
DrawOriginalText(control, ds, textOnlyLayer, line, playbackState, settings);
DrawTranslated(ds, textOnlyLayer, line);
}
private void DrawPhonetic(CanvasDrawingSession ds, ICanvasImage source, LyricsLine line)
{
if (line.PhoneticCanvasTextLayout == null) return;
var opacity = line.PhoneticTextOpacityTransition.Value;
var blur = line.BlurAmountTransition.Value;
var bounds = line.PhoneticCanvasTextLayout.LayoutBounds;
var destRect = new Rect(
bounds.X + line.PhoneticPosition.X,
bounds.Y + line.PhoneticPosition.Y,
bounds.Width,
bounds.Height
);
using (var blurEffect = new GaussianBlurEffect
{
BlurAmount = (float)blur,
Source = source,
BorderMode = EffectBorderMode.Soft
})
{
ds.DrawImage(blurEffect, destRect, destRect, (float)opacity);
}
}
private void DrawTranslated(CanvasDrawingSession ds, ICanvasImage source, LyricsLine line)
{
if (line.TranslatedCanvasTextLayout == null) return;
var opacity = line.TranslatedTextOpacityTransition.Value;
var blur = line.BlurAmountTransition.Value;
var bounds = line.TranslatedCanvasTextLayout.LayoutBounds;
var destRect = new Rect(
bounds.X + line.TranslatedPosition.X,
bounds.Y + line.TranslatedPosition.Y,
bounds.Width,
bounds.Height
);
using (var blurEffect = new GaussianBlurEffect
{
BlurAmount = (float)blur,
Source = source,
BorderMode = EffectBorderMode.Soft
})
{
ds.DrawImage(blurEffect, destRect, destRect, (float)opacity);
}
}
private void DrawOriginalText(
ICanvasResourceCreator resourceCreator,
CanvasDrawingSession ds,
ICanvasImage source,
LyricsLine line,
LinePlaybackState state,
LyricsEffectSettings settings)
{
if (line.OriginalCanvasTextLayout == null) return;
var originalTextOpacity = line.OriginalTextOpacityTransition.Value;
var curCharIndex = state.SyllableStartIndex + state.SyllableLength * state.SyllableProgress;
float fadeWidth = (1f / Math.Max(1, line.OriginalText.Length)) * 0.5f;
var lineRegions = line.OriginalCanvasTextLayout.GetCharacterRegions(0, line.OriginalText.Length);
foreach (var subLineRegion in lineRegions)
{
DrawSubLineRegion(resourceCreator, ds, source, line, subLineRegion, curCharIndex, fadeWidth, originalTextOpacity, state, settings);
}
}
private void DrawSubLineRegion(
ICanvasResourceCreator resourceCreator,
CanvasDrawingSession ds,
ICanvasImage source,
LyricsLine line,
CanvasTextLayoutRegion subLineRegion,
double curCharIndex,
float fadeWidth,
double originalTextOpacity,
LinePlaybackState state,
LyricsEffectSettings settings)
{
var subLineLayoutBounds = subLineRegion.LayoutBounds;
Rect subLineRect = new(
subLineLayoutBounds.X + line.OriginalPosition.X,
subLineLayoutBounds.Y + line.OriginalPosition.Y,
subLineLayoutBounds.Width,
subLineLayoutBounds.Height
);
using (var maskLayer = new CanvasCommandList(resourceCreator))
{
using (var maskLayerDs = maskLayer.CreateDrawingSession())
{
float progressInRegion = (float)((curCharIndex - subLineRegion.CharacterIndex) / subLineRegion.CharacterCount);
progressInRegion = Math.Clamp(progressInRegion, 0, 1 + fadeWidth);
var stop1 = Colors.White.WithAlpha((byte)(255 * originalTextOpacity));
var stop2 = Color.FromArgb((byte)(255 * Math.Min(0.3, originalTextOpacity)), 255, 255, 255);
using (var maskBrush = new CanvasLinearGradientBrush(resourceCreator,
[
new CanvasGradientStop { Position = 0, Color = stop1 },
new CanvasGradientStop { Position = progressInRegion, Color = stop1 },
new CanvasGradientStop { Position = progressInRegion + fadeWidth, Color = stop2 },
new CanvasGradientStop { Position = 1 + fadeWidth, Color = stop2 }
]))
{
maskBrush.StartPoint = new Vector2((float)subLineRect.X, (float)subLineRect.Y);
maskBrush.EndPoint = new Vector2((float)(subLineRect.X + subLineRect.Width), (float)subLineRect.Y);
maskLayerDs.FillRectangle(subLineRect, maskBrush);
}
}
using (var cropEffect = new CropEffect
{
Source = source,
SourceRectangle = subLineRect,
BorderMode = EffectBorderMode.Soft
})
using (var textWithOpacityLayer = new AlphaMaskEffect
{
Source = cropEffect,
AlphaMask = maskLayer
})
{
int endCharIndex = subLineRegion.CharacterIndex + subLineRegion.CharacterCount;
for (int i = subLineRegion.CharacterIndex; i < endCharIndex; i++)
{
DrawSingleCharacter(ds, line, i, curCharIndex, textWithOpacityLayer, state, settings);
}
}
}
}
private void DrawSingleCharacter(
CanvasDrawingSession ds,
LyricsLine line,
int charIndex,
double exactProgressIndex,
ICanvasImage maskedSource,
LinePlaybackState state,
LyricsEffectSettings settings)
{
var curCharIndexInt = (int)Math.Floor(exactProgressIndex);
if (line.OriginalCanvasTextLayout == null) return;
var charRegions = line.OriginalCanvasTextLayout.GetCharacterRegions(charIndex, 1);
if (charRegions.Length == 0) return;
var charRegion = charRegions[0];
var charLayoutBounds = charRegion.LayoutBounds;
var sourceCharRect = new Rect(
charLayoutBounds.X + line.OriginalPosition.X,
charLayoutBounds.Y + line.OriginalPosition.Y,
charLayoutBounds.Width,
charLayoutBounds.Height
);
double floatOffset = 0;
double scale = 1;
double glow = 0;
if (settings.IsLyricsFloatAnimationEnabled)
{
double targetFloatOffset = sourceCharRect.Height * 0.05;
if (charIndex < curCharIndexInt) floatOffset = 0;
else if (charIndex == curCharIndexInt)
{
var p = exactProgressIndex - curCharIndexInt;
floatOffset = -targetFloatOffset + p * targetFloatOffset;
}
else floatOffset = -targetFloatOffset;
}
var parentSyllable = line.LyricsSyllables.FirstOrDefault(x => x.StartIndex <= charIndex && charIndex < x.StartIndex + x.Text.Length);
if (parentSyllable != null && parentSyllable.IsLongDuration && parentSyllable.StartIndex == state.SyllableStartIndex)
{
if (settings.IsLyricsScaleEffectEnabled)
{
scale += Math.Sin(state.SyllableProgress * Math.PI) * 0.15;
}
if (settings.IsLyricsGlowEffectEnabled)
{
glow = Math.Sin(state.SyllableProgress * Math.PI) * sourceCharRect.Height * 0.2;
}
}
var destCharRect = sourceCharRect.Scale(scale).AddY(-floatOffset);
using (var singleCharCrop = new CropEffect
{
Source = maskedSource,
SourceRectangle = sourceCharRect,
BorderMode = EffectBorderMode.Soft
})
{
// 这里不做 glow > 0 的判断而总是加入辉光效果是为了平滑不断层
using (var glowEffect = new GaussianBlurEffect
{
Source = singleCharCrop,
BlurAmount = (float)glow,
BorderMode = EffectBorderMode.Soft
})
{
ds.DrawImage(glowEffect, destCharRect.Extend(sourceCharRect.Height), sourceCharRect.Extend(sourceCharRect.Height));
}
ds.DrawImage(singleCharCrop, destCharRect, sourceCharRect);
}
}
}
}

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Extensions;
using Microsoft.Graphics.Canvas;
using Windows.Foundation;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class PureColorBackgroundRenderer
{
public void Draw(
CanvasDrawingSession ds,
Rect bounds,
Color color,
double opacity,
bool isEnabled)
{
if (!isEnabled || opacity <= 0) return;
ds.FillRectangle(
bounds,
color.WithAlpha((byte)(opacity * 255))
);
}
}
}

View File

@@ -0,0 +1,53 @@
using BetterLyrics.WinUI3.Shaders;
using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
namespace BetterLyrics.WinUI3.Renderer
{
public class SnowRenderer : IDisposable
{
private PixelShaderEffect<SnowEffect>? _snowEffect;
private float _timeAccumulator = 0f;
public bool IsEnabled { get; set; } = false;
public float Amount { get; set; } = 0.5f;
public float Speed { get; set; } = 1.0f;
public void LoadResources()
{
Dispose();
_snowEffect = new PixelShaderEffect<SnowEffect>();
}
public void Update(double deltaTime)
{
if (_snowEffect == null || !IsEnabled) return;
_timeAccumulator += (float)deltaTime;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_snowEffect == null || !IsEnabled) return;
float width = control.ConvertDipsToPixels((float)control.Size.Width, CanvasDpiRounding.Round);
float height = control.ConvertDipsToPixels((float)control.Size.Height, CanvasDpiRounding.Round);
_snowEffect.ConstantBuffer = new SnowEffect(
_timeAccumulator,
new float2(width, height),
Amount, // 0.0 ~ 1.0
Speed
);
ds.DrawImage(_snowEffect);
}
public void Dispose()
{
_snowEffect?.Dispose();
_snowEffect = null;
}
}
}

View File

@@ -0,0 +1,150 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.UI;
using System;
using System.Linq;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class SpectrumRenderer : IDisposable
{
private CanvasGeometry? _spectrumGeometry;
public void Draw(
ICanvasResourceCreator resourceCreator,
CanvasDrawingSession ds,
float[]? spectrumData,
int barCount,
bool isEnabled,
SpectrumPlacement placement,
double canvasWidth,
double canvasHeight,
Color fillColor
)
{
_spectrumGeometry?.Dispose();
_spectrumGeometry = null;
if (!isEnabled || spectrumData == null || spectrumData.Length == 0) return;
_spectrumGeometry = CreateGeometry(resourceCreator, spectrumData, barCount, placement, canvasWidth, canvasHeight);
if (_spectrumGeometry != null)
{
DrawGeometry(ds, _spectrumGeometry, fillColor, placement, canvasHeight);
}
}
private CanvasGeometry? CreateGeometry(
ICanvasResourceCreator creator,
float[] data,
int barCount,
SpectrumPlacement placement,
double width,
double height)
{
if (barCount < 2) return null;
var points = new Vector2[barCount];
float pointSpacing = (float)width / (barCount - 1);
for (int i = 0; i < barCount; i++)
{
float val = i < data.Length ? data[i] : 0;
points[i] = new Vector2(i * pointSpacing, val);
}
// 限制高度
float maxY = 0;
foreach (var p in points) if (p.Y > maxY) maxY = p.Y;
float limitY = (float)height * 0.2f;
if (maxY > limitY)
{
float ratio = limitY / maxY;
for (int i = 0; i < points.Length; i++) points[i].Y *= ratio;
}
// 翻转 Y 轴
if (placement == SpectrumPlacement.Bottom)
{
for (int i = 0; i < points.Length; i++)
{
points[i].Y = (float)height - points[i].Y;
}
}
using var pathBuilder = new CanvasPathBuilder(creator);
pathBuilder.BeginFigure(points[0]);
for (int i = 0; i < barCount - 1; i++)
{
Vector2 p0 = points[Math.Max(i - 1, 0)];
Vector2 p1 = points[i];
Vector2 p2 = points[i + 1];
Vector2 p3 = points[Math.Min(i + 2, barCount - 1)];
Vector2 cp1 = p1 + (p2 - p0) / 6.0f;
Vector2 cp2 = p2 - (p3 - p1) / 6.0f;
pathBuilder.AddCubicBezier(cp1, cp2, p2);
}
// 封口
if (placement == SpectrumPlacement.Top)
{
pathBuilder.AddLine(new Vector2(points[barCount - 1].X, 0));
pathBuilder.AddLine(new Vector2(points[0].X, 0));
}
else
{
pathBuilder.AddLine(new Vector2(points[barCount - 1].X, (float)height));
pathBuilder.AddLine(new Vector2(points[0].X, (float)height));
}
pathBuilder.EndFigure(CanvasFigureLoop.Closed);
return CanvasGeometry.CreatePath(pathBuilder);
}
private void DrawGeometry(
CanvasDrawingSession ds,
CanvasGeometry geometry,
Color color,
SpectrumPlacement placement,
double height)
{
var stops = new CanvasGradientStop[]
{
new() { Position = 0.0f, Color = Colors.Transparent },
new() { Position = 0.7f, Color = Colors.Transparent },
new() { Position = 1.0f, Color = color }
};
using var brush = new CanvasLinearGradientBrush(ds, stops);
if (placement == SpectrumPlacement.Top)
{
brush.StartPoint = new Vector2(0, (float)height);
brush.EndPoint = new Vector2(0, 0);
}
else
{
brush.StartPoint = new Vector2(0, 0);
brush.EndPoint = new Vector2(0, (float)height);
}
ds.FillGeometry(geometry, brush);
}
public void Dispose()
{
_spectrumGeometry?.Dispose();
_spectrumGeometry = null;
}
}
}

View File

@@ -0,0 +1,76 @@
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using System.Numerics;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Renderer
{
public class UnplayingLineRenderer
{
public void Draw(
CanvasDrawingSession ds,
ICanvasImage textOnlyLayer,
LyricsLine line)
{
var blurAmount = (float)line.BlurAmountTransition.Value;
if (line.PhoneticCanvasTextLayout != null)
{
DrawPart(ds, textOnlyLayer,
line.PhoneticCanvasTextLayout,
line.PhoneticPosition,
blurAmount,
(float)line.PhoneticTextOpacityTransition.Value);
}
if (line.OriginalCanvasTextLayout != null)
{
DrawPart(ds, textOnlyLayer,
line.OriginalCanvasTextLayout,
line.OriginalPosition,
blurAmount,
(float)line.OriginalTextOpacityTransition.Value);
}
if (line.TranslatedCanvasTextLayout != null)
{
DrawPart(ds, textOnlyLayer,
line.TranslatedCanvasTextLayout,
line.TranslatedPosition,
blurAmount,
(float)line.TranslatedTextOpacityTransition.Value);
}
}
private void DrawPart(
CanvasDrawingSession ds,
ICanvasImage source,
CanvasTextLayout layout,
Vector2 position,
float blur,
float opacity)
{
if (opacity <= 0) return;
var bounds = layout.LayoutBounds;
var destRect = new Rect(
bounds.X + position.X,
bounds.Y + position.Y,
bounds.Width,
bounds.Height
);
using (var blurEffect = new GaussianBlurEffect
{
BlurAmount = blur,
Source = source,
BorderMode = EffectBorderMode.Soft
})
{
ds.DrawImage(blurEffect, destRect, destRect, opacity);
}
}
}
}

View File

@@ -63,9 +63,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
await bitmapImage.SetSourceAsync(ImageHelper.ToIRandomAccessStream(buffer));
if (token.IsCancellationRequested) return;
AlbumArtBitmapImage = bitmapImage;
LightAccentColors = lightPalette.Palette.Select(Helper.ColorHelper.FromVector3).ToList();
DarkAccentColors = darkPalette.Palette.Select(Helper.ColorHelper.FromVector3).ToList();
AlbumArtBitmapImage = bitmapImage;
}
}
}

View File

@@ -25,7 +25,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private int _langIndex = 0;
private List<LyricsData> _lyricsDataArr = [];
public LyricsData? CurrentLyricsData => _lyricsDataArr.ElementAtOrDefault(_langIndex);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsData? CurrentLyricsData { get; private set; }
public event EventHandler<LyricsChangedEventArgs>? LyricsChanged;
@@ -35,15 +35,17 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
[ObservableProperty] public partial bool IsTranslating { get; set; } = false;
private void SetCurrentLyricsData()
{
CurrentLyricsData = _lyricsDataArr.ElementAtOrDefault(_langIndex);
}
private async Task RefreshTranslationAsync(CancellationToken token)
{
TranslationSearchProvider = null;
_lyricsDataArr.ElementAtOrDefault(0)?.ClearTranslatedText();
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
});
SetCurrentLyricsData();
IsTranslating = true;
@@ -53,10 +55,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
IsTranslating = false;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
});
SetCurrentLyricsData();
}
private async Task SetTranslatedTextAsync(CancellationToken token)
@@ -86,7 +86,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_logger.LogInformation("Found translated text in lyrics data at index {FoundIndex}", found);
_lyricsDataArr.FirstOrDefault()?.SetTranslatedText(_lyricsDataArr[found], _liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsTranslationSeparator, 50);
_lyricsDataArr.FirstOrDefault()?.SetTranslatedText(_lyricsDataArr[found], 50);
TranslationSearchProvider = CurrentLyricsSearchResult?.Provider.ToTranslationSearchProvider();
}
else if (_settingsService.AppSettings.TranslationSettings.IsLibreTranslateEnabled)
@@ -99,7 +99,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (token.IsCancellationRequested) return;
if (translated == string.Empty) return;
_lyricsDataArr.FirstOrDefault()?.SetTranslation(translated, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsTranslationSeparator);
_lyricsDataArr.FirstOrDefault()?.SetTranslation(translated);
TranslationSearchProvider = Enums.TranslationSearchProvider.LibreTranslate;
}
@@ -141,7 +141,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (found >= 0)
{
_logger.LogInformation("Found phonetic text in lyrics data at index {FoundIndex}", found);
_lyricsDataArr.FirstOrDefault()?.SetPhoneticText(_lyricsDataArr[found], _liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsTranslationSeparator, 50);
_lyricsDataArr.FirstOrDefault()?.SetPhoneticText(_lyricsDataArr[found], 50);
}
}
@@ -153,10 +153,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
CurrentLyricsSearchResult = null;
_lyricsDataArr = [LyricsData.GetLoadingPlaceholder()];
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
});
SetCurrentLyricsData();
if (CurrentSongInfo != null)
{

View File

@@ -714,14 +714,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsTranslationSeparator))
{
UpdateTranslations();
}
}
else if (message.Sender is TranslationSettings)
if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.SelectedTargetLanguageCode))
{

View File

@@ -12,6 +12,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using DevWinUI;
using Lyricify.Lyrics.Providers.Web.Netease;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
@@ -25,35 +27,27 @@ using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsPageViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<BitmapImage?>>
IRecipient<PropertyChangedMessage<BitmapImage?>>,
IRecipient<PropertyChangedMessage<LyricsLayoutOrientation>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>
{
public IMediaSessionsService MediaSessionsService { get; private set; }
private readonly ILiveStatesService _liveStatesService;
private readonly ThrottleHelper _timelineThrottle = new(TimeSpan.FromSeconds(1));
[ObservableProperty]
public partial Vector3 AlbumArtTranslation { get; set; } = new();
[ObservableProperty] public partial double AlbumArtWithSongInfoStackPanelHeight { get; set; } = 0;
[ObservableProperty]
public partial double LastAlbumArtOpacity { get; set; } = 1;
[ObservableProperty] public partial double LastAlbumArtOpacity { get; set; } = 1;
[ObservableProperty] public partial double AlbumArtOpacity { get; set; } = 1;
[ObservableProperty] public partial BitmapImage? LastAlbumArtBitmapImage { get; set; }
[ObservableProperty] public partial BitmapImage? AlbumArtBitmapImage { get; set; }
[ObservableProperty]
public partial double AlbumArtOpacity { get; set; } = 1;
[ObservableProperty]
public partial BitmapImage? LastAlbumArtBitmapImage { get; set; }
[ObservableProperty]
public partial BitmapImage? AlbumArtBitmapImage { get; set; }
public LyricsRendererViewModel.LyricsRendererViewModel LyricsRendererViewModel { get; private set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double LyricsX { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double LyricsY { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double LyricsWidth { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double LyricsOpacity { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Matrix4x4 Lyrics3DMatrix { get; set; } = Matrix4x4.Identity;
[ObservableProperty]
public partial LiveStates LiveStates { get; set; }
@@ -79,24 +73,13 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial double TimelineSliderThumbSeconds { get; set; } = 0;
[ObservableProperty]
public partial double RootWidth { get; set; } = 0;
[ObservableProperty]
public partial double RootHeight { get; set; } = 0;
public TextBlock? TitleTextBlock { get; set; }
public TextBlock? ArtistsTextBlock { get; set; }
public TextBlock? AlbumTextBlock { get; set; }
[ObservableProperty]
public partial double SongInfoOpacity { get; set; } = 1;
public LyricsPageViewModel(IMediaSessionsService mediaSessionsService, ILiveStatesService liveStatesService, LyricsRendererViewModel.LyricsRendererViewModel lyricsRendererViewModel)
public LyricsPageViewModel(IMediaSessionsService mediaSessionsService, ILiveStatesService liveStatesService)
{
_liveStatesService = liveStatesService;
MediaSessionsService = mediaSessionsService;
LyricsRendererViewModel = lyricsRendererViewModel;
LiveStates = _liveStatesService.LiveStates;
@@ -109,61 +92,11 @@ namespace BetterLyrics.WinUI3.ViewModels
Volume = e;
}
private void RenderTextBlock(TextBlock? sender, string? text, int fontSize)
{
if (sender == null || string.IsNullOrEmpty(text)) return;
var lyricsStyleSettings = LiveStates.LyricsWindowStatus.LyricsStyleSettings;
sender.Inlines.Clear();
foreach (char c in text)
{
var fontFamilyName = LanguageHelper.IsCJK(c) ? lyricsStyleSettings.LyricsCJKFontFamily : lyricsStyleSettings.LyricsWesternFontFamily;
var run = new Run
{
Text = c.ToString(),
FontFamily = new Microsoft.UI.Xaml.Media.FontFamily(fontFamilyName),
FontSize = fontSize,
};
sender.Inlines.Add(run);
}
}
private int GetTitleFontSize()
{
var albumArtLayoutSettings = LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings;
if (albumArtLayoutSettings.IsAutoSongInfoFontSize)
{
return (int)Math.Clamp(Math.Min(RootHeight, RootWidth) / 20, 8, 72);
}
else
{
return albumArtLayoutSettings.SongInfoFontSize;
}
}
private int GetArtistsAlbumFontSize()
{
return (int)(GetTitleFontSize() * 0.8);
}
private void RenderSongInfo()
{
RenderTextBlock(TitleTextBlock, MediaSessionsService.CurrentSongInfo?.Title, GetTitleFontSize());
RenderTextBlock(ArtistsTextBlock, MediaSessionsService.CurrentSongInfo?.DisplayArtists, GetArtistsAlbumFontSize());
RenderTextBlock(AlbumTextBlock, MediaSessionsService.CurrentSongInfo?.Album, GetArtistsAlbumFontSize());
}
partial void OnTimelineSliderThumbSecondsChanged(double value)
{
TimelineSliderThumbLyricsLine = MediaSessionsService.CurrentLyricsData?.GetLyricsLine(value);
}
partial void OnRootHeightChanged(double value)
{
RenderSongInfo();
}
[RelayCommand]
private static void OpenSettingsWindow()
{
@@ -194,89 +127,6 @@ namespace BetterLyrics.WinUI3.ViewModels
await MediaSessionsService.NextAsync();
}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is LyricsRendererViewModel.LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.TotalTime))
{
if (_timelineThrottle.CanTrigger())
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
TimelinePositionSeconds = message.NewValue.TotalSeconds;
});
}
}
}
}
public void Receive(PropertyChangedMessage<double> message)
{
if (message.Sender is LyricsRendererViewModel.LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.AlbumArtX))
{
AlbumArtTranslation = AlbumArtTranslation.WithElement(0, (float)message.NewValue - 32);
}
else if (message.PropertyName == nameof(LyricsRendererViewModel.AlbumArtY))
{
AlbumArtTranslation = AlbumArtTranslation.WithElement(1, (float)message.NewValue - 32);
}
}
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is AlbumArtLayoutSettings)
{
if (message.PropertyName == nameof(AlbumArtLayoutSettings.SongInfoFontSize))
{
RenderSongInfo();
}
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is AlbumArtLayoutSettings)
{
if (message.PropertyName == nameof(AlbumArtLayoutSettings.IsAutoSongInfoFontSize))
{
RenderSongInfo();
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsCJKFontFamily))
{
RenderSongInfo();
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsWesternFontFamily))
{
RenderSongInfo();
}
}
}
public async void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
SongInfoOpacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
RenderSongInfo();
SongInfoOpacity = 1;
}
}
}
public async void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
@@ -296,5 +146,28 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
}
public void Receive(PropertyChangedMessage<LyricsLayoutOrientation> message)
{
if (message.Sender is LyricsWindowStatus)
{
if (message.PropertyName == nameof(LyricsWindowStatus.LyricsLayoutOrientation))
{
//OnLayoutChanged();
}
}
}
public void Receive(PropertyChangedMessage<LyricsDisplayType> message)
{
if (message.Sender is LyricsWindowStatus)
{
if (message.PropertyName == nameof(LyricsWindowStatus.LyricsDisplayType))
{
//OnLayoutChanged();
}
}
}
}
}

View File

@@ -1,25 +0,0 @@
using Microsoft.Extensions.Logging;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel
{
public void CreateResources(Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
{
_logger.LogInformation("Creating resources... Reason: {Reason}", args.Reason);
switch (args.Reason)
{
case Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesReason.FirstTime:
_isDeviceChanged = true;
break;
case Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesReason.NewDevice:
_isDeviceChanged = true;
break;
case Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesReason.DpiChanged:
break;
default:
break;
}
}
}
}

View File

@@ -1,540 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Linq;
using System.Numerics;
using System.Windows.Media.Media3D;
using Windows.Foundation;
using Windows.Graphics.Effects;
using Windows.UI;
using static Vanara.PInvoke.Kernel32;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel
{
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.IsPureColorOverlayEnabled)
{
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsAdaptToEnvironment)
{
FillBackground(ds, _immersiveBgColorTransition.Value, 0f,
_immersiveBgOpacityTransition.Value * _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PureColorOverlayOpacity / 100f);
}
else
{
FillBackground(ds, _albumArtAccentColor1Transition.Value, 0f,
_liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PureColorOverlayOpacity / 100.0);
}
}
DrawFluidBackground(control, ds);
DrawSpectrum(control, ds);
DrawSnowEffect(ds);
if (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings.Is3DLyricsEnabled)
{
// Blurred lyrics layer
using var blurredLyrics = new CanvasCommandList(control);
using (var blurredLyricsDs = blurredLyrics.CreateDrawingSession())
{
DrawLyrics(control, blurredLyricsDs);
}
ds.DrawImage(new Transform3DEffect
{
Source = blurredLyrics,
TransformMatrix = _lyrics3DMatrix
});
}
else
{
DrawLyrics(control, ds);
}
DrawFogEffect(ds);
//DrawRaindropEffect(ds, combined);
if (_isDebugOverlayEnabled)
{
_drawFrameCount++;
var currentPlayingLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine != null)
{
GetLinePlayingProgress(
_playingLineIndex,
out int syllableStartIndex,
out int syllableLength,
out double syllableProgress
);
ds.DrawText(
$"[DEBUG]\n" +
$"Canvas size: {_canvasWidth}x{_canvasHeight}\n" +
$"FPS (Draw): {_displayedDrawFrameCount}\n" +
$"Playing line: {_playingLineIndex}\n" +
$"Syllable start idx: {syllableStartIndex}\n" +
$"Syllable len: {syllableLength}\n" +
$"Syllable prog: {syllableProgress}\n" +
$"Visible lines: [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
$"Total line count: {GetMaxLyricsLineIndexBoundaries().Item2 + 1}\n" +
$"Cur time: {TotalTime + TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0)}\n" +
$"Song duration: {TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0)}\n" +
$"Y offset: {_canvasYScrollTransition.Value}",
new Vector2(10, 40),
ThemeTypeSent == Microsoft.UI.Xaml.ElementTheme.Light ? Colors.Black : Colors.White,
_debugTextFormat
);
}
if (_drawFrameStopwatch?.Elapsed.TotalSeconds >= 1.0)
{
_displayedDrawFrameCount = _drawFrameCount;
_drawFrameStopwatch?.Restart();
_drawFrameCount = 0;
}
}
}
public void DrawSpectrum(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_spectrumGeometry != null)
{
var gradientStops = new CanvasGradientStop[]
{
new() { Position = 0.0f, Color = Colors.Transparent },
new() { Position = 0.7f, Color = Colors.Transparent },
new() { Position = 1.0f, Color = _adaptiveColoredFontColor ?? _albumArtAccentColor1Transition.Value }
};
using var gradientBrush = new CanvasLinearGradientBrush(ds, gradientStops);
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.SpectrumPlacement)
{
case SpectrumPlacement.Top:
gradientBrush.StartPoint = new Vector2(0, (float)_canvasHeight);
gradientBrush.EndPoint = new Vector2(0, 0);
break;
case SpectrumPlacement.Bottom:
gradientBrush.StartPoint = new Vector2(0, 0);
gradientBrush.EndPoint = new Vector2(0, (float)_canvasHeight);
break;
default:
break;
}
// 使用渐变画刷填充
ds.FillGeometry(_spectrumGeometry, gradientBrush);
// 纯色
//ds.FillGeometry(geometry, _adaptiveColoredFontColor ?? _albumArtAccentColor1Transition.Value);
// 绘制轮廓线
//var lineColor = Colors.SkyBlue;
//float strokeWidth = 2f;
//ds.DrawGeometry(geometry, _albumArtAccentColor4Transition.Value, strokeWidth);
}
}
private void DrawFluidBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_fluidEffect != null && _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.IsFluidOverlayEnabled)
{
ds.DrawImage(new OpacityEffect
{
Source = _fluidEffect,
Opacity = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.FluidOverlayOpacity / 100f
});
}
}
private void DrawLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
var currentPlayingLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine == null)
{
return;
}
var lyricsEffectSettings = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings;
var rotationY = currentPlayingLine.OriginalPosition.WithX(lyricsEffectSettings.FanLyricsAngle < 0 ? (float)_maxLyricsWidth : 0);
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var textLayout = line.OriginalCanvasTextLayout;
if (textLayout == null) continue;
double layoutWidth = (double)textLayout.LayoutBounds.Width;
double layoutHeight = (double)textLayout.LayoutBounds.Height;
if (layoutWidth <= 0 || layoutHeight <= 0) continue;
double yOffset = line.YOffsetTransition.Value + _canvasHeight / 2 + _lyricsYTransition.Value;
//// 组合变换:缩放 -> 旋转 -> 平移
ds.Transform =
Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition) *
Matrix3x2.CreateRotation((float)line.AngleTransition.Value, rotationY) *
Matrix3x2.CreateTranslation((float)_lyricsX, (float)yOffset);
using var textOnlyLayer = new CanvasCommandList(control);
using (var textOnlyLayerDs = textOnlyLayer.CreateDrawingSession())
{
var strokeWidth = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontStrokeWidth;
// 描边
if (strokeWidth > 0)
{
if (line.PhoneticCanvasGeometry != null)
{
textOnlyLayerDs.DrawGeometry(line.PhoneticCanvasGeometry, line.PhoneticPosition, _strokeFontColor, strokeWidth);
}
if (line.OriginalCanvasGeometry != null)
{
textOnlyLayerDs.DrawGeometry(line.OriginalCanvasGeometry, line.OriginalPosition, _strokeFontColor, strokeWidth);
}
if (line.TranslatedCanvasGeometry != null)
{
textOnlyLayerDs.DrawGeometry(line.TranslatedCanvasGeometry, line.TranslatedPosition, _strokeFontColor, strokeWidth);
}
}
// 绘制文本(填充)
if (line.PhoneticCanvasTextLayout != null)
{
textOnlyLayerDs.DrawTextLayout(line.PhoneticCanvasTextLayout, line.PhoneticPosition, _bgFontColor);
}
if (line.OriginalCanvasTextLayout != null)
{
textOnlyLayerDs.DrawTextLayout(line.OriginalCanvasTextLayout, line.OriginalPosition, _bgFontColor);
}
if (line.TranslatedCanvasTextLayout != null)
{
textOnlyLayerDs.DrawTextLayout(line.TranslatedCanvasTextLayout, line.TranslatedPosition, _bgFontColor);
}
}
if (i == _playingLineIndex)
{
DrawPlayingLine(control, ds, textOnlyLayer, line, i);
}
else
{
DrawUnPlayingLine(ds, textOnlyLayer, line);
}
// Reset scale
ds.Transform = Matrix3x2.Identity;
}
}
private void DrawPlayingLine(ICanvasAnimatedControl control, CanvasDrawingSession ds, ICanvasImage textOnlyLayer, LyricsLine line, int lineIndex)
{
var lyricsEffectSettings = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings;
var lineBlurAmount = line.BlurAmountTransition.Value;
var phoneticTextOpacity = line.PhoneticTextOpacityTransition.Value;
var originalTextOpacity = line.OriginalTextOpacityTransition.Value;
var translatedTextOpacity = line.TranslatedTextOpacityTransition.Value;
var phoneticTextLayout = line.PhoneticCanvasTextLayout;
if (phoneticTextLayout != null)
{
Rect bounds = phoneticTextLayout.LayoutBounds;
var rect = new Rect(
bounds.X + line.PhoneticPosition.X,
bounds.Y + line.PhoneticPosition.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new GaussianBlurEffect
{
BlurAmount = (float)lineBlurAmount,
Source = textOnlyLayer,
BorderMode = EffectBorderMode.Soft
}, rect, rect, (float)phoneticTextOpacity);
}
var originalTextLayout = line.OriginalCanvasTextLayout;
if (originalTextLayout != null)
{
GetLinePlayingProgress(lineIndex, out int syllableStartIndex, out int syllableLength, out double syllableProgress);
var curCharIndex = syllableStartIndex + syllableLength * syllableProgress; // 当前唱的字符(相对于整行歌词,非子行)
float fadeWidth = (1f / line.OriginalText.Length) * 0.5f; // 渐变边缘宽度:半个字
var lineRegions = originalTextLayout.GetCharacterRegions(0, line.OriginalText.Length);
var charCountUpToCurRegion = 0;
foreach (var subLineRegion in lineRegions)
{
var subLineLayoutBounds = subLineRegion.LayoutBounds;
charCountUpToCurRegion += subLineRegion.CharacterCount;
Rect subLineRect = new Rect(
subLineLayoutBounds.X + line.OriginalPosition.X,
subLineLayoutBounds.Y + line.OriginalPosition.Y,
subLineLayoutBounds.Width,
subLineLayoutBounds.Height
);
using (var maskLayer = new CanvasCommandList(control))
{
using (var maskLayerDs = maskLayer.CreateDrawingSession())
{
float currentPos = (float)((curCharIndex - subLineRegion.CharacterIndex) / subLineRegion.CharacterCount);
currentPos = Math.Clamp(currentPos, 0, 1 + fadeWidth);
using (var maskBrush = new CanvasLinearGradientBrush(ds,
[
new CanvasGradientStop { Position = 0, Color = Colors.White.WithAlpha((byte)(255 * originalTextOpacity)) }, // 左侧:亮
new CanvasGradientStop { Position = currentPos, Color = Colors.White.WithAlpha((byte)(255 * originalTextOpacity)) },
new CanvasGradientStop { Position = currentPos + fadeWidth, Color = Color.FromArgb((byte)(255 * Math.Min(0.3, originalTextOpacity)), 255, 255, 255) }, // 过渡到暗
new CanvasGradientStop { Position = 1 + fadeWidth, Color = Color.FromArgb((byte)(255 * Math.Min(0.3, originalTextOpacity)), 255, 255, 255) } // 右侧:暗
]))
{
if (maskBrush != null)
{
maskBrush.StartPoint = new Vector2((float)subLineRect.X, (float)subLineRect.Y);
maskBrush.EndPoint = new Vector2((float)(subLineRect.X + subLineRect.Width), (float)subLineRect.Y);
maskLayerDs.FillRectangle(subLineRect, maskBrush);
}
}
}
using var textWithOpacityLayer = new AlphaMaskEffect
{
Source = new CropEffect
{
Source = textOnlyLayer,
SourceRectangle = subLineRect,
BorderMode = EffectBorderMode.Soft,
},
AlphaMask = maskLayer,
};
for (int i = subLineRegion.CharacterIndex; i < subLineRegion.CharacterIndex + subLineRegion.CharacterCount; i++)
{
int curCharIndexInt = (int)Math.Floor(curCharIndex);
var charRegions = originalTextLayout.GetCharacterRegions(i, 1);
if (charRegions.Length > 0)
{
// START 处理浮动动画
double floatOffset = 0;
double targetFloatOffset = 2;
if (lyricsEffectSettings.IsLyricsFloatAnimationEnabled)
{
if (i < curCharIndexInt)
{
floatOffset = 0;
}
else if (i == curCharIndexInt)
{
var charProgress = curCharIndex - curCharIndexInt;
floatOffset = -targetFloatOffset + charProgress * targetFloatOffset;
}
else
{
floatOffset = -targetFloatOffset;
}
}
// END 处理浮动动画
var charRegion = charRegions.FirstOrDefault();
var charLayoutBounds = charRegion.LayoutBounds;
var sourceCharRect = new Rect(
charLayoutBounds.X + line.OriginalPosition.X,
charLayoutBounds.Y + line.OriginalPosition.Y,
charLayoutBounds.Width,
charLayoutBounds.Height
);
// START 处理缩放、辉光动画
double scale = 1;
double glow = 0;
var parentSyllable = line.LyricsSyllables.FirstOrDefault(x => x.StartIndex <= i && i < x.StartIndex + x.Text.Length);
if (parentSyllable != null && parentSyllable.IsLongDuration && parentSyllable.StartIndex == syllableStartIndex)
{
if (lyricsEffectSettings.IsLyricsScaleEffectEnabled)
{
scale += Math.Sin(syllableProgress * Math.PI) * 0.15;
}
if (lyricsEffectSettings.IsLyricsGlowEffectEnabled)
{
glow = Math.Sin(syllableProgress * Math.PI) * 8;
}
}
// END 处理缩放、辉光动画
using (var charWithOpacityLayer = new CropEffect
{
Source = textWithOpacityLayer,
SourceRectangle = sourceCharRect,
BorderMode = EffectBorderMode.Soft,
})
{
var destCharRect = sourceCharRect.Scale(scale).AddY(-floatOffset);
if (glow > 0)
{
ds.DrawImage(new GaussianBlurEffect
{
Source = charWithOpacityLayer,
BlurAmount = (float)glow,
BorderMode = EffectBorderMode.Soft,
}, destCharRect.Extend(16), sourceCharRect.Extend(16));
}
ds.DrawImage(charWithOpacityLayer, destCharRect, sourceCharRect);
}
}
}
}
}
}
var translatedTextLayout = line.TranslatedCanvasTextLayout;
if (translatedTextLayout != null)
{
Rect bounds = translatedTextLayout.LayoutBounds;
var rect = new Rect(
bounds.X + line.TranslatedPosition.X,
bounds.Y + line.TranslatedPosition.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new GaussianBlurEffect
{
BlurAmount = (float)lineBlurAmount,
Source = textOnlyLayer,
BorderMode = EffectBorderMode.Soft
}, rect, rect, (float)translatedTextOpacity);
}
}
private void DrawUnPlayingLine(CanvasDrawingSession ds, ICanvasImage textOnlyLayer, LyricsLine line)
{
var lineBlurAmount = line.BlurAmountTransition.Value;
var phoneticTextOpacity = line.PhoneticTextOpacityTransition.Value;
var originalTextOpacity = line.OriginalTextOpacityTransition.Value;
var translatedTextOpacity = line.TranslatedTextOpacityTransition.Value;
var phoneticTextLayout = line.PhoneticCanvasTextLayout;
if (phoneticTextLayout != null)
{
Rect bounds = phoneticTextLayout.LayoutBounds;
var rect = new Rect(
bounds.X + line.PhoneticPosition.X,
bounds.Y + line.PhoneticPosition.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new GaussianBlurEffect
{
BlurAmount = (float)lineBlurAmount,
Source = textOnlyLayer,
BorderMode = EffectBorderMode.Soft
}, rect, rect, (float)phoneticTextOpacity);
}
var originalTextLayout = line.OriginalCanvasTextLayout;
if (originalTextLayout != null)
{
Rect bounds = originalTextLayout.LayoutBounds;
var rect = new Rect(
bounds.X + line.OriginalPosition.X,
bounds.Y + line.OriginalPosition.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new GaussianBlurEffect
{
BlurAmount = (float)lineBlurAmount,
Source = textOnlyLayer,
BorderMode = EffectBorderMode.Soft
}, rect, rect, (float)originalTextOpacity);
}
var translatedTextLayout = line.TranslatedCanvasTextLayout;
if (translatedTextLayout != null)
{
Rect bounds = translatedTextLayout.LayoutBounds;
var rect = new Rect(
bounds.X + line.TranslatedPosition.X,
bounds.Y + line.TranslatedPosition.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new GaussianBlurEffect
{
BlurAmount = (float)lineBlurAmount,
Source = textOnlyLayer,
BorderMode = EffectBorderMode.Soft
}, rect, rect, (float)translatedTextOpacity);
}
}
private void DrawSnowEffect(CanvasDrawingSession ds)
{
if (_snowEffect != null && _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.IsSnowFlakeOverlayEnabled)
{
ds.DrawImage(_snowEffect);
}
}
private void DrawFogEffect(CanvasDrawingSession ds)
{
if (_fogEffect != null && _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.IsFogOverlayEnabled)
{
ds.DrawImage(_fogEffect);
}
}
private void DrawRaindropEffect(CanvasDrawingSession ds, IGraphicsEffectSource source)
{
if (_raindropEffect != null)
{
_raindropEffect.Sources[0] = source;
ds.DrawImage(_raindropEffect);
}
}
private void FillBackground(CanvasDrawingSession ds, Color color, double radius, double opacity)
{
ds.FillRoundedRectangle(
new Rect(0, 0, _canvasWidth, _canvasHeight),
(float)radius,
(float)radius,
color.WithAlpha((byte)(opacity * 255))
);
}
}
}

View File

@@ -1,71 +0,0 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Shaders;
using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel
{
private PixelShaderEffect? _fluidEffect;
private PixelShaderEffect<SnowEffect>? _snowEffect;
private PixelShaderEffect<FogEffect>? _fogEffect;
private PixelShaderEffect<RaindropEffect>? _raindropEffect;
private void DisposeFluidEffect()
{
_fluidEffect?.Dispose();
_fluidEffect = null;
}
private async void RecreateFluidEffect(ICanvasAnimatedControl control)
{
DisposeFluidEffect();
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/FluidEffect.bin"));
IBuffer buffer = await FileIO.ReadBufferAsync(file);
var bytes = buffer.ToArray();
_fluidEffect = new PixelShaderEffect(bytes);
_fluidEffect.Properties["Width"] = (float)control.ConvertDipsToPixels((float)control.Size.Width, CanvasDpiRounding.Round);
_fluidEffect.Properties["Height"] = (float)control.ConvertDipsToPixels((float)control.Size.Height, CanvasDpiRounding.Round);
_fluidEffect.Properties["color1"] = _albumArtAccentColor1Transition.Value.ToVector3RGB();
_fluidEffect.Properties["color2"] = _albumArtAccentColor2Transition.Value.ToVector3RGB();
_fluidEffect.Properties["color3"] = _albumArtAccentColor3Transition.Value.ToVector3RGB();
_fluidEffect.Properties["color4"] = _albumArtAccentColor4Transition.Value.ToVector3RGB();
_fluidEffect.Properties["EnableLightWave"] = false;
}
private void DisposeSnowEffect()
{
_snowEffect?.Dispose();
_snowEffect = null;
}
private void RecreateSnowEffect()
{
DisposeSnowEffect();
_snowEffect = new();
}
private void DisposeFogEffect()
{
_fogEffect?.Dispose();
_fogEffect = null;
}
private void RecreateFogEffect()
{
DisposeFogEffect();
_fogEffect = new();
}
}
}

View File

@@ -1,414 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel
: IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<Color>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<LyricsLayoutOrientation>>,
IRecipient<PropertyChangedMessage<LyricsFontColorType>>,
IRecipient<PropertyChangedMessage<TextAlignmentType>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<EasingType>>,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<AlbumArtLayoutSettings>>,
IRecipient<PropertyChangedMessage<LyricsBackgroundSettings>>,
IRecipient<PropertyChangedMessage<LyricsWindowStatus>>,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<List<Color>>>
{
private void OnSongInfoChanged()
{
_songDurationMs = (int)(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? TimeSpan.FromMinutes(99).TotalMilliseconds);
_songInfoOpacityTransition.Reset(0f);
_songInfoOpacityTransition.StartTransition(1f);
TotalTime = TimeSpan.Zero;
// 处理 Last.fm 追踪
_totalPlayingTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is AboutControlViewModel)
{
if (message.PropertyName == nameof(AboutControlViewModel.IsDebugOverlayEnabled))
{
_isDebugOverlayEnabled = message.NewValue;
_isDebugOverlayEnabledChanged = true;
}
}
else if (message.Sender is LyricsEffectSettings)
{
if (message.PropertyName == nameof(LyricsEffectSettings.IsLyricsGlowEffectEnabled))
{
}
else if (message.PropertyName == nameof(LyricsEffectSettings.IsFanLyricsEnabled))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.IsLyricsFloatAnimationEnabled))
{
}
else if (message.PropertyName == nameof(LyricsEffectSettings.Is3DLyricsEnabled))
{
_isLyrics3DMatrixChanged = true;
}
}
else if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.IsDynamicLyricsFontSize))
{
_isLayoutChanged = true;
}
}
else if (message.Sender is AlbumArtLayoutSettings)
{
if (message.PropertyName == nameof(AlbumArtLayoutSettings.AutoAlbumArtSize))
{
_isAlbumArtSizeChanged = true;
}
}
else if (message.Sender is LyricsBackgroundSettings)
{
if (message.PropertyName == nameof(LyricsBackgroundSettings.IsSpectrumOverlayEnabled))
{
_isSpectrumOverlayEnabledChanged = true;
}
else if (message.PropertyName == nameof(LyricsBackgroundSettings.IsFluidOverlayEnabled))
{
_isFluidOverlayEnabledChanged = true;
}
else if (message.PropertyName == nameof(LyricsBackgroundSettings.IsSnowFlakeOverlayEnabled))
{
_isSnowOverlayEnabledChanged = true;
}
else if (message.PropertyName == nameof(LyricsBackgroundSettings.IsFogOverlayEnabled))
{
_isFogOverlayEnabledChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<Color> message)
{
if (message.Sender is LyricsWindowViewModel)
{
if (message.PropertyName == nameof(LyricsWindowViewModel.BackdropAccentColor))
{
_immersiveBgColorTransition.StartTransition(message.NewValue);
_environmentalColor = message.NewValue;
UpdateColorConfig();
}
}
else if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsCustomBgFontColor))
{
UpdateColorConfig();
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsCustomFgFontColor))
{
UpdateColorConfig();
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsCustomStrokeFontColor))
{
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<double> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsLineSpacingFactor))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is AlbumArtLayoutSettings)
{
if (message.PropertyName == nameof(AlbumArtLayoutSettings.AlbumArtSize))
{
_isAlbumArtSizeChanged = true;
}
}
else if (message.Sender is LyricsEffectSettings)
{
if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollDuration))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollTopDuration))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollBottomDuration))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollTopDelay))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollBottomDelay))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.FanLyricsAngle))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.Lyrics3DXAngle))
{
_isLyrics3DMatrixChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.Lyrics3DYAngle))
{
_isLyrics3DMatrixChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.Lyrics3DZAngle))
{
_isLyrics3DMatrixChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.Lyrics3DDepth))
{
_isLyrics3DMatrixChanged = true;
}
}
else if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.PhoneticLyricsFontSize))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.OriginalLyricsFontSize))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.TranslatedLyricsFontSize))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsFontStrokeWidth))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<TextAlignmentType> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsAlignmentType))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<LyricsDisplayType> message)
{
if (message.Sender is LyricsWindowStatus)
{
if (message.PropertyName == nameof(LyricsWindowStatus.LyricsDisplayType))
{
_isDisplayTypeChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<LyricsLayoutOrientation> message)
{
if (message.Sender is LyricsWindowStatus)
{
if (message.PropertyName == nameof(LyricsWindowStatus.LyricsLayoutOrientation))
{
_isLyricsLayoutOrientationChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<LyricsFontColorType> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsBgFontColorType))
{
UpdateColorConfig();
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsFgFontColorType))
{
UpdateColorConfig();
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsStrokeFontColorType))
{
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<LyricsFontWeight> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsFontWeight))
{
_isLyricsFontWeightChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
if (message.Sender is LyricsBackgroundSettings)
{
if (message.PropertyName == nameof(LyricsBackgroundSettings.LyricsBackgroundTheme))
{
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<EasingType> message)
{
if (message.Sender is LyricsEffectSettings)
{
if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollEasingType))
{
_canvasYScrollTransition.SetEasingType(message.NewValue);
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsCJKFontFamily))
{
_isLyricsFontFamilyChanged = true;
}
}
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsWesternFontFamily))
{
_isLyricsFontFamilyChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<LyricsWindowStatus> message)
{
if (message.Sender is LiveStates)
{
if (message.PropertyName == nameof(LiveStates.LyricsWindowStatus))
{
UpdateColorConfig();
_isLayoutChanged = true;
_isLyricsWindowsStatusChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<AlbumArtLayoutSettings> message)
{
if (message.Sender is LyricsWindowStatus)
{
if (message.PropertyName == nameof(LyricsWindowStatus.AlbumArtLayoutSettings))
{
}
}
}
public void Receive(PropertyChangedMessage<LyricsBackgroundSettings> message)
{
if (message.Sender is LyricsWindowStatus)
{
if (message.PropertyName == nameof(LyricsWindowStatus.LyricsBackgroundSettings))
{
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
OnSongInfoChanged();
}
}
}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{
var diff = Math.Abs(TotalTime.TotalMilliseconds - _mediaSessionsService.CurrentPosition.TotalMilliseconds);
var timelineSyncThreshold = _mediaSessionsService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
if (diff >= timelineSyncThreshold)
{
TotalTime = _mediaSessionsService.CurrentPosition;
if (TotalTime.TotalSeconds <= 1)
{
_totalPlayingTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
// 大跨度,刷新布局,避免歌词不显示
if (diff >= timelineSyncThreshold + 5000)
{
_isLayoutChanged = true;
}
}
}
}
public void Receive(PropertyChangedMessage<List<Color>> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.LightAccentColors))
{
UpdateColorConfig();
}
else if (message.PropertyName == nameof(IMediaSessionsService.DarkAccentColors))
{
UpdateColorConfig();
}
}
}
}
}

View File

@@ -1,90 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel
{
private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutQuad
);
private readonly ValueTransition<Color> _immersiveBgColorTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _albumArtAccentColor1Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _albumArtAccentColor2Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _albumArtAccentColor3Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _albumArtAccentColor4Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f,
durationSeconds: 0.3f
);
private readonly ValueTransition<double> _lyricsYTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutQuad
);
private readonly ValueTransition<double> _lyricsOpacityTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<double> _albumArtOpacityTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<double> _albumArtXTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutQuad
);
private readonly ValueTransition<double> _albumArtYTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutQuad
);
private readonly ValueTransition<double> _songInfoOpacityTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<double> _lyricsBgBrightnessTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
}
}

View File

@@ -1,294 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using System;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel : BaseViewModel
{
[ObservableProperty]
public partial AppSettings AppSettings { get; set; }
private bool _isLastFMTracked = false;
/// <summary>
/// This includes all the time the song has been played, even if the user seeks back.
/// </summary>
private TimeSpan _totalPlayingTime = TimeSpan.Zero;
/// <summary>
/// Refresh elapsed time for every frame by render.
/// </summary>
private TimeSpan _elapsedTime = TimeSpan.Zero;
/// <summary>
/// Get or set the current time position of the song.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
private int _songDurationMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
private Stopwatch? _drawFrameStopwatch;
private int _drawFrameCount = 0;
private int _displayedDrawFrameCount = 0;
private double _albumArtSize = 0f;
[ObservableProperty] public partial double AlbumArtSize { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double AlbumArtX { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double AlbumArtY { get; set; } = 0;
private double _canvasWidth = 0f;
private double _canvasHeight = 0f;
private readonly double _defaultScale = 0.75f;
private readonly double _highlightedScale = 1.0f;
private double _canvasTargetYScrollOffset = 0;
private double _lyricsX = 0f;
private double _maxLyricsWidth = 0f;
private double _maxSongInfoWidth = 0f;
private readonly ISettingsService _settingsService;
private readonly IMediaSessionsService _mediaSessionsService;
private readonly ILastFMService _lastFMService;
private readonly ILiveStatesService _liveStatesService;
private readonly ILogger _logger;
private double _leftMargin = 36f;
private double _middleMargin = 36f;
private double _rightMargin = 36f;
private double _topMargin = 36f;
private double _bottomMargin = 36f;
private Color _adaptiveGrayedFontColor = Colors.Transparent;
private Color? _adaptiveColoredFontColor = null;
private Color _environmentalColor = Colors.Transparent;
private Color _grayedEnvironmentalColor = Colors.Transparent;
private Color _lightColor = Colors.White;
private Color _darkColor = Colors.Black;
private Color _bgFontColor;
private Color _fgFontColor;
private Color _strokeFontColor;
private int _playingLineIndex = -1;
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private LyricsData? _currentLyricsData;
private bool _isDebugOverlayEnabled = false;
private int _phoneticLyricsFontSize = 18;
private int _originalLyricsFontSize = 36;
private int _translatedLyricsFontSize = 18;
private LyricsFontWeight _originalLyricsFontWeight = LyricsFontWeight.Bold;
private CanvasTextFormat _debugTextFormat = new()
{
FontSize = 12,
FontWeight = FontWeights.ExtraBlack,
};
private CanvasGeometry? _spectrumGeometry = null;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeTypeSent { get; set; }
private SpectrumAnalyzer? _spectrumAnalyzer;
private Matrix4x4 _lyrics3DMatrix = Matrix4x4.Identity;
public LyricsRendererViewModel(
ISettingsService settingsService,
IMediaSessionsService mediaSessionsService,
ILastFMService lastFMService,
ILiveStatesService liveStatesService)
{
_settingsService = settingsService;
_mediaSessionsService = mediaSessionsService;
_liveStatesService = liveStatesService;
_lastFMService = lastFMService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsRendererViewModel>>();
AppSettings = _settingsService.AppSettings;
_mediaSessionsService.LyricsChanged += MediaSessionsService_LyricsChanged;
UpdateColorConfig();
_spectrumAnalyzer = new SpectrumAnalyzer();
}
private void MediaSessionsService_LyricsChanged(object? sender, LyricsChangedEventArgs e)
{
_currentLyricsData = e.LyricsData;
_isLayoutChanged = true;
}
private int GetCurrentPlayingLineIndex()
{
var totalMs = TotalTime.TotalMilliseconds + _mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0;
if (totalMs < _currentLyricsData?.LyricsLines.FirstOrDefault()?.StartMs) return 0;
for (int i = 0; i < _currentLyricsData?.LyricsLines.Count; i++)
{
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var nextLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i + 1);
if (nextLine != null && line.StartMs <= totalMs && totalMs < nextLine.StartMs)
{
return i;
}
else if (nextLine == null && line.StartMs <= totalMs)
{
return i;
}
}
return GetMaxLyricsLineIndexBoundaries().Item2;
}
private void GetLinePlayingProgress(int lineIndex, out int syllableStartIndex, out int syllableLength, out double syllableProgress)
{
syllableStartIndex = 0;
syllableLength = 0;
syllableProgress = 0f;
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(lineIndex);
if (line == null) return;
var nextLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(lineIndex + 1);
int lineEndMs;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = _songDurationMs;
double now = (double)TotalTime.TotalMilliseconds + (double)(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
// 1. 还没到本句
if (now < line.StartMs)
{
return;
}
// 2. 已经超过本句
if (now > lineEndMs)
{
syllableProgress = 1f;
syllableStartIndex = line.OriginalText.Length - 1;
syllableLength = 1;
return;
}
// 3. 有逐字时间轴
if (line.LyricsSyllables != null && line.LyricsSyllables.Count > 1)
{
int charTimingsCount = line.LyricsSyllables.Count;
for (int i = 0; i < charTimingsCount; i++)
{
var timing = line.LyricsSyllables[i];
var nextTiming = line.LyricsSyllables.ElementAtOrDefault(i + 1);
int timingEndMs;
if (timing.EndMs != null) timingEndMs = timing.EndMs.Value;
else if (nextTiming != null) timingEndMs = nextTiming.StartMs;
else timingEndMs = lineEndMs;
syllableStartIndex = timing.StartIndex;
syllableLength = timing.Text.Length;
// 当前时间在某个字的高亮区间
if (now >= timing.StartMs && now <= timingEndMs)
{
if (timingEndMs != timing.StartMs)
{
syllableProgress = (now - timing.StartMs) / (timingEndMs - timing.StartMs);
}
else
{
syllableProgress = 0f;
}
return;
}
else if (now > timingEndMs && (nextTiming == null || now < nextTiming?.StartMs))
{
syllableProgress = 1f;
return;
}
}
}
else
{
int textLength = line.OriginalText.Length;
if (textLength == 0) return;
if (_settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect)
{
// 没有逐字时间轴,均匀分配每个字的高亮时间
double lineProgress = (now - line.StartMs) / (lineEndMs - line.StartMs);
lineProgress = Math.Clamp(lineProgress, 0f, 1f);
// 计算当前高亮到第几个字
double charFloatIndex = lineProgress * textLength;
int charIndex = (int)charFloatIndex;
syllableStartIndex = Math.Clamp(charIndex, 0, textLength - 1);
syllableLength = 1;
// 当前字的进度0~1
syllableProgress = charFloatIndex - charIndex;
}
else
{
syllableStartIndex = textLength;
syllableProgress = 1f;
}
}
}
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (_mediaSessionsService.CurrentSongInfo == null
|| _currentLyricsData == null
|| _currentLyricsData.LyricsLines.Count == 0)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _currentLyricsData.LyricsLines.Count - 1);
}
}
}

View File

@@ -9,7 +9,6 @@ using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
@@ -211,13 +210,13 @@ namespace BetterLyrics.WinUI3
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.ThemeTypeSent))
{
ThemeType = message.NewValue;
}
}
//if (message.Sender is LyricsRendererViewModel)
//{
// if (message.PropertyName == nameof(LyricsRendererViewModel.ThemeTypeSent))
// {
// ThemeType = message.NewValue;
// }
//}
}
public void Receive(PropertyChangedMessage<bool> message)

View File

@@ -17,6 +17,8 @@
xmlns:renderer="using:BetterLyrics.WinUI3.Renderer"
xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
Loaded="Page_Loaded"
Unloaded="Page_Unloaded"
mc:Ignorable="d">
<Grid
@@ -24,25 +26,50 @@
RightTapped="RootGrid_RightTapped"
SizeChanged="RootGrid_SizeChanged">
<!-- Lyrics area -->
<renderer:LyricsRenderer />
<!-- Win2D drawing area -->
<uc:NowPlayingCanvas x:Name="NowPlayingCanvas" />
<!-- Album art and song info area -->
<StackPanel
x:Name="AlbumArtWithSongInfoStackPanel"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsLayoutOrientation, Converter={StaticResource LyricsLayoutOrientationNegationToOrientationConverter}, Mode=OneWay}"
Translation="{x:Bind ViewModel.AlbumArtTranslation, Mode=OneWay}">
SizeChanged="AlbumArtWithSongInfoStackPanel_SizeChanged"
Translation="0,0,0">
<StackPanel.TranslationTransition>
<Vector3Transition />
</StackPanel.TranslationTransition>
<!-- Album art -->
<Grid Padding="32">
<Grid x:Name="ShadowCastGrid" CornerRadius="32">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsDisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsDisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
ComparisonCondition="Equal"
Value="1">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsDisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
ComparisonCondition="Equal"
Value="2">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<Grid x:Name="ShadowCastGrid" CornerRadius="{x:Bind AlbumArtCornerRadius, Mode=OneWay}">
<Image
Width="{x:Bind ViewModel.LyricsRendererViewModel.AlbumArtSize, Mode=OneWay}"
Height="{x:Bind ViewModel.LyricsRendererViewModel.AlbumArtSize, Mode=OneWay}"
Width="{x:Bind AlbumArtSize, Mode=OneWay}"
Height="{x:Bind AlbumArtSize, Mode=OneWay}"
Opacity="{x:Bind ViewModel.LastAlbumArtOpacity, Mode=OneWay}"
Source="{x:Bind ViewModel.LastAlbumArtBitmapImage, Mode=OneWay}">
<Image.OpacityTransition>
@@ -50,21 +77,22 @@
</Image.OpacityTransition>
</Image>
<Image
Width="{x:Bind ViewModel.LyricsRendererViewModel.AlbumArtSize, Mode=OneWay}"
Height="{x:Bind ViewModel.LyricsRendererViewModel.AlbumArtSize, Mode=OneWay}"
Width="{x:Bind AlbumArtSize, Mode=OneWay}"
Height="{x:Bind AlbumArtSize, Mode=OneWay}"
Opacity="{x:Bind ViewModel.AlbumArtOpacity, Mode=OneWay}"
Source="{x:Bind ViewModel.AlbumArtBitmapImage, Mode=OneWay}">
<Image.OpacityTransition>
<ScalarTransition Duration="{x:Bind const:Time.AnimationDuration}" />
</Image.OpacityTransition>
</Image>
<dev:AnimatedImage ImageUrl="" />
</Grid>
<Border
x:Name="ShadowRect"
Width="{x:Bind ViewModel.LyricsRendererViewModel.AlbumArtSize, Mode=OneWay}"
Height="{x:Bind ViewModel.LyricsRendererViewModel.AlbumArtSize, Mode=OneWay}"
Width="{x:Bind AlbumArtSize, Mode=OneWay}"
Height="{x:Bind AlbumArtSize, Mode=OneWay}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="32"
CornerRadius="{x:Bind AlbumArtCornerRadius, Mode=OneWay}"
Loaded="ShadowRect_Loaded"
Translation="0,0,64">
<Border.Shadow>
@@ -74,7 +102,7 @@
</Grid>
<!-- Song info -->
<StackPanel Width="{x:Bind ViewModel.LyricsRendererViewModel.AlbumArtSize, Mode=OneWay}" Opacity="{x:Bind ViewModel.SongInfoOpacity, Mode=OneWay}">
<StackPanel x:Name="SongInfoStackPanel" Width="{x:Bind AlbumArtSize, Mode=OneWay}">
<StackPanel.OpacityTransition>
<ScalarTransition Duration="{x:Bind const:Time.AnimationDuration}" />
</StackPanel.OpacityTransition>
@@ -92,32 +120,90 @@
<interactivity:ChangePropertyAction PropertyName="Margin" Value="-16,32,0,0" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<TextBlock
x:Name="TitleTextBlock"
HorizontalAlignment="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoAlignmentType, Converter={StaticResource TextAlignmentTypeToHorizontalAlignmentConverter}, Mode=OneWay}"
FontWeight="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontWeight, Converter={StaticResource LyricsFontWeightToFontWeightConverter}, Mode=OneWay}"
IsTextSelectionEnabled="True"
Loaded="TitleTextBlock_Loaded"
TextWrapping="Wrap"
Visibility="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
x:Name="ArtistsTextBlock"
HorizontalAlignment="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoAlignmentType, Converter={StaticResource TextAlignmentTypeToHorizontalAlignmentConverter}, Mode=OneWay}"
FontWeight="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontWeight, Converter={StaticResource LyricsFontWeightToFontWeightConverter}, Mode=OneWay}"
IsTextSelectionEnabled="True"
Loaded="ArtistsTextBlock_Loaded"
Opacity="0.5"
TextWrapping="Wrap"
Visibility="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.ShowArtists, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
x:Name="AlbumTextBlock"
HorizontalAlignment="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoAlignmentType, Converter={StaticResource TextAlignmentTypeToHorizontalAlignmentConverter}, Mode=OneWay}"
FontWeight="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontWeight, Converter={StaticResource LyricsFontWeightToFontWeightConverter}, Mode=OneWay}"
IsTextSelectionEnabled="True"
Loaded="AlbumTextBlock_Loaded"
Opacity="0.5"
TextWrapping="Wrap"
Visibility="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.ShowAlbum, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<dev:OpacityMaskView>
<dev:OpacityMaskView.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.02" Color="#FFFFFFFF" />
<GradientStop Offset="0.98" Color="#FFFFFFFF" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
</dev:OpacityMaskView.OpacityMask>
<dev:AutoScrollView
x:Name="TitleAutoScrollHoverEffectView"
Width="{x:Bind AlbumArtSize, Mode=OneWay}"
Padding="5,0,0,0"
IsPlaying="False"
PointerCanceled="TitleAutoScrollHoverEffectView_PointerCanceled"
PointerEntered="TitleAutoScrollHoverEffectView_PointerEntered"
PointerExited="TitleAutoScrollHoverEffectView_PointerExited"
ScrollingPixelsPreSecond="20">
<TextBlock
x:Name="TitleTextBlock"
HorizontalAlignment="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoAlignmentType, Converter={StaticResource TextAlignmentTypeToHorizontalAlignmentConverter}, Mode=OneWay}"
FontWeight="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontWeight, Converter={StaticResource LyricsFontWeightToFontWeightConverter}, Mode=OneWay}"
TextTrimming="None"
Visibility="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</dev:AutoScrollView>
</dev:OpacityMaskView>
<dev:OpacityMaskView>
<dev:OpacityMaskView.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.02" Color="#FFFFFFFF" />
<GradientStop Offset="0.98" Color="#FFFFFFFF" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
</dev:OpacityMaskView.OpacityMask>
<dev:AutoScrollView
x:Name="ArtistsAutoScrollHoverEffectView"
Width="{x:Bind AlbumArtSize, Mode=OneWay}"
Padding="5,0,0,0"
IsPlaying="False"
PointerCanceled="ArtistsAutoScrollHoverEffectView_PointerCanceled"
PointerEntered="ArtistsAutoScrollHoverEffectView_PointerEntered"
PointerExited="ArtistsAutoScrollHoverEffectView_PointerExited"
ScrollingPixelsPreSecond="20">
<TextBlock
x:Name="ArtistsTextBlock"
HorizontalAlignment="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoAlignmentType, Converter={StaticResource TextAlignmentTypeToHorizontalAlignmentConverter}, Mode=OneWay}"
FontWeight="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontWeight, Converter={StaticResource LyricsFontWeightToFontWeightConverter}, Mode=OneWay}"
Opacity="0.5"
TextTrimming="None"
Visibility="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.ShowArtists, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</dev:AutoScrollView>
</dev:OpacityMaskView>
<dev:OpacityMaskView>
<dev:OpacityMaskView.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.02" Color="#FFFFFFFF" />
<GradientStop Offset="0.98" Color="#FFFFFFFF" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
</dev:OpacityMaskView.OpacityMask>
<dev:AutoScrollView
x:Name="AlbumAutoScrollHoverEffectView"
Width="{x:Bind AlbumArtSize, Mode=OneWay}"
Padding="5,0,0,0"
IsPlaying="False"
PointerCanceled="AlbumAutoScrollHoverEffectView_PointerCanceled"
PointerEntered="AlbumAutoScrollHoverEffectView_PointerEntered"
PointerExited="AlbumAutoScrollHoverEffectView_PointerExited"
ScrollingPixelsPreSecond="20">
<TextBlock
x:Name="AlbumTextBlock"
HorizontalAlignment="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.SongInfoAlignmentType, Converter={StaticResource TextAlignmentTypeToHorizontalAlignmentConverter}, Mode=OneWay}"
FontWeight="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontWeight, Converter={StaticResource LyricsFontWeightToFontWeightConverter}, Mode=OneWay}"
Opacity="0.5"
TextTrimming="None"
Visibility="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.ShowAlbum, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</dev:AutoScrollView>
</dev:OpacityMaskView>
</StackPanel>
</StackPanel>
@@ -403,8 +489,7 @@
Maximum="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.TimelinePositionSeconds, Mode=OneWay}" />
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}" />
<Grid
x:Name="TimelineSliderLyricsLineInfo"
Margin="0,-48,0,0"

View File

@@ -4,25 +4,63 @@ using BetterLyrics.WinUI3.Controls;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using DevWinUI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using System;
using System.Numerics;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Views
{
public sealed partial class LyricsPage : Page
public sealed partial class LyricsPage : Page,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<SongInfo?>>
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly ILiveStatesService _liveStatesService = Ioc.Default.GetRequiredService<ILiveStatesService>();
private readonly LyricsRendererViewModel _lyricsRendererViewModel = Ioc.Default.GetRequiredService<LyricsRendererViewModel>();
private double _leftMargin = 36f;
private double _middleMargin = 36f;
private double _rightMargin = 36f;
private double _topMargin = 36f;
private double _bottomMargin = 36f;
private readonly DispatcherQueueTimer _rootGridSizeChangedTimer;
public double AlbumArtSize
{
get { return (double)GetValue(AlbumArtSizeProperty); }
set { SetValue(AlbumArtSizeProperty, value); }
}
public static readonly DependencyProperty AlbumArtSizeProperty =
DependencyProperty.Register(nameof(AlbumArtSize), typeof(double), typeof(NowPlayingCanvas), new PropertyMetadata(0.0));
public CornerRadius AlbumArtCornerRadius
{
get { return (CornerRadius)GetValue(AlbumArtCornerRadiusProperty); }
set { SetValue(AlbumArtCornerRadiusProperty, value); }
}
public static readonly DependencyProperty AlbumArtCornerRadiusProperty =
DependencyProperty.Register(nameof(AlbumArtCornerRadius), typeof(double), typeof(NowPlayingCanvas), new PropertyMetadata(new CornerRadius(0)));
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
@@ -31,6 +69,299 @@ namespace BetterLyrics.WinUI3.Views
this.InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<LyricsPageViewModel>();
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<int>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<bool>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<string>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<SongInfo?>>(this);
_rootGridSizeChangedTimer = App.Current.Resources.DispatcherQueue.CreateTimer();
}
private void CompositionTarget_Rendering(object? sender, object e)
{
var currentTime = NowPlayingCanvas.SongPosition.TotalSeconds;
TimelineSlider.Value = currentTime;
}
private void RenderTextBlock(TextBlock? sender, string? text, int fontSize)
{
if (sender == null || string.IsNullOrEmpty(text) || fontSize == 0) return;
var lyricsStyleSettings = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings;
sender.Inlines.Clear();
foreach (var ch in text)
{
var fontFamilyName = LanguageHelper.IsCJK(ch) ? lyricsStyleSettings.LyricsCJKFontFamily : lyricsStyleSettings.LyricsWesternFontFamily;
sender.Inlines.Add(new Run { Text = $"{ch}", FontFamily = new Microsoft.UI.Xaml.Media.FontFamily(fontFamilyName) });
}
sender.FontSize = fontSize;
}
private void RenderTextBlock(AnimatedTextBlock? sender, string? text, int fontSize)
{
if (sender == null || string.IsNullOrEmpty(text) || fontSize == 0) return;
var lyricsStyleSettings = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings;
var fontFamilyName = LanguageHelper.IsCJK(text) ? lyricsStyleSettings.LyricsCJKFontFamily : lyricsStyleSettings.LyricsWesternFontFamily;
sender.FontFamily = new Microsoft.UI.Xaml.Media.FontFamily(fontFamilyName);
sender.FontSize = fontSize;
sender.Text = text;
}
private void RenderSongInfo()
{
RenderTextBlock(TitleTextBlock, _mediaSessionsService.CurrentSongInfo?.Title, GetTitleFontSize());
RenderTextBlock(ArtistsTextBlock, _mediaSessionsService.CurrentSongInfo?.DisplayArtists, GetArtistsAlbumFontSize());
RenderTextBlock(AlbumTextBlock, _mediaSessionsService.CurrentSongInfo?.Album, GetArtistsAlbumFontSize());
}
private void UpdateAlbumArtSize()
{
var lyricsWindowStatus = _liveStatesService.LiveStates.LyricsWindowStatus;
var albumArtLayoutSettings = lyricsWindowStatus.AlbumArtLayoutSettings;
double temp = 0;
switch (lyricsWindowStatus.LyricsLayoutOrientation)
{
case LyricsLayoutOrientation.Horizontal:
if (albumArtLayoutSettings.AutoAlbumArtSize)
{
temp = Math.Min(
(RootGrid.ActualHeight - _topMargin - _bottomMargin) * 8.5 / 16.0,
(RootGrid.ActualWidth - _leftMargin - _middleMargin - _rightMargin) / 2.0);
}
else
{
temp = albumArtLayoutSettings.AlbumArtSize;
}
break;
case LyricsLayoutOrientation.Vertical:
if (albumArtLayoutSettings.AutoAlbumArtSize)
{
temp = Math.Min(
(RootGrid.ActualHeight - _topMargin - _bottomMargin) * 3.0 / 16.0,
(RootGrid.ActualWidth - _leftMargin - _middleMargin - _rightMargin) * 4.0 / 16.0);
}
else
{
temp = albumArtLayoutSettings.AlbumArtSize;
}
break;
}
AlbumArtSize = Math.Max(0, temp);
}
private void UpdateAlbumArtCornerRadius()
{
var factor = _liveStatesService.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings.CoverImageRadius / 100.0;
AlbumArtCornerRadius = new((AlbumArtSize / 2) * factor);
}
private double GetAlbumArtY()
{
double temp = 0;
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsLayoutOrientation)
{
case LyricsLayoutOrientation.Horizontal:
temp = (RootGrid.ActualHeight - AlbumArtWithSongInfoStackPanel.ActualHeight) / 2.0;
break;
case LyricsLayoutOrientation.Vertical:
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsDisplayType)
{
case LyricsDisplayType.AlbumArtOnly:
temp = (RootGrid.ActualHeight - AlbumArtSize) / 2.0;
break;
case LyricsDisplayType.SplitView:
temp = _topMargin;
break;
default:
break;
}
break;
default:
break;
}
return (float)temp - 32;
}
private double GetAlbumArtX()
{
double temp = 0;
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsLayoutOrientation)
{
case LyricsLayoutOrientation.Horizontal:
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsDisplayType)
{
case LyricsDisplayType.AlbumArtOnly:
temp = RootGrid.ActualWidth / 2.0 - AlbumArtSize / 2.0;
break;
case LyricsDisplayType.SplitView:
temp = _leftMargin + ((RootGrid.ActualWidth - _leftMargin - _middleMargin - _rightMargin) / 2.0 - AlbumArtSize) / 2.0;
break;
default:
break;
}
break;
case LyricsLayoutOrientation.Vertical:
temp = _leftMargin;
break;
default:
break;
}
return (float)temp - 32;
}
private void UpdateAlbumArtTranslation()
{
var x = GetAlbumArtX();
var y = GetAlbumArtY();
AlbumArtWithSongInfoStackPanel.Translation = new((float)x, (float)y, 0);
}
private void UpdateMargin()
{
_topMargin = _bottomMargin = _leftMargin = _middleMargin = _rightMargin = Math.Max(RootGrid.ActualWidth, RootGrid.ActualHeight) / 30.0;
}
private void UpdateLyricsStartY()
{
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsLayoutOrientation)
{
case LyricsLayoutOrientation.Horizontal:
NowPlayingCanvas.LyricsStartY = 0;
break;
case LyricsLayoutOrientation.Vertical:
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsDisplayType)
{
case LyricsDisplayType.LyricsOnly:
NowPlayingCanvas.LyricsStartY = 0;
break;
case LyricsDisplayType.SplitView:
NowPlayingCanvas.LyricsStartY = _topMargin;
break;
default:
break;
}
break;
default:
break;
}
}
private void UpdateLyricsOpacity()
{
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsDisplayType)
{
case LyricsDisplayType.AlbumArtOnly:
NowPlayingCanvas.LyricsOpacity = 0;
break;
case LyricsDisplayType.LyricsOnly:
case LyricsDisplayType.SplitView:
NowPlayingCanvas.LyricsOpacity = 1;
break;
default:
break;
}
}
private void UpdateLyricsStartX()
{
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsLayoutOrientation)
{
case LyricsLayoutOrientation.Horizontal:
switch (_liveStatesService.LiveStates.LyricsWindowStatus.LyricsDisplayType)
{
case LyricsDisplayType.LyricsOnly:
NowPlayingCanvas.LyricsStartX = _leftMargin;
break;
case LyricsDisplayType.SplitView:
NowPlayingCanvas.LyricsStartX = (RootGrid.ActualWidth - _leftMargin - _middleMargin - _rightMargin) / 2.0 + _leftMargin + _middleMargin;
break;
default:
break;
}
break;
case LyricsLayoutOrientation.Vertical:
NowPlayingCanvas.LyricsStartX = _leftMargin;
break;
default:
break;
}
}
private void UpdateLyricsWidth()
{
NowPlayingCanvas.LyricsWidth = Math.Max(RootGrid.ActualWidth - NowPlayingCanvas.LyricsStartX - _rightMargin, 0);
}
private void OnLayoutChanged()
{
UpdateMargin();
UpdateAlbumArtSize();
UpdateAlbumArtCornerRadius();
UpdateAlbumArtTranslation();
UpdateLyricsStartX();
UpdateLyricsStartY();
UpdateLyricsWidth();
}
private int GetTitleFontSize()
{
var albumArtLayoutSettings = _liveStatesService.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings;
if (albumArtLayoutSettings.IsAutoSongInfoFontSize)
{
return (int)Math.Clamp(Math.Min(RootGrid.ActualHeight, RootGrid.ActualWidth) / 20, 8, 72);
}
else
{
return albumArtLayoutSettings.SongInfoFontSize;
}
}
private int GetArtistsAlbumFontSize()
{
return (int)(GetTitleFontSize() * 0.8);
}
// ====
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
_rootGridSizeChangedTimer.Debounce(() =>
{
RenderSongInfo();
OnLayoutChanged();
if (e.NewSize.Width < 500 || e.NewSize.Height < 100)
{
if (BottomCommandGrid.Children.Count != 0)
{
BottomCommandGrid.Children.Remove(BottomCommandContent);
BottomCommandFlyoutContainer.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 0, 0);
}
else
{
if (BottomCommandFlyoutContainer.Children.Count != 0)
{
BottomCommandFlyout.Hide();
BottomCommandFlyoutContainer.Children.Remove(BottomCommandContent);
BottomCommandGrid.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 12, 0);
}
}, Constants.Time.DebounceTimeout);
}
private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -61,32 +392,6 @@ namespace BetterLyrics.WinUI3.Views
PlaybackSettingsFlyout.ShowAt(BottomRightCommandStackPanel);
}
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.RootWidth = e.NewSize.Width;
ViewModel.RootHeight = e.NewSize.Height;
if (e.NewSize.Width < 500 || e.NewSize.Height < 100)
{
if (BottomCommandGrid.Children.Count != 0)
{
BottomCommandGrid.Children.Remove(BottomCommandContent);
BottomCommandFlyoutContainer.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 0, 0);
}
else
{
if (BottomCommandFlyoutContainer.Children.Count != 0)
{
BottomCommandFlyout.Hide();
BottomCommandFlyoutContainer.Children.Remove(BottomCommandContent);
BottomCommandGrid.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 12, 0);
}
}
private void VolumeButton_Click(object sender, RoutedEventArgs e)
{
VolumeFlyout.ShowAt(BottomLeftCommandStackPanel);
@@ -195,19 +500,118 @@ namespace BetterLyrics.WinUI3.Views
Shadow.Receivers.Add(ShadowCastGrid);
}
private void TitleTextBlock_Loaded(object sender, RoutedEventArgs e)
private void AlbumArtWithSongInfoStackPanel_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.TitleTextBlock = TitleTextBlock;
ViewModel.AlbumArtWithSongInfoStackPanelHeight = e.NewSize.Height;
}
private void ArtistsTextBlock_Loaded(object sender, RoutedEventArgs e)
private void TitleAutoScrollHoverEffectView_PointerCanceled(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
ViewModel.ArtistsTextBlock = ArtistsTextBlock;
TitleAutoScrollHoverEffectView.IsPlaying = false;
}
private void AlbumTextBlock_Loaded(object sender, RoutedEventArgs e)
private void TitleAutoScrollHoverEffectView_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
ViewModel.AlbumTextBlock = AlbumTextBlock;
TitleAutoScrollHoverEffectView.IsPlaying = true;
}
private void TitleAutoScrollHoverEffectView_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
TitleAutoScrollHoverEffectView.IsPlaying = false;
}
private void ArtistsAutoScrollHoverEffectView_PointerCanceled(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
ArtistsAutoScrollHoverEffectView.IsPlaying = false;
}
private void ArtistsAutoScrollHoverEffectView_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
ArtistsAutoScrollHoverEffectView.IsPlaying = true;
}
private void ArtistsAutoScrollHoverEffectView_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
ArtistsAutoScrollHoverEffectView.IsPlaying = false;
}
private void AlbumAutoScrollHoverEffectView_PointerCanceled(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
AlbumAutoScrollHoverEffectView.IsPlaying = false;
}
private void AlbumAutoScrollHoverEffectView_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
AlbumAutoScrollHoverEffectView.IsPlaying = true;
}
private void AlbumAutoScrollHoverEffectView_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
AlbumAutoScrollHoverEffectView.IsPlaying = false;
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
CompositionTarget.Rendering -= CompositionTarget_Rendering;
}
// ====
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is AlbumArtLayoutSettings)
{
if (message.PropertyName == nameof(AlbumArtLayoutSettings.SongInfoFontSize))
{
RenderSongInfo();
}
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is AlbumArtLayoutSettings)
{
if (message.PropertyName == nameof(AlbumArtLayoutSettings.IsAutoSongInfoFontSize))
{
RenderSongInfo();
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsCJKFontFamily))
{
RenderSongInfo();
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsWesternFontFamily))
{
RenderSongInfo();
}
}
}
public async void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
SongInfoStackPanel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
RenderSongInfo();
SongInfoStackPanel.Opacity = 1;
}
}
}
}
}