diff --git a/src/app/features/lotus/chatBackground.ts b/src/app/features/lotus/chatBackground.ts index 38170969c..665e21e62 100644 --- a/src/app/features/lotus/chatBackground.ts +++ b/src/app/features/lotus/chatBackground.ts @@ -1,5 +1,12 @@ import { CSSProperties } from 'react'; import { ChatBackground } from '../../state/settings'; +import { + animRainKeyframe, + animStarsDriftKeyframe, + animGridPulseKeyframe, + animAuroraKeyframe, + animFirefliesKeyframe, +} from '../../styles/Animations.css'; export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [ { value: 'none', label: 'None' }, @@ -19,6 +26,11 @@ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [ { value: 'waves', label: 'Waves' }, { value: 'neon', label: 'Neon Grid' }, { value: 'aurora', label: 'Aurora' }, + { value: 'anim-rain', label: 'Digital Rain' }, + { value: 'anim-stars', label: 'Star Drift' }, + { value: 'anim-pulse', label: 'Grid Pulse' }, + { value: 'anim-aurora', label: 'Aurora Flow' }, + { value: 'anim-fireflies', label: 'Fireflies' }, ]; const DARK: Record = { @@ -184,6 +196,71 @@ const DARK: Record = { 'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.06) 0%, transparent 50%)', ].join(','), }, + + // Animated: Matrix digital rain — scrolling vertical green stripes + 'anim-rain': { + backgroundColor: '#010804', + backgroundImage: [ + 'repeating-linear-gradient(180deg, rgba(0,255,136,0.13) 0px, rgba(0,255,136,0.13) 1px, transparent 1px, transparent 20px)', + 'repeating-linear-gradient(180deg, rgba(0,255,136,0.07) 0px, rgba(0,255,136,0.07) 1px, transparent 1px, transparent 8px)', + ].join(','), + backgroundSize: '40px 200px, 12px 200px', + backgroundPosition: '0 0, 0 0', + animation: `${animRainKeyframe} 8s linear infinite`, + }, + + // Animated: drifting star field — three layers at different speeds + 'anim-stars': { + backgroundColor: '#050510', + backgroundImage: [ + 'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)', + 'radial-gradient(circle, rgba(200,220,255,0.55) 1px, transparent 1px)', + 'radial-gradient(circle, rgba(180,200,255,0.3) 1px, transparent 1px)', + ].join(','), + backgroundSize: '130px 130px, 190px 190px, 260px 260px', + backgroundPosition: '0 0, 65px 32px, 32px 97px', + animation: `${animStarsDriftKeyframe} 25s linear infinite`, + }, + + // Animated: neon grid pulse — grid lines that expand/contract + 'anim-pulse': { + backgroundColor: '#030508', + backgroundImage: [ + 'linear-gradient(rgba(255,107,0,0.12) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(255,107,0,0.12) 1px, transparent 1px)', + 'linear-gradient(rgba(0,212,255,0.06) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(0,212,255,0.06) 1px, transparent 1px)', + ].join(','), + backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px', + animation: `${animGridPulseKeyframe} 4s ease-in-out infinite`, + }, + + // Animated: aurora borealis — slowly drifting gradient bands + 'anim-aurora': { + backgroundColor: '#020a10', + backgroundImage: [ + 'radial-gradient(ellipse at 20% 30%, rgba(0,255,136,0.10) 0%, transparent 55%)', + 'radial-gradient(ellipse at 80% 70%, rgba(0,100,255,0.10) 0%, transparent 55%)', + 'radial-gradient(ellipse at 50% 10%, rgba(191,95,255,0.08) 0%, transparent 50%)', + 'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.08) 0%, transparent 50%)', + ].join(','), + backgroundSize: '200% 200%', + backgroundPosition: '0% 0%', + animation: `${animAuroraKeyframe} 20s ease-in-out infinite`, + }, + + // Animated: fireflies — three layers of glowing dots at different speeds + 'anim-fireflies': { + backgroundColor: '#030508', + backgroundImage: [ + 'radial-gradient(circle, rgba(255,220,50,0.55) 1.5px, rgba(255,160,0,0.15) 3px, transparent 4px)', + 'radial-gradient(circle, rgba(255,200,30,0.45) 1px, rgba(255,140,0,0.12) 2.5px, transparent 3.5px)', + 'radial-gradient(circle, rgba(255,240,100,0.35) 1px, transparent 2px)', + ].join(','), + backgroundSize: '200px 200px, 280px 280px, 160px 160px', + backgroundPosition: '0 0, 120px 80px, 60px 140px', + animation: `${animFirefliesKeyframe} 15s linear infinite`, + }, }; const LIGHT: Record = { @@ -340,7 +417,82 @@ const LIGHT: Record = { 'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.07) 0%, transparent 50%)', ].join(','), }, + + // Animated light variants + + 'anim-rain': { + backgroundColor: '#f0fff4', + backgroundImage: [ + 'repeating-linear-gradient(180deg, rgba(0,160,80,0.14) 0px, rgba(0,160,80,0.14) 1px, transparent 1px, transparent 20px)', + 'repeating-linear-gradient(180deg, rgba(0,160,80,0.07) 0px, rgba(0,160,80,0.07) 1px, transparent 1px, transparent 8px)', + ].join(','), + backgroundSize: '40px 200px, 12px 200px', + backgroundPosition: '0 0, 0 0', + animation: `${animRainKeyframe} 8s linear infinite`, + }, + + 'anim-stars': { + backgroundColor: '#f5f5ff', + backgroundImage: [ + 'radial-gradient(circle, rgba(60,60,160,0.50) 1px, transparent 1px)', + 'radial-gradient(circle, rgba(80,80,180,0.35) 1px, transparent 1px)', + 'radial-gradient(circle, rgba(100,100,200,0.20) 1px, transparent 1px)', + ].join(','), + backgroundSize: '130px 130px, 190px 190px, 260px 260px', + backgroundPosition: '0 0, 65px 32px, 32px 97px', + animation: `${animStarsDriftKeyframe} 25s linear infinite`, + }, + + 'anim-pulse': { + backgroundColor: '#ffffff', + backgroundImage: [ + 'linear-gradient(rgba(0,98,184,0.14) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(0,98,184,0.14) 1px, transparent 1px)', + 'linear-gradient(rgba(0,98,184,0.06) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(0,98,184,0.06) 1px, transparent 1px)', + ].join(','), + backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px', + animation: `${animGridPulseKeyframe} 4s ease-in-out infinite`, + }, + + 'anim-aurora': { + backgroundColor: '#f0f8f4', + backgroundImage: [ + 'radial-gradient(ellipse at 20% 30%, rgba(0,160,80,0.12) 0%, transparent 55%)', + 'radial-gradient(ellipse at 80% 70%, rgba(0,80,200,0.12) 0%, transparent 55%)', + 'radial-gradient(ellipse at 50% 10%, rgba(140,60,220,0.09) 0%, transparent 50%)', + 'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.09) 0%, transparent 50%)', + ].join(','), + backgroundSize: '200% 200%', + backgroundPosition: '0% 0%', + animation: `${animAuroraKeyframe} 20s ease-in-out infinite`, + }, + + 'anim-fireflies': { + backgroundColor: '#fffdf0', + backgroundImage: [ + 'radial-gradient(circle, rgba(180,120,0,0.55) 1.5px, rgba(160,90,0,0.15) 3px, transparent 4px)', + 'radial-gradient(circle, rgba(160,100,0,0.45) 1px, rgba(140,80,0,0.12) 2.5px, transparent 3.5px)', + 'radial-gradient(circle, rgba(200,140,0,0.35) 1px, transparent 2px)', + ].join(','), + backgroundSize: '200px 200px, 280px 280px, 160px 160px', + backgroundPosition: '0 0, 120px 80px, 60px 140px', + animation: `${animFirefliesKeyframe} 15s linear infinite`, + }, }; -export const getChatBg = (bg: ChatBackground, isDark: boolean): CSSProperties => - isDark ? DARK[bg] : LIGHT[bg]; +export const getChatBg = ( + bg: ChatBackground, + isDark: boolean, + pauseAnimations?: boolean, +): CSSProperties => { + const style = isDark ? DARK[bg] : LIGHT[bg]; + const reducedMotion = + typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if ((pauseAnimations || reducedMotion) && style.animation) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { animation: _anim, ...rest } = style; + return rest; + } + return style; +}; diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx index 7d5e01301..9abd0aecf 100644 --- a/src/app/features/room/RoomView.tsx +++ b/src/app/features/room/RoomView.tsx @@ -61,6 +61,7 @@ export function RoomView({ eventId }: { eventId?: string }) { const roomViewRef = useRef(null) as React.RefObject; const [chatBackground] = useSetting(settingsAtom, 'chatBackground'); const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); + const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations'); const theme = useTheme(); const isDark = theme.kind === ThemeKind.Dark; @@ -98,8 +99,12 @@ export function RoomView({ eventId }: { eventId?: string }) { const chatBgStyle = useMemo( () => - getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark), - [chatBackground, lotusTerminal, isDark], + getChatBg( + lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, + isDark, + pauseAnimations, + ), + [chatBackground, lotusTerminal, isDark, pauseAnimations], ); return ( diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index c406cb2df..94ada8d74 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -331,6 +331,7 @@ function Appearance() { settingsAtom, 'glassmorphismSidebar', ); + const [pauseAnimations, setPauseAnimations] = useSetting(settingsAtom, 'pauseAnimations'); return ( @@ -390,6 +391,14 @@ function Appearance() { + + } + /> + + { style.removeProperty('background-image'); style.removeProperty('background-color'); style.removeProperty('background-size'); style.removeProperty('background-position'); + style.removeProperty('animation'); }; - }, [glassmorphismSidebar, chatBackground, lotusTerminal, isDark]); + }, [glassmorphismSidebar, chatBackground, lotusTerminal, isDark, pauseAnimations]); return ( diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 74e8609fe..852283b15 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -26,7 +26,12 @@ export type ChatBackground = | 'hexgrid' | 'waves' | 'neon' - | 'aurora'; + | 'aurora' + | 'anim-rain' + | 'anim-stars' + | 'anim-pulse' + | 'anim-aurora' + | 'anim-fireflies'; export enum MessageLayout { Modern = 0, Compact = 1, @@ -94,6 +99,8 @@ export interface Settings { deafenKey: string; warnOnUnverifiedDevices: boolean; + + pauseAnimations: boolean; } const defaultSettings: Settings = { @@ -157,6 +164,8 @@ const defaultSettings: Settings = { deafenKey: 'KeyM', warnOnUnverifiedDevices: false, + + pauseAnimations: false, }; export const getSettings = (): Settings => { diff --git a/src/app/styles/Animations.css.ts b/src/app/styles/Animations.css.ts index 5a92b9a64..b3a1488fd 100644 --- a/src/app/styles/Animations.css.ts +++ b/src/app/styles/Animations.css.ts @@ -69,3 +69,39 @@ export const MsgAppearClass = style({ }, }, }); + +// Animated chat background keyframes + +// Animated chat background keyframes + +/** Matrix rain — two stripe layers scroll at different speeds for parallax depth */ +export const animRainKeyframe = keyframes({ + from: { backgroundPosition: '0 0, 0 0' }, + to: { backgroundPosition: '0 200px, 0 100px' }, +}); + +/** Drifting stars — three layers drift diagonally */ +export const animStarsDriftKeyframe = keyframes({ + from: { backgroundPosition: '0 0, 65px 32px, 32px 97px' }, + to: { backgroundPosition: '130px 130px, 195px 162px, 162px 227px' }, +}); + +/** Grid pulse — expands/contracts backgroundSize slightly */ +export const animGridPulseKeyframe = keyframes({ + '0%': { backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px' }, + '50%': { backgroundSize: '66px 66px, 66px 66px, 13px 13px, 13px 13px' }, + '100%': { backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px' }, +}); + +/** Aurora — sweeps backgroundPosition in large cycle */ +export const animAuroraKeyframe = keyframes({ + '0%': { backgroundPosition: '0% 0%' }, + '50%': { backgroundPosition: '-50% -25%' }, + '100%': { backgroundPosition: '0% 0%' }, +}); + +/** Fireflies — three layers of glowing dots drift diagonally */ +export const animFirefliesKeyframe = keyframes({ + from: { backgroundPosition: '0 0, 120px 80px, 60px 140px' }, + to: { backgroundPosition: '200px 150px, 320px 230px, 260px 290px' }, +});