chores: extra data update algo from draw method to update method

This commit is contained in:
Zhe Fang
2026-01-07 20:08:28 -05:00
parent 3947050d6f
commit a0b6511a53
8 changed files with 209 additions and 146 deletions

View File

@@ -12,7 +12,7 @@
<Identity <Identity
Name="37412.BetterLyrics" Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9" Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.2.245.0" /> Version="1.2.246.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -1,6 +1,9 @@
using BetterLyrics.WinUI3.Helper; using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using DevWinUI;
using Microsoft.UI.Xaml;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -31,7 +34,7 @@ namespace BetterLyrics.WinUI3.Logic
bool isLayoutChanged, bool isLayoutChanged,
bool isPrimaryPlayingLineChanged, bool isPrimaryPlayingLineChanged,
bool isMouseScrollingChanged, bool isMouseScrollingChanged,
double currentProgressMs double currentPositionMs
) )
{ {
if (lines == null) return; if (lines == null) return;
@@ -48,7 +51,7 @@ namespace BetterLyrics.WinUI3.Logic
var line = lines.ElementAtOrDefault(i); var line = lines.ElementAtOrDefault(i);
if (line == null) continue; if (line == null) continue;
bool isSecondaryLinePlaying = currentProgressMs >= line.StartMs && currentProgressMs <= line.EndMs; bool isSecondaryLinePlaying = line.StartMs <= currentPositionMs && currentPositionMs <= line.EndMs;
if (i == primaryPlayingLineIndex) isSecondaryLinePlaying = true; if (i == primaryPlayingLineIndex) isSecondaryLinePlaying = true;
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying; bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
line.IsPlayingLastFrame = isSecondaryLinePlaying; line.IsPlayingLastFrame = isSecondaryLinePlaying;
@@ -150,6 +153,93 @@ namespace BetterLyrics.WinUI3.Logic
line.YOffsetTransition.StartTransition(targetYScrollOffset); line.YOffsetTransition.StartTransition(targetYScrollOffset);
} }
if (line.RenderLyricsOriginalChars != null)
{
foreach (var renderChar in line.RenderLyricsOriginalChars)
{
var syllable = line.LyricsSyllables.FirstOrDefault(x => x.StartIndex <= renderChar.Index && renderChar.Index <= x.EndIndex);
if (syllable == null) continue;
var avgCharDuration = syllable.DurationMs / syllable.Length;
if (avgCharDuration == null || avgCharDuration == 0) continue;
var charStartMs = syllable.StartMs + (renderChar.Index - syllable.StartIndex) * avgCharDuration.Value;
var charEndMs = charStartMs + avgCharDuration;
var progressPlayed = (currentPositionMs - charStartMs) / avgCharDuration.Value;
progressPlayed = Math.Clamp(progressPlayed, 0, 1);
renderChar.ProgressPlayed = progressPlayed;
bool isCharPlaying = charStartMs <= currentPositionMs && currentPositionMs <= charEndMs;
bool isCharPlayingChanged = renderChar.IsPlayingLastFrame != isCharPlaying;
if (isSecondaryLinePlayingChanged || isCharPlayingChanged)
{
if (lyricsEffect.IsLyricsScaleEffectEnabled)
{
double targetScale =
lyricsEffect.IsLyricsScaleEffectAmountAutoAdjust ? 1.15 : lyricsEffect.LyricsScaleEffectAmount / 100.0;
if (isCharPlayingChanged)
{
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration)
{
renderChar.ScaleTransition.SetDuration((syllable.DurationMs ?? 0) / 1000.0 / 2);
renderChar.ScaleTransition.StartTransition(isCharPlaying ? targetScale : 1);
}
}
}
if (lyricsEffect.IsLyricsGlowEffectEnabled)
{
double targetGlow = lyricsEffect.IsLyricsGlowEffectAmountAutoAdjust ? renderChar.LayoutRect.Height * 0.2 : lyricsEffect.LyricsGlowEffectAmount;
switch (lyricsEffect.LyricsGlowEffectScope)
{
case Enums.LyricsEffectScope.LongDurationSyllable:
if (isCharPlayingChanged)
{
if (syllable.DurationMs >= lyricsEffect.LyricsGlowEffectLongSyllableDuration)
{
renderChar.GlowTransition.SetDuration((syllable.DurationMs ?? 0) / 1000.0 / 2);
renderChar.GlowTransition.StartTransition(isCharPlaying ? targetGlow : 0);
}
}
break;
case Enums.LyricsEffectScope.LineStartToCurrentChar:
if (isSecondaryLinePlayingChanged)
{
renderChar.GlowTransition.SetDuration(renderChar.AnimationDuration);
renderChar.GlowTransition.StartTransition(isSecondaryLinePlaying ? targetGlow : 0);
}
break;
default:
break;
}
}
if (lyricsEffect.IsLyricsFloatAnimationEnabled)
{
double targetFloat =
lyricsEffect.IsLyricsFloatAnimationAmountAutoAdjust ? renderChar.LayoutRect.Height * 0.1 : lyricsEffect.LyricsFloatAnimationAmount;
if (isSecondaryLinePlayingChanged)
{
renderChar.FloatTransition.StartTransition(isSecondaryLinePlaying ? targetFloat : 0);
}
if (isCharPlayingChanged)
{
renderChar.FloatTransition.StartTransition(0);
}
}
renderChar.IsPlayingLastFrame = isCharPlaying;
}
renderChar.ScaleTransition.Update(elapsedTime);
renderChar.GlowTransition.Update(elapsedTime);
renderChar.FloatTransition.Update(elapsedTime);
}
}
line.AngleTransition.Update(elapsedTime); line.AngleTransition.Update(elapsedTime);
line.ScaleTransition.Update(elapsedTime); line.ScaleTransition.Update(elapsedTime);
line.BlurAmountTransition.Update(elapsedTime); line.BlurAmountTransition.Update(elapsedTime);

View File

@@ -123,6 +123,8 @@ namespace BetterLyrics.WinUI3.Logic
// 更新中心点 // 更新中心点
line.UpdateCenterPosition(lyricsWidth, style.LyricsAlignmentType); line.UpdateCenterPosition(lyricsWidth, style.LyricsAlignmentType);
line.RecalculateCharacterGeometries();
} }
} }

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsChar
{
public string Text { get; set; } = "";
public int Index { get; set; }
}
}

View File

@@ -4,10 +4,15 @@ namespace BetterLyrics.WinUI3.Models
{ {
public class LyricsSyllable public class LyricsSyllable
{ {
public int? EndMs { get; set; }
public int StartIndex { get; set; }
public int StartMs { get; set; }
public string Text { get; set; } = string.Empty; public string Text { get; set; } = string.Empty;
public int Length => Text.Length;
public int StartIndex { get; set; }
public int EndIndex => StartIndex + Length - 1;
public int StartMs { get; set; }
public int? EndMs { get; set; }
public int? DurationMs => EndMs - StartMs; public int? DurationMs => EndMs - StartMs;
public bool IsLongDuration => DurationMs >= 700; public bool IsLongDuration => DurationMs >= 700;
} }

View File

@@ -0,0 +1,43 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
using System.Collections.Generic;
using System.Text;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models
{
public class RenderLyricsChar : LyricsChar
{
public Rect LayoutRect { get; set; }
public double AnimationDuration { get; set; } = 0.3;
public ValueTransition<double> ScaleTransition { get; set; }
public ValueTransition<double> GlowTransition { get; set; }
public ValueTransition<double> FloatTransition { get; set; }
public double ProgressPlayed { get; set; } = 0; // 0~1
public bool IsPlayingLastFrame { get; set; } = false;
public RenderLyricsChar()
{
ScaleTransition = new(
initialValue: 1.0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
GlowTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
FloatTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
}
}
}

View File

@@ -5,14 +5,22 @@ using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text; using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI; using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Windows.Documents;
using Windows.Foundation;
using Windows.UI; using Windows.UI;
namespace BetterLyrics.WinUI3.Models namespace BetterLyrics.WinUI3.Models
{ {
public class RenderLyricsLine : LyricsLine public class RenderLyricsLine : LyricsLine
{ {
public List<RenderLyricsChar> RenderLyricsOriginalChars { get; set; } = [];
public double AnimationDuration { get; set; } = 0.3; public double AnimationDuration { get; set; } = 0.3;
public ValueTransition<double> AngleTransition { get; set; } public ValueTransition<double> AngleTransition { get; set; }
public ValueTransition<double> BlurAmountTransition { get; set; } public ValueTransition<double> BlurAmountTransition { get; set; }
public ValueTransition<double> PhoneticOpacityTransition { get; set; } public ValueTransition<double> PhoneticOpacityTransition { get; set; }
@@ -231,5 +239,26 @@ namespace BetterLyrics.WinUI3.Models
} }
} }
public void RecalculateCharacterGeometries()
{
RenderLyricsOriginalChars.Clear();
if (OriginalCanvasTextLayout == null) return;
var textLength = OriginalText.Length;
for (int i = 0; i < textLength; i++)
{
var region = OriginalCanvasTextLayout.GetCharacterRegions(i, 1).FirstOrDefault();
var bounds = region.LayoutBounds;
RenderLyricsOriginalChars.Add(new RenderLyricsChar()
{
Index = i,
LayoutRect = bounds,
Text = OriginalText[i].ToString()
});
}
}
} }
} }

View File

@@ -118,7 +118,7 @@ namespace BetterLyrics.WinUI3.Renderer
foreach (var subLineRegion in lineRegions) foreach (var subLineRegion in lineRegions)
{ {
DrawSubLineRegion(resourceCreator, ds, source, line, subLineRegion, curCharIndex, fadeWidth, bgColor, fgColor, state, settings); DrawSubLineRegion(resourceCreator, ds, source, line, subLineRegion, curCharIndex, fadeWidth, bgColor, fgColor, settings);
} }
} }
@@ -132,7 +132,6 @@ namespace BetterLyrics.WinUI3.Renderer
float fadeWidth, float fadeWidth,
Color bgColor, Color bgColor,
Color fgColor, Color fgColor,
LinePlaybackState state,
LyricsEffectSettings settings) LyricsEffectSettings settings)
{ {
var playedOpacity = line.PlayedOriginalOpacityTransition.Value; var playedOpacity = line.PlayedOriginalOpacityTransition.Value;
@@ -187,7 +186,7 @@ namespace BetterLyrics.WinUI3.Renderer
int endCharIndex = subLineRegion.CharacterIndex + subLineRegion.CharacterCount; int endCharIndex = subLineRegion.CharacterIndex + subLineRegion.CharacterCount;
for (int i = subLineRegion.CharacterIndex; i < endCharIndex; i++) for (int i = subLineRegion.CharacterIndex; i < endCharIndex; i++)
{ {
DrawSingleCharacter(ds, line, i, curCharIndex, textWithOpacityLayer, state, settings); DrawSingleCharacter(ds, line, i, textWithOpacityLayer);
} }
} }
} }
@@ -199,161 +198,43 @@ namespace BetterLyrics.WinUI3.Renderer
/// </summary> /// </summary>
/// <param name="ds"></param> /// <param name="ds"></param>
/// <param name="line"></param> /// <param name="line"></param>
/// <param name="charIndex">遍历的字符索引</param> /// <param name="charIndex">遍历的字符索引(相对于整行)</param>
/// <param name="exactProgressIndex">当前播放字符相对于整行的索引</param> /// <param name="exactProgressIndex">当前播放字符的索引(相对于整行</param>
/// <param name="source"></param> /// <param name="source"></param>
/// <param name="state"></param> /// <param name="state"></param>
/// <param name="settings"></param>
private void DrawSingleCharacter( private void DrawSingleCharacter(
CanvasDrawingSession ds, CanvasDrawingSession ds,
RenderLyricsLine line, RenderLyricsLine line,
int charIndex, int charIndex,
double exactProgressIndex, ICanvasImage source)
ICanvasImage source,
LinePlaybackState state,
LyricsEffectSettings settings)
{ {
var curCharIndexInt = (int)Math.Floor(exactProgressIndex); if (charIndex >= line.RenderLyricsOriginalChars.Count) return;
if (line.OriginalCanvasTextLayout == null) return;
var charRegions = line.OriginalCanvasTextLayout.GetCharacterRegions(charIndex, 1); RenderLyricsChar renderChar = line.RenderLyricsOriginalChars[charIndex];
if (charRegions.Length == 0) return;
var charRegion = charRegions[0];
var charLayoutBounds = charRegion.LayoutBounds;
var rect = renderChar.LayoutRect;
var sourceCharRect = new Rect( var sourceCharRect = new Rect(
charLayoutBounds.X + line.OriginalPosition.X, rect.X + line.OriginalPosition.X,
charLayoutBounds.Y + line.OriginalPosition.Y, rect.Y + line.OriginalPosition.Y,
charLayoutBounds.Width, rect.Width,
charLayoutBounds.Height rect.Height
); );
double floatOffset = 0; double scale = renderChar.ScaleTransition.Value;
double scale = 1; double glow = renderChar.GlowTransition.Value;
double glow = 0; double floatOffset = renderChar.FloatTransition.Value;
bool drawGlow = false; var destCharRect = sourceCharRect.Scale(scale).AddY(floatOffset);
if (settings.IsLyricsFloatAnimationEnabled) // Draw glow
{ if (glow > 0)
double targetFloatOffset;
if (settings.IsLyricsFloatAnimationAmountAutoAdjust)
{
targetFloatOffset = sourceCharRect.Height * 0.1;
}
else
{
targetFloatOffset = settings.LyricsFloatAnimationAmount;
}
// 已经浮完了的
if (charIndex < curCharIndexInt)
{
floatOffset = 0;
}
// 正在浮的
else if (charIndex == curCharIndexInt)
{
var p = exactProgressIndex - curCharIndexInt;
floatOffset = -targetFloatOffset + p * targetFloatOffset;
}
// 还没浮的
else
{
floatOffset = -targetFloatOffset;
}
// 制造句间上浮过度动画,这里用任何一个 Transition 都行,主要是获取当前行的进入视野的 Progress
floatOffset *= line.YOffsetTransition.Progress;
}
var parentSyllable = line.LyricsSyllables.FirstOrDefault(x => x.StartIndex <= charIndex && charIndex < x.StartIndex + x.Text.Length);
if (settings.IsLyricsScaleEffectEnabled)
{
if (parentSyllable != null && parentSyllable.StartIndex == state.SyllableStartIndex)
{
if (parentSyllable.DurationMs >= settings.LyricsScaleEffectLongSyllableDuration)
{
if (settings.IsLyricsScaleEffectAmountAutoAdjust)
{
scale += Math.Sin(state.SyllableProgress * Math.PI) * 0.15;
}
else
{
scale += Math.Sin(state.SyllableProgress * Math.PI) * (settings.LyricsScaleEffectAmount / 100.0 - 1);
}
}
}
}
if (settings.IsLyricsGlowEffectEnabled)
{
double maxGlow;
if (settings.IsLyricsGlowEffectAmountAutoAdjust)
{
maxGlow = sourceCharRect.Height * 0.2;
}
else
{
maxGlow = settings.LyricsGlowEffectAmount;
}
switch (settings.LyricsGlowEffectScope)
{
case Enums.LyricsEffectScope.LongDurationSyllable:
if (parentSyllable != null && parentSyllable.StartIndex == state.SyllableStartIndex)
{
if (parentSyllable.DurationMs >= settings.LyricsGlowEffectLongSyllableDuration)
{
glow = maxGlow * Math.Sin(state.SyllableProgress * Math.PI);
drawGlow = true;
}
}
break;
case Enums.LyricsEffectScope.LineStartToCurrentChar:
// 已经唱了的
if (charIndex < curCharIndexInt)
{
glow = maxGlow;
drawGlow = true;
}
// 正在唱的
else if (charIndex == curCharIndexInt)
{
var p = exactProgressIndex - curCharIndexInt;
glow = p * maxGlow;
drawGlow = true;
}
// 还没唱的
else { }
glow *= Math.Clamp(line.OriginalText.Length - exactProgressIndex, 0, 1);
break;
default:
break;
}
}
var destCharRect = sourceCharRect.Scale(scale).AddY(-floatOffset);
if (drawGlow)
{ {
var sourcePlayedCharRect = new Rect( var sourcePlayedCharRect = new Rect(
sourceCharRect.X, sourceCharRect.X,
sourceCharRect.Y, sourceCharRect.Y,
sourceCharRect.Width, sourceCharRect.Width * renderChar.ProgressPlayed,
sourceCharRect.Height sourceCharRect.Height
); );
if (charIndex == curCharIndexInt)
{
var p = exactProgressIndex - curCharIndexInt;
sourcePlayedCharRect.Width *= p;
}
else if (charIndex > curCharIndexInt)
{
sourcePlayedCharRect.Width = 0;
}
using (var glowEffect = new GaussianBlurEffect using (var glowEffect = new GaussianBlurEffect
{ {
Source = new CropEffect Source = new CropEffect
@@ -366,10 +247,11 @@ namespace BetterLyrics.WinUI3.Renderer
BorderMode = EffectBorderMode.Soft BorderMode = EffectBorderMode.Soft
}) })
{ {
ds.DrawImage(glowEffect, destCharRect.Extend(sourceCharRect.Height), sourceCharRect.Extend(sourceCharRect.Height)); ds.DrawImage(glowEffect, destCharRect.Extend(destCharRect.Height), sourceCharRect.Extend(sourceCharRect.Height));
} }
} }
// Draw the top layer
ds.DrawImage(source, destCharRect, sourceCharRect); ds.DrawImage(source, destCharRect, sourceCharRect);
} }
} }