Enhance background effect with continuous matrix rain and mouse gravity

Update client-side rendering to implement a dynamic matrix rain effect with ambient particle movement, enhanced blue color palette, and interactive cursor gravity, alongside adding Japanese Katakana characters to the character set.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 8a5dc88f-13c6-40ab-96e7-e09ad06db4dd
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0a96ed7d-db0f-4da3-8775-c22ce131e135
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/804258ec-282b-434a-9b89-a7ccc1690e42/8a5dc88f-13c6-40ab-96e7-e09ad06db4dd/6x1aYhq
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
keshavanandmusi
2025-12-14 23:55:49 +00:00
parent 0b21ba282e
commit a63ccf96bf
3 changed files with 196 additions and 79 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -4,28 +4,23 @@ import { Button } from "@/components/ui/button";
const SSH_COMMAND = "ssh portfolio@keshavanand.net";
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`";
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`アイウエオカキクケコサシスセソタチツテト";
interface Particle {
interface MatrixColumn {
x: number;
y: number;
char: string;
opacity: number;
targetX: number;
targetY: number;
vx: number;
vy: number;
life: number;
maxLife: number;
size: number;
color: string;
speed: number;
chars: string[];
length: number;
baseOpacity: number;
}
function ParticleBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const particlesRef = useRef<Particle[]>([]);
const mouseRef = useRef({ x: 0, y: 0 });
const columnsRef = useRef<MatrixColumn[]>([]);
const mouseRef = useRef({ x: -1000, y: -1000 });
const animationRef = useRef<number>();
const timeRef = useRef(0);
useEffect(() => {
const canvas = canvasRef.current;
@@ -34,97 +29,147 @@ function ParticleBackground() {
const ctx = canvas.getContext("2d");
if (!ctx) return;
const fontSize = 14;
const columnSpacing = fontSize * 1.2;
const initColumns = () => {
const cols: MatrixColumn[] = [];
const numColumns = Math.ceil(canvas.width / columnSpacing);
for (let i = 0; i < numColumns; i++) {
const length = Math.floor(Math.random() * 20) + 10;
const chars: string[] = [];
for (let j = 0; j < length; j++) {
chars.push(CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)]);
}
cols.push({
x: i * columnSpacing,
y: Math.random() * canvas.height - canvas.height,
speed: Math.random() * 1.5 + 0.5,
chars,
length,
baseOpacity: Math.random() * 0.4 + 0.1,
});
}
columnsRef.current = cols;
};
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initColumns();
};
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
const colors = [
"#a6e3a1",
"#94e2d5",
"#89dceb",
"#74c7ec",
"#89b4fa",
"#cba6f7",
];
const createParticle = (x: number, y: number): Particle => {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 2 + 1;
const color = colors[Math.floor(Math.random() * colors.length)];
return {
x: x + (Math.random() - 0.5) * 100,
y: y + (Math.random() - 0.5) * 100,
char: CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)],
opacity: Math.random() * 0.8 + 0.2,
targetX: x,
targetY: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 0,
maxLife: Math.random() * 60 + 40,
size: Math.random() * 10 + 12,
color,
};
};
const handleMouseMove = (e: MouseEvent) => {
mouseRef.current = { x: e.clientX, y: e.clientY };
};
for (let i = 0; i < 3; i++) {
if (particlesRef.current.length < 200) {
particlesRef.current.push(createParticle(e.clientX, e.clientY));
}
const getColor = (y: number, distanceToMouse: number, charIndex: number, totalChars: number) => {
const heightRatio = y / canvas.height;
const mouseInfluence = Math.max(0, 1 - distanceToMouse / 250);
const headGlow = charIndex === totalChars - 1 ? 1 : 0;
if (headGlow && mouseInfluence > 0.3) {
return `rgba(255, 255, 255, ${0.9 + mouseInfluence * 0.1})`;
}
const r = Math.floor(30 + mouseInfluence * 50 + heightRatio * 20);
const g = Math.floor(180 + mouseInfluence * 75 - heightRatio * 40);
const b = Math.floor(160 + heightRatio * 95 + mouseInfluence * 40);
return `rgb(${r}, ${g}, ${b})`;
};
const animate = () => {
ctx.fillStyle = "#000000";
timeRef.current++;
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
particlesRef.current = particlesRef.current.filter((p) => {
p.life++;
ctx.font = `${fontSize}px 'JetBrains Mono', monospace`;
const dx = mouseRef.current.x - p.x;
const dy = mouseRef.current.y - p.y;
const distance = Math.sqrt(dx * dx + dy * dy);
columnsRef.current.forEach((col) => {
const dx = mouseRef.current.x - col.x;
const dy = mouseRef.current.y - col.y;
const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
if (distance < 150) {
const force = (150 - distance) / 150;
p.vx += (dx / distance) * force * 0.3;
p.vy += (dy / distance) * force * 0.3;
const gravityRadius = 200;
let gravityEffect = 0;
let pullX = 0;
if (distanceToMouse < gravityRadius) {
gravityEffect = (gravityRadius - distanceToMouse) / gravityRadius;
pullX = (dx / distanceToMouse) * gravityEffect * 2;
}
p.vx *= 0.96;
p.vy *= 0.96;
for (let i = 0; i < col.chars.length; i++) {
const charY = col.y + i * fontSize;
p.x += p.vx;
p.y += p.vy;
if (charY < -fontSize || charY > canvas.height + fontSize) continue;
const lifeRatio = p.life / p.maxLife;
const fadeOpacity = lifeRatio < 0.2
? lifeRatio * 5
: lifeRatio > 0.7
? (1 - lifeRatio) * 3.33
: 1;
const charDx = mouseRef.current.x - (col.x + pullX);
const charDy = mouseRef.current.y - charY;
const charDistance = Math.sqrt(charDx * charDx + charDy * charDy);
const distanceOpacity = Math.max(0, 1 - distance / 300);
const finalOpacity = p.opacity * fadeOpacity * distanceOpacity;
const fadeRatio = i / col.chars.length;
let opacity = col.baseOpacity * (0.3 + fadeRatio * 0.7);
if (charDistance < 250) {
const boost = (250 - charDistance) / 250;
opacity = Math.min(1, opacity + boost * 0.8);
}
if (i === col.chars.length - 1) {
opacity = Math.min(1, opacity * 1.5);
}
const displayX = col.x + pullX * (1 - i / col.chars.length);
ctx.globalAlpha = opacity;
ctx.fillStyle = getColor(charY, charDistance, i, col.chars.length);
ctx.fillText(col.chars[i], displayX, charY);
if (charDistance < 150 && i === col.chars.length - 1) {
ctx.globalAlpha = opacity * 0.3;
ctx.shadowColor = getColor(charY, charDistance, i, col.chars.length);
ctx.shadowBlur = 10;
ctx.fillText(col.chars[i], displayX, charY);
ctx.shadowBlur = 0;
}
}
if (finalOpacity > 0.01) {
ctx.font = `${p.size}px 'JetBrains Mono', monospace`;
ctx.fillStyle = p.color;
ctx.globalAlpha = finalOpacity;
ctx.fillText(p.char, p.x, p.y);
ctx.globalAlpha = 1;
if (Math.random() < 0.02) {
const randomIndex = Math.floor(Math.random() * col.chars.length);
col.chars[randomIndex] = CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)];
}
return p.life < p.maxLife;
const speedMultiplier = 1 + gravityEffect * 0.5;
col.y += col.speed * speedMultiplier;
if (col.y > canvas.height + col.length * fontSize) {
col.y = -col.length * fontSize;
col.speed = Math.random() * 1.5 + 0.5;
col.baseOpacity = Math.random() * 0.4 + 0.1;
}
});
if (mouseRef.current.x > 0 && mouseRef.current.y > 0) {
const gradient = ctx.createRadialGradient(
mouseRef.current.x, mouseRef.current.y, 0,
mouseRef.current.x, mouseRef.current.y, 150
);
gradient.addColorStop(0, "rgba(148, 226, 213, 0.03)");
gradient.addColorStop(0.5, "rgba(137, 180, 250, 0.02)");
gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
animationRef.current = requestAnimationFrame(animate);
};

72
replit.md Normal file
View File

@@ -0,0 +1,72 @@
# Keshav Anand Portfolio
## Overview
A minimalist terminal-themed portfolio website featuring an interactive particle background with cursor-following character effects. The site displays a single SSH command that visitors can copy to connect. Built with React frontend and Express backend, using the Catppuccin Mocha color palette for a dark terminal aesthetic.
## User Preferences
Preferred communication style: Simple, everyday language.
## System Architecture
### Frontend Architecture
- **Framework**: React 18 with TypeScript
- **Routing**: Wouter (lightweight React router)
- **Styling**: Tailwind CSS with custom Catppuccin Mocha theme variables
- **UI Components**: shadcn/ui component library (New York style variant)
- **State Management**: TanStack React Query for server state
- **Build Tool**: Vite with path aliases (@/, @shared/, @assets/)
### Backend Architecture
- **Runtime**: Node.js with Express
- **Language**: TypeScript (ESM modules)
- **API Pattern**: RESTful routes prefixed with /api
- **Storage**: Pluggable storage interface (currently in-memory, prepared for PostgreSQL)
### Project Structure
```
client/ # React frontend
src/
components/ # UI components (shadcn/ui)
pages/ # Route pages
hooks/ # Custom React hooks
lib/ # Utilities and query client
server/ # Express backend
index.ts # Entry point
routes.ts # API route definitions
storage.ts # Data storage interface
shared/ # Shared types and schemas
schema.ts # Drizzle ORM schema definitions
```
### Design System
- **Theme**: Catppuccin Mocha (dark-only)
- **Typography**: JetBrains Mono monospace font
- **Key Colors**:
- Background: #1e1e2e
- Primary green: #a6e3a1
- Teal accent: #94e2d5
- Text: #cdd6f4
### Build Configuration
- Development: `npm run dev` (tsx with Vite dev server)
- Production: `npm run build` (esbuild for server, Vite for client)
- Output: `dist/` directory with `index.cjs` and `public/` folder
## External Dependencies
### Database
- **ORM**: Drizzle ORM with PostgreSQL dialect
- **Schema Location**: shared/schema.ts
- **Migrations**: ./migrations directory
- **Connection**: DATABASE_URL environment variable required for production
### Third-Party Services
- **Google Fonts**: JetBrains Mono font family loaded via CDN
### Key NPM Packages
- **UI**: Radix UI primitives, Lucide React icons, class-variance-authority
- **Forms**: react-hook-form with zod validation
- **Data**: @tanstack/react-query for async state
- **Utilities**: clsx, tailwind-merge, date-fns