41 lines
1.6 KiB
TypeScript
41 lines
1.6 KiB
TypeScript
import { useEffect, useRef, type CSSProperties } from 'react';
|
|
|
|
import s from './Ambient.module.css';
|
|
|
|
/**
|
|
* Ambient-фон (docs/design.md#1): три размытых дрейфующих пятна; третье — в цвете обложки текущего трека.
|
|
* Отключается в настройках; при prefers-reduced-motion пятна неподвижны.
|
|
*/
|
|
export function Ambient({ color, readAudio }: {
|
|
color: string;
|
|
readAudio?: () => { bass: number; mid: number; treble: number };
|
|
}) {
|
|
const root = useRef<HTMLDivElement>(null);
|
|
useEffect(() => {
|
|
if (!readAudio) return;
|
|
const mq = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
let raf = 0;
|
|
let last = performance.now();
|
|
let level = 0;
|
|
const frame = (now: number) => {
|
|
const dt = Math.min(0.05, (now - last) / 1000);
|
|
last = now;
|
|
const audio = !mq?.matches && !document.hidden ? readAudio() : undefined;
|
|
const target = audio ? audio.bass * 0.65 + audio.mid * 0.25 + audio.treble * 0.1 : 0;
|
|
level += (target - level) * (1 - Math.exp(-dt * 7));
|
|
root.current?.style.setProperty('--music-scale', String(1 + level * 0.16));
|
|
root.current?.style.setProperty('--music-light', String(1 + level * 0.3));
|
|
raf = requestAnimationFrame(frame);
|
|
};
|
|
raf = requestAnimationFrame(frame);
|
|
return () => cancelAnimationFrame(raf);
|
|
}, [readAudio]);
|
|
return (
|
|
<div ref={root} className={s.ambient} aria-hidden>
|
|
<span className={s.a} />
|
|
<span className={s.b} />
|
|
<span className={s.c} style={{ '--amb-c3': `${color}99` } as CSSProperties} />
|
|
</div>
|
|
);
|
|
}
|