fix(lyrics): lift timed words together

This commit is contained in:
zarzet committed 2026-09-30 22:09:41 +07:00
1 parent fe88048db2
commit 1d3f7a0d44
4 files changed
+32 -74

No files matched your search

+24 -32
View File
@@ -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(
+3 -8
View File
@@ -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;
}