reduce ticker animation lag

This commit is contained in:
sk1982 2024-03-18 03:15:43 -04:00
parent 58290aeeed
commit 657bdb3d2b
3 changed files with 51 additions and 18 deletions

View File

@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import React, { ReactNode } from 'react';
const BACKGROUNDS = [
['bg-[#02a076]'],
@ -17,10 +17,10 @@ export type ChuniDifficultyContainerProps = {
className?: string,
difficulty: number,
containerClassName?: string
};
} & React.HTMLAttributes<HTMLDivElement>;
export const ChuniDifficultyContainer = ({ children, className, difficulty, containerClassName }: ChuniDifficultyContainerProps) => {
return (<div className={`relative ${className ?? ''}`}>
export const ChuniDifficultyContainer = ({ children, className, difficulty, containerClassName, ...props }: ChuniDifficultyContainerProps) => {
return (<div className={`relative ${className ?? ''}`} {...props}>
{BACKGROUNDS[difficulty].map((className, i) => <div className={`${className} w-full h-full absolute inset-0 z-0 rounded`} key={i} />)}
<div className={`z-0 relative w-full h-full ${containerClassName ?? ''}`}>{children}</div>
</div>)

View File

@ -4,7 +4,7 @@ import { Filterers, FilterSorter, Sorter } from '@/components/filter-sorter';
import { WindowScroller, Grid, AutoSizer, List } from 'react-virtualized';
import { Button, SelectItem } from '@nextui-org/react';
import { addFavoriteMusic, getMusic, removeFavoriteMusic } from '@/actions/chuni/music';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { worldsEndStars } from '@/helpers/chuni/worlds-end-stars';
import { ChuniDifficultyContainer } from '@/components/chuni/difficulty-container';
import { getJacketUrl } from '@/helpers/assets';
@ -14,7 +14,7 @@ import { ChuniRating } from '@/components/chuni/rating';
import Link from 'next/link';
import { HeartIcon as OutlineHeartIcon, Squares2X2Icon } from '@heroicons/react/24/outline';
import { HeartIcon as SolidHeartIcon } from '@heroicons/react/24/solid';
import { Ticker } from '@/components/ticker';
import { Ticker, TickerHoverProvider } from '@/components/ticker';
import { CHUNI_DIFFICULTIES } from '@/helpers/chuni/difficulties';
import { CHUNI_SCORE_RANKS } from '@/helpers/chuni/score-ranks';
import { CHUNI_LAMPS } from '@/helpers/chuni/lamps';
@ -93,7 +93,7 @@ const MusicGrid = ({ music, size, setMusicList, fullMusicList }: ChuniMusicListP
}, [size])
return (<WindowScroller>
{({ height, isScrolling, onChildScroll, scrollTop }) =>
{useCallback(({ height, isScrolling, onChildScroll, scrollTop }) =>
(<AutoSizer disableHeight>
{({ width }) => {
const itemsPerRow = Math.max(1, Math.floor(width / itemWidth));
@ -103,14 +103,19 @@ const MusicGrid = ({ music, size, setMusicList, fullMusicList }: ChuniMusicListP
onScroll={onChildScroll} scrollTop={scrollTop} ref={listRef}
rowRenderer={({ index, key, style }) => <div key={key} style={style} className="w-full h-full flex justify-center">
{music.slice(index * itemsPerRow, (index + 1) * itemsPerRow).map(item => <div key={`${item.songId}-${item.chartId}`} className={itemClass}>
<ChuniDifficultyContainer difficulty={item.chartId!} containerClassName="flex flex-col" className="w-full h-full border border-gray-500/75 rounded-md [&:hover_.ticker]:[animation-play-state:running]">
<TickerHoverProvider>
{setHover => <ChuniDifficultyContainer difficulty={item.chartId!}
containerClassName="flex flex-col"
className="w-full h-full border border-gray-500/75 rounded-md"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}>
<div className="aspect-square w-full p-[0.2rem] relative">
<img src={getJacketUrl(`chuni/jacket/${item.jacketPath}`)} alt={item.title ?? 'Music'} className="rounded" />
{item.rating && !item.worldsEndTag && <div className={`${size === 'lg' ? 'text-2xl' : ''} absolute bottom-0.5 left-0.5 bg-gray-200/60 backdrop-blur-sm px-0.5 rounded`}>
<ChuniRating rating={+item.rating * 100} className="-my-0.5">
{item.rating.slice(0, item.rating.indexOf('.') + 3)}
</ChuniRating>
</div>}
<ChuniRating rating={+item.rating * 100} className="-my-0.5">
{item.rating.slice(0, item.rating.indexOf('.') + 3)}
</ChuniRating>
</div>}
<ChuniLevelBadge className={`${size === 'lg' ? 'h-14' : 'w-14'} absolute bottom-px right-px`} music={item} />
<Button isIconOnly className={`absolute top-0 left-0 pt-1 bg-gray-600/25 ${item.favorite ? 'text-red-500': ''}`}
@ -143,7 +148,7 @@ const MusicGrid = ({ music, size, setMusicList, fullMusicList }: ChuniMusicListP
<div className="px-0.5 mb-1 flex">
{size === 'lg' && <div className="h-full w-1/3 mr-0.5">
{item.isSuccess ? <ChuniLampSuccessBadge success={item.isSuccess} /> : null}
</div>}
</div>}
<div className={`h-full ${size === 'lg' ? 'w-1/3' : 'w-1/2'}`}>
{item.scoreRank !== null && <ChuniScoreBadge variant={getVariantFromRank(item.scoreRank)} className="h-full">
@ -160,11 +165,12 @@ const MusicGrid = ({ music, size, setMusicList, fullMusicList }: ChuniMusicListP
<Ticker hoverOnly noDelay>{item.title}</Ticker>
</Link>
<Ticker className={`${size === 'lg' ? 'text-medium mb-1.5' : 'text-xs mb-0.5' } text-center px-1 drop-shadow-2xl text-white`} hoverOnly noDelay>{item.artist}</Ticker>
</ChuniDifficultyContainer>
</ChuniDifficultyContainer>}
</TickerHoverProvider>
</div>)}
</div>} />)
}}
</AutoSizer>)}
</AutoSizer>), [music, fullMusicList, pendingFavorite, itemWidth])}
</WindowScroller>);
};

View File

@ -1,4 +1,6 @@
import { ReactNode } from 'react';
'use client';
import React, { createContext, ReactNode, useContext, useState } from 'react';
import './ticker.scss';
export type TickerProps = {
@ -8,12 +10,37 @@ export type TickerProps = {
noDelay?: boolean
}
const TickerHoverContext = createContext<boolean | null>(null);
type TickerHoverProviderProps = {
children: (setHover: (hovering: boolean) => void) => ReactNode
};
export const TickerHoverProvider = ({ children }: TickerHoverProviderProps) => {
const [hovering, setHovering] = useState(false);
return <TickerHoverContext.Provider value={hovering}>
{ children(setHovering) }
</TickerHoverContext.Provider>;
};
export const Ticker = ({ children, hoverOnly, className, noDelay }: TickerProps) => {
const hoverClass = hoverOnly ? '[&:hover_*]:[animation-play-state:running] [&_*]:[animation-play-state:paused]' : '[&:hover_*]:[animation-play-state:paused]';
const outerAnimation = noDelay ? 'animate-[outer-overflow-nodelay_15s_linear_infinite_alternate]' : 'animate-[outer-overflow_15s_linear_infinite_alternate]';
const innerAnimation = noDelay ? 'animate-[inner-overflow-nodelay_15s_linear_infinite_alternate]' : 'animate-[inner-overflow_15s_linear_infinite_alternate]';
const hoverContext = useContext(TickerHoverContext);
const [textHovering, setTextHovering] = useState(false);
const hovering = (hoverContext !== null && hoverContext) || textHovering;
return (<div className={`text-nowrap whitespace-nowrap overflow-hidden w-full ${hoverClass} ${className ?? ''}`}>
const hoverClass = !hoverOnly && hovering ? '[&:hover_*]:[animation-play-state:paused]' : '';
if (hoverOnly && !hovering)
return (<div className={`text-nowrap whitespace-nowrap overflow-hidden w-full ${className ?? ''}`}
onMouseEnter={() => setTextHovering(true)}>
{ children }
</div>);
return (<div className={`text-nowrap whitespace-nowrap overflow-hidden w-full ${hoverClass} ${className ?? ''}`} onMouseLeave={() => setTextHovering(false)}>
<div className={`${outerAnimation} ticker max-w-full inline-block`}>
<div className={`${innerAnimation} ticker inline-block`}>
{ children }