Files
holy-flavor-rater/frontend/src/components/ui/GlassDefs.tsx
T
2026-09-04 23:38:55 +02:00

144 lines
4.8 KiB
TypeScript

/**
* The SVG filter definitions behind the liquid-glass look, mounted exactly once
* (from `AppProviders`) so `url(#…)` resolves anywhere in the app.
*
* Why these are static: refraction reads as *liquid* because the backdrop moves
* behind a fixed displacement map — not because the map itself animates.
* Animating `baseFrequency` would re-rasterise the turbulence every frame; a
* static seed over the drifting aurora/canvas backdrop costs one raster pass
* and looks the same.
*
* These are consumed through `theme.tokens.glass.rimFilter` /
* `rimFilterChroma`, and only ever on a thin masked ring (see `Glass.tsx`) —
* never over a whole pane, which would be Chrome-only and far too expensive on
* a grid of cards.
*/
/** Turbulence shared by both rim filters: soft, lens-like lobes rather than sand. */
const Turbulence = () => (
<>
<feTurbulence
type="fractalNoise"
baseFrequency="0.008 0.012"
numOctaves={2}
seed={7}
result="noise"
/>
<feGaussianBlur in="noise" stdDeviation="1.2" result="soft" />
</>
);
/** Isolates one channel of the source so it can be displaced independently. */
const channelMatrix = (channel: 'r' | 'g' | 'b') =>
({
r: '1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0',
g: '0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0',
b: '0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0',
})[channel];
export const GlassDefs = () => (
<svg
width="0"
height="0"
aria-hidden="true"
focusable="false"
style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden' }}
>
<defs>
{/* Monochrome refraction — the workhorse, used on the `panel` tier. */}
<filter
id="hfr-rim"
x="-20%"
y="-20%"
width="140%"
height="140%"
colorInterpolationFilters="sRGB"
>
<Turbulence />
<feDisplacementMap
in="SourceGraphic"
in2="soft"
scale={11}
xChannelSelector="R"
yChannelSelector="G"
result="disp"
/>
{/* Displacement near an edge pulls in samples from outside the backdrop
image, which come back as transparent black and paint a dark fringe
along the rim. Compositing the displaced result *over* the original
backdrop fills those gaps with the undisplaced pixels instead. */}
<feComposite in="disp" in2="SourceGraphic" operator="over" />
</filter>
{/* Chromatic refraction — R/G/B displaced by different amounts, then
screened back together, giving the prismatic edge real glass has.
Three displacement passes, so `chrome` tier only. */}
<filter
id="hfr-rim-chroma"
x="-20%"
y="-20%"
width="140%"
height="140%"
colorInterpolationFilters="sRGB"
>
<Turbulence />
<feColorMatrix
in="SourceGraphic"
type="matrix"
values={channelMatrix('r')}
result="rOnly"
/>
<feDisplacementMap
in="rOnly"
in2="soft"
scale={14}
xChannelSelector="R"
yChannelSelector="G"
result="rDisp"
/>
<feColorMatrix
in="SourceGraphic"
type="matrix"
values={channelMatrix('g')}
result="gOnly"
/>
<feDisplacementMap
in="gOnly"
in2="soft"
scale={11}
xChannelSelector="R"
yChannelSelector="G"
result="gDisp"
/>
<feColorMatrix
in="SourceGraphic"
type="matrix"
values={channelMatrix('b')}
result="bOnly"
/>
<feDisplacementMap
in="bOnly"
in2="soft"
scale={8}
xChannelSelector="R"
yChannelSelector="G"
result="bDisp"
/>
<feBlend in="rDisp" in2="gDisp" mode="screen" result="rg" />
<feBlend in="rg" in2="bDisp" mode="screen" result="chroma" />
{/* Same edge-fringe patch as #hfr-rim. */}
<feComposite in="chroma" in2="SourceGraphic" operator="over" />
</filter>
</defs>
</svg>
);
/**
* Fine grain, as a self-contained data URI rather than a `filter: url()` chain —
* it tiles as a plain background-image, so it costs nothing per frame.
* Layered at low opacity over the aurora it kills gradient banding and gives
* the rim refraction something fine-grained to bend.
*/
export const GRAIN_DATA_URI =
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E\")";