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