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:
BIN
attached_assets/image_1765756444333.png
Normal file
BIN
attached_assets/image_1765756444333.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -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
72
replit.md
Normal 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
|
||||
Reference in New Issue
Block a user