5a019bd35c
Two things made the Vibe page crawl on mobile. The ambient aura pushed three counter-rotating layers through an SVG turbulence and displacement pass whose noise field was animated in SMIL. SVG filters rasterise on the CPU, so that regenerated the whole field every frame, under a blur, across a 420x380 element. Below the breakpoint the filter is no longer mounted and the layers churn behind a plain blur instead. A phone gets the motion, not the noise pass. The start screen read all 5,000 tracks to show fifty of them. The sample is now drawn in the database, which keeps Surprise me uniform over the whole library while sending only what is on screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KENqSChfyqWnor6ud2WWH6
128 lines
5.7 KiB
TypeScript
128 lines
5.7 KiB
TypeScript
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 = (
|
|
<div className="grid h-16 w-16 place-items-center rounded-full">
|
|
<div className="vibe-aura-halo" />
|
|
<div className="vibe-aura-orbit vibe-aura-orbit-one" />
|
|
<div className="vibe-aura-orbit vibe-aura-orbit-two" />
|
|
<div className="vibe-aura-core" style={{ opacity: 'var(--vibe-core-opacity, 1)' }}>
|
|
<span className="vibe-aura-spark vibe-aura-spark-one" />
|
|
<span className="vibe-aura-spark vibe-aura-spark-two" />
|
|
<span className="vibe-aura-heart" />
|
|
</div>
|
|
{!ambient && (
|
|
<div className="pointer-events-none absolute right-0 top-[calc(100%+0.4rem)] z-10 w-44 rounded-md border border-border bg-surface1 px-3 py-2 text-xs leading-relaxed text-muted opacity-0 shadow-xl transition-opacity group-hover:opacity-100 group-focus:opacity-100">
|
|
<span className="block font-medium text-text">Your Vibe is {energyLabel}</span>
|
|
<span>Leaning {discoveryLabel}; it shifts as you listen.</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
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 (
|
|
<div
|
|
className="vibe-aura-blob pointer-events-none absolute z-0 h-[380px] w-[420px] -translate-x-1/2 -translate-y-1/2 opacity-85 mix-blend-screen sm:h-[560px] sm:w-[660px] sm:opacity-95"
|
|
style={style}
|
|
role="img"
|
|
aria-label={`Current Vibe: ${energyLabel} energy and ${discoveryLabel} discovery`}
|
|
>
|
|
{plasma && (
|
|
<svg aria-hidden className="absolute h-0 w-0">
|
|
<filter id="vibe-plasma" x="-30%" y="-30%" width="160%" height="160%">
|
|
<feTurbulence
|
|
type="fractalNoise"
|
|
baseFrequency="0.009 0.014"
|
|
numOctaves={3}
|
|
seed={7}
|
|
result="noise"
|
|
>
|
|
<animate
|
|
attributeName="baseFrequency"
|
|
dur={`${(18 - energy * 8).toFixed(1)}s`}
|
|
values="0.009 0.014; 0.021 0.007; 0.009 0.014"
|
|
repeatCount="indefinite"
|
|
/>
|
|
</feTurbulence>
|
|
<feDisplacementMap
|
|
in="SourceGraphic"
|
|
in2="noise"
|
|
scale={String(Math.round(46 + energy * 70))}
|
|
xChannelSelector="R"
|
|
yChannelSelector="G"
|
|
/>
|
|
</filter>
|
|
</svg>
|
|
)}
|
|
<div className={plasma ? 'vibe-aura-stack vibe-aura-stack-plasma' : 'vibe-aura-stack'}>
|
|
<div className="vibe-aura-layer vibe-aura-rays-layer" />
|
|
<div className="vibe-aura-layer vibe-aura-swirl-layer" />
|
|
<div className="vibe-aura-layer vibe-aura-core-layer" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="group relative grid h-16 w-16 shrink-0 place-items-center rounded-full focus:outline-none"
|
|
style={style}
|
|
role="img"
|
|
tabIndex={0}
|
|
aria-label={`Current Vibe: ${energyLabel} energy and ${discoveryLabel} discovery`}
|
|
title={`Current Vibe: ${energyLabel} energy, ${discoveryLabel} discovery`}
|
|
>
|
|
{aura}
|
|
</div>
|
|
);
|
|
}
|