feature: lyric smooth scrolling using new arch
todo: var `nextUpdate`'s assignment when backtracking
This commit is contained in:
parent
0c315972c1
commit
c7da9fd5ea
@ -7,7 +7,6 @@
|
|||||||
export let line: ScriptItem;
|
export let line: ScriptItem;
|
||||||
export let index: number;
|
export let index: number;
|
||||||
export let debugMode: Boolean;
|
export let debugMode: Boolean;
|
||||||
export let initPos: LyricPos;
|
|
||||||
let ref: HTMLDivElement;
|
let ref: HTMLDivElement;
|
||||||
|
|
||||||
let time = 0;
|
let time = 0;
|
||||||
@ -21,25 +20,18 @@
|
|||||||
let springX: Spring | undefined = undefined;
|
let springX: Spring | undefined = undefined;
|
||||||
let isCurrentLyric = false;
|
let isCurrentLyric = false;
|
||||||
|
|
||||||
$: {
|
|
||||||
if (initPos) {
|
|
||||||
positionX = initPos.x;
|
|
||||||
positionY = initPos.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateY(timestamp: number) {
|
function updateY(timestamp: number) {
|
||||||
if (lastUpdateY === undefined) {
|
if (lastUpdateY === undefined) {
|
||||||
lastUpdateY = timestamp;
|
lastUpdateY = new Date().getTime();
|
||||||
}
|
}
|
||||||
if (springY === undefined) return;
|
if (springY === undefined) return;
|
||||||
time = (timestamp - lastUpdateY) / 1000;
|
time = (new Date().getTime() - lastUpdateY) / 1000;
|
||||||
springY.update(time);
|
springY.update(time);
|
||||||
positionY = springY.getCurrentPosition();
|
positionY = springY.getCurrentPosition();
|
||||||
if (!springY.arrived()) {
|
if (!springY.arrived()) {
|
||||||
requestAnimationFrame(updateY);
|
requestAnimationFrame(updateY);
|
||||||
}
|
}
|
||||||
lastUpdateY = timestamp;
|
lastUpdateY = new Date().getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateX(timestamp: number) {
|
function updateX(timestamp: number) {
|
||||||
@ -47,7 +39,7 @@
|
|||||||
lastUpdateX = timestamp;
|
lastUpdateX = timestamp;
|
||||||
}
|
}
|
||||||
if (springX === undefined) return;
|
if (springX === undefined) return;
|
||||||
time = (timestamp - lastUpdateX) / 1000;
|
time = (new Date().getTime() - lastUpdateX) / 1000;
|
||||||
springX.update(time);
|
springX.update(time);
|
||||||
positionX = springX.getCurrentPosition();
|
positionX = springX.getCurrentPosition();
|
||||||
if (!springX.arrived()) {
|
if (!springX.arrived()) {
|
||||||
@ -77,16 +69,18 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const update = (pos: LyricPos, delay: number = 0) => {
|
export const update = (pos: LyricPos, delay: number = 0) => {
|
||||||
if (lastPosX === undefined) {
|
if (lastPosX === undefined || lastPosY === undefined) {
|
||||||
lastPosX = pos.x;
|
lastPosX = pos.x;
|
||||||
}
|
|
||||||
if (lastPosY === undefined) {
|
|
||||||
lastPosY = pos.y;
|
lastPosY = pos.y;
|
||||||
}
|
}
|
||||||
springY = createSpring(lastPosY, pos.y, 0.12, 0.7, delay);
|
springY = createSpring(lastPosY, pos.y, .126, .85, delay);
|
||||||
springX = createSpring(lastPosX, pos.x, 0.12, 0.7, delay);
|
springX = createSpring(lastPosX, pos.x, .126, .85, delay);
|
||||||
|
lastUpdateY = new Date().getTime();
|
||||||
|
lastUpdateX = new Date().getTime();
|
||||||
requestAnimationFrame(updateY);
|
requestAnimationFrame(updateY);
|
||||||
requestAnimationFrame(updateX);
|
requestAnimationFrame(updateX);
|
||||||
|
lastPosX = pos.x;
|
||||||
|
lastPosY = pos.y;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getInfo = () => {
|
export const getInfo = () => {
|
||||||
@ -109,7 +103,7 @@
|
|||||||
|
|
||||||
<div style="transform: translateY({positionY}px) translateX({positionX}px);" class="absolute z-50" bind:this={ref}>
|
<div style="transform: translateY({positionY}px) translateX({positionX}px);" class="absolute z-50" bind:this={ref}>
|
||||||
{#if debugMode}
|
{#if debugMode}
|
||||||
<span class="text-lg absolute -translate-y-7">Line idx: {index}</span>
|
<span class="text-lg absolute -translate-y-7">Line idx: {index}, duration: {(line.end - line.start).toFixed(3)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class={`text-white text-5xl font-semibold text-shadow-lg ${!isCurrentLyric && 'opacity-50'} `}>
|
<span class={`text-white text-5xl font-semibold text-shadow-lg ${!isCurrentLyric && 'opacity-50'} `}>
|
||||||
{line.text}
|
{line.text}
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { ScriptItem } from '$lib/lyrics/type';
|
import type { ScriptItem } from '$lib/lyrics/type';
|
||||||
import LyricLine from './lyricLine.svelte';
|
import LyricLine from './lyricLine.svelte';
|
||||||
import type { LyricPos } from './type';
|
import type { LyricLayout, LyricPos } from './type';
|
||||||
|
import createLyricsSearcher from '$lib/lyrics/lyricSearcher';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
export let originalLyrics: LrcJsonData;
|
export let originalLyrics: LrcJsonData;
|
||||||
@ -14,23 +15,28 @@
|
|||||||
let lyricLines: ScriptItem[] = [];
|
let lyricLines: ScriptItem[] = [];
|
||||||
let lyricExists = false;
|
let lyricExists = false;
|
||||||
let lyricsContainer: HTMLDivElement | null;
|
let lyricsContainer: HTMLDivElement | null;
|
||||||
|
let lyricLayouts: LyricLayout[] = [];
|
||||||
let debugMode = false;
|
let debugMode = false;
|
||||||
let showTranslation = 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<LyricPos>(lyricLines.length).fill({ x: 0, y: 0 });
|
|
||||||
|
|
||||||
// References to lyric elements
|
// References to lyric elements
|
||||||
let lyricElements: HTMLDivElement[] = [];
|
let lyricElements: HTMLDivElement[] = [];
|
||||||
let lyricComponents: LyricLine[] = [];
|
let lyricComponents: LyricLine[] = [];
|
||||||
|
let lyricTopList: number[] = [];
|
||||||
|
let nextUpdate = 0;
|
||||||
|
const marginY = 48;
|
||||||
|
|
||||||
|
$: getLyricIndex = createLyricsSearcher(originalLyrics);
|
||||||
|
|
||||||
function initLyricComponents() {
|
function initLyricComponents() {
|
||||||
|
initLyricTopList();
|
||||||
|
for (let i = 0; i < lyricComponents.length; i++) {
|
||||||
|
lyricComponents[i].init({ x: 0, y: lyricTopList[i] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLyricTopList() {
|
||||||
let cumulativeHeight = 0;
|
let cumulativeHeight = 0;
|
||||||
const marginY = 48;
|
|
||||||
for (let i = 0; i < lyricLines.length; i++) {
|
for (let i = 0; i < lyricLines.length; i++) {
|
||||||
const c = lyricComponents[i];
|
const c = lyricComponents[i];
|
||||||
lyricElements.push(c.getRef());
|
lyricElements.push(c.getRef());
|
||||||
@ -38,10 +44,37 @@
|
|||||||
const elementHeight = e.getBoundingClientRect().height;
|
const elementHeight = e.getBoundingClientRect().height;
|
||||||
const elementTargetTop = cumulativeHeight;
|
const elementTargetTop = cumulativeHeight;
|
||||||
cumulativeHeight += elementHeight + marginY;
|
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) {
|
if (originalLyrics && originalLyrics.scripts) {
|
||||||
lyricExists = true;
|
lyricExists = true;
|
||||||
@ -55,6 +88,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (lyricsContainer && lyricComponents.length > 0) {
|
||||||
|
if (progress >= nextUpdate) computeLayout(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Initialize
|
// Initialize
|
||||||
if (localStorage.getItem('debugMode') == null) {
|
if (localStorage.getItem('debugMode') == null) {
|
||||||
@ -72,13 +111,7 @@
|
|||||||
bind:this={lyricsContainer}
|
bind:this={lyricsContainer}
|
||||||
>
|
>
|
||||||
{#each lyricLines as lyric, i}
|
{#each lyricLines as lyric, i}
|
||||||
<LyricLine
|
<LyricLine line={lyric} index={i} bind:this={lyricComponents[i]} {debugMode} />
|
||||||
line={lyric}
|
|
||||||
index={i}
|
|
||||||
bind:this={lyricComponents[i]}
|
|
||||||
{debugMode}
|
|
||||||
initPos={lyricComponentInitPos[i]}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
<div class="relative w-full h-[50rem]"></div>
|
<div class="relative w-full h-[50rem]"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,158 +1,147 @@
|
|||||||
import { getVelocity } from "./derivative";
|
import { getVelocity } from './derivative';
|
||||||
|
|
||||||
/** MIT License github.com/pushkine/ */
|
/** MIT License github.com/pushkine/ */
|
||||||
export interface SpringParams {
|
export interface SpringParams {
|
||||||
mass: number; // = 1.0
|
mass: number; // = 1.0
|
||||||
damping: number; // = 10.0
|
damping: number; // = 10.0
|
||||||
stiffness: number; // = 100.0
|
stiffness: number; // = 100.0
|
||||||
soft: boolean; // = false
|
soft: boolean; // = false
|
||||||
}
|
}
|
||||||
|
|
||||||
type seconds = number;
|
type seconds = number;
|
||||||
|
|
||||||
export class Spring {
|
export class Spring {
|
||||||
private currentPosition = 0;
|
private currentPosition = 0;
|
||||||
private targetPosition = 0;
|
private targetPosition = 0;
|
||||||
private currentTime = 0;
|
private currentTime = 0;
|
||||||
private params: Partial<SpringParams> = {};
|
private params: Partial<SpringParams> = {};
|
||||||
private currentSolver: (t: seconds) => number;
|
private currentSolver: (t: seconds) => number;
|
||||||
private getV: (t: seconds) => number;
|
private getV: (t: seconds) => number;
|
||||||
private getV2: (t: seconds) => number;
|
private getV2: (t: seconds) => number;
|
||||||
private queueParams:
|
private queueParams:
|
||||||
| (Partial<SpringParams> & {
|
| (Partial<SpringParams> & {
|
||||||
time: number;
|
time: number;
|
||||||
})
|
})
|
||||||
| undefined;
|
| undefined;
|
||||||
private queuePosition:
|
private queuePosition:
|
||||||
| {
|
| {
|
||||||
time: number;
|
time: number;
|
||||||
position: number;
|
position: number;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
constructor(currentPosition = 0) {
|
constructor(currentPosition = 0) {
|
||||||
this.targetPosition = currentPosition;
|
this.targetPosition = currentPosition;
|
||||||
this.currentPosition = this.targetPosition;
|
this.currentPosition = this.targetPosition;
|
||||||
this.currentSolver = () => this.targetPosition;
|
this.currentSolver = () => this.targetPosition;
|
||||||
this.getV = () => 0;
|
this.getV = () => 0;
|
||||||
this.getV2 = () => 0;
|
this.getV2 = () => 0;
|
||||||
}
|
}
|
||||||
private resetSolver() {
|
private resetSolver() {
|
||||||
const curV = this.getV(this.currentTime);
|
const curV = this.getV(this.currentTime);
|
||||||
this.currentTime = 0;
|
this.currentTime = 0;
|
||||||
this.currentSolver = solveSpring(
|
this.currentSolver = solveSpring(this.currentPosition, curV, this.targetPosition, 0, this.params);
|
||||||
this.currentPosition,
|
this.getV = getVelocity(this.currentSolver);
|
||||||
curV,
|
this.getV2 = getVelocity(this.getV);
|
||||||
this.targetPosition,
|
}
|
||||||
0,
|
arrived() {
|
||||||
this.params,
|
return (
|
||||||
);
|
Math.abs(this.targetPosition - this.currentPosition) < 0.01 &&
|
||||||
this.getV = getVelocity(this.currentSolver);
|
this.getV(this.currentTime) < 0.01 &&
|
||||||
this.getV2 = getVelocity(this.getV);
|
this.getV2(this.currentTime) < 0.01 &&
|
||||||
}
|
this.queueParams === undefined &&
|
||||||
arrived() {
|
this.queuePosition === undefined
|
||||||
return (
|
);
|
||||||
Math.abs(this.targetPosition - this.currentPosition) < 0.01 &&
|
}
|
||||||
this.getV(this.currentTime) < 0.01 &&
|
setPosition(targetPosition: number) {
|
||||||
this.getV2(this.currentTime) < 0.01 &&
|
this.targetPosition = targetPosition;
|
||||||
this.queueParams === undefined &&
|
this.currentPosition = targetPosition;
|
||||||
this.queuePosition === undefined
|
this.currentSolver = () => this.targetPosition;
|
||||||
);
|
this.getV = () => 0;
|
||||||
}
|
this.getV2 = () => 0;
|
||||||
setPosition(targetPosition: number) {
|
}
|
||||||
this.targetPosition = targetPosition;
|
update(delta = 0) {
|
||||||
this.currentPosition = targetPosition;
|
this.currentTime += delta;
|
||||||
this.currentSolver = () => this.targetPosition;
|
this.currentPosition = this.currentSolver(this.currentTime);
|
||||||
this.getV = () => 0;
|
if (this.queueParams) {
|
||||||
this.getV2 = () => 0;
|
this.queueParams.time -= delta;
|
||||||
}
|
if (this.queueParams.time <= 0) {
|
||||||
update(delta = 0) {
|
this.updateParams({
|
||||||
this.currentTime += delta;
|
...this.queueParams
|
||||||
this.currentPosition = this.currentSolver(this.currentTime);
|
});
|
||||||
if (this.queueParams) {
|
}
|
||||||
this.queueParams.time -= delta;
|
}
|
||||||
if (this.queueParams.time <= 0) {
|
if (this.queuePosition) {
|
||||||
this.updateParams({
|
this.queuePosition.time -= delta;
|
||||||
...this.queueParams,
|
if (this.queuePosition.time <= 0) {
|
||||||
});
|
this.setTargetPosition(this.queuePosition.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.queuePosition) {
|
if (this.arrived()) {
|
||||||
this.queuePosition.time -= delta;
|
this.setPosition(this.targetPosition);
|
||||||
if (this.queuePosition.time <= 0) {
|
}
|
||||||
this.setTargetPosition(this.queuePosition.position);
|
}
|
||||||
}
|
updateParams(params: Partial<SpringParams>, delay = 0) {
|
||||||
}
|
if (delay > 0) {
|
||||||
if (this.arrived()) {
|
this.queueParams = {
|
||||||
this.setPosition(this.targetPosition);
|
...(this.queuePosition ?? {}),
|
||||||
}
|
...params,
|
||||||
}
|
time: delay
|
||||||
updateParams(params: Partial<SpringParams>, delay = 0) {
|
};
|
||||||
if (delay > 0) {
|
} else {
|
||||||
this.queueParams = {
|
this.queuePosition = undefined;
|
||||||
...(this.queuePosition ?? {}),
|
this.params = {
|
||||||
...params,
|
...this.params,
|
||||||
time: delay,
|
...params
|
||||||
};
|
};
|
||||||
} else {
|
this.resetSolver();
|
||||||
this.queuePosition = undefined;
|
}
|
||||||
this.params = {
|
}
|
||||||
...this.params,
|
setTargetPosition(targetPosition: number, delay = 0) {
|
||||||
...params,
|
if (delay > 0) {
|
||||||
};
|
this.queuePosition = {
|
||||||
this.resetSolver();
|
...(this.queuePosition ?? {}),
|
||||||
}
|
position: targetPosition,
|
||||||
}
|
time: delay
|
||||||
setTargetPosition(targetPosition: number, delay = 0) {
|
};
|
||||||
if (delay > 0) {
|
} else {
|
||||||
this.queuePosition = {
|
this.queuePosition = undefined;
|
||||||
...(this.queuePosition ?? {}),
|
this.targetPosition = targetPosition;
|
||||||
position: targetPosition,
|
this.resetSolver();
|
||||||
time: delay,
|
}
|
||||||
};
|
}
|
||||||
} else {
|
getCurrentPosition() {
|
||||||
this.queuePosition = undefined;
|
return this.currentPosition;
|
||||||
this.targetPosition = targetPosition;
|
}
|
||||||
this.resetSolver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getCurrentPosition() {
|
|
||||||
return this.currentPosition;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function solveSpring(
|
function solveSpring(
|
||||||
from: number,
|
from: number,
|
||||||
velocity: number,
|
velocity: number,
|
||||||
to: number,
|
to: number,
|
||||||
delay: seconds = 0,
|
delay: seconds = 0,
|
||||||
params?: Partial<SpringParams>,
|
params?: Partial<SpringParams>
|
||||||
): (t: seconds) => number {
|
): (t: seconds) => number {
|
||||||
const soft = params?.soft ?? false;
|
const soft = params?.soft ?? false;
|
||||||
const stiffness = params?.stiffness ?? 100;
|
const stiffness = params?.stiffness ?? 100;
|
||||||
const damping = params?.damping ?? 10;
|
const damping = params?.damping ?? 10;
|
||||||
const mass = params?.mass ?? 1;
|
const mass = params?.mass ?? 1;
|
||||||
const delta = to - from;
|
const delta = to - from;
|
||||||
if (soft || 1.0 <= damping / (2.0 * Math.sqrt(stiffness * mass))) {
|
if (soft || 1.0 <= damping / (2.0 * Math.sqrt(stiffness * mass))) {
|
||||||
const angular_frequency = -Math.sqrt(stiffness / mass);
|
const angular_frequency = -Math.sqrt(stiffness / mass);
|
||||||
const leftover = -angular_frequency * delta - velocity;
|
const leftover = -angular_frequency * delta - velocity;
|
||||||
return (t: seconds) => {
|
return (t: seconds) => {
|
||||||
t -= delay;
|
t -= delay;
|
||||||
if (t < 0) return from;
|
if (t < 0) return from;
|
||||||
return to - (delta + t * leftover) * Math.E ** (t * angular_frequency);
|
return to - (delta + t * leftover) * Math.E ** (t * angular_frequency);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const damping_frequency = Math.sqrt(4.0 * mass * stiffness - damping ** 2.0);
|
const damping_frequency = Math.sqrt(4.0 * mass * stiffness - damping ** 2.0);
|
||||||
const leftover =
|
const leftover = (damping * delta - 2.0 * mass * velocity) / damping_frequency;
|
||||||
(damping * delta - 2.0 * mass * velocity) / damping_frequency;
|
const dfm = (0.5 * damping_frequency) / mass;
|
||||||
const dfm = (0.5 * damping_frequency) / mass;
|
const dm = -(0.5 * damping) / mass;
|
||||||
const dm = -(0.5 * damping) / mass;
|
return (t: seconds) => {
|
||||||
return (t: seconds) => {
|
t -= delay;
|
||||||
t -= delay;
|
if (t < 0) return from;
|
||||||
if (t < 0) return from;
|
return to - (Math.cos(t * dfm) * delta + Math.sin(t * dfm) * leftover) * Math.E ** (t * dm);
|
||||||
return (
|
};
|
||||||
to -
|
|
||||||
(Math.cos(t * dfm) * delta + Math.sin(t * dfm) * leftover) *
|
|
||||||
Math.E ** (t * dm)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,7 @@
|
|||||||
|
|
||||||
<NewLyrics {originalLyrics} progress={currentProgress} player={audioPlayer}/>
|
<NewLyrics {originalLyrics} progress={currentProgress} player={audioPlayer}/>
|
||||||
|
|
||||||
<Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} player={audioPlayer} class="hidden" />
|
<!-- <Lyrics lyrics={lyricsText} {originalLyrics} progress={currentProgress} player={audioPlayer} class="hidden" /> -->
|
||||||
|
|
||||||
<audio
|
<audio
|
||||||
bind:this={audioPlayer}
|
bind:this={audioPlayer}
|
||||||
|
Loading…
Reference in New Issue
Block a user