@@ -63,6 +63,39 @@ const useImageColors = (imageSrc) => {
6363 return colors
6464}
6565
66+ const useBlurredBg = ( imageSrc ) => {
67+ const [ bgUrls , setBgUrls ] = useState ( { current : null , previous : null , transitioning : false } )
68+ const canvasRef = useRef ( null )
69+ const timeoutRef = useRef ( null )
70+ const prevSrc = useRef ( null )
71+
72+ useEffect ( ( ) => {
73+ if ( ! imageSrc || imageSrc === prevSrc . current ) return
74+ prevSrc . current = imageSrc
75+ const img = new Image ( )
76+ img . crossOrigin = "anonymous"
77+ img . onload = ( ) => {
78+ if ( ! canvasRef . current ) canvasRef . current = document . createElement ( "canvas" )
79+ const canvas = canvasRef . current
80+ canvas . width = 64
81+ canvas . height = 64
82+ const ctx = canvas . getContext ( "2d" )
83+ ctx . filter = "blur(4px) saturate(1.3) brightness(0.65)"
84+ ctx . drawImage ( img , - 4 , - 4 , 72 , 72 )
85+ const dataUrl = canvas . toDataURL ( "image/jpeg" , 0.7 )
86+ setBgUrls ( ( prev ) => ( { current : dataUrl , previous : prev . current , transitioning : true } ) )
87+ if ( timeoutRef . current ) clearTimeout ( timeoutRef . current )
88+ timeoutRef . current = setTimeout ( ( ) => {
89+ setBgUrls ( ( prev ) => ( { ...prev , previous : null , transitioning : false } ) )
90+ } , 800 )
91+ }
92+ img . src = imageSrc
93+ return ( ) => { if ( timeoutRef . current ) clearTimeout ( timeoutRef . current ) }
94+ } , [ imageSrc ] )
95+
96+ return bgUrls
97+ }
98+
6699const useCrossfadeImage = ( src ) => {
67100 const [ images , setImages ] = useState ( { current : src , previous : null , transitioning : false } )
68101 const timeoutRef = useRef ( null )
@@ -121,31 +154,22 @@ const UpNextHint = memo(({ nextSong }) => {
121154UpNextHint . displayName = "UpNextHint"
122155
123156const CrossfadeAvatar = memo ( ( { images, size, shadow, name } ) => (
124- < div className = "relative np-img-hover" style = { { width : size , height : size } } >
157+ < div className = "relative np-img-hover overflow-hidden rounded-2xl" style = { { width : size , height : size , boxShadow : shadow } } >
158+ < div className = "absolute inset-0 flex items-center justify-center bg-white/5" >
159+ < Music className = "w-24 h-24 text-white/20" />
160+ </ div >
125161 { images . previous && (
126- < Avatar
127- className = "rounded-2xl absolute inset-0"
128- style = { { width : size , height : size , boxShadow : shadow } }
129- >
130- < AvatarImage
131- src = { images . previous }
132- className = "object-cover np-fade-out"
133- />
134- </ Avatar >
135- ) }
136- < Avatar
137- className = "rounded-2xl relative"
138- style = { { width : size , height : size , boxShadow : shadow } }
139- >
140- < AvatarImage
141- src = { images . current }
142- alt = { name }
143- className = { `object-cover ${ images . transitioning ? "np-fade-in" : "" } ` }
162+ < img
163+ src = { images . previous }
164+ className = "absolute inset-0 w-full h-full object-cover np-fade-out"
165+ aria-hidden = "true"
144166 />
145- < AvatarFallback className = "text-6xl bg-white/5" >
146- < Music className = "w-24 h-24 text-white/20" />
147- </ AvatarFallback >
148- </ Avatar >
167+ ) }
168+ < img
169+ src = { images . current }
170+ alt = { name }
171+ className = { `absolute inset-0 w-full h-full object-cover ${ images . transitioning ? "np-fade-in" : "" } ` }
172+ />
149173 </ div >
150174) )
151175CrossfadeAvatar . displayName = "CrossfadeAvatar"
@@ -154,6 +178,7 @@ const NowPlayingTab = memo(({ currentSong }) => {
154178 const songImage = useMemo ( ( ) => currentSong ?. image ?. [ 2 ] ?. link , [ currentSong ] )
155179 const colors = useImageColors ( songImage )
156180 const images = useCrossfadeImage ( songImage )
181+ const mobileBg = useBlurredBg ( songImage )
157182 const nextSong = useNextSong ( currentSong )
158183 const hasAnimated = useRef ( false )
159184 const [ showEntrance , setShowEntrance ] = useState ( false )
@@ -196,42 +221,57 @@ const NowPlayingTab = memo(({ currentSong }) => {
196221
197222 return (
198223 < div className = "w-full h-full relative overflow-hidden bg-[#050508]" >
199- { /* Layer 1: Album art dissolved into pure color fields */ }
224+
225+ { /* === MOBILE BG: Pre-blurred canvas, no CSS filter === */ }
226+ { mobileBg . previous && (
227+ < div
228+ className = "lg:hidden absolute inset-0 bg-cover bg-center np-bg-fade-out"
229+ style = { { backgroundImage : `url(${ mobileBg . previous } )` } }
230+ />
231+ ) }
232+ { mobileBg . current && (
233+ < div
234+ className = { `lg:hidden absolute inset-0 bg-cover bg-center ${ mobileBg . transitioning ? "np-bg-fade-in" : "" } ` }
235+ style = { { backgroundImage : `url(${ mobileBg . current } )` } }
236+ />
237+ ) }
238+
239+ { /* === DESKTOP BG: Full liquid glass with CSS blur === */ }
200240 { images . previous && (
201241 < div
202- className = "absolute inset-0 bg-cover bg-center np-bg-fade-out"
242+ className = "hidden lg:block absolute inset-0 bg-cover bg-center np-bg-fade-out"
203243 style = { { backgroundImage : `url(${ images . previous } )` , filter : "blur(160px) saturate(1.4) brightness(0.65)" , transform : "scale(2)" } }
204244 />
205245 ) }
206246 < div
207- className = { `absolute inset-0 bg-cover bg-center ${ images . transitioning ? "np-bg-fade-in" : "" } ` }
247+ className = { `hidden lg:block absolute inset-0 bg-cover bg-center ${ images . transitioning ? "np-bg-fade-in" : "" } ` }
208248 style = { { backgroundImage : `url(${ images . current } )` , filter : "blur(160px) saturate(1.4) brightness(0.65)" , transform : "scale(2)" , transition : "background-image 0.6s ease" } }
209249 />
210250
211- { /* Layer 2: Glass noise texture */ }
212- < div className = "absolute inset-0 np-glass-noise" />
251+ { /* Desktop-only: glass noise */ }
252+ < div className = "hidden lg:block absolute inset-0 np-glass-noise" />
213253
214- { /* Layer 3: Specular highlights — top-left and bottom-right light catches */ }
215- < div className = "absolute inset-0" style = { {
254+ { /* Desktop-only: specular highlights */ }
255+ < div className = "hidden lg:block absolute inset-0" style = { {
216256 background : "radial-gradient(ellipse 70% 50% at 25% 20%, rgba(255,255,255,0.08) 0%, transparent 50%)"
217257 } } />
218- < div className = "absolute inset-0" style = { {
258+ < div className = "hidden lg:block absolute inset-0" style = { {
219259 background : "radial-gradient(ellipse 50% 35% at 75% 75%, rgba(255,255,255,0.04) 0%, transparent 40%)"
220260 } } />
221261
222- { /* Layer 4: Animated liquid shimmer */ }
223- < div className = "absolute inset-0 np-orb-layer" >
262+ { /* Desktop-only: animated liquid shimmer */ }
263+ < div className = "hidden lg:block absolute inset-0 np-orb-layer" >
224264 < div className = "np-orb np-o1" style = { orbColor ( c1 , 0.18 ) } />
225265 < div className = "np-orb np-o2" style = { orbColor ( c2 , 0.14 ) } />
226266 < div className = "np-shimmer" />
227267 </ div >
228268
229- { /* Layer 5: Luminous depth — glass curvature effect */ }
230- < div className = "absolute inset-0" style = { {
269+ { /* Desktop-only: luminous depth */ }
270+ < div className = "hidden lg:block absolute inset-0" style = { {
231271 background : "radial-gradient(ellipse 120% 80% at 50% 40%, rgba(255,255,255,0.03) 0%, transparent 50%)"
232272 } } />
233273
234- { /* Layer 6: Vignette */ }
274+ { /* Shared: vignette (single gradient, very light on GPU) */ }
235275 < div className = "absolute inset-0" style = { {
236276 background : "radial-gradient(ellipse at 50% 45%, transparent 30%, rgba(5,5,8,0.3) 60%, rgba(5,5,8,0.85) 100%)"
237277 } } />
0 commit comments