2023/12
In the past week I have been working to perfect my music player at the bottom. Previous Post
Click me to jump to music player.
Initally the music player had a simple background that was a static linear gradient of two colors from left to right. This was fine, but boring, and did not always work out with different album covers. I wanted to implement something that was dynamic based on the colors of the album.
This first step was to know what colors we will be working with. A simple search on npm.js gave me the extract-colors package, perfect for my use case.
Since I want the function to only run in the browser, I wrapped the call inside a useEffect that reruns whenever the album cover url changes.
I also filtered out black & white colors.
useEffect(() => {
(async () => {
if (!img) return;
const colors = await extractColors(img, {
crossOrigin: "anonymous",
colorValidator(red, green, blue, alpha) {
const s = red + green + blue;
// prevent overwhite and overblack
return 255 * 0.4 < s && s < 255 * 2.5;
},
});
const toRGBA = (c: { red: number; green: number; blue: number }) => {
const kred =
((c.red + c.green + c.blue) % 7) / 36 + (darkMode ? 0.9 : 0.39);
const calc = (x: number) => Math.round(255 - (255 - x) * kred);
let red = calc(c.red);
let green = calc(c.green);
let blue = calc(c.blue);
// reduce the color of the lowest
const min = Math.min(red, green, blue);
// from 0.78 to 0.96
const reduction = ((red + green + blue) % 8) / 45 + 0.78;
if (min === red) red = Math.round(red * reduction);
else if (min === green) green = Math.round(green * reduction);
else if (min === blue) blue = Math.round(blue * reduction);
return `rgba(${red},${green},${blue},0.9)`;
};
setColors(colors.slice(0, 5).map(toRGBA));
})();
}, [img, darkMode]);
Now that I have my list of colors, I need to animate them in the background.
I have no prior experience with browser animations, so I used a simple yet dumb solution, combining CSS transitions with a setInterval.
interface RandomMovingGradientProps {
background: string;
className?: string;
}
function RandomMovingGradient({
background,
className,
}: RandomMovingGradientProps) {
const random = () => {
// get random number from -20 to 30, 70 to 120
const r = Math.random();
if (r < 0.5) return Math.random() * 60 - 30;
else return Math.random() * 60 + 70;
};
// opacity should favor 0-0.2 and 0.8-1
const opacity = () => {
const r = Math.random();
if (r < 0.2) return Math.random() * 0.3;
else return Math.random() * 0.2 + 0.8;
};
const newSize = () => Math.random() * 200 + 500;
const [width, setWidth] = useState(newSize());
const [height, setHeight] = useState(newSize());
const [pos, setPos] = useState({ x: random(), y: random() });
const [show, setShow] = useState(opacity());
const [i, _setInterval] = useState((Math.random() * 5 + 8) * 1000);
const update = useCallback(() => {
setPos({
x: random(),
y: random(),
});
setShow(opacity());
setWidth(newSize());
setHeight(newSize());
}, []);
useEffect(() => {
setTimeout(update, 1000);
const interval = setInterval(update, i);
return () => clearInterval(interval);
}, [i, update]);
return (
<div
className={
"absolute -z-20 rounded-full transition-all ease-in-out " + className
}
style={{
left: `${pos.x}%`,
top: `${pos.y}%`,
transform: `translate(-50%, -50%)`,
position: "absolute",
transitionDuration: i + "ms",
background: background,
width: `${width}px`,
height: `${height}px`,
opacity: show,
}}
/>
);
}
Dumb, but effective.