diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index 8e627cf5..1d21c015 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -3501,6 +3501,7 @@ Widget _withLyricSupplements( Color color, { required Offset visibility, Widget Function(String, List, TextStyle)? timedText, + Widget Function(String, List, TextStyle)? timedSupplementText, }) { if (line.romanization == null && line.translation == null) return primary; final supplements = _lyricSupplements(context, line).toList(); @@ -3523,8 +3524,8 @@ Widget _withLyricSupplements( opacity: translation ? visibility.dy : visibility.dx, child: Padding( padding: const EdgeInsets.only(top: 6), - child: words.isNotEmpty && timedText != null - ? timedText(text, words, style) + child: words.isNotEmpty && timedSupplementText != null + ? timedSupplementText(text, words, style) : Text( text, textAlign: context.isMornye @@ -3567,6 +3568,7 @@ Widget _withLyricSupplements( visibility: visibility.dx, primaryStyle: primaryStyle, pronunciationStyle: pronunciationStyle, + pronunciationBuilder: timedSupplementText, textBuilder: timedText ?? (text, words, style) => @@ -3781,11 +3783,18 @@ class _WordHighlightedLyricLineState primary, widget.colorScheme.onSurface, timedText: _buildTimedText, + timedSupplementText: (text, words, style) => + _buildTimedText(text, words, style, lift: false), visibility: widget.supplementVisibility, ); } - Widget _buildTimedText(String text, List words, TextStyle style) { + Widget _buildTimedText( + String text, + List words, + TextStyle style, { + bool lift = true, + }) { final highlightedColor = widget.colorScheme.onSurface; final mornye = context.isMornye; final pendingColor = mornye @@ -3812,7 +3821,7 @@ class _WordHighlightedLyricLineState pendingColor: pendingColor, highlightedColor: highlightedColor, semanticsLabel: text, - liftEnabled: mornye && !MediaQuery.disableAnimationsOf(context), + liftEnabled: lift && mornye && !MediaQuery.disableAnimationsOf(context), ); } } diff --git a/lib/utils/synced_lyrics_scroll.dart b/lib/utils/synced_lyrics_scroll.dart index c183f626..10f81a60 100644 --- a/lib/utils/synced_lyrics_scroll.dart +++ b/lib/utils/synced_lyrics_scroll.dart @@ -98,14 +98,14 @@ double syncedLyricSegmentLift({ return t * t * (3 - 2 * t); } - // Anticipate the highlight slightly without moving the list's layout. - final normalLift = ease( - (elapsed + 100) / (100 + math.min(160, duration * 0.35)), - ); + // Start gently before the color sweep and let the movement trail it. Keep + // this envelope independent of word length: fast syllables must not snap + // upwards just because their highlight finishes in a few frames. + final normalLift = ease((elapsed + 180) / 720); final heldStrength = ((duration - 1000) / 1500).clamp(0.0, 1.0); if (heldStrength == 0) return normalLift; - final rise = ease((elapsed - 200) / math.min(900, (duration - 200) * 0.5)); - final settle = ease((duration - elapsed) / 300); + final rise = ease((elapsed - 400) / math.min(1200, (duration - 400) * 0.6)); + final settle = ease((duration - elapsed) / 600); return normalLift + 1.3 * heldStrength * rise * settle; } diff --git a/lib/widgets/aligned_lyric_pronunciation.dart b/lib/widgets/aligned_lyric_pronunciation.dart index e2f4d6b2..ec21129d 100644 --- a/lib/widgets/aligned_lyric_pronunciation.dart +++ b/lib/widgets/aligned_lyric_pronunciation.dart @@ -172,6 +172,8 @@ class AlignedLyricPronunciation extends StatelessWidget { final TextStyle primaryStyle; final TextStyle pronunciationStyle; final Widget Function(String, List, TextStyle) textBuilder; + final Widget Function(String, List, TextStyle)? + pronunciationBuilder; const AlignedLyricPronunciation({ super.key, @@ -180,6 +182,7 @@ class AlignedLyricPronunciation extends StatelessWidget { required this.primaryStyle, required this.pronunciationStyle, required this.textBuilder, + this.pronunciationBuilder, }); @override @@ -231,7 +234,7 @@ class AlignedLyricPronunciation extends StatelessWidget { ), child: SizedBox( height: row.pronunciationHeight, - child: textBuilder( + child: (pronunciationBuilder ?? textBuilder)( group.pronunciation.map((word) => word.text).join(), group.pronunciation, pronunciationStyle, diff --git a/test/now_playing_lyrics_transition_test.dart b/test/now_playing_lyrics_transition_test.dart index 2c6868e5..9275320d 100644 --- a/test/now_playing_lyrics_transition_test.dart +++ b/test/now_playing_lyrics_transition_test.dart @@ -2572,11 +2572,19 @@ void main() { for (final text in ['First second', 'Firsu secondu']) { final singingFirst = await pixelsAt(1100, text); - final firstEnded = await pixelsAt(1307, text); + final moves = mornye && text == 'First second'; + final firstEnded = await pixelsAt(moves ? 1600 : 1307, text); expect(firstEnded, isNot(orderedEquals(singingFirst))); + if (moves) { + expect( + await pixelsAt(1307, text), + isNot(orderedEquals(firstEnded)), + reason: 'Movement trails the finished color sweep', + ); + } expect(await pixelsAt(1800, text), orderedEquals(firstEnded)); final singingLast = await pixelsAt(2150, text); - final lastEnded = await pixelsAt(2497, text); + final lastEnded = await pixelsAt(moves ? 2600 : 2497, text); expect(lastEnded, isNot(orderedEquals(singingLast))); expect(await pixelsAt(2900, text), orderedEquals(lastEnded)); expect(await pixelsAt(1100, text), orderedEquals(singingFirst)); diff --git a/test/synced_lyrics_scroll_test.dart b/test/synced_lyrics_scroll_test.dart index a9ea1835..2692ea0d 100644 --- a/test/synced_lyrics_scroll_test.dart +++ b/test/synced_lyrics_scroll_test.dart @@ -65,14 +65,16 @@ void main() { expect(liftAt(800), 0); expect(liftAt(950), inExclusiveRange(0, 1)); expect(liftAt(1000), greaterThan(liftAt(950))); - expect(liftAt(1200), 1); - expect(liftAt(1500), 1); + expect(liftAt(1000), lessThan(0.2)); + expect(liftAt(1200), inExclusiveRange(0.4, 0.7)); + expect(liftAt(1500), lessThan(1)); + expect(liftAt(1540), 1); expect(liftAt(3000), 1); }); test('held words rise further then settle to the normal highlight', () { final peak = liftAt(2300, end: 4000); - expect(liftAt(1200, end: 4000), 1); + expect(liftAt(1200, end: 4000), inExclusiveRange(0.4, 0.7)); expect(peak, greaterThan(1)); expect(liftAt(3900, end: 4000), inExclusiveRange(1, peak)); expect(liftAt(4000, end: 4000), 1); @@ -81,6 +83,17 @@ void main() { expect(liftAt(800, end: 4000), 0); }); + test('short syllables keep the slow rise and trail their color sweep', () { + expect(liftAt(1100, end: 1100), lessThan(0.4)); + expect(liftAt(1200, end: 1100), lessThan(0.7)); + expect(liftAt(1540, end: 1100), 1); + for (var position = 820; position < 1540; position += 16) { + final before = liftAt(position, end: 1100); + final after = liftAt(position + 16, end: 1100); + expect(after - before, inInclusiveRange(0, 0.034)); + } + }); + test('invalid or zero-length timing does not move the text', () { expect(liftAt(1200, end: 1000), 0); expect(liftAt(1200, end: 500), 0);