From 10c5ea39168e14b155ce96a7762dd9b035e7b6fe Mon Sep 17 00:00:00 2001 From: alikia2x Date: Thu, 24 Oct 2024 00:44:17 +0800 Subject: [PATCH] feature: lyric smooth scrolling using new arch todo: var `nextUpdate`'s assignment when backtracking --- src/lib/components/lyrics/lyricLine.svelte | 30 +-- src/lib/components/lyrics/newLyrics.svelte | 67 +++-- src/lib/graphics/spring/spring.ts | 279 ++++++++++----------- src/routes/play/[id]/+page.svelte | 2 +- 4 files changed, 197 insertions(+), 181 deletions(-) diff --git a/src/lib/components/lyrics/lyricLine.svelte b/src/lib/components/lyrics/lyricLine.svelte index 89da493..aa48317 100644 --- a/src/lib/components/lyrics/lyricLine.svelte +++ b/src/lib/components/lyrics/lyricLine.svelte @@ -7,7 +7,6 @@ export let line: ScriptItem; export let index: number; export let debugMode: Boolean; - export let initPos: LyricPos; let ref: HTMLDivElement; let time = 0; @@ -21,25 +20,18 @@ let springX: Spring | undefined = undefined; let isCurrentLyric = false; - $: { - if (initPos) { - positionX = initPos.x; - positionY = initPos.y; - } - } - function updateY(timestamp: number) { if (lastUpdateY === undefined) { - lastUpdateY = timestamp; + lastUpdateY = new Date().getTime(); } if (springY === undefined) return; - time = (timestamp - lastUpdateY) / 1000; + time = (new Date().getTime() - lastUpdateY) / 1000; springY.update(time); positionY = springY.getCurrentPosition(); if (!springY.arrived()) { requestAnimationFrame(updateY); } - lastUpdateY = timestamp; + lastUpdateY = new Date().getTime(); } function updateX(timestamp: number) { @@ -47,7 +39,7 @@ lastUpdateX = timestamp; } if (springX === undefined) return; - time = (timestamp - lastUpdateX) / 1000; + time = (new Date().getTime() - lastUpdateX) / 1000; springX.update(time); positionX = springX.getCurrentPosition(); if (!springX.arrived()) { @@ -77,16 +69,18 @@ }; export const update = (pos: LyricPos, delay: number = 0) => { - if (lastPosX === undefined) { + if (lastPosX === undefined || lastPosY === undefined) { lastPosX = pos.x; - } - if (lastPosY === undefined) { lastPosY = pos.y; } - springY = createSpring(lastPosY, pos.y, 0.12, 0.7, delay); - springX = createSpring(lastPosX, pos.x, 0.12, 0.7, delay); + springY = createSpring(lastPosY, pos.y, .126, .85, delay); + springX = createSpring(lastPosX, pos.x, .126, .85, delay); + lastUpdateY = new Date().getTime(); + lastUpdateX = new Date().getTime(); requestAnimationFrame(updateY); requestAnimationFrame(updateX); + lastPosX = pos.x; + lastPosY = pos.y; }; export const getInfo = () => { @@ -109,7 +103,7 @@
{#if debugMode} - Line idx: {index} + Line idx: {index}, duration: {(line.end - line.start).toFixed(3)} {/if} {line.text} diff --git a/src/lib/components/lyrics/newLyrics.svelte b/src/lib/components/lyrics/newLyrics.svelte index 97b6349..d6611e4 100644 --- a/src/lib/components/lyrics/newLyrics.svelte +++ b/src/lib/components/lyrics/newLyrics.svelte @@ -3,7 +3,8 @@ import { onMount } from 'svelte'; import type { ScriptItem } from '$lib/lyrics/type'; import LyricLine from './lyricLine.svelte'; - import type { LyricPos } from './type'; + import type { LyricLayout, LyricPos } from './type'; + import createLyricsSearcher from '$lib/lyrics/lyricSearcher'; // Props export let originalLyrics: LrcJsonData; @@ -14,23 +15,28 @@ let lyricLines: ScriptItem[] = []; let lyricExists = false; let lyricsContainer: HTMLDivElement | null; + let lyricLayouts: LyricLayout[] = []; let debugMode = false; let showTranslation = false; - // Exlpaination: - // The hot module reloading makes the each lyric component position to be re-initialized, - // which causes the lyrics are all at {x: 0, y: 0}, - // instead of the value calculated in the initLyricComponents. - // So, we need to store the initial position of each lyric component and restore it. - let lyricComponentInitPos = Array(lyricLines.length).fill({ x: 0, y: 0 }); - // References to lyric elements let lyricElements: HTMLDivElement[] = []; let lyricComponents: LyricLine[] = []; + let lyricTopList: number[] = []; + let nextUpdate = 0; + const marginY = 48; + + $: getLyricIndex = createLyricsSearcher(originalLyrics); function initLyricComponents() { + initLyricTopList(); + for (let i = 0; i < lyricComponents.length; i++) { + lyricComponents[i].init({ x: 0, y: lyricTopList[i] }); + } + } + + function initLyricTopList() { let cumulativeHeight = 0; - const marginY = 48; for (let i = 0; i < lyricLines.length; i++) { const c = lyricComponents[i]; lyricElements.push(c.getRef()); @@ -38,10 +44,37 @@ const elementHeight = e.getBoundingClientRect().height; const elementTargetTop = cumulativeHeight; cumulativeHeight += elementHeight + marginY; - lyricComponentInitPos[i] = { x: 0, y: elementTargetTop }; + lyricTopList.push(elementTargetTop); } } + function computeLayout(progress: number) { + if (!originalLyrics.scripts) return; + const currentLyricIndex = getLyricIndex(progress); + const currentLyricDuration = + originalLyrics.scripts[currentLyricIndex].end - originalLyrics.scripts[currentLyricIndex].start; + const relativeOrigin = lyricTopList[currentLyricIndex]; + 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)); + } + currentLyricComponent.update({ x: 0, y: lyricTopList[i] - relativeOrigin }, delay); + } + nextUpdate = originalLyrics.scripts[currentLyricIndex + 1].start; + } + $: { if (originalLyrics && originalLyrics.scripts) { lyricExists = true; @@ -55,6 +88,12 @@ } } + $: { + if (lyricsContainer && lyricComponents.length > 0) { + if (progress >= nextUpdate) computeLayout(progress); + } + } + onMount(() => { // Initialize if (localStorage.getItem('debugMode') == null) { @@ -72,13 +111,7 @@ bind:this={lyricsContainer} > {#each lyricLines as lyric, i} - + {/each}
diff --git a/src/lib/graphics/spring/spring.ts b/src/lib/graphics/spring/spring.ts index 511bce5..bbbbf5f 100644 --- a/src/lib/graphics/spring/spring.ts +++ b/src/lib/graphics/spring/spring.ts @@ -1,158 +1,147 @@ -import { getVelocity } from "./derivative"; +import { getVelocity } from './derivative'; /** MIT License github.com/pushkine/ */ export interface SpringParams { - mass: number; // = 1.0 - damping: number; // = 10.0 - stiffness: number; // = 100.0 - soft: boolean; // = false + mass: number; // = 1.0 + damping: number; // = 10.0 + stiffness: number; // = 100.0 + soft: boolean; // = false } type seconds = number; export class Spring { - private currentPosition = 0; - private targetPosition = 0; - private currentTime = 0; - private params: Partial = {}; - private currentSolver: (t: seconds) => number; - private getV: (t: seconds) => number; - private getV2: (t: seconds) => number; - private queueParams: - | (Partial & { - time: number; - }) - | undefined; - private queuePosition: - | { - time: number; - position: number; - } - | undefined; - constructor(currentPosition = 0) { - this.targetPosition = currentPosition; - this.currentPosition = this.targetPosition; - this.currentSolver = () => this.targetPosition; - this.getV = () => 0; - this.getV2 = () => 0; - } - private resetSolver() { - const curV = this.getV(this.currentTime); - this.currentTime = 0; - this.currentSolver = solveSpring( - this.currentPosition, - curV, - this.targetPosition, - 0, - this.params, - ); - this.getV = getVelocity(this.currentSolver); - this.getV2 = getVelocity(this.getV); - } - arrived() { - return ( - Math.abs(this.targetPosition - this.currentPosition) < 0.01 && - this.getV(this.currentTime) < 0.01 && - this.getV2(this.currentTime) < 0.01 && - this.queueParams === undefined && - this.queuePosition === undefined - ); - } - setPosition(targetPosition: number) { - this.targetPosition = targetPosition; - this.currentPosition = targetPosition; - this.currentSolver = () => this.targetPosition; - this.getV = () => 0; - this.getV2 = () => 0; - } - update(delta = 0) { - this.currentTime += delta; - this.currentPosition = this.currentSolver(this.currentTime); - if (this.queueParams) { - this.queueParams.time -= delta; - if (this.queueParams.time <= 0) { - this.updateParams({ - ...this.queueParams, - }); - } - } - if (this.queuePosition) { - this.queuePosition.time -= delta; - if (this.queuePosition.time <= 0) { - this.setTargetPosition(this.queuePosition.position); - } - } - if (this.arrived()) { - this.setPosition(this.targetPosition); - } - } - updateParams(params: Partial, delay = 0) { - if (delay > 0) { - this.queueParams = { - ...(this.queuePosition ?? {}), - ...params, - time: delay, - }; - } else { - this.queuePosition = undefined; - this.params = { - ...this.params, - ...params, - }; - this.resetSolver(); - } - } - setTargetPosition(targetPosition: number, delay = 0) { - if (delay > 0) { - this.queuePosition = { - ...(this.queuePosition ?? {}), - position: targetPosition, - time: delay, - }; - } else { - this.queuePosition = undefined; - this.targetPosition = targetPosition; - this.resetSolver(); - } - } - getCurrentPosition() { - return this.currentPosition; - } + private currentPosition = 0; + private targetPosition = 0; + private currentTime = 0; + private params: Partial = {}; + private currentSolver: (t: seconds) => number; + private getV: (t: seconds) => number; + private getV2: (t: seconds) => number; + private queueParams: + | (Partial & { + time: number; + }) + | undefined; + private queuePosition: + | { + time: number; + position: number; + } + | undefined; + constructor(currentPosition = 0) { + this.targetPosition = currentPosition; + this.currentPosition = this.targetPosition; + this.currentSolver = () => this.targetPosition; + this.getV = () => 0; + this.getV2 = () => 0; + } + private resetSolver() { + const curV = this.getV(this.currentTime); + this.currentTime = 0; + this.currentSolver = solveSpring(this.currentPosition, curV, this.targetPosition, 0, this.params); + this.getV = getVelocity(this.currentSolver); + this.getV2 = getVelocity(this.getV); + } + arrived() { + return ( + Math.abs(this.targetPosition - this.currentPosition) < 0.01 && + this.getV(this.currentTime) < 0.01 && + this.getV2(this.currentTime) < 0.01 && + this.queueParams === undefined && + this.queuePosition === undefined + ); + } + setPosition(targetPosition: number) { + this.targetPosition = targetPosition; + this.currentPosition = targetPosition; + this.currentSolver = () => this.targetPosition; + this.getV = () => 0; + this.getV2 = () => 0; + } + update(delta = 0) { + this.currentTime += delta; + this.currentPosition = this.currentSolver(this.currentTime); + if (this.queueParams) { + this.queueParams.time -= delta; + if (this.queueParams.time <= 0) { + this.updateParams({ + ...this.queueParams + }); + } + } + if (this.queuePosition) { + this.queuePosition.time -= delta; + if (this.queuePosition.time <= 0) { + this.setTargetPosition(this.queuePosition.position); + } + } + if (this.arrived()) { + this.setPosition(this.targetPosition); + } + } + updateParams(params: Partial, delay = 0) { + if (delay > 0) { + this.queueParams = { + ...(this.queuePosition ?? {}), + ...params, + time: delay + }; + } else { + this.queuePosition = undefined; + this.params = { + ...this.params, + ...params + }; + this.resetSolver(); + } + } + setTargetPosition(targetPosition: number, delay = 0) { + if (delay > 0) { + this.queuePosition = { + ...(this.queuePosition ?? {}), + position: targetPosition, + time: delay + }; + } else { + this.queuePosition = undefined; + this.targetPosition = targetPosition; + this.resetSolver(); + } + } + getCurrentPosition() { + return this.currentPosition; + } } function solveSpring( - from: number, - velocity: number, - to: number, - delay: seconds = 0, - params?: Partial, + from: number, + velocity: number, + to: number, + delay: seconds = 0, + params?: Partial ): (t: seconds) => number { - const soft = params?.soft ?? false; - const stiffness = params?.stiffness ?? 100; - const damping = params?.damping ?? 10; - const mass = params?.mass ?? 1; - const delta = to - from; - if (soft || 1.0 <= damping / (2.0 * Math.sqrt(stiffness * mass))) { - const angular_frequency = -Math.sqrt(stiffness / mass); - const leftover = -angular_frequency * delta - velocity; - return (t: seconds) => { - t -= delay; - if (t < 0) return from; - return to - (delta + t * leftover) * Math.E ** (t * angular_frequency); - }; - } - const damping_frequency = Math.sqrt(4.0 * mass * stiffness - damping ** 2.0); - const leftover = - (damping * delta - 2.0 * mass * velocity) / damping_frequency; - const dfm = (0.5 * damping_frequency) / mass; - const dm = -(0.5 * damping) / mass; - return (t: seconds) => { - t -= delay; - if (t < 0) return from; - return ( - to - - (Math.cos(t * dfm) * delta + Math.sin(t * dfm) * leftover) * - Math.E ** (t * dm) - ); - }; + const soft = params?.soft ?? false; + const stiffness = params?.stiffness ?? 100; + const damping = params?.damping ?? 10; + const mass = params?.mass ?? 1; + const delta = to - from; + if (soft || 1.0 <= damping / (2.0 * Math.sqrt(stiffness * mass))) { + const angular_frequency = -Math.sqrt(stiffness / mass); + const leftover = -angular_frequency * delta - velocity; + return (t: seconds) => { + t -= delay; + if (t < 0) return from; + return to - (delta + t * leftover) * Math.E ** (t * angular_frequency); + }; + } + const damping_frequency = Math.sqrt(4.0 * mass * stiffness - damping ** 2.0); + const leftover = (damping * delta - 2.0 * mass * velocity) / damping_frequency; + const dfm = (0.5 * damping_frequency) / mass; + const dm = -(0.5 * damping) / mass; + return (t: seconds) => { + t -= delay; + if (t < 0) return from; + return to - (Math.cos(t * dfm) * delta + Math.sin(t * dfm) * leftover) * Math.E ** (t * dm); + }; } diff --git a/src/routes/play/[id]/+page.svelte b/src/routes/play/[id]/+page.svelte index 96be32f..fae5122 100644 --- a/src/routes/play/[id]/+page.svelte +++ b/src/routes/play/[id]/+page.svelte @@ -214,7 +214,7 @@ -