Restored to '0b21ba282eef95fac68017b564697d9e128bab4e'
Replit-Restored-To: 0b21ba282e
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -4,23 +4,28 @@ 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 MatrixColumn {
|
interface Particle {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
speed: number;
|
char: string;
|
||||||
chars: string[];
|
opacity: number;
|
||||||
length: number;
|
targetX: number;
|
||||||
baseOpacity: number;
|
targetY: 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 columnsRef = useRef<MatrixColumn[]>([]);
|
const particlesRef = useRef<Particle[]>([]);
|
||||||
const mouseRef = useRef({ x: -1000, y: -1000 });
|
const mouseRef = useRef({ x: 0, y: 0 });
|
||||||
const animationRef = useRef<number>();
|
const animationRef = useRef<number>();
|
||||||
const timeRef = useRef(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -29,147 +34,97 @@ 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 handleMouseMove = (e: MouseEvent) => {
|
const colors = [
|
||||||
mouseRef.current = { x: e.clientX, y: e.clientY };
|
"#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 getColor = (y: number, distanceToMouse: number, charIndex: number, totalChars: number) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
const heightRatio = y / canvas.height;
|
mouseRef.current = { x: e.clientX, y: e.clientY };
|
||||||
const mouseInfluence = Math.max(0, 1 - distanceToMouse / 250);
|
|
||||||
const headGlow = charIndex === totalChars - 1 ? 1 : 0;
|
|
||||||
|
|
||||||
if (headGlow && mouseInfluence > 0.3) {
|
for (let i = 0; i < 3; i++) {
|
||||||
return `rgba(255, 255, 255, ${0.9 + mouseInfluence * 0.1})`;
|
if (particlesRef.current.length < 200) {
|
||||||
|
particlesRef.current.push(createParticle(e.clientX, e.clientY));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = () => {
|
||||||
timeRef.current++;
|
ctx.fillStyle = "#000000";
|
||||||
|
|
||||||
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
ctx.font = `${fontSize}px 'JetBrains Mono', monospace`;
|
particlesRef.current = particlesRef.current.filter((p) => {
|
||||||
|
p.life++;
|
||||||
|
|
||||||
columnsRef.current.forEach((col) => {
|
const dx = mouseRef.current.x - p.x;
|
||||||
const dx = mouseRef.current.x - col.x;
|
const dy = mouseRef.current.y - p.y;
|
||||||
const dy = mouseRef.current.y - col.y;
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
const gravityRadius = 200;
|
if (distance < 150) {
|
||||||
let gravityEffect = 0;
|
const force = (150 - distance) / 150;
|
||||||
let pullX = 0;
|
p.vx += (dx / distance) * force * 0.3;
|
||||||
|
p.vy += (dy / distance) * force * 0.3;
|
||||||
if (distanceToMouse < gravityRadius) {
|
|
||||||
gravityEffect = (gravityRadius - distanceToMouse) / gravityRadius;
|
|
||||||
pullX = (dx / distanceToMouse) * gravityEffect * 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < col.chars.length; i++) {
|
p.vx *= 0.96;
|
||||||
const charY = col.y + i * fontSize;
|
p.vy *= 0.96;
|
||||||
|
|
||||||
if (charY < -fontSize || charY > canvas.height + fontSize) continue;
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
|
||||||
const charDx = mouseRef.current.x - (col.x + pullX);
|
const lifeRatio = p.life / p.maxLife;
|
||||||
const charDy = mouseRef.current.y - charY;
|
const fadeOpacity = lifeRatio < 0.2
|
||||||
const charDistance = Math.sqrt(charDx * charDx + charDy * charDy);
|
? lifeRatio * 5
|
||||||
|
: lifeRatio > 0.7
|
||||||
|
? (1 - lifeRatio) * 3.33
|
||||||
|
: 1;
|
||||||
|
|
||||||
const fadeRatio = i / col.chars.length;
|
const distanceOpacity = Math.max(0, 1 - distance / 300);
|
||||||
let opacity = col.baseOpacity * (0.3 + fadeRatio * 0.7);
|
const finalOpacity = p.opacity * fadeOpacity * distanceOpacity;
|
||||||
|
|
||||||
if (charDistance < 250) {
|
if (finalOpacity > 0.01) {
|
||||||
const boost = (250 - charDistance) / 250;
|
ctx.font = `${p.size}px 'JetBrains Mono', monospace`;
|
||||||
opacity = Math.min(1, opacity + boost * 0.8);
|
ctx.fillStyle = p.color;
|
||||||
}
|
ctx.globalAlpha = finalOpacity;
|
||||||
|
ctx.fillText(p.char, p.x, p.y);
|
||||||
if (i === col.chars.length - 1) {
|
ctx.globalAlpha = 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.globalAlpha = 1;
|
return p.life < p.maxLife;
|
||||||
|
|
||||||
if (Math.random() < 0.02) {
|
|
||||||
const randomIndex = Math.floor(Math.random() * col.chars.length);
|
|
||||||
col.chars[randomIndex] = CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
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
72
replit.md
@@ -1,72 +0,0 @@
|
|||||||
# 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