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 SSH_COMMAND = "ssh portfolio@keshavanand.net";
|
||||||
|
|
||||||
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`";
|
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`アイウエオカキクケコサシスセソタチツテト";
|
||||||
|
|
||||||
interface Particle {
|
interface MatrixColumn {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
char: string;
|
speed: number;
|
||||||
opacity: number;
|
chars: string[];
|
||||||
targetX: number;
|
length: number;
|
||||||
targetY: number;
|
baseOpacity: number;
|
||||||
vx: number;
|
|
||||||
vy: number;
|
|
||||||
life: number;
|
|
||||||
maxLife: number;
|
|
||||||
size: number;
|
|
||||||
color: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ParticleBackground() {
|
function ParticleBackground() {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const particlesRef = useRef<Particle[]>([]);
|
const columnsRef = useRef<MatrixColumn[]>([]);
|
||||||
const mouseRef = useRef({ x: 0, y: 0 });
|
const mouseRef = useRef({ x: -1000, y: -1000 });
|
||||||
const animationRef = useRef<number>();
|
const animationRef = useRef<number>();
|
||||||
|
const timeRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -34,97 +29,147 @@ function ParticleBackground() {
|
|||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
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 = () => {
|
const resizeCanvas = () => {
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
|
initColumns();
|
||||||
};
|
};
|
||||||
|
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
window.addEventListener("resize", 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) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
mouseRef.current = { x: e.clientX, y: e.clientY };
|
mouseRef.current = { x: e.clientX, y: e.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
const getColor = (y: number, distanceToMouse: number, charIndex: number, totalChars: number) => {
|
||||||
if (particlesRef.current.length < 200) {
|
const heightRatio = y / canvas.height;
|
||||||
particlesRef.current.push(createParticle(e.clientX, e.clientY));
|
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 = () => {
|
const animate = () => {
|
||||||
ctx.fillStyle = "#000000";
|
timeRef.current++;
|
||||||
|
|
||||||
|
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
particlesRef.current = particlesRef.current.filter((p) => {
|
ctx.font = `${fontSize}px 'JetBrains Mono', monospace`;
|
||||||
p.life++;
|
|
||||||
|
|
||||||
const dx = mouseRef.current.x - p.x;
|
columnsRef.current.forEach((col) => {
|
||||||
const dy = mouseRef.current.y - p.y;
|
const dx = mouseRef.current.x - col.x;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const dy = mouseRef.current.y - col.y;
|
||||||
|
const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
if (distance < 150) {
|
const gravityRadius = 200;
|
||||||
const force = (150 - distance) / 150;
|
let gravityEffect = 0;
|
||||||
p.vx += (dx / distance) * force * 0.3;
|
let pullX = 0;
|
||||||
p.vy += (dy / distance) * force * 0.3;
|
|
||||||
|
if (distanceToMouse < gravityRadius) {
|
||||||
|
gravityEffect = (gravityRadius - distanceToMouse) / gravityRadius;
|
||||||
|
pullX = (dx / distanceToMouse) * gravityEffect * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.vx *= 0.96;
|
for (let i = 0; i < col.chars.length; i++) {
|
||||||
p.vy *= 0.96;
|
const charY = col.y + i * fontSize;
|
||||||
|
|
||||||
p.x += p.vx;
|
if (charY < -fontSize || charY > canvas.height + fontSize) continue;
|
||||||
p.y += p.vy;
|
|
||||||
|
|
||||||
const lifeRatio = p.life / p.maxLife;
|
const charDx = mouseRef.current.x - (col.x + pullX);
|
||||||
const fadeOpacity = lifeRatio < 0.2
|
const charDy = mouseRef.current.y - charY;
|
||||||
? lifeRatio * 5
|
const charDistance = Math.sqrt(charDx * charDx + charDy * charDy);
|
||||||
: lifeRatio > 0.7
|
|
||||||
? (1 - lifeRatio) * 3.33
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
const distanceOpacity = Math.max(0, 1 - distance / 300);
|
const fadeRatio = i / col.chars.length;
|
||||||
const finalOpacity = p.opacity * fadeOpacity * distanceOpacity;
|
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;
|
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);
|
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