02b2ce8109
Same treatment as the seasonal themes: split the 502-line chatBackground.ts Record into one premium module per background under lotus/backgrounds/ (each exposes a tuned dark + light ChatBgVariants), one Opus agent per background against a shared brief. chatBackground.ts now assembles DARK/LIGHT from the modules; getChatBg is unchanged. Carbon + Aurora are kept inline as-is (user favorites); none stays the empty layer. Every redesign: layered oklch palettes, seamless tiling with worked-out tile math (integer-multiple periods; edge-wrapping inline-SVG data-URIs for circuit/hexgrid/waves/herringbone/chevron/tactical), independently-tuned dark+light (not a recolor), and low "felt-not-read" opacity so chat text stays WCAG-AA legible. The 5 animated backgrounds (rain, star drift, grid pulse, aurora flow, fireflies) each colocate a vanilla-extract keyframe .css.ts, animate only background-position for a jump-free loop, and — since getChatBg strips animation for reduced-motion — render a finished static frame too. Redesigned: blueprint, stars, topographic, herringbone, crosshatch, chevron, polka, triangles, plaid, tactical, circuit, hexgrid, waves, neon, anim-rain, anim-stars, anim-pulse, anim-aurora, anim-fireflies. Gates: tsc clean, ESLint clean, Prettier clean, build OK, 551 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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`,
|
||
},
|
||
};
|