fix: potential UI bugs in lyrics component

- Add .idea for WebStorm users
- Fix typo in a function name
- Optimize display logic of lyrics component’s debug mode
  - Now you can use Cmd+Opt+Shift+Enter to toggle debug mode
- Improve async logic in lyrics' script
- Standardize naming in lyrics' script
- Add detailed comments in lyrics component
- Use CSS variables to enhance styles for lyric lines
- Refactor nextUpdate to a Svelte store and subscribe to its changes
  - Avoid detecting changes in frequently triggered Svelte reactivity statement (line 148 in origin)
- [TODO] Improve the interactive logic of the progress bar
This commit is contained in:
alikia2x 2024-07-25 02:16:38 +08:00
parent 00549a504c
commit d0f562452a
14 changed files with 450 additions and 205 deletions

View File

@ -1,4 +0,0 @@
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

12
.idea/AquaVox.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,48 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/AquaVox.iml" filepath="$PROJECT_DIR$/.idea/AquaVox.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev Server" type="ShConfigurationType" focusToolWindowBeforeRun="true">
<option name="SCRIPT_TEXT" value="bun dev" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/opt/homebrew/bin/nu" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,6 +1,6 @@
{
"name": "aquavox",
"version": "1.10.1",
"version": "1.10.2",
"private": false,
"scripts": {
"dev": "vite dev",

View File

@ -1,3 +0,0 @@
<div>
</div>

View File

@ -35,11 +35,11 @@
}
function volumeBarChangeTouch(e: TouchEvent) {
const value = turncate(
e.touches[0].clientX - volumeBar.getBoundingClientRect().x,
0,
volumeBar.getBoundingClientRect().width
) / volumeBar.getBoundingClientRect().width;
const value = truncate(
e.touches[0].clientX - volumeBar.getBoundingClientRect().x,
0,
volumeBar.getBoundingClientRect().width
) / volumeBar.getBoundingClientRect().width;
adjustVolume(value);
localStorage.setItem('volume', value.toString());
}
@ -49,10 +49,14 @@
progressBarSlideValue.set((e.offsetX / progressBar.getBoundingClientRect().width) * duration);
}
function turncate(value: number, min: number, max: number) {
function truncate(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
function progressBarMouseUp(offsetX: number) {
adjustDisplayProgress(offsetX / progressBar.getBoundingClientRect().width);
}
onMount(() => {
mql.addEventListener('change', (e) => {
showInfoTop = e.matches && hasLyrics;
@ -85,7 +89,7 @@
>
{#if !showInfoTop}
<div class="song-info">
<div class="song-info-top {isInfoTopOverflowing ? 'animate' : ''}" bind:this={songInfoTopContainer}>
<div class="song-info-regular {isInfoTopOverflowing ? 'animate' : ''}" bind:this={songInfoTopContainer}>
<span
class="song-name text-shadow {isInfoTopOverflowing ? 'animate' : ''}"
bind:this={songInfoTopContent}>{name}</span
@ -100,51 +104,34 @@
{formatDuration(progress)}
</div>
<div
class="progress-bar shadow-md"
on:click={(e) => progressBarOnClick(e)}
aria-valuemax={duration}
aria-valuemin="0"
aria-valuenow={progress}
bind:this={progressBar}
class="progress-bar shadow-md"
on:keydown
on:keyup
on:mousedown={() => {
userAdjustingProgress.set(true);
}}
on:mousemove={(e) => {
if ($userAdjustingProgress) {
console.log(e.offsetX )
adjustDisplayProgress(e.offsetX / progressBar.getBoundingClientRect().width);
}
}}
on:touchstart={(e) => {
if (e.cancelable) {
e.preventDefault();
}
userAdjustingProgress.set(true);
}}
on:touchmove={(e) => {
e.preventDefault();
userAdjustingProgress.set(true);
if ($userAdjustingProgress) {
lastTouchProgress =
turncate(
e.touches[0].clientX - progressBar.getBoundingClientRect().x,
0,
progressBar.getBoundingClientRect().width
) / progressBar.getBoundingClientRect().width;
adjustDisplayProgress(lastTouchProgress);
}
}}
on:touchend={(e) => {
e.preventDefault();
userAdjustingProgress.set(false);
adjustProgress(lastTouchProgress);
}}
on:mouseup={() => {
userAdjustingProgress.set(false);
on:mouseup={(e) => {
const offsetX = e.offsetX;
progressBarOnClick(e);
// Q: why it needs delay?
// A: I do not know.
setTimeout(()=> {
userAdjustingProgress.set(false);
progressBarMouseUp(offsetX);
}, 50);
}}
role="slider"
aria-valuemin="0"
aria-valuemax={duration}
aria-valuenow={progress}
tabindex="0"
on:keydown
on:keyup
>
<div class="bar" style={`width: ${(progress / (duration + 0.001)) * 100}%;`}></div>
</div>
@ -152,26 +139,49 @@
<div class="time-indicator text-shadow-md time-total">{formatDuration(duration)}</div>
</div>
<div class="controls top-32 flex h-16 overflow-hidden items-center justify-center">
<button style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );" class="control-btn previous">
<img class="control-img switch-song-img" src="/previous.svg" alt="上一曲" />
<button class="control-btn previous" style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );">
<img alt="上一曲" class="control-img switch-song-img" src="/previous.svg" />
</button>
<button
style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );"
class="control-btn play-btn"
on:click={() => clickPlay()}
on:click={(e) => clickPlay()}
on:focus={null}
on:mouseleave={(e) => {
e.currentTarget.style.backgroundColor = '';
}}
on:mouseover={(e) => {
e.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
}}
on:touchend={(e) => {
e.preventDefault();
e.currentTarget.style.backgroundColor = '';
e.currentTarget.style.scale = '1';
clickPlay();
}}
on:touchstart={(e) => {
e.preventDefault();
e.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
e.currentTarget.style.scale = '0.8';
}}
style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );"
>
<img class="control-img" src={paused ? '/play.svg' : '/pause.svg'} alt="暂停或播放" />
<img alt={paused ? '播放' : '暂停'} class="control-img" src={paused ? '/play.svg' : '/pause.svg'} />
</button>
<button style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );" class="control-btn next">
<img class="control-img switch-song-img" src="/next.svg" alt="下一曲" />
<button class="control-btn next" style="filter: drop-shadow( 0 4px 8px rgba(0, 0, 0, 0.12) );">
<img alt="下一曲" class="control-img switch-song-img" src="/next.svg" />
</button>
</div>
<div class="relative top-52 h-6 flex">
<img class="scale-75" src="/volumeDown.svg" alt="最小音量" />
<img alt="最小音量" class="scale-75" src="/volumeDown.svg" />
<div
aria-valuemax="1"
aria-valuemin="0"
aria-valuenow={volume}
bind:this={volumeBar}
class="progress-bar shadow-md !top-1/2 !translate-y-[-50%]"
on:click={(e) => volumeBarOnChange(e)}
bind:this={volumeBar}
on:keydown
on:keyup
on:mousedown={() => {
userAdjustingVolume = true;
}}
@ -180,11 +190,12 @@
volumeBarOnChange(e);
}
}}
on:touchstart={(e) => {
if (e.cancelable) {
e.preventDefault();
}
userAdjustingVolume = true;
on:mouseup={() => {
userAdjustingVolume = false;
}}
on:touchend={(e) => {
e.preventDefault();
userAdjustingVolume = false;
}}
on:touchmove={(e) => {
e.preventDefault();
@ -193,24 +204,18 @@
volumeBarChangeTouch(e);
}
}}
on:touchend={(e) => {
e.preventDefault();
userAdjustingVolume = false;
}}
on:mouseup={() => {
userAdjustingVolume = false;
on:touchstart={(e) => {
if (e.cancelable) {
e.preventDefault();
}
userAdjustingVolume = true;
}}
role="slider"
aria-valuemin="0"
aria-valuemax="1"
aria-valuenow={volume}
tabindex="0"
on:keydown
on:keyup
>
<div class="bar" style={`width: ${volume * 100}%;`}></div>
</div>
<img class="scale-75" src="/volumeUp.svg" alt="最大音量" />
<img alt="最大音量" class="scale-75" src="/volumeUp.svg" />
</div>
</div>
@ -221,6 +226,7 @@
left: 50%;
transform: translate(-50%, 0);
}
.control-btn {
display: inline-block;
height: 3.7rem;
@ -228,11 +234,10 @@
cursor: pointer;
margin: 0 0.5rem;
border-radius: 0.5rem;
transition: 0.1s;
}
.control-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
transition: 0.45s;
scale: 1;
}
.control-img {
height: 2rem;
width: 2rem;
@ -240,6 +245,7 @@
left: 50%;
transform: translateX(-50%);
}
.switch-song-img {
width: auto !important;
height: 1.7rem !important;
@ -256,19 +262,21 @@
font-family: sans-serif;
text-align: center;
}
.song-info-top {
.song-info-regular {
white-space: nowrap;
overflow: hidden;
position: relative;
height: 2.375rem;
}
.song-info-top.animate {
.song-info-regular.animate {
mask-image: linear-gradient(
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 5rem),
rgba(0, 0, 0, 0) 100%
90deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 5rem),
rgba(0, 0, 0, 0) 100%
);
}
@ -283,12 +291,15 @@
height: 2.5rem;
display: inline-block;
}
.song-name.animate {
animation: scroll 10s linear infinite;
}
.song-name::-webkit-scrollbar {
display: none;
}
@keyframes scroll {
0% {
transform: translateX(100%);
@ -300,10 +311,12 @@
transform: translateX(-100%);
}
}
.song-author {
font-size: 1.2rem;
color: rgba(255, 255, 255, 0.8);
}
.progress {
position: absolute;
width: 100%;
@ -311,6 +324,7 @@
transform: translate(-50%, 0);
height: 2.4rem;
}
.progress-bar {
-webkit-appearance: none;
appearance: none;
@ -325,9 +339,11 @@
cursor: pointer;
transition: 0.3s;
}
.progress-bar:hover {
height: 0.7rem;
}
.bar {
background-color: white;
position: absolute;
@ -351,10 +367,18 @@
display: inline-block;
top: 0.2rem;
}
.time-current {
left: 0;
}
.time-total {
right: 0;
}
@media (min-width: 768px) {
.control-btn {
transition: 0.1s
}
}
</style>

View File

@ -4,19 +4,32 @@
import progressBarRaw from '$lib/state/progressBarRaw';
import type { LrcJsonData } from 'lrc-parser-ts';
import progressBarSlideValue from '$lib/state/progressBarSlideValue';
import nextUpdate from '$lib/state/nextUpdate';
// Component input properties
export let lyrics: string[];
export let originalLyrics: LrcJsonData;
export let progress: number;
let getLyricIndex: Function;
const debugMode = false;
let debugMode = false;
if (localStorage.getItem('debugMode') == null) {
localStorage.setItem('debugMode', 'false');
}
else {
debugMode = localStorage.getItem('debugMode')!.toLowerCase() === "true";
}
let currentLyricIndex = -1;
let currentPositionIndex = -1;
let currentAnimationIndex = -1;
let lyricsContainer: HTMLDivElement;
let nextUpdate = -1;
let lyricsContainer: HTMLDivElement | null;
let lastAdjustProgress = 0;
let localProgress = 0;
let lastScroll = 0;
let scrolling = false;
let scriptScrolling = false;
let currentLyricTopMargin = 288;
let refs: HTMLParagraphElement[] = [];
let _refs: any[] = [];
@ -30,57 +43,37 @@
else return 'previous-lyric';
}
$: {
if (lyricsContainer && originalLyrics && originalLyrics.scripts) {
const scripts = originalLyrics.scripts;
currentPositionIndex = getLyricIndex(progress);
const cl = scripts[currentPositionIndex];
if (cl.start <= progress && progress <= cl.end) {
currentLyricIndex = currentPositionIndex;
nextUpdate = cl.end;
} else {
currentLyricIndex = -1;
nextUpdate = cl.start;
}
if ($userAdjustingProgress === false) {
for (let i = 0; i < scripts.length; i++) {
const offset = Math.abs(i - currentPositionIndex);
const blurRadius = Math.min(offset * 1.5, 16);
if (refs[i]) {
refs[i].style.filter = `blur(${blurRadius}px)`;
}
}
}
}
}
// Function to move the lyrics up smoothly
async function moveToNextLine(h: number) {
console.debug(new Date().getTime() , 'moveToNextLine', h);
// the line that's going to process (like a pointer)
// by default, it's "the next line" after the lift
let processingLineIndex = currentPositionIndex + 2;
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function a(h: number) {
let pos = currentPositionIndex + 2;
for (let i = currentPositionIndex + 2; i < refs.length; i++) {
// modify translateY of all lines in viewport one by one to lift them up
for (let i = processingLineIndex; i < refs.length; i++) {
const lyric = refs[i];
lyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
`transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease,
font-size 200ms ease, scale 250ms ease`;
lyric.style.transform = `translateY(${-h}px)`;
pos = i;
processingLineIndex = i;
await sleep(75);
if (refs[i - 2].getBoundingClientRect().top > lyricsContainer.getBoundingClientRect().height) break;
const twoLinesAhead = refs[i - 2];
if (lyricsContainer && twoLinesAhead.getBoundingClientRect().top > lyricsContainer.getBoundingClientRect().height) break;
}
// 特判,鬼知道为什么
if (refs.length - pos < 3) {
for (let i = pos; i < refs.length; i++) {
if (refs.length - processingLineIndex < 3) {
for (let i = processingLineIndex; i < refs.length; i++) {
const lyric = refs[i];
lyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
lyric.style.transform = `translateY(${-h}px)`;
pos = i;
processingLineIndex = i;
await sleep(75);
}
} else {
for (let i = pos; i < refs.length; i++) {
for (let i = processingLineIndex; i < refs.length; i++) {
refs[i].style.transition =
'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
const h = refs[i].getBoundingClientRect().height;
@ -88,27 +81,44 @@
}
}
// wait until the animation end
await sleep(650);
// clear the transition to let the following style changes could be done without animation
for (let i = 0; i < refs.length; i++) {
refs[i].style.transition =
'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
}
// reset the translateY, and immediately scroll down to provide visual stability
for (let i = 0; i < refs.length; i++) {
refs[i].style.transform = `translateY(0px)`;
}
lyricsContainer.scrollTop += h;
scriptScrolling = true;
if (lyricsContainer !== null) {
lyricsContainer.scrollTop += h;
}
await sleep(500);
scriptScrolling = false;
}
async function b(currentLyric: HTMLParagraphElement) {
if (!originalLyrics || !originalLyrics.scripts || !lyricsContainer) return;
lyricsContainer.scrollTop += currentLyric.getBoundingClientRect().top - 144;
scriptScrolling = true;
lyricsContainer.scrollTop += currentLyric.getBoundingClientRect().top - currentLyricTopMargin;
for (let i = 0; i < refs.length; i++) {
refs[i].style.transform = 'translateY(0px)';
}
setTimeout(() => {
scriptScrolling = false;
}, 500);
}
userAdjustingProgress.subscribe((v) => {
// Handle user adjusting progress state changes
userAdjustingProgress.subscribe((adjusting) => {
if (!originalLyrics) return;
const scripts = originalLyrics.scripts;
if (!scripts) return;
if (v) {
if (adjusting) {
for (let i = 0; i < scripts.length; i++) {
refs[i].style.filter = `blur(0px)`;
}
@ -141,63 +151,150 @@
}
});
$: {
if ($userAdjustingProgress) {
nextUpdate = progress;
} else {
if (nextUpdate - progress < 0.05) {
if (
currentPositionIndex >= 0 &&
currentPositionIndex !== currentAnimationIndex &&
currentPositionIndex !== lastAdjustProgress
) {
const offsetHeight =
refs[currentPositionIndex].getBoundingClientRect().height +
refs[currentPositionIndex].getBoundingClientRect().top -
144;
const currentLyric = refs[currentPositionIndex];
currentLyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
currentLyric.style.transform = `translateY(${-offsetHeight}px)`;
for (let i = currentPositionIndex - 1; i >= 0; i--) {
refs[i].style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
const h = refs[i].getBoundingClientRect().height;
refs[i].style.transform = `translateY(${-offsetHeight}px)`;
}
if (currentPositionIndex + 1 < refs.length) {
const nextLyric = refs[currentPositionIndex + 1];
nextLyric.style.transition =
'transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
nextLyric.style.transform = `translateY(${-offsetHeight}px)`;
a(offsetHeight);
}
currentAnimationIndex = currentPositionIndex;
// Handle scroll events in the lyrics container
function scrollHandler() {
scrolling = !scriptScrolling;
if (scrolling && originalLyrics.scripts) {
lastScroll = new Date().getTime();
for (let i = 0; i < originalLyrics.scripts.length; i++) {
if (refs[i]) {
refs[i].style.filter = 'blur(0px)';
}
}
}
setTimeout(() => {
if (new Date().getTime() - lastScroll > 5000) {
scrolling = false;
}
}, 5500);
}
// Utility function to create a sleep/delay
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Scroll to corresponding lyric while adjusting progress
$: {
if ($userAdjustingProgress == true) {
const currentLyric = refs[getLyricIndex(progress)];
scrollToLyric(currentLyric);
}
}
// Update the current lyric and apply blur effect based on the progress
$: {
(() => {
if (!lyricsContainer || !originalLyrics.scripts) return;
const scripts = originalLyrics.scripts;
currentPositionIndex = getLyricIndex(progress);
const cl = scripts[currentPositionIndex];
if (cl.start <= progress && progress <= cl.end) {
currentLyricIndex = currentPositionIndex;
nextUpdate.set(cl.end);
} else {
currentLyricIndex = -1;
nextUpdate.set(cl.start);
}
const currentLyric = refs[currentPositionIndex];
if ($userAdjustingProgress || scrolling || currentLyric.getBoundingClientRect().top < 0) return;
for (let i = 0; i < scripts.length; i++) {
const offset = Math.abs(i - currentPositionIndex);
const blurRadius = Math.min(offset * 0.96, 16);
if (refs[i]) {
refs[i].style.filter = `blur(${blurRadius}px)`;
}
}
})();
}
nextUpdate.subscribe(async (nextUpdate) => {
if (
currentPositionIndex < 0 ||
currentPositionIndex === currentAnimationIndex ||
currentPositionIndex === lastAdjustProgress ||
$userAdjustingProgress === true ||
scrolling
) return;
const currentLyric = refs[currentPositionIndex];
if (originalLyrics.scripts && currentLyric.getBoundingClientRect().top < 0) return;
const offsetHeight =
refs[currentPositionIndex].getBoundingClientRect().top -
currentLyricTopMargin;
// prepare current line
currentLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
currentLyric.style.transform = `translateY(${-offsetHeight}px)`;
for (let i = currentPositionIndex - 1; i >= 0; i--) {
refs[i].style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
refs[i].style.transform = `translateY(${-offsetHeight}px)`;
}
if (currentPositionIndex + 1 < refs.length) {
const nextLyric = refs[currentPositionIndex + 1];
nextLyric.style.transition = `transform .6s cubic-bezier(.28,.01,.29,.99), filter 200ms ease,
opacity 200ms ease, font-size 200ms ease, scale 250ms ease`;
nextLyric.style.transform = `translateY(${-offsetHeight}px)`;
await moveToNextLine(offsetHeight);
}
currentAnimationIndex = currentPositionIndex;
})
function onKeyDown(e: KeyboardEvent) {
if (e.altKey && e.shiftKey && (e.metaKey || e.key === 'OS') && e.key === 'Enter') {
debugMode = !debugMode;
localStorage.setItem('debugMode', debugMode ? 'true' : 'false');
}
}
function extractTranslateValue(s: string): string | null {
const regex = /translateY\((-?\d*px)\)/;
let arr = regex.exec(s);
return arr==null ? null : arr[1];
}
</script>
{#if lyrics && originalLyrics}
<svelte:window on:keydown={onKeyDown} />
{#if debugMode && lyricsContainer}
<div
class="absolute top-6 right-10 font-mono text-sm backdrop-blur-md z-20 bg-[rgba(255,255,255,0.15)] px-2 rounded-xl">
<p>
LyricIndex: {currentLyricIndex} PositionIndex: {currentPositionIndex}
AnimationIndex:{currentAnimationIndex}
NextUpdate: {$nextUpdate}
Progress: {progress.toFixed(2)}
lastAdjustProgress: {lastAdjustProgress}
scrollPosition: {lyricsContainer.scrollTop}
</p>
</div>
{/if}
{#if lyrics && originalLyrics && originalLyrics.scripts}
<div
class="absolute top-[6.5rem] md:top-36 xl:top-0 w-screen xl:w-[52vw] px-6 md:px-12 lg:px-[7.5rem] xl:left-[45vw] xl:px-[3vw] h-[calc(100vh-17rem)] xl:h-screen font-sans
text-left no-scrollbar overflow-y-auto z-[1] pt-16 lyrics"
bind:this={lyricsContainer}
>
{#if debugMode}
<p class="fixed top-6 right-20 font-mono text-sm">
LyricIndex: {currentLyricIndex} PositionIndex: {currentPositionIndex} AnimationIndex:{currentAnimationIndex}
NextUpdate: {nextUpdate}
Progress: {progress.toFixed(2)}
lastAdjustProgress: {lastAdjustProgress}
</p>
{/if}
{#each lyrics as lyric, i}
<p bind:this={_refs[i]} class={`${getClass(i, progress)} text-shadow-lg`}>
{#if debugMode}
<span class="text-lg absolute">{i}</span>
{#if debugMode && refs[i] && refs[i].style !== undefined}
<span class="text-lg absolute -translate-y-4">{i} &nbsp;
{originalLyrics.scripts[i].start} ~ {originalLyrics.scripts[i].end}
tY: {extractTranslateValue(refs[i].style.transform)}
top: {Math.round(refs[i].getBoundingClientRect().top)}px
</span>
{/if}
{lyric}
</p>
@ -206,82 +303,103 @@
</div>
{/if}
<!--suppress CssUnusedSymbol -->
<style>
:root {
--lyric-mobile-font-size: 2rem;
--lyric-mobile-line-height: 2.4rem;
--lyric-mobile-margin: 1.5rem 0;
--lyric-mobile-font-weight: 700;
--lyric-desktop-font-size: 3.5rem;
--lyric-desktop-line-height: 4.5rem;
--lyric-desktop-margin: 1.75rem 0;
}
.lyrics {
mask-image: linear-gradient(
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 5rem),
rgba(0, 0, 0, 0) 100%
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 2rem,
rgba(0, 0, 0, 1) calc(100% - 5rem),
rgba(0, 0, 0, 0) 100%
);
}
.no-scrollbar {
scrollbar-width: 10px;
}
.no-scrollbar::-webkit-scrollbar {
width: 10px;
width: 0;
}
.current-lyric {
position: relative;
color: white;
font-weight: 600;
font-size: 2.1rem;
line-height: 2.7rem;
margin: 1rem 0rem;
font-weight: var(--lyric-mobile-font-weight);
font-size: var(--lyric-mobile-font-size);
line-height: var(--lyric-mobile-line-height);
margin: var(--lyric-mobile-margin);
scale: 1.02 1;
top: 1rem;
}
.previous-lyric {
position: relative;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
font-size: 2.1rem;
line-height: 2.7rem;
margin: 1rem 0rem;
color: rgba(255, 255, 255, 0.48);
font-weight: var(--lyric-mobile-font-weight);
font-size: var(--lyric-mobile-font-size);
line-height: var(--lyric-mobile-line-height);
margin: var(--lyric-mobile-margin);
top: 1rem;
}
.after-lyric {
position: relative;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
font-size: 2.1rem;
line-height: 2.7rem;
margin: 1rem 0rem;
color: rgba(255, 255, 255, 0.48);
font-weight: var(--lyric-mobile-font-weight);
font-size: var(--lyric-mobile-font-size);
line-height: var(--lyric-mobile-line-height);
margin: var(--lyric-mobile-margin);
top: 1rem;
}
@media (min-width: 768px) {
.current-lyric {
font-size: 3rem;
line-height: 4rem;
margin: 2.4rem 0rem;
margin: 2.4rem 0;
}
.after-lyric {
font-size: 3rem;
line-height: 3.3rem;
margin: 2.4rem 0rem;
margin: 2.4rem 0;
}
.previous-lyric {
font-size: 3em;
line-height: 3.3rem;
margin: 2.4rem 0rem;
margin: 2.4rem 0;
}
}
@media (min-width: 1024px) {
.current-lyric {
font-size: 3.5rem;
line-height: 6.5rem;
margin: 0rem 0rem;
font-size: var(--lyric-desktop-font-size);
line-height: var(--lyric-desktop-line-height);
margin: var(--lyric-desktop-margin);
}
.after-lyric {
font-size: 3.5rem;
line-height: 6.5rem;
margin: 0rem 0rem;
font-size: var(--lyric-desktop-font-size);
line-height: var(--lyric-desktop-line-height);
margin: var(--lyric-desktop-margin);
}
.previous-lyric {
font-size: 3.5rem;
line-height: 6.5rem;
margin: 0rem 0rem;
font-size: var(--lyric-desktop-font-size);
line-height: var(--lyric-desktop-line-height);
margin: var(--lyric-desktop-margin);
}
}
</style>

View File

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
const nextUpdate = writable(-1);
export default nextUpdate;