From cf327211df904262e04d1d0d4438370df58eb0dc Mon Sep 17 00:00:00 2001 From: hoelee Date: Tue, 20 Oct 2026 21:32:43 +0800 Subject: [PATCH] fix(lyrics): centre the active line with rect deltas, not offsetTop offsetTop is measured from the nearest positioned ancestor, so inside an unpositioned .yue2-box it returned the line's distance from the page top (thousands of px). scrollTop clamped that to maxScroll, so the box jumped to the bottom the moment singing started and the text was unreadable. Now: box.scrollTop += (pr.top - br.top) - (clientHeight - lineHeight)/2, plus position:relative on the box. Verified in headless Chrome with a numeric probe: the active line sits ~124px into a 274px box at t=48/100, nothing is highlighted and scrollTop=0 at t=0, and the box settles at maxScroll only for the final lines. --- .../posts/how-i-made-my-own-songs-with-suno-ai.md | 9 ++++++--- .../yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md | 9 ++++++--- .../posts/zh/how-i-made-my-own-songs-with-suno-ai.md | 9 ++++++--- .../yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md | 9 ++++++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/content/posts/how-i-made-my-own-songs-with-suno-ai.md b/src/content/posts/how-i-made-my-own-songs-with-suno-ai.md index 248fc7c..ed54315 100644 --- a/src/content/posts/how-i-made-my-own-songs-with-suno-ai.md +++ b/src/content/posts/how-i-made-my-own-songs-with-suno-ai.md @@ -88,7 +88,7 @@ Publitio) and embedded with plain HTML5 audio. Press play: @@ -297,8 +297,11 @@ document.querySelectorAll('audio[data-lrc]').forEach((audio) => { if (cur >= 0) { const p = box.children[cur]; p.classList.add('on'); - // scroll the lyric box only (scrollIntoView would drag the whole page) - box.scrollTo({ top: p.offsetTop - box.clientHeight / 2 + p.offsetHeight / 2, behavior: 'smooth' }); + // centre the active line inside the box. Use rect deltas, NOT offsetTop: + // offsetTop is measured from the nearest *positioned* ancestor, so without + // position:relative on the box it is huge and scrollTop clamps to the bottom. + const br = box.getBoundingClientRect(), pr = p.getBoundingClientRect(); + box.scrollTop += (pr.top - br.top) - (box.clientHeight - pr.height) / 2; } else { box.scrollTop = 0; // back to the top before the first line } diff --git a/src/content/posts/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md b/src/content/posts/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md index 283025f..cc4b1e4 100644 --- a/src/content/posts/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md +++ b/src/content/posts/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md @@ -227,7 +227,7 @@ against the written lyrics, since the model emits no timestamps. @@ -961,8 +961,11 @@ document.querySelectorAll('audio[data-lrc]').forEach((audio) => { if (cur >= 0) { const p = box.children[cur]; p.classList.add('on'); - // scroll the lyric box only (scrollIntoView would drag the whole page) - box.scrollTo({ top: p.offsetTop - box.clientHeight / 2 + p.offsetHeight / 2, behavior: 'smooth' }); + // centre the active line inside the box. Use rect deltas, NOT offsetTop: + // offsetTop is measured from the nearest *positioned* ancestor, so without + // position:relative on the box it is huge and scrollTop clamps to the bottom. + const br = box.getBoundingClientRect(), pr = p.getBoundingClientRect(); + box.scrollTop += (pr.top - br.top) - (box.clientHeight - pr.height) / 2; } else { box.scrollTop = 0; // back to the top before the first line } diff --git a/src/content/posts/zh/how-i-made-my-own-songs-with-suno-ai.md b/src/content/posts/zh/how-i-made-my-own-songs-with-suno-ai.md index 2d8e987..b43e670 100644 --- a/src/content/posts/zh/how-i-made-my-own-songs-with-suno-ai.md +++ b/src/content/posts/zh/how-i-made-my-own-songs-with-suno-ai.md @@ -74,7 +74,7 @@ v6 家族新增的控制功能,直接改变了我做事的方式: @@ -276,8 +276,11 @@ document.querySelectorAll('audio[data-lrc]').forEach((audio) => { if (cur >= 0) { const p = box.children[cur]; p.classList.add('on'); - // scroll the lyric box only (scrollIntoView would drag the whole page) - box.scrollTo({ top: p.offsetTop - box.clientHeight / 2 + p.offsetHeight / 2, behavior: 'smooth' }); + // centre the active line inside the box. Use rect deltas, NOT offsetTop: + // offsetTop is measured from the nearest *positioned* ancestor, so without + // position:relative on the box it is huge and scrollTop clamps to the bottom. + const br = box.getBoundingClientRect(), pr = p.getBoundingClientRect(); + box.scrollTop += (pr.top - br.top) - (box.clientHeight - pr.height) / 2; } else { box.scrollTop = 0; // back to the top before the first line } diff --git a/src/content/posts/zh/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md b/src/content/posts/zh/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md index 8367626..1ab694f 100644 --- a/src/content/posts/zh/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md +++ b/src/content/posts/zh/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.md @@ -172,7 +172,7 @@ clean = "\n".join(l for l in raw.split("\n") @@ -906,8 +906,11 @@ document.querySelectorAll('audio[data-lrc]').forEach((audio) => { if (cur >= 0) { const p = box.children[cur]; p.classList.add('on'); - // scroll the lyric box only (scrollIntoView would drag the whole page) - box.scrollTo({ top: p.offsetTop - box.clientHeight / 2 + p.offsetHeight / 2, behavior: 'smooth' }); + // centre the active line inside the box. Use rect deltas, NOT offsetTop: + // offsetTop is measured from the nearest *positioned* ancestor, so without + // position:relative on the box it is huge and scrollTop clamps to the bottom. + const br = box.getBoundingClientRect(), pr = p.getBoundingClientRect(); + box.scrollTop += (pr.top - br.top) - (box.clientHeight - pr.height) / 2; } else { box.scrollTop = 0; // back to the top before the first line }