From afbc82badb46d947f0a58f4800c91a66a03024db Mon Sep 17 00:00:00 2001 From: alikia2x Date: Thu, 24 Oct 2024 05:00:53 +0800 Subject: [PATCH] feature: complete lyric features --- package.json | 2 +- src/app.css | 7 + src/lib/components/lyrics/lyricLine.svelte | 63 ++++++- src/lib/components/lyrics/newLyrics.svelte | 186 ++++++++++++++++++--- src/lib/components/lyrics/type.d.ts | 6 - 5 files changed, 229 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 773c30a..d64e817 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aquavox", - "version": "1.15.0", + "version": "2.3.0", "private": false, "scripts": { "dev": "vite dev", diff --git a/src/app.css b/src/app.css index 03b75b4..6ceabcd 100644 --- a/src/app.css +++ b/src/app.css @@ -29,3 +29,10 @@ h2 { .text-shadow-none { text-shadow: none; } + +body, +html { + position: fixed; + overflow: hidden; + overscroll-behavior: none; +} diff --git a/src/lib/components/lyrics/lyricLine.svelte b/src/lib/components/lyrics/lyricLine.svelte index aa48317..dedc193 100644 --- a/src/lib/components/lyrics/lyricLine.svelte +++ b/src/lib/components/lyrics/lyricLine.svelte @@ -4,14 +4,23 @@ import type { LyricPos } from './type'; import type { Spring } from '$lib/graphics/spring/spring'; + const viewportWidth = document.documentElement.clientWidth; + const scaleCurrentLine = viewportWidth > 640 ? 1.02 : 1.045 ; + export let line: ScriptItem; export let index: number; export let debugMode: Boolean; + export let lyricClick: Function; + let ref: HTMLDivElement; + let clickMask: HTMLSpanElement; let time = 0; let positionX: number = 0; let positionY: number = 0; + let scale = 1; + let opacity = 1; + let stopped = false; let lastPosX: number | undefined = undefined; let lastPosY: number | undefined = undefined; let lastUpdateY: number | undefined = undefined; @@ -28,7 +37,7 @@ time = (new Date().getTime() - lastUpdateY) / 1000; springY.update(time); positionY = springY.getCurrentPosition(); - if (!springY.arrived()) { + if (!springY.arrived() && !stopped) { requestAnimationFrame(updateY); } lastUpdateY = new Date().getTime(); @@ -66,6 +75,12 @@ export const setCurrent = (isCurrent: boolean) => { isCurrentLyric = isCurrent; + opacity = isCurrent ? 1 : 0.36; + scale = isCurrent ? scaleCurrentLine : 1; + }; + + export const setBlur = (blur: number) => { + ref.style.filter = `blur(${blur}px)`; }; export const update = (pos: LyricPos, delay: number = 0) => { @@ -73,10 +88,11 @@ lastPosX = pos.x; lastPosY = pos.y; } - springY = createSpring(lastPosY, pos.y, .126, .85, delay); - springX = createSpring(lastPosX, pos.x, .126, .85, delay); + springX!.setTargetPosition(pos.x, delay); + springY!.setTargetPosition(pos.y, delay); lastUpdateY = new Date().getTime(); lastUpdateX = new Date().getTime(); + stopped = false; requestAnimationFrame(updateY); requestAnimationFrame(updateX); lastPosX = pos.x; @@ -96,16 +112,51 @@ lastPosY = pos.y; positionX = pos.x; positionY = pos.y; + springX = createSpring(pos.x, pos.x, 0.126, 0.8); + springY = createSpring(pos.y, pos.y, 0.126, 0.8); + }; + + export const stop = () => { + stopped = true; }; export const getRef = () => ref; -
+ + +
{ + clickMask.style.backgroundColor = "rgba(255,255,255,.3)"; + }} + on:touchend={() => { + clickMask.style.backgroundColor = "transparent"; + }} + on:click={() => { + lyricClick(index); + }} +> + + + {#if debugMode} - Line idx: {index}, duration: {(line.end - line.start).toFixed(3)} + + {index}: duration: {(line.end - line.start).toFixed(3)}, {line.start.toFixed(3)}~{line.end.toFixed(3)} + {/if} - + {line.text} + {#if line.translation} +
+ + {line.translation} + + {/if}
diff --git a/src/lib/components/lyrics/newLyrics.svelte b/src/lib/components/lyrics/newLyrics.svelte index d6611e4..a1b061d 100644 --- a/src/lib/components/lyrics/newLyrics.svelte +++ b/src/lib/components/lyrics/newLyrics.svelte @@ -3,9 +3,17 @@ import { onMount } from 'svelte'; import type { ScriptItem } from '$lib/lyrics/type'; import LyricLine from './lyricLine.svelte'; - import type { LyricLayout, LyricPos } from './type'; import createLyricsSearcher from '$lib/lyrics/lyricSearcher'; + // constants + const viewportHeight = document.documentElement.clientHeight; + const viewportWidth = document.documentElement.clientWidth; + const marginY = viewportWidth > 640 ? 36 : 0 ; + const currentLyrictTop = viewportHeight * 0.02; + const deceleration = 0.95; // Velocity decay factor for inertia + const minVelocity = 0.1; // Minimum velocity to stop inertia + document.body.style.overflow = 'hidden'; + // Props export let originalLyrics: LrcJsonData; export let progress: number; @@ -15,19 +23,31 @@ let lyricLines: ScriptItem[] = []; let lyricExists = false; let lyricsContainer: HTMLDivElement | null; - let lyricLayouts: LyricLayout[] = []; let debugMode = false; + let nextUpdate = 0; + let lastProgress = 0; let showTranslation = false; + let scrollEventAdded = false; + let scrolling = false; + let scrollingTimeout: Timer; + let lastY: number; // For tracking touch movements + let lastTime: number; // For tracking time between touch moves + let velocityY = 0; // Vertical scroll velocity + let inertiaFrame: number; // For storing the requestAnimationFrame reference // References to lyric elements let lyricElements: HTMLDivElement[] = []; let lyricComponents: LyricLine[] = []; let lyricTopList: number[] = []; - let nextUpdate = 0; - const marginY = 48; + + let currentLyricIndex: number; $: getLyricIndex = createLyricsSearcher(originalLyrics); + $: { + currentLyricIndex = getLyricIndex(progress); + } + function initLyricComponents() { initLyricTopList(); for (let i = 0; i < lyricComponents.length; i++) { @@ -48,31 +68,24 @@ } } - function computeLayout(progress: number) { + function computeLayout() { if (!originalLyrics.scripts) return; - const currentLyricIndex = getLyricIndex(progress); const currentLyricDuration = originalLyrics.scripts[currentLyricIndex].end - originalLyrics.scripts[currentLyricIndex].start; - const relativeOrigin = lyricTopList[currentLyricIndex]; + const relativeOrigin = lyricTopList[currentLyricIndex] - currentLyrictTop; for (let i = 0; i < lyricElements.length; i++) { const currentLyricComponent = lyricComponents[i]; - lyricLayouts[i] = { - blur: 0, - scale: 1, - pos: { - y: lyricTopList[i] - relativeOrigin, - x: 0 - } - }; let delay = 0; if (i <= currentLyricIndex) { delay = 0; } else { - delay = 0.013 + Math.min(Math.min(currentLyricDuration, 0.3), 0.075 * (i - currentLyricIndex)); + delay = 0.013 + Math.min(Math.min(currentLyricDuration, 0.1), 0.075 * (i - currentLyricIndex)); } + const offset = Math.abs(i - currentLyricIndex); + let blurRadius = Math.min(offset * 1.7, 16); + currentLyricComponent.setBlur(blurRadius); currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay); } - nextUpdate = originalLyrics.scripts[currentLyricIndex + 1].start; } $: { @@ -88,9 +101,112 @@ } } + function handleScroll(deltaY: number) { + for (let i = 0; i < lyricElements.length; i++) { + const currentLyricComponent = lyricComponents[i]; + const currentY = currentLyricComponent.getInfo().y; + currentLyricComponent.setBlur(0); + currentLyricComponent.stop(); + currentLyricComponent.setY(currentY - deltaY); + } + scrolling = true; + //if (scrollingTimeout) clearTimeout(scrollingTimeout); + scrollingTimeout = setTimeout(() => { + scrolling = false; + }, 5000); + } + + // Handle the touch start event + function handleTouchStart(event: TouchEvent) { + lastY = event.touches[0].clientY; + } + + // Handle the touch move event + function handleTouchMove(event: TouchEvent) { + const currentY = event.touches[0].clientY; + const currentTime = Date.now(); + const deltaY = lastY - currentY; // Calculate vertical swipe distance + const deltaTime = currentTime - lastTime; + + // Calculate the scroll velocity (change in Y over time) + if (deltaTime > 0) { + velocityY = deltaY / deltaTime; + } + + handleScroll(deltaY); // Simulate the scroll event + lastY = currentY; // Update lastY for the next move event + lastTime = currentTime; // Update the lastTime for the next move event + } + + // Handle the touch end event + function handleTouchEnd() { + // Start inertia scrolling based on the velocity + function inertiaScroll() { + if (Math.abs(velocityY) < minVelocity) { + cancelAnimationFrame(inertiaFrame); + return; + } + handleScroll(velocityY * 16); // Multiply by frame time (16ms) to get smooth scroll + velocityY *= deceleration; // Apply deceleration to velocity + inertiaFrame = requestAnimationFrame(inertiaScroll); // Continue scrolling in next frame + } + inertiaScroll(); + } + + $: { + if (lyricsContainer && !scrollEventAdded) { + // Wheel event for desktop + lyricsContainer.addEventListener( + 'wheel', + (e) => { + e.preventDefault(); + const deltaY = e.deltaY; + handleScroll(deltaY); + }, + { passive: false } + ); + + // Touch events for mobile + lyricsContainer.addEventListener('touchstart', handleTouchStart, { passive: true }); + lyricsContainer.addEventListener('touchmove', handleTouchMove, { passive: false }); + lyricsContainer.addEventListener('touchend', handleTouchEnd, { passive: true }); + + scrollEventAdded = true; + } + } + $: { if (lyricsContainer && lyricComponents.length > 0) { - if (progress >= nextUpdate) computeLayout(progress); + if (progress >= nextUpdate - 0.5 && !scrolling) { + console.log("computeLayout") + computeLayout(); + } + if (Math.abs(lastProgress - progress) > 0) { + scrolling = false; + } + if (lastProgress - progress > 0) { + computeLayout(); + nextUpdate = progress; + } else { + const lyricLength = originalLyrics.scripts!.length; + const currentEnd = originalLyrics.scripts![currentLyricIndex].end; + const nextStart = originalLyrics.scripts![Math.min(currentLyricIndex + 1, lyricLength - 1)].start; + if (currentEnd !== nextStart) { + nextUpdate = currentEnd; + } + else { + nextUpdate = nextStart; + } + } + } + lastProgress = progress; + } + + $: { + for (let i = 0; i < lyricElements.length; i++) { + const isCurrent = i == currentLyricIndex; + const currentLyricComponent = lyricComponents[i]; + currentLyricComponent.setCurrent(isCurrent); } } @@ -102,17 +218,43 @@ debugMode = localStorage.getItem('debugMode')!.toLowerCase() === 'true'; } }); + + // handle KeyDown event + 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'); + } else if (e.key === 't') { + showTranslation = !showTranslation; + localStorage.setItem('showTranslation', showTranslation ? 'true' : 'false'); + computeLayout(); + } + } + + function lyricClick(lyricIndex: number) { + if (player === null || originalLyrics.scripts === undefined) return; + player.currentTime = originalLyrics.scripts[lyricIndex].start; + player.play(); + } + + +{#if debugMode} + + progress: {progress.toFixed(2)}, nextUpdate: {nextUpdate}, scrolling: {scrolling}, current: {currentLyricIndex} + +{/if} + {#if originalLyrics && originalLyrics.scripts}
{#each lyricLines as lyric, i} - + {/each} -
{/if} diff --git a/src/lib/components/lyrics/type.d.ts b/src/lib/components/lyrics/type.d.ts index 13bfc64..115703b 100644 --- a/src/lib/components/lyrics/type.d.ts +++ b/src/lib/components/lyrics/type.d.ts @@ -1,10 +1,4 @@ export interface LyricPos { x: number; y: number; -} - -export interface LyricLayout { - pos: LyricPos; - blur: number; - scale: number; } \ No newline at end of file