Skip to content

Commit 3ee665e

Browse files
committed
feat: dynamically generate player background and glow effects from album art colors and unify Now Playing UI.
1 parent afd0912 commit 3ee665e

2 files changed

Lines changed: 219 additions & 291 deletions

File tree

client/src/Pages/Music/BottomPlayer/NowPlayingTab.jsx

Lines changed: 124 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,63 @@
11
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
2-
import { Badge } from "@/components/ui/badge"
32
import { SheetTitle } from "@/components/ui/sheet"
43
import he from "he"
54
import { Music } from "lucide-react"
6-
import { memo, useMemo } from "react"
5+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
76
import { MusicControls, ProgressBarMusic } from "../Common"
87

9-
const NowPlayingTab = memo(({ currentSong, isDesktop = false }) => {
8+
const useImageColors = (imageSrc) => {
9+
const [colors, setColors] = useState(null)
10+
const canvasRef = useRef(null)
11+
12+
const extractColors = useCallback((src) => {
13+
if (!src) return
14+
const img = new Image()
15+
img.crossOrigin = "anonymous"
16+
img.onload = () => {
17+
if (!canvasRef.current) canvasRef.current = document.createElement("canvas")
18+
const canvas = canvasRef.current
19+
const ctx = canvas.getContext("2d", { willReadFrequently: true })
20+
canvas.width = 32
21+
canvas.height = 32
22+
ctx.drawImage(img, 0, 0, 32, 32)
23+
const data = ctx.getImageData(0, 0, 32, 32).data
24+
25+
const buckets = {}
26+
for (let i = 0; i < data.length; i += 8) {
27+
const r = data[i], g = data[i + 1], b = data[i + 2]
28+
const brightness = (r + g + b) / 3
29+
if (brightness < 15 || brightness > 245) continue
30+
const sat = Math.max(r, g, b) - Math.min(r, g, b)
31+
if (sat < 12) continue
32+
const key = `${Math.round(r / 24) * 24},${Math.round(g / 24) * 24},${Math.round(b / 24) * 24}`
33+
buckets[key] = (buckets[key] || 0) + 1
34+
}
35+
36+
const sorted = Object.entries(buckets).sort((a, b) => b[1] - a[1])
37+
const boost = (c) => c.map((v) => {
38+
const avg = (c[0] + c[1] + c[2]) / 3
39+
return Math.min(255, Math.round(v + (v - avg) * 0.5 + 30))
40+
})
41+
42+
const c1 = sorted[0] ? boost(sorted[0][0].split(",").map(Number)) : [120, 80, 200]
43+
const c2 = sorted[1] ? boost(sorted[1][0].split(",").map(Number)) : [200, 80, 120]
44+
const c3 = sorted[2] ? boost(sorted[2][0].split(",").map(Number)) : c1.map((v, i) => Math.round((v + c2[i]) / 2))
45+
46+
setColors({ c1, c2, c3 })
47+
}
48+
img.src = src
49+
}, [])
50+
51+
useEffect(() => {
52+
extractColors(imageSrc)
53+
}, [imageSrc, extractColors])
54+
55+
return colors
56+
}
57+
58+
const NowPlayingTab = memo(({ currentSong }) => {
1059
const songImage = useMemo(() => currentSong?.image?.[2]?.link, [currentSong])
60+
const colors = useImageColors(songImage)
1161

1262
const artistName = useMemo(
1363
() =>
@@ -18,114 +68,91 @@ const NowPlayingTab = memo(({ currentSong, isDesktop = false }) => {
1868
[currentSong],
1969
)
2070

21-
if (isDesktop) {
22-
return (
23-
<div className="w-full h-full flex items-center justify-center px-8 py-12 relative overflow-hidden">
24-
<div
25-
className="absolute inset-0 bg-cover bg-center opacity-25 blur-3xl scale-125"
26-
style={{ backgroundImage: `url(${songImage})` }}
27-
/>
28-
<div className="absolute inset-0 bg-linear-to-t from-background via-background/70 to-background/50" />
71+
const c1 = colors?.c1 || [30, 30, 30]
72+
const c2 = colors?.c2 || [20, 20, 20]
73+
const c3 = colors?.c3 || [25, 25, 25]
2974

30-
<div className="relative z-10 flex flex-col items-center gap-12 w-full">
31-
<div
32-
className="relative group shrink-0"
33-
style={{ width: "min(400px, 45vh)", height: "min(400px, 45vh)" }}
34-
>
35-
<div className="absolute inset-0 rounded-2xl bg-linear-to-br from-primary/30 via-transparent to-primary/20 opacity-60 blur-2xl scale-110 group-hover:opacity-80 transition-opacity duration-300" />
36-
<Avatar className="w-full h-full rounded-2xl shadow-2xl relative z-10 ring-1 ring-white/20">
37-
<AvatarImage src={songImage} alt={currentSong.name} className="object-cover" />
38-
<AvatarFallback className="text-6xl bg-linear-to-br from-primary/10 to-primary/5">
39-
<Music className="w-24 h-24 text-muted-foreground" />
40-
</AvatarFallback>
41-
</Avatar>
42-
{currentSong?.genre && (
43-
<div className="absolute top-4 left-4 z-20">
44-
<Badge
45-
variant="secondary"
46-
className="text-xs bg-black/40 backdrop-blur-xs border border-white/20 text-white"
47-
>
48-
{currentSong.genre}
49-
</Badge>
50-
</div>
51-
)}
52-
</div>
75+
const bgGradient = useMemo(() => `
76+
radial-gradient(ellipse 80% 80% at 15% 10%, rgba(${c1[0]},${c1[1]},${c1[2]},0.7) 0%, transparent 60%),
77+
radial-gradient(ellipse 70% 90% at 85% 85%, rgba(${c2[0]},${c2[1]},${c2[2]},0.6) 0%, transparent 55%),
78+
radial-gradient(ellipse 60% 60% at 55% 45%, rgba(${c3[0]},${c3[1]},${c3[2]},0.4) 0%, transparent 50%),
79+
radial-gradient(ellipse 90% 50% at 30% 90%, rgba(${c2[0]},${c2[1]},${c2[2]},0.35) 0%, transparent 55%),
80+
radial-gradient(ellipse 50% 80% at 80% 20%, rgba(${c1[0]},${c1[1]},${c1[2]},0.3) 0%, transparent 50%),
81+
black
82+
`, [c1, c2, c3])
5383

54-
<div className="flex-1 flex flex-col gap-6 text-center">
55-
<div>
56-
<SheetTitle className="text-2xl xl:text-3xl font-bold mb-3 line-clamp-2">
57-
{he.decode(currentSong.name)}
58-
</SheetTitle>
59-
<p className="text-lg text-muted-foreground line-clamp-1">{he.decode(artistName)}</p>
60-
{currentSong?.album?.name && (
61-
<p className="text-sm text-muted-foreground/60 mt-2">
62-
{he.decode(currentSong.album.name)}
63-
</p>
64-
)}
65-
</div>
66-
<ProgressBarMusic isTimeVisible={true} />
67-
68-
<div className="flex justify-center">
69-
<MusicControls size="large" />
70-
</div>
71-
</div>
72-
</div>
73-
</div>
74-
)
75-
}
84+
const glowBg = useMemo(
85+
() => `radial-gradient(circle, rgba(${c1[0]},${c1[1]},${c1[2]},0.5) 0%, transparent 70%)`,
86+
[c1],
87+
)
7688

7789
return (
78-
<div className="h-full flex flex-col p-4 sm:p-6 max-w-2xl mx-auto gap-10 relative">
79-
<div className="absolute inset-0 bg-linear-to-b from-transparent via-primary/2 to-transparent pointer-events-none" />
90+
<div className="w-full h-full flex items-center justify-center relative overflow-hidden bg-black">
91+
<div
92+
className="absolute inset-0 np-gradient-bg"
93+
style={{ background: bgGradient }}
94+
/>
8095

81-
<div className="flex justify-center relative z-10">
82-
<div className="w-full h-96 relative group">
83-
<div className="absolute inset-0 rounded-2xl bg-linear-to-br from-primary/20 via-transparent to-primary/10 opacity-50 blur-xl scale-105 group-hover:opacity-70 transition-opacity duration-300" />
84-
<Avatar className="w-full h-full rounded-2xl shadow-2xl relative z-10 ring-1 ring-white/10">
96+
<div className="absolute inset-0 bg-black/25" />
97+
98+
<div className="relative z-10 flex flex-col items-center w-full h-full justify-center px-6 sm:px-10 py-20 gap-6 sm:gap-8 max-w-xl mx-auto">
99+
<div className="relative group shrink-0">
100+
<div
101+
className="absolute -inset-6 rounded-3xl opacity-60 group-hover:opacity-80 transition-opacity duration-500"
102+
style={{ background: glowBg }}
103+
/>
104+
<Avatar
105+
className="rounded-2xl relative z-10"
106+
style={{
107+
width: "min(85vw, 50vh, 520px)",
108+
height: "min(85vw, 50vh, 520px)",
109+
boxShadow: `0 8px 40px rgba(0,0,0,0.5), 0 0 80px rgba(${c1[0]},${c1[1]},${c1[2]},0.2)`,
110+
}}
111+
>
85112
<AvatarImage src={songImage} alt={currentSong.name} className="object-cover" />
86-
<AvatarFallback className="text-4xl sm:text-6xl bg-linear-to-br from-primary/10 to-primary/5 backdrop-blur-xs">
87-
<Music className="w-16 h-16 sm:w-20 sm:h-20 text-muted-foreground" />
113+
<AvatarFallback className="text-6xl bg-white/5">
114+
<Music className="w-24 h-24 text-white/30" />
88115
</AvatarFallback>
89116
</Avatar>
90-
91-
{currentSong?.genre && (
92-
<div className="absolute top-3 left-3 z-20">
93-
<Badge
94-
variant="secondary"
95-
className="text-xs bg-white/20 backdrop-blur-xs border border-white/20 text-foreground"
96-
>
97-
{currentSong.genre}
98-
</Badge>
99-
</div>
100-
)}
101117
</div>
102-
</div>
103118

104-
<div className="flex items-start justify-between relative z-10">
105-
<div className="flex-1 min-w-0 pr-4">
106-
<SheetTitle className="text-xl sm:text-2xl lg:text-3xl mb-1 line-clamp-2">
107-
{he.decode(currentSong.name)}
108-
</SheetTitle>
109-
<p className="text-sm sm:text-base text-muted-foreground line-clamp-1">
110-
{he.decode(artistName)}
111-
</p>
112-
{currentSong?.album?.name && (
113-
<p className="text-xs sm:text-sm text-muted-foreground/70 mt-1">
114-
{he.decode(currentSong.album.name)}
115-
</p>
116-
)}
117-
</div>
118-
</div>
119+
<div className="flex flex-col gap-5 text-center w-full">
120+
<div>
121+
<SheetTitle className="text-2xl sm:text-3xl font-bold mb-2 line-clamp-2 text-white drop-shadow-lg">
122+
{he.decode(currentSong.name)}
123+
</SheetTitle>
124+
<p className="text-sm sm:text-base text-white/60 line-clamp-1 font-medium">{he.decode(artistName)}</p>
125+
{currentSong?.album?.name && (
126+
<p className="text-xs sm:text-sm text-white/35 mt-1.5 font-light">
127+
{he.decode(currentSong.album.name)}
128+
</p>
129+
)}
130+
</div>
119131

120-
<div>
121-
<ProgressBarMusic isTimeVisible={true} />
122-
</div>
132+
<ProgressBarMusic isTimeVisible={true} />
123133

124-
<div className="space-y-6 relative z-10">
125-
<div className="flex justify-center">
126-
<MusicControls size="large" />
134+
<div className="flex justify-center">
135+
<MusicControls size="large" />
136+
</div>
127137
</div>
128138
</div>
139+
140+
<style>{`
141+
.np-gradient-bg {
142+
transition: background 2s ease;
143+
animation: npShift 20s ease-in-out infinite alternate;
144+
}
145+
@keyframes npShift {
146+
0% { filter: hue-rotate(0deg) brightness(1); transform: scale(1); }
147+
25% { filter: hue-rotate(8deg) brightness(1.1); transform: scale(1.05); }
148+
50% { filter: hue-rotate(-5deg) brightness(0.95); transform: scale(1.02); }
149+
75% { filter: hue-rotate(12deg) brightness(1.05); transform: scale(1.08); }
150+
100% { filter: hue-rotate(-8deg) brightness(1); transform: scale(1); }
151+
}
152+
@media (prefers-reduced-motion: reduce) {
153+
.np-gradient-bg { animation: none; }
154+
}
155+
`}</style>
129156
</div>
130157
)
131158
})

0 commit comments

Comments
 (0)