124 lines
6.2 KiB
TypeScript
124 lines
6.2 KiB
TypeScript
|
|
import { ChatBgVariants } from './types';
|
|||
|
|
import { rainFall } from './animRain.css';
|
|||
|
|
|
|||
|
|
// anim-rain — "Digital Rain" — a premium take on the Matrix code-rain motif.
|
|||
|
|
//
|
|||
|
|
// Concept: sparse vertical columns of falling glyph-streaks. Each streak is a
|
|||
|
|
// soft vertical gradient that fades from a brighter LEADING glyph (the drop's
|
|||
|
|
// head) up into a dim trailing tail, punctuated by a scatter of faint monospace
|
|||
|
|
// glyph marks so it reads as CODE rather than plain stripes. It floats over a
|
|||
|
|
// near-black base carrying a subtle green phosphor cast and a gentle vignette.
|
|||
|
|
// Columns are deliberately sparse (only a handful across the 260px-wide tile)
|
|||
|
|
// so the reading area breathes and text always wins the contrast fight.
|
|||
|
|
//
|
|||
|
|
// SEAMLESS TILING + PAN — the streak SVG tile is 260×200. Its content is
|
|||
|
|
// authored to wrap top↔bottom: each streak's gradient and glyphs are placed so
|
|||
|
|
// the tile is vertically continuous, and the animation (see animRain.css.ts)
|
|||
|
|
// pans this first layer down by EXACTLY one tile height (200px) per cycle, so
|
|||
|
|
// the "fall" loops with no seam. The base / vignette layers are 100% 100% and
|
|||
|
|
// stay fixed (the keyframe holds them at '0 0').
|
|||
|
|
//
|
|||
|
|
// ANIMATION-STRIP SAFETY — getChatBg removes `animation` for reduced-motion /
|
|||
|
|
// pause-animations users, so the non-animation properties below already read as
|
|||
|
|
// a finished, gorgeous STATIC rain: a frozen frame of streaks over the base.
|
|||
|
|
//
|
|||
|
|
// CSP / Tauri-safe: inline SVG via encodeURIComponent (NOT base64). oklch used
|
|||
|
|
// throughout; alphas kept low so both themes stay WCAG-AA-friendly for text.
|
|||
|
|
|
|||
|
|
// One vertical streak-column, colour-parameterised. Placed at x within a
|
|||
|
|
// 260-wide tile. `head` is the bright leading-glyph colour, `tail` the dim
|
|||
|
|
// trailing colour, `glyph` the colour of the riding monospace glyph ticks.
|
|||
|
|
const streak = (
|
|||
|
|
x: number,
|
|||
|
|
headY: number, // y of the leading glyph (drop head)
|
|||
|
|
len: number, // trailing tail length upward
|
|||
|
|
head: string,
|
|||
|
|
tail: string,
|
|||
|
|
glyph: string,
|
|||
|
|
): string => {
|
|||
|
|
const topY = headY - len;
|
|||
|
|
const id = `g${x}_${headY}`; // unique even when two columns share an x
|
|||
|
|
// Vertical fade: transparent at the tail top → tail colour → bright head.
|
|||
|
|
const grad = `
|
|||
|
|
<linearGradient id='${id}' x1='0' y1='${topY}' x2='0' y2='${headY}' gradientUnits='userSpaceOnUse'>
|
|||
|
|
<stop offset='0' stop-color='${tail}' stop-opacity='0'/>
|
|||
|
|
<stop offset='0.55' stop-color='${tail}'/>
|
|||
|
|
<stop offset='1' stop-color='${head}'/>
|
|||
|
|
</linearGradient>`;
|
|||
|
|
// The streak body is a soft, slightly-blurred vertical bar.
|
|||
|
|
const bar = `<rect x='${x - 3}' y='${topY}' width='6' height='${len}' rx='3' fill='url(#${id})'/>`;
|
|||
|
|
// A few monospace glyph ticks riding the column (short horizontal dashes).
|
|||
|
|
const ticks = [0.22, 0.45, 0.68, 0.86]
|
|||
|
|
.map((f, i) => {
|
|||
|
|
const gy = Math.round(topY + len * f);
|
|||
|
|
const gw = i % 2 === 0 ? 5 : 3;
|
|||
|
|
const op = i === 3 ? '0.9' : '0.5';
|
|||
|
|
return `<rect x='${x - gw / 2}' y='${gy}' width='${gw}' height='1.4' rx='0.7' fill='${glyph}' fill-opacity='${op}'/>`;
|
|||
|
|
})
|
|||
|
|
.join('');
|
|||
|
|
// The leading glyph: a brighter small square cap at the head.
|
|||
|
|
const cap = `<rect x='${x - 2.5}' y='${headY - 3}' width='5' height='5' rx='1' fill='${head}'/>`;
|
|||
|
|
return grad + bar + ticks + cap;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Full 260×200 tile. Columns are wrapped vertically: a column whose head sits
|
|||
|
|
// low in the tile has its tail running off the top, and a companion column
|
|||
|
|
// re-enters that space, so panning by one tile height reads as continuous fall.
|
|||
|
|
const tile = (head: string, tail: string, glyph: string): string => {
|
|||
|
|
const cols = [
|
|||
|
|
streak(24, 150, 140, head, tail, glyph),
|
|||
|
|
streak(78, 60, 120, head, tail, glyph),
|
|||
|
|
streak(122, 196, 160, head, tail, glyph), // head near bottom → tail wraps up
|
|||
|
|
streak(122, 40, 160, head, tail, glyph), // partner near top completes the wrap
|
|||
|
|
streak(178, 110, 100, head, tail, glyph),
|
|||
|
|
streak(232, 176, 130, head, tail, glyph),
|
|||
|
|
].join('');
|
|||
|
|
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='260' height='200' viewBox='0 0 260 200'><defs></defs>${cols}</svg>`;
|
|||
|
|
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const animRain: ChatBgVariants = {
|
|||
|
|
// Dark: phosphor-green streaks on deep near-black with a faint green cast.
|
|||
|
|
dark: {
|
|||
|
|
backgroundColor: 'oklch(0.16 0.02 150)',
|
|||
|
|
backgroundImage: [
|
|||
|
|
// 1) the falling streak columns (this is the panned layer)
|
|||
|
|
tile(
|
|||
|
|
'oklch(0.75 0.14 150 / 0.5)', // head — bright phosphor glyph
|
|||
|
|
'oklch(0.68 0.12 150 / 0.28)', // tail — dim phosphor
|
|||
|
|
'oklch(0.82 0.1 150 / 0.5)', // glyph ticks — brightest
|
|||
|
|
),
|
|||
|
|
// 2) soft top-down phosphor haze so the rain has atmosphere
|
|||
|
|
'linear-gradient(180deg, oklch(0.24 0.04 150 / 0.55) 0%, transparent 40%)',
|
|||
|
|
// 3) subtle green cast pooling toward the bottom
|
|||
|
|
'radial-gradient(120% 90% at 50% 100%, oklch(0.28 0.05 150 / 0.45) 0%, transparent 60%)',
|
|||
|
|
// 4) vignette — quiet the corners so the reading column stays clean
|
|||
|
|
'radial-gradient(140% 140% at 50% 45%, transparent 60%, oklch(0.1 0.02 150 / 0.6) 100%)',
|
|||
|
|
].join(','),
|
|||
|
|
backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
|
|||
|
|
backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
|
|||
|
|
animation: `${rainFall} 12s linear infinite`,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// Light: soft teal-grey streaks on a pale cool base — elegant, never neon.
|
|||
|
|
light: {
|
|||
|
|
backgroundColor: 'oklch(0.97 0.008 165)',
|
|||
|
|
backgroundImage: [
|
|||
|
|
tile(
|
|||
|
|
'oklch(0.55 0.07 165 / 0.4)', // head — soft teal-grey drop
|
|||
|
|
'oklch(0.62 0.05 165 / 0.22)', // tail — faint teal-grey
|
|||
|
|
'oklch(0.5 0.06 165 / 0.42)', // glyph ticks
|
|||
|
|
),
|
|||
|
|
// gentle cool wash from the top
|
|||
|
|
'linear-gradient(180deg, oklch(0.94 0.015 175 / 0.6) 0%, transparent 42%)',
|
|||
|
|
// faint teal pooling at the bottom edge
|
|||
|
|
'radial-gradient(120% 90% at 50% 100%, oklch(0.9 0.02 170 / 0.5) 0%, transparent 60%)',
|
|||
|
|
// soft vignette in cool grey
|
|||
|
|
'radial-gradient(140% 140% at 50% 45%, transparent 62%, oklch(0.88 0.02 165 / 0.5) 100%)',
|
|||
|
|
].join(','),
|
|||
|
|
backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
|
|||
|
|
backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
|
|||
|
|
animation: `${rainFall} 12s linear infinite`,
|
|||
|
|
},
|
|||
|
|
};
|