import type { CSSProperties } from 'react'; import { useMediaQuery } from '../hooks/useMediaQuery'; export interface VibeProfile { energy?: number; noveltyHunger?: number; explorationCoefficient?: number; discoveryRadius?: number; sessionGoal?: { type?: string; progress?: number; target?: number }; } const clamp = (value: number | undefined, fallback: number) => Math.min(1, Math.max(0, typeof value === 'number' ? value : fallback)); function describeProfile(energy: number, novelty: number) { const energyLabel = energy < 0.36 ? 'settled' : energy > 0.68 ? 'charged' : 'flowing'; const discoveryLabel = novelty < 0.36 ? 'familiar' : novelty > 0.64 ? 'adventurous' : 'balanced'; return { energyLabel, discoveryLabel }; } /** An ambient representation of the live recommendation profile. */ export function VibeAura({ profile, ambient = false }: { profile: VibeProfile; ambient?: boolean }) { // The displacement pass is desktop-only; see the comment in the ambient // branch below. Same breakpoint the layout already uses for this element. const plasma = useMediaQuery('(min-width: 640px)'); const energy = clamp(profile.energy, 0.5); const novelty = clamp(profile.noveltyHunger ?? profile.explorationCoefficient, 0.3); const { energyLabel, discoveryLabel } = describeProfile(energy, novelty); // Hue is discovery alone: ember red when the Vibe stays familiar, gold when it // reaches. Energy is deliberately absent — it already drives four motion // channels below, and mixing it in here cancelled half the novelty swing. // Warm range only; the old 205 base put a teal-blue glow in a warm brown room. const hue = Math.round(15 + novelty * 40); const style = { '--vibe-hue': String(hue), '--vibe-pulse': `${(4.8 - energy * 2.3).toFixed(2)}s`, '--vibe-orbit': `${(11 - energy * 4).toFixed(2)}s`, '--vibe-scale': String((0.9 + energy * 0.16).toFixed(2)), '--vibe-core-opacity': ambient ? '0.38' : '1', } as CSSProperties; const aura = (
{!ambient && (
Your Vibe is {energyLabel} Leaning {discoveryLabel}; it shifts as you listen.
)}
); if (ambient) { // Displacing clean gradients by animated fractal noise is what separates a // plasma from a blurred blob. baseFrequency is animated in SMIL rather than // CSS because no CSS property reaches inside an SVG filter primitive. // // That pass is also why the Vibe page crawled on a phone. An SVG filter is // rasterised on the CPU, and animating baseFrequency regenerates the whole // turbulence field every frame — for a 420x380 element, under a blur, over // three counter-rotating layers. Below the breakpoint the filter is not // mounted at all and the same layers churn behind a plain blur, which is // the difference between a heavy effect and a scrolling page. return (
{plasma && ( )}
); } return (
{aura}
); }