using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; using System.Text; using System.Threading.Tasks; using BetterInAppLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Events; using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Messages; using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Rendering; using BetterLyrics.WinUI3.Services.Playback; using BetterLyrics.WinUI3.Services.Settings; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging.Messages; 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.Dispatching; using Microsoft.UI.Text; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Shapes; using Windows.Foundation; using Windows.Graphics.Imaging; using Windows.UI; namespace BetterLyrics.WinUI3.ViewModels { public partial class LyricsRendererViewModel : BaseRendererViewModel, IRecipient>, IRecipient>, IRecipient>, IRecipient>, IRecipient>, IRecipient>, IRecipient>, IRecipient>, IRecipient>, IRecipient>, IRecipient> { private protected CanvasTextFormat _textFormat = new() { HorizontalAlignment = CanvasHorizontalAlignment.Left, VerticalAlignment = CanvasVerticalAlignment.Top, }; public LyricsDisplayType DisplayType { get; set; } private float _rotateAngle = 0f; private byte[] _shaderByteCode = File.ReadAllBytes(AppInfo.CustomShaderPath); private Color ActivatedWindowAccentColor { get; set; } = Colors.Transparent; private bool IsDockMode { get; set; } = false; [ObservableProperty] public partial SongInfo? SongInfo { get; set; } private List? _lyricsForGlowEffect = []; private SoftwareBitmap? _lastSoftwareBitmap = null; private SoftwareBitmap? _softwareBitmap = null; private SoftwareBitmap? SoftwareBitmap { get => _softwareBitmap; set { if (_softwareBitmap != null) { _lastSoftwareBitmap = _softwareBitmap; _transitionStartTime = DateTimeOffset.Now; _isTransitioning = true; _transitionAlpha = 0f; } _softwareBitmap = value; } } public int CoverImageRadius { get; set; } public bool IsCoverOverlayEnabled { get; set; } public bool IsDynamicCoverOverlayEnabled { get; set; } public int CoverOverlayOpacity { get; set; } public int CoverOverlayBlurAmount { get; set; } [ObservableProperty] public partial bool IsPlaying { get; set; } private protected Color _fontColor; private Color _lightFontColor = Colors.White; private Color _darkFontColor = Colors.Black; private readonly float _defaultOpacity = 0.3f; private readonly float _highlightedOpacity = 1.0f; private readonly float _defaultScale = 0.95f; private readonly float _highlightedScale = 1.0f; private readonly int _lineEnteringDurationMs = 800; private readonly int _lineExitingDurationMs = 800; private readonly int _lineScrollDurationMs = 800; private float _lastTotalYScroll = 0.0f; private float _totalYScroll = 0.0f; private int _startVisibleLineIndex = -1; private int _endVisibleLineIndex = -1; private bool _forceToScroll = false; private readonly float _lyricsGlowEffectAmount = 6f; private readonly double _rightMargin = 36; private readonly float _topMargin = 0f; [ObservableProperty] public partial double LimitedLineWidth { get; set; } private protected bool _isRelayoutNeeded = true; [ObservableProperty] public partial ElementTheme Theme { get; set; } [ObservableProperty] public partial LyricsFontColorType LyricsFontColorType { get; set; } [ObservableProperty] public partial LyricsFontWeight LyricsFontWeight { get; set; } public LyricsAlignmentType LyricsAlignmentType { get; set; } public int LyricsVerticalEdgeOpacity { get; set; } [ObservableProperty] public partial float LyricsLineSpacingFactor { get; set; } [ObservableProperty] public partial int LyricsFontSize { get; set; } public int LyricsBlurAmount { get; set; } public bool IsLyricsGlowEffectEnabled { get; set; } public LyricsGlowEffectScope LyricsGlowEffectScope { get; set; } private protected readonly IPlaybackService _playbackService; private float _transitionAlpha = 1f; private TimeSpan _transitionDuration = TimeSpan.FromMilliseconds(1000); private DateTimeOffset _transitionStartTime; private bool _isTransitioning = false; private readonly float _coverRotateSpeed = 0.003f; private Color _currentBgColor; private Color _targetBgColor; private float _colorTransitionProgress = 1f; private const float ColorTransitionDuration = 0.3f; // 秒 private bool _isColorTransitioning = false; public LyricsRendererViewModel( ISettingsService settingsService, IPlaybackService playbackService ) : base(settingsService) { CoverImageRadius = _settingsService.CoverImageRadius; IsCoverOverlayEnabled = _settingsService.IsCoverOverlayEnabled; IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled; CoverOverlayOpacity = _settingsService.CoverOverlayOpacity; CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount; LyricsFontColorType = _settingsService.LyricsFontColorType; LyricsFontWeight = _settingsService.LyricsFontWeight; LyricsAlignmentType = _settingsService.LyricsAlignmentType; LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity; LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor; LyricsFontSize = _settingsService.LyricsFontSize; LyricsBlurAmount = _settingsService.LyricsBlurAmount; IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled; LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope; _playbackService = playbackService; _playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged; _playbackService.SongInfoChanged += PlaybackService_SongInfoChanged; _playbackService.PositionChanged += PlaybackService_PositionChanged; RefreshPlaybackInfo(); UpdateFontColor(); } public void RequestRelayout() { _isRelayoutNeeded = true; } private void PlaybackService_PositionChanged(object? sender, PositionChangedEventArgs e) { TotalTime = e.Position; } private void PlaybackService_SongInfoChanged(object? sender, SongInfoChangedEventArgs e) { SongInfo = e.SongInfo; } private void PlaybackService_IsPlayingChanged(object? sender, IsPlayingChangedEventArgs e) { IsPlaying = e.IsPlaying; } public void RefreshPlaybackInfo() { IsPlaying = _playbackService.IsPlaying; SongInfo = _playbackService.SongInfo; TotalTime = _playbackService.Position; } partial void OnLimitedLineWidthChanged(double value) { _isRelayoutNeeded = true; } async partial void OnSongInfoChanged(SongInfo? value) { if (value?.AlbumArt is byte[] bytes) SoftwareBitmap = await ( await ImageHelper.GetDecoderFromByte(bytes) ).GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); UpdateFontColor(); _isRelayoutNeeded = true; } partial void OnLyricsFontSizeChanged(int value) { _isRelayoutNeeded = true; } partial void OnLyricsFontWeightChanged(LyricsFontWeight value) { _textFormat.FontWeight = value.ToFontWeight(); } partial void OnLyricsLineSpacingFactorChanged(float value) { _isRelayoutNeeded = true; } partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value) { UpdateFontColor(); } partial void OnThemeChanged(ElementTheme value) { UpdateFontColor(); } private protected void UpdateFontColor() { switch (LyricsFontColorType) { case LyricsFontColorType.Default: switch (Theme) { case ElementTheme.Default: switch (Application.Current.RequestedTheme) { case ApplicationTheme.Light: _fontColor = _darkFontColor; break; case ApplicationTheme.Dark: _fontColor = _lightFontColor; break; default: break; } break; case ElementTheme.Light: _fontColor = _darkFontColor; break; case ElementTheme.Dark: _fontColor = _lightFontColor; break; default: break; } break; case LyricsFontColorType.Dominant: _fontColor = SongInfo?.CoverImageDominantColors?[0] ?? _lightFontColor; break; default: break; } } private int GetCurrentPlayingLineIndex() { for (int i = 0; i < SongInfo?.LyricsLines?.Count; i++) { var line = SongInfo?.LyricsLines?[i]; if (line.EndPlayingTimestampMs < TotalTime.TotalMilliseconds) { continue; } return i; } return -1; } private Tuple GetVisibleLyricsLineIndexBoundaries() { // _logger.LogDebug($"{_startVisibleLineIndex} {_endVisibleLineIndex}"); return new Tuple(_startVisibleLineIndex, _endVisibleLineIndex); } private Tuple GetMaxLyricsLineIndexBoundaries() { if (SongInfo == null || SongInfo.LyricsLines == null || SongInfo.LyricsLines.Count == 0) { return new Tuple(-1, -1); } return new Tuple(0, SongInfo.LyricsLines.Count - 1); } private void DrawLyrics( ICanvasAnimatedControl control, CanvasDrawingSession ds, List? source, float defaultOpacity, LyricsHighlightType currentLineHighlightType ) { var (displayStartLineIndex, displayEndLineIndex) = GetVisibleLyricsLineIndexBoundaries(); var currentPlayingLineIndex = GetCurrentPlayingLineIndex(); for ( int i = displayStartLineIndex; source?.Count > 0 && i >= 0 && i < source?.Count && i <= displayEndLineIndex; i++ ) { var line = source?[i]; using var textLayout = new CanvasTextLayout( control, line?.Text, _textFormat, (float)LimitedLineWidth, (float)control.Size.Height ); float progressPerChar = 1f / line.Text.Length; var position = new Vector2(line.Position.X, line.Position.Y); float centerX = position.X; float centerY = position.Y + (float)textLayout.LayoutBounds.Height / 2; switch (LyricsAlignmentType) { case LyricsAlignmentType.Left: textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left; break; case LyricsAlignmentType.Center: textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center; centerX += (float)LimitedLineWidth / 2; break; case LyricsAlignmentType.Right: textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right; centerX += (float)LimitedLineWidth; break; default: break; } int startIndex = 0; // Set brush for (int j = 0; j < textLayout.LineCount; j++) { int count = textLayout.LineMetrics[j].CharacterCount; var regions = textLayout.GetCharacterRegions(startIndex, count); float subLinePlayingProgress = Math.Clamp( (line.PlayingProgress * line.Text.Length - startIndex) / count, 0, 1 ); float startX = (float)(regions[0].LayoutBounds.Left + position.X); float endX = (float)(regions[^1].LayoutBounds.Right + position.X); if (currentLineHighlightType == LyricsHighlightType.LineByLine) { float[] pos = [ 0, subLinePlayingProgress * (1 + progressPerChar) - progressPerChar, subLinePlayingProgress * (1 + progressPerChar), 1.5f, ]; float[] opacity = [ line.Opacity, line.Opacity, defaultOpacity, defaultOpacity, ]; using var brush = GetHorizontalFillBrush( control, pos, opacity, startX, endX ); textLayout.SetBrush(startIndex, count, brush); } else if (currentLineHighlightType == LyricsHighlightType.CharByChar) { float[] pos = [ subLinePlayingProgress * (1 + progressPerChar) - 3 * progressPerChar, subLinePlayingProgress * (1 + progressPerChar) - progressPerChar, subLinePlayingProgress * (1 + progressPerChar), 1.5f, ]; float[] opacity = [ defaultOpacity, line.Opacity, defaultOpacity, defaultOpacity, ]; using var brush = GetHorizontalFillBrush( control, pos, opacity, startX, endX ); textLayout.SetBrush(startIndex, count, brush); } startIndex += count; } // Scale ds.Transform = Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY)) * Matrix3x2.CreateTranslation( (float)(control.Size.Width - _rightMargin - LimitedLineWidth), _totalYScroll + (float)(control.Size.Height / 2) ); ds.DrawTextLayout(textLayout, position, Colors.Transparent); // Reset scale ds.Transform = Matrix3x2.Identity; } } private CanvasLinearGradientBrush GetHorizontalFillBrush( ICanvasAnimatedControl control, float[] stopPosition, float[] stopOpacity, float startX, float endX ) { var r = _fontColor.R; var g = _fontColor.G; var b = _fontColor.B; return new CanvasLinearGradientBrush( control, [ new() { Position = stopPosition[0], Color = Color.FromArgb((byte)(255 * stopOpacity[0]), r, g, b), }, new() { Position = stopPosition[1], Color = Color.FromArgb((byte)(255 * stopOpacity[1]), r, g, b), }, new() { Position = stopPosition[2], Color = Color.FromArgb((byte)(255 * stopOpacity[2]), r, g, b), }, new() { Position = stopPosition[3], Color = Color.FromArgb((byte)(255 * stopOpacity[3]), r, g, b), }, ] ) { StartPoint = new Vector2(startX, 0), EndPoint = new Vector2(endX, 0), }; } private void DrawImmersiveBackground( ICanvasAnimatedControl control, CanvasDrawingSession ds, bool withGradient ) { ds.FillRectangle( new Rect(0, 0, control.Size.Width, control.Size.Height), new CanvasLinearGradientBrush( control, [ new CanvasGradientStop { Position = 0f, Color = withGradient ? Color.FromArgb( 211, _currentBgColor.R, _currentBgColor.G, _currentBgColor.B ) : _currentBgColor, }, new CanvasGradientStop { Position = 1, Color = _currentBgColor }, ] ) { StartPoint = new Vector2(0, 0), EndPoint = new Vector2(0, (float)control.Size.Height), } ); } private void DrawAlbumArtBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds) { ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f); var overlappedCovers = new CanvasCommandList(control.Device); using var overlappedCoversDs = overlappedCovers.CreateDrawingSession(); if (_isTransitioning && _lastSoftwareBitmap != null) { DrawImgae(control, overlappedCoversDs, _lastSoftwareBitmap, 1 - _transitionAlpha); DrawImgae(control, overlappedCoversDs, SoftwareBitmap, _transitionAlpha); } else { DrawImgae(control, overlappedCoversDs, SoftwareBitmap, 1); } using var coverOverlayEffect = new OpacityEffect { Opacity = CoverOverlayOpacity / 100f, Source = new GaussianBlurEffect { BlurAmount = CoverOverlayBlurAmount, Source = overlappedCovers, }, }; ds.DrawImage(coverOverlayEffect); ds.Transform = Matrix3x2.Identity; } public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds) { bool isAlbumArtOverlayDrawn = IsCoverOverlayEnabled && SoftwareBitmap != null; if (isAlbumArtOverlayDrawn) { DrawAlbumArtBackground(control, ds); } if (IsDockMode) { DrawImmersiveBackground(control, ds, isAlbumArtOverlayDrawn); } // Original lyrics only layer using var lyrics = new CanvasCommandList(control); using (var lyricsDs = lyrics.CreateDrawingSession()) { switch (DisplayType) { case LyricsDisplayType.AlbumArtOnly: case LyricsDisplayType.PlaceholderOnly: break; case LyricsDisplayType.LyricsOnly: case LyricsDisplayType.SplitView: DrawLyrics( control, lyricsDs, SongInfo?.LyricsLines, _defaultOpacity, LyricsHighlightType.LineByLine ); break; default: break; } } // Lyrics layer with opacity modification (used for glow effect) using var modifiedLyrics = new CanvasCommandList(control); using (var modifiedLyricsDs = modifiedLyrics.CreateDrawingSession()) { if (IsLyricsGlowEffectEnabled) { switch (DisplayType) { case LyricsDisplayType.AlbumArtOnly: case LyricsDisplayType.PlaceholderOnly: break; case LyricsDisplayType.LyricsOnly: case LyricsDisplayType.SplitView: switch (LyricsGlowEffectScope) { case LyricsGlowEffectScope.WholeLyrics: modifiedLyricsDs.DrawImage(lyrics); break; case LyricsGlowEffectScope.CurrentLine: DrawLyrics( control, modifiedLyricsDs, _lyricsForGlowEffect, 0, LyricsHighlightType.LineByLine ); break; case LyricsGlowEffectScope.CurrentChar: DrawLyrics( control, modifiedLyricsDs, _lyricsForGlowEffect, 0, LyricsHighlightType.CharByChar ); break; default: break; } break; default: break; } } } using var glowedLyrics = new CanvasCommandList(control); using (var glowedLyricsDs = glowedLyrics.CreateDrawingSession()) { glowedLyricsDs.DrawImage( new ShadowEffect { Source = modifiedLyrics, BlurAmount = _lyricsGlowEffectAmount, ShadowColor = _fontColor, Optimization = EffectOptimization.Quality, } ); glowedLyricsDs.DrawImage(lyrics); } // Mock gradient blurred lyrics layer using var blurredLyrics = new CanvasCommandList(control); using var blurredLyricsDs = blurredLyrics.CreateDrawingSession(); if (LyricsBlurAmount == 0) { blurredLyricsDs.DrawImage(glowedLyrics); } else { double step = 0.05; double overlapFactor = 0; for (double i = 0; i <= 0.5 - step; i += step) { using var halfBlurredLyrics = new GaussianBlurEffect { Source = glowedLyrics, BlurAmount = (float)(LyricsBlurAmount * (1 - i / (0.5 - step))), Optimization = EffectOptimization.Quality, BorderMode = EffectBorderMode.Soft, }; using var topCropped = new CropEffect { Source = halfBlurredLyrics, SourceRectangle = new Rect( 0, control.Size.Height * i, control.Size.Width, control.Size.Height * step * (1 + overlapFactor) ), }; using var bottomCropped = new CropEffect { Source = halfBlurredLyrics, SourceRectangle = new Rect( 0, control.Size.Height * (1 - i - step * (1 + overlapFactor)), control.Size.Width, control.Size.Height * step * (1 + overlapFactor) ), }; blurredLyricsDs.DrawImage(topCropped); blurredLyricsDs.DrawImage(bottomCropped); } } // Masked mock gradient blurred lyrics layer using var maskedBlurredLyrics = new CanvasCommandList(control); using (var maskedBlurredLyricsDs = maskedBlurredLyrics.CreateDrawingSession()) { if (LyricsVerticalEdgeOpacity == 100) { maskedBlurredLyricsDs.DrawImage(blurredLyrics); } else { using var mask = new CanvasCommandList(control); using (var maskDs = mask.CreateDrawingSession()) { DrawGradientOpacityMask(control, maskDs); } maskedBlurredLyricsDs.DrawImage( new AlphaMaskEffect { Source = blurredLyrics, AlphaMask = mask } ); } } // Draw the final composed layer ds.DrawImage(maskedBlurredLyrics); } private void DrawGradientOpacityMask( ICanvasAnimatedControl control, CanvasDrawingSession ds ) { byte verticalEdgeAlpha = (byte)(255 * LyricsVerticalEdgeOpacity / 100f); using var maskBrush = new CanvasLinearGradientBrush( control, [ new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) }, new() { Position = 0.5f, Color = Color.FromArgb(255, 0, 0, 0) }, new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) }, ] ) { StartPoint = new Vector2(0, 0), EndPoint = new Vector2(0, (float)control.Size.Height), }; ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush); } private void ReLayout(ICanvasAnimatedControl control) { if (control == null) return; _textFormat.FontSize = LyricsFontSize; float y = _topMargin; // Init Positions for (int i = 0; i < SongInfo?.LyricsLines?.Count; i++) { var line = SongInfo?.LyricsLines?[i]; // Calculate layout bounds using var textLayout = new CanvasTextLayout( control, line.Text, _textFormat, (float)LimitedLineWidth, (float)control.Size.Height ); line.Position = new Vector2(0, y); y += (float)textLayout.LayoutBounds.Height / textLayout.LineCount * (textLayout.LineCount + LyricsLineSpacingFactor); } } public override void Calculate( ICanvasAnimatedControl control, CanvasAnimatedUpdateEventArgs args ) { base.Calculate(control, args); if (_isColorTransitioning) { _colorTransitionProgress += (float)ElapsedTime.TotalSeconds / ColorTransitionDuration; if (_colorTransitionProgress >= 1f) { _colorTransitionProgress = 1f; _isColorTransitioning = false; _currentBgColor = _targetBgColor; } else { _currentBgColor = Helper.ColorHelper.GetInterpolatedColor( _colorTransitionProgress, _currentBgColor, _targetBgColor ); } } if (_isTransitioning) { var elapsed = DateTimeOffset.Now - _transitionStartTime; float progress = (float)( elapsed.TotalMilliseconds / _transitionDuration.TotalMilliseconds ); _transitionAlpha = Math.Clamp(progress, 0f, 1f); if (_transitionAlpha >= 1f) { _isTransitioning = false; _lastSoftwareBitmap?.Dispose(); _lastSoftwareBitmap = null; } } if (IsDynamicCoverOverlayEnabled) { _rotateAngle += _coverRotateSpeed; _rotateAngle %= MathF.PI * 2; } if (_isRelayoutNeeded) { ReLayout(control); _isRelayoutNeeded = false; _forceToScroll = true; } int currentPlayingLineIndex = GetCurrentPlayingLineIndex(); CalculateLinesProps(SongInfo?.LyricsLines, currentPlayingLineIndex, _defaultOpacity); CalculateCanvasYScrollOffset(control, currentPlayingLineIndex); if (IsLyricsGlowEffectEnabled) { // Deep copy lyrics lines for glow effect _lyricsForGlowEffect = SongInfo?.LyricsLines?.Select(line => line.Clone()).ToList(); switch (LyricsGlowEffectScope) { case LyricsGlowEffectScope.WholeLyrics: break; case LyricsGlowEffectScope.CurrentLine: CalculateLinesProps(_lyricsForGlowEffect, currentPlayingLineIndex, 0); break; case LyricsGlowEffectScope.CurrentChar: CalculateLinesProps(_lyricsForGlowEffect, currentPlayingLineIndex, 0); break; default: break; } } } private void CalculateLinesProps( List? source, int currentPlayingLineIndex, float defaultOpacity ) { var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries(); for (int i = startLineIndex; source?.Count > 0 && i <= endLineIndex; i++) { var line = source?[i]; bool linePlaying = i == currentPlayingLineIndex; var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs); var lineExitingDurationMs = _lineExitingDurationMs; if (i + 1 <= endLineIndex) { lineExitingDurationMs = Math.Min( source?[i + 1].DurationMs ?? 0, lineExitingDurationMs ); } float lineEnteringProgress = 0.0f; float lineExitingProgress = 0.0f; bool lineEntering = false; bool lineExiting = false; float scale = _defaultScale; float opacity = defaultOpacity; float playProgress = 0; if (linePlaying) { line.PlayingState = LyricsPlayingState.Playing; scale = _highlightedScale; opacity = _highlightedOpacity; playProgress = ((float)TotalTime.TotalMilliseconds - line.StartPlayingTimestampMs) / line.DurationMs; var durationFromStartMs = TotalTime.TotalMilliseconds - line.StartPlayingTimestampMs; lineEntering = durationFromStartMs <= lineEnteringDurationMs; if (lineEntering) { lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs; scale = _defaultScale + (_highlightedScale - _defaultScale) * (float)lineEnteringProgress; opacity = defaultOpacity + (_highlightedOpacity - defaultOpacity) * (float)lineEnteringProgress; } } else { if (i < currentPlayingLineIndex) { line.PlayingState = LyricsPlayingState.Played; playProgress = 1; var durationToEndMs = TotalTime.TotalMilliseconds - line.EndPlayingTimestampMs; lineExiting = durationToEndMs <= lineExitingDurationMs; if (lineExiting) { lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs; scale = _highlightedScale - (_highlightedScale - _defaultScale) * (float)lineExitingProgress; opacity = _highlightedOpacity - (_highlightedOpacity - defaultOpacity) * (float)lineExitingProgress; } } else { line.PlayingState = LyricsPlayingState.NotPlayed; } } line.EnteringProgress = lineEnteringProgress; line.ExitingProgress = lineExitingProgress; line.Scale = scale; line.Opacity = opacity; line.PlayingProgress = playProgress; } } private void CalculateCanvasYScrollOffset( ICanvasAnimatedControl control, int currentPlayingLineIndex ) { if (currentPlayingLineIndex < 0) { return; } var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries(); if (startLineIndex < 0 || endLineIndex < 0) { return; } // Set _scrollOffsetY LyricsLine? currentPlayingLine = SongInfo?.LyricsLines?[currentPlayingLineIndex]; if (currentPlayingLine == null) { return; } using var playingTextLayout = new CanvasTextLayout( control, currentPlayingLine.Text, _textFormat, (float)LimitedLineWidth, (float)control.Size.Height ); var lineScrollingProgress = (TotalTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs) / Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs); float targetYScrollOffset = (float?)( -currentPlayingLine.Position.Y + SongInfo?.LyricsLines?[0].Position.Y - playingTextLayout.LayoutBounds.Height / 2 - _lastTotalYScroll ) ?? 0f; var yScrollOffset = targetYScrollOffset * EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress)); bool isScrollingNow = lineScrollingProgress <= 1; if (isScrollingNow) { _totalYScroll = _lastTotalYScroll + yScrollOffset; } else { if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1) { _totalYScroll = _lastTotalYScroll + targetYScrollOffset; _forceToScroll = false; } _lastTotalYScroll = _totalYScroll; } _startVisibleLineIndex = _endVisibleLineIndex = -1; // Update visible line indices for ( int i = startLineIndex; i >= 0 && i <= endLineIndex && i < SongInfo?.LyricsLines?.Count; i++ ) { var line = SongInfo?.LyricsLines?[i]; using var textLayout = new CanvasTextLayout( control, line.Text, _textFormat, (float)LimitedLineWidth, (float)control.Size.Height ); if ( _totalYScroll + (float)(control.Size.Height / 2) + line.Position.Y + textLayout.LayoutBounds.Height >= 0 ) { if (_startVisibleLineIndex == -1) { _startVisibleLineIndex = i; } } if ( _totalYScroll + (float)(control.Size.Height / 2) + line.Position.Y + textLayout.LayoutBounds.Height >= control.Size.Height ) { if (_endVisibleLineIndex == -1) { _endVisibleLineIndex = i; } } } if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1) { _endVisibleLineIndex = endLineIndex; } } public void Receive(PropertyChangedMessage message) { if (message.Sender is SettingsViewModel) { if (message.PropertyName == nameof(SettingsViewModel.ThemeType)) { Theme = message.NewValue; } } } private static void DrawImgae( ICanvasAnimatedControl control, CanvasDrawingSession ds, SoftwareBitmap softwareBitmap, float opacity ) { using var canvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap); float imageWidth = (float)canvasBitmap.Size.Width; float imageHeight = (float)canvasBitmap.Size.Height; var scaleFactor = (float)Math.Sqrt(Math.Pow(control.Size.Width, 2) + Math.Pow(control.Size.Height, 2)) / Math.Min(imageWidth, imageHeight); ds.DrawImage( new OpacityEffect { Source = new ScaleEffect { InterpolationMode = CanvasImageInterpolation.HighQualityCubic, BorderMode = EffectBorderMode.Hard, Scale = new Vector2(scaleFactor), Source = canvasBitmap, }, Opacity = opacity, }, (float)control.Size.Width / 2 - imageWidth * scaleFactor / 2, (float)control.Size.Height / 2 - imageHeight * scaleFactor / 2 ); } public void Receive(PropertyChangedMessage message) { if (message.Sender is SettingsViewModel) { if (message.PropertyName == nameof(SettingsViewModel.IsDynamicCoverOverlayEnabled)) { IsDynamicCoverOverlayEnabled = message.NewValue; } else if (message.PropertyName == nameof(SettingsViewModel.IsCoverOverlayEnabled)) { IsCoverOverlayEnabled = message.NewValue; } } else if (message.Sender is LyricsSettingsControlViewModel) { if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled) ) { IsLyricsGlowEffectEnabled = message.NewValue; } } else if (message.Sender is HostWindowViewModel) { if (message.PropertyName == nameof(HostWindowViewModel.IsDockMode)) { IsDockMode = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { if (message.Sender is SettingsViewModel) { if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius)) { CoverImageRadius = message.NewValue; } else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayOpacity)) { CoverOverlayOpacity = message.NewValue; } else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayBlurAmount)) { CoverOverlayBlurAmount = message.NewValue; } } else if (message.Sender is LyricsSettingsControlViewModel) { if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsVerticalEdgeOpacity) ) { LyricsVerticalEdgeOpacity = message.NewValue; } else if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsBlurAmount) ) { LyricsBlurAmount = message.NewValue; } else if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize) ) { LyricsFontSize = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { if (message.Sender is LyricsSettingsControlViewModel) { if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontColorType) ) { LyricsFontColorType = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { if (message.Sender is LyricsSettingsControlViewModel) { if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsAlignmentType) ) { LyricsAlignmentType = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { if (message.Sender is LyricsSettingsControlViewModel) { if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsLineSpacingFactor) ) { LyricsLineSpacingFactor = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { if (message.Sender is LyricsPageViewModel) { if (message.PropertyName == nameof(LyricsPageViewModel.LimitedLineWidth)) { LimitedLineWidth = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { DisplayType = message.NewValue; } public void Receive(PropertyChangedMessage message) { if (message.Sender is LyricsSettingsControlViewModel) { if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontWeight)) { LyricsFontWeight = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { if (message.Sender is LyricsSettingsControlViewModel) { if ( message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsGlowEffectScope) ) { LyricsGlowEffectScope = message.NewValue; } } } public void Receive(PropertyChangedMessage message) { if (message.Sender is HostWindowViewModel) { if (message.PropertyName == nameof(HostWindowViewModel.ActivatedWindowAccentColor)) { _currentBgColor = _isColorTransitioning ? Helper.ColorHelper.GetInterpolatedColor( _colorTransitionProgress, _currentBgColor, _targetBgColor ) : ActivatedWindowAccentColor; _targetBgColor = message.NewValue; _colorTransitionProgress = 0f; _isColorTransitioning = true; ActivatedWindowAccentColor = message.NewValue; } } } } }