mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-02 22:26:52 +02:00
fix(lyrics): lift timed words together
This commit is contained in:
1 parent
fe88048db2
commit
1d3f7a0d44
4 files changed
+32
-74
No files matched your search
@@ -4390,25 +4390,18 @@ class _SweepingTimedLyricTextState extends State<_SweepingTimedLyricText> {
|
||||
var segmentOffset = 0;
|
||||
for (final segment in widget.segments) {
|
||||
final segmentEnd = segmentOffset + segment.length;
|
||||
final boxes = <Rect>[];
|
||||
// Keep paragraph shaping/wrapping intact. Select whole graphemes so
|
||||
// accents, surrogate pairs and joined emoji never lift in pieces.
|
||||
final fragments = widget.liftEnabled ? segment.characters : [segment];
|
||||
var offset = segmentOffset;
|
||||
for (final fragment in fragments) {
|
||||
boxes.addAll(
|
||||
highlightedPainter
|
||||
.getBoxesForSelection(
|
||||
TextSelection(
|
||||
baseOffset: offset,
|
||||
extentOffset: offset + fragment.length,
|
||||
),
|
||||
boxHeightStyle: BoxHeightStyle.max,
|
||||
)
|
||||
.map((box) => box.toRect()),
|
||||
);
|
||||
offset += fragment.length;
|
||||
}
|
||||
// Select the whole timed word, keeping paragraph shaping and wrapping
|
||||
// intact. All of its text runs share the same vertical movement.
|
||||
final boxes = highlightedPainter
|
||||
.getBoxesForSelection(
|
||||
TextSelection(
|
||||
baseOffset: segmentOffset,
|
||||
extentOffset: segmentEnd,
|
||||
),
|
||||
boxHeightStyle: BoxHeightStyle.max,
|
||||
)
|
||||
.map((box) => box.toRect())
|
||||
.toList();
|
||||
boxes.sort((a, b) {
|
||||
final row = a.top.compareTo(b.top);
|
||||
return row == 0 ? a.left.compareTo(b.left) : row;
|
||||
@@ -4478,23 +4471,22 @@ class _TimedLyricSweepPainter extends CustomPainter {
|
||||
: 0.0;
|
||||
final boxes = segmentBoxes[index];
|
||||
final width = boxes.fold<double>(0, (sum, box) => sum + box.width);
|
||||
final lift = highlightLift > 0 && timed
|
||||
? highlightLift *
|
||||
syncedLyricSegmentLift(
|
||||
position: position,
|
||||
start: starts[index],
|
||||
end: ends[index],
|
||||
)
|
||||
: 0.0;
|
||||
var consumed = 0.0;
|
||||
for (final box in boxes) {
|
||||
if (box.width <= 0) continue;
|
||||
final lift = highlightLift > 0 && timed
|
||||
? highlightLift *
|
||||
syncedLyricSegmentLift(
|
||||
position: position,
|
||||
start: starts[index],
|
||||
end: ends[index],
|
||||
progressOffset: consumed / width,
|
||||
)
|
||||
: 0.0;
|
||||
if (highlightLift > 0) {
|
||||
pendingPaths.putIfAbsent(lift, Path.new).addRect(box);
|
||||
}
|
||||
// Consume the same word progress across graphemes, wrapping and font
|
||||
// fallback. The sweep timing never restarts at a glyph boundary.
|
||||
// Keep the color sweep continuous across wrapping and font fallback,
|
||||
// independently of the movement shared by the whole word.
|
||||
final revealWidth = width * value - consumed;
|
||||
final feather = ((highlightLift > 0 ? width : box.width) * 0.18).clamp(
|
||||
3.0,
|
||||
@@ -4523,8 +4515,8 @@ class _TimedLyricSweepPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
for (final (box, value, lift, feather) in partialBoxes) {
|
||||
// The feather can cross into the following grapheme before its solid
|
||||
// fill arrives, avoiding a hard flash at each letter boundary.
|
||||
// Feather the leading edge so the highlight flows through each letter
|
||||
// while the word rises as a single unit.
|
||||
final boundary = box.left + box.width * value;
|
||||
final revealRight = (boundary + feather).clamp(box.left, box.right);
|
||||
final revealRect = Rect.fromLTRB(
|
||||
|
||||
@@ -89,7 +89,6 @@ double syncedLyricSegmentLift({
|
||||
required Duration position,
|
||||
required Duration start,
|
||||
required Duration end,
|
||||
double progressOffset = 0,
|
||||
}) {
|
||||
if (end <= start) return 0;
|
||||
final elapsed = (position - start).inMicroseconds / 1000;
|
||||
@@ -102,15 +101,11 @@ double syncedLyricSegmentLift({
|
||||
// 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.
|
||||
// Each grapheme begins as the sweep approaches its own position. Retain the
|
||||
// full word's held-note envelope rather than treating letters as syllables.
|
||||
final offset = duration * progressOffset.clamp(0.0, 1.0);
|
||||
final normalLift = ease((elapsed - offset + 180) / 720);
|
||||
// The whole timed word shares this movement, independent of the color sweep.
|
||||
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 - 400) / math.min(1200, (duration - 400) * 0.6));
|
||||
final settle = ease((duration - elapsed) / 600);
|
||||
final wordLift = ease((elapsed + 180) / 720);
|
||||
final emphasis = wordLift > 0 ? normalLift / wordLift : 0;
|
||||
return normalLift + 1.3 * heldStrength * rise * settle * emphasis;
|
||||
return normalLift + 1.3 * heldStrength * rise * settle;
|
||||
}
|
||||
Reference in new issue
Block a user