diff --git a/attached_assets/image_1765756444333.png b/attached_assets/image_1765756444333.png deleted file mode 100644 index aa6c3f5..0000000 Binary files a/attached_assets/image_1765756444333.png and /dev/null differ diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 7606dcf..abd71b7 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -4,23 +4,28 @@ import { Button } from "@/components/ui/button"; const SSH_COMMAND = "ssh portfolio@keshavanand.net"; -const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`アイウエオカキクケコサシスセソタチツテト"; +const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()_+-=[]{}|;:,.<>?/\\~`"; -interface MatrixColumn { +interface Particle { x: number; y: number; - speed: number; - chars: string[]; - length: number; - baseOpacity: number; + char: string; + opacity: number; + targetX: number; + targetY: number; + vx: number; + vy: number; + life: number; + maxLife: number; + size: number; + color: string; } function ParticleBackground() { const canvasRef = useRef(null); - const columnsRef = useRef([]); - const mouseRef = useRef({ x: -1000, y: -1000 }); + const particlesRef = useRef([]); + const mouseRef = useRef({ x: 0, y: 0 }); const animationRef = useRef(); - const timeRef = useRef(0); useEffect(() => { const canvas = canvasRef.current; @@ -29,147 +34,97 @@ 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 handleMouseMove = (e: MouseEvent) => { - mouseRef.current = { x: e.clientX, y: e.clientY }; + 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 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; + const handleMouseMove = (e: MouseEvent) => { + mouseRef.current = { x: e.clientX, y: e.clientY }; - if (headGlow && mouseInfluence > 0.3) { - return `rgba(255, 255, 255, ${0.9 + mouseInfluence * 0.1})`; + for (let i = 0; i < 3; i++) { + 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 = () => { - timeRef.current++; - - ctx.fillStyle = "rgba(0, 0, 0, 0.1)"; + ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.font = `${fontSize}px 'JetBrains Mono', monospace`; - - 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); + particlesRef.current = particlesRef.current.filter((p) => { + p.life++; - const gravityRadius = 200; - let gravityEffect = 0; - let pullX = 0; + const dx = mouseRef.current.x - p.x; + const dy = mouseRef.current.y - p.y; + const distance = Math.sqrt(dx * dx + dy * dy); - if (distanceToMouse < gravityRadius) { - gravityEffect = (gravityRadius - distanceToMouse) / gravityRadius; - pullX = (dx / distanceToMouse) * gravityEffect * 2; - } - - for (let i = 0; i < col.chars.length; i++) { - const charY = col.y + i * fontSize; - - if (charY < -fontSize || charY > canvas.height + fontSize) continue; - - const charDx = mouseRef.current.x - (col.x + pullX); - const charDy = mouseRef.current.y - charY; - const charDistance = Math.sqrt(charDx * charDx + charDy * charDy); - - 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 (distance < 150) { + const force = (150 - distance) / 150; + p.vx += (dx / distance) * force * 0.3; + p.vy += (dy / distance) * force * 0.3; } - ctx.globalAlpha = 1; + p.vx *= 0.96; + p.vy *= 0.96; - if (Math.random() < 0.02) { - const randomIndex = Math.floor(Math.random() * col.chars.length); - col.chars[randomIndex] = CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)]; + p.x += p.vx; + p.y += p.vy; + + const lifeRatio = p.life / p.maxLife; + const fadeOpacity = lifeRatio < 0.2 + ? lifeRatio * 5 + : lifeRatio > 0.7 + ? (1 - lifeRatio) * 3.33 + : 1; + + const distanceOpacity = Math.max(0, 1 - distance / 300); + const finalOpacity = p.opacity * fadeOpacity * distanceOpacity; + + 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; } - 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; - } + return p.life < p.maxLife; }); - 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); }; diff --git a/replit.md b/replit.md deleted file mode 100644 index 011eff9..0000000 --- a/replit.md +++ /dev/null @@ -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 \ No newline at end of file