From d5fde28b151e4a8233bc8033e76261a6d38a3b2a Mon Sep 17 00:00:00 2001 From: keshavananddev <53607429-keshavananddev@users.noreply.replit.com> Date: Fri, 24 Apr 2026 03:37:24 +0000 Subject: [PATCH] Improve homepage by adding interactive cursor effects and animation Implement a canvas-based, cursor-reactive dot field background and update the typewriter animation to include a scramble effect. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 3df64324-7337-48d0-9099-7bab54585b37 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/8hzGboW Replit-Helium-Checkpoint-Created: true --- .replit | 3 + index.html | 27 +++--- src/main.ts | 224 ++++++++++++++++++++++++++++++++++++++++++++------ src/style.css | 102 +++++------------------ 4 files changed, 236 insertions(+), 120 deletions(-) diff --git a/.replit b/.replit index a9d2a79..a9c6418 100644 --- a/.replit +++ b/.replit @@ -30,3 +30,6 @@ outputType = "webview" [[ports]] localPort = 5000 externalPort = 80 + +[agent] +expertMode = true diff --git a/index.html b/index.html index 97235b9..a2b0ec6 100644 --- a/index.html +++ b/index.html @@ -10,28 +10,21 @@ -
-

hi, i'm keshav.

+ -
-
- $ - -
- click to copy +
+
+ $ +
- how it works → + how it works →
-
- // hand-written, not vibe-coded -
- diff --git a/src/main.ts b/src/main.ts index e2b0d72..e264bdb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,43 +6,69 @@ const typedEl = document.getElementById('typed') as HTMLSpanElement; const caretEl = document.getElementById('caret') as HTMLSpanElement; const commandBox = document.getElementById('commandBox') as HTMLDivElement; const copyBtn = document.getElementById('copyBtn') as HTMLButtonElement; -const hintEl = document.getElementById('hint') as HTMLSpanElement; +const copyIcon = document.getElementById('copyIcon') as unknown as SVGSVGElement; -// Typewriter intro — small, intentional, then the caret keeps blinking. const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; -function type(i = 0) { - if (i > COMMAND.length) return; - typedEl.textContent = COMMAND.slice(0, i); - // slight humanized rhythm - const next = 28 + Math.random() * 38; - setTimeout(() => type(i + 1), next); +/* ---------- Typewriter with a small "decoder" scramble ---------- */ + +const SCRAMBLE = '!<>-_\\/[]{}—=+*^?#abcdef0123456789'; + +function typeWithScramble(target: string, onDone?: () => void) { + let resolved = ''; + let i = 0; + + function nextChar() { + if (i >= target.length) { + typedEl.textContent = resolved; + onDone?.(); + return; + } + let cycles = target[i] === ' ' ? 0 : 2 + Math.floor(Math.random() * 2); + function flicker() { + if (cycles <= 0) { + resolved += target[i]; + typedEl.textContent = resolved; + i++; + setTimeout(nextChar, 22 + Math.random() * 30); + return; + } + const ch = SCRAMBLE[Math.floor(Math.random() * SCRAMBLE.length)]; + typedEl.textContent = resolved + ch; + cycles--; + setTimeout(flicker, 28); + } + flicker(); + } + nextChar(); } if (reduceMotion) { typedEl.textContent = COMMAND; } else { - // small lead-in delay so the page settles first - setTimeout(() => type(), 380); + setTimeout(() => typeWithScramble(COMMAND), 280); } -// Copy on click / keypress +/* ---------- Silent copy (no flashy feedback, just a brief icon swap) ---------- */ + +const COPY_PATH = 'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z'; +const CHECK_PATH = 'M5 13l4 4L19 7'; + +function setIcon(d: string) { + const path = copyIcon.querySelector('path'); + if (path) path.setAttribute('d', d); +} + +let copyResetTimer: number | undefined; + async function copy() { try { await navigator.clipboard.writeText(COMMAND); - commandBox.classList.add('copied'); - copyBtn.classList.add('copied'); - hintEl.classList.add('copied'); - const original = hintEl.textContent; - hintEl.textContent = 'copied'; - setTimeout(() => { - commandBox.classList.remove('copied'); - copyBtn.classList.remove('copied'); - hintEl.classList.remove('copied'); - hintEl.textContent = original; - }, 1400); + setIcon(CHECK_PATH); + window.clearTimeout(copyResetTimer); + copyResetTimer = window.setTimeout(() => setIcon(COPY_PATH), 1200); } catch { - // silent — clipboard can be blocked in some contexts + /* clipboard can be blocked; do nothing */ } } @@ -57,3 +83,155 @@ copyBtn.addEventListener('click', (e) => { e.stopPropagation(); copy(); }); + +/* ---------- Cursor-reactive dot field ---------- */ + +const canvas = document.getElementById('field') as HTMLCanvasElement; +const ctx = canvas.getContext('2d')!; + +type Dot = { + bx: number; // base x (CSS px) + by: number; // base y (CSS px) + ox: number; // offset x (current) + oy: number; // offset y (current) +}; + +let dots: Dot[] = []; +let dpr = Math.max(1, window.devicePixelRatio || 1); +let w = 0; +let h = 0; + +const SPACING = 26; +const RADIUS = 170; // influence radius around cursor +const PUSH = 18; // max displacement in px + +// Cursor position (CSS px). Lerped for smoothness. +let targetX = -9999; +let targetY = -9999; +let curX = -9999; +let curY = -9999; +let active = false; + +function build() { + dpr = Math.max(1, window.devicePixelRatio || 1); + w = window.innerWidth; + h = window.innerHeight; + canvas.width = Math.floor(w * dpr); + canvas.height = Math.floor(h * dpr); + canvas.style.width = w + 'px'; + canvas.style.height = h + 'px'; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + dots = []; + // pad outside viewport so the grid feels infinite during displacement + const pad = SPACING; + for (let y = -pad; y < h + pad; y += SPACING) { + for (let x = -pad; x < w + pad; x += SPACING) { + dots.push({ bx: x, by: y, ox: 0, oy: 0 }); + } + } +} + +function onMove(x: number, y: number) { + targetX = x; + targetY = y; + active = true; +} + +window.addEventListener('mousemove', (e) => onMove(e.clientX, e.clientY), { passive: true }); +window.addEventListener('mouseleave', () => { active = false; }); +window.addEventListener('touchmove', (e) => { + if (e.touches[0]) onMove(e.touches[0].clientX, e.touches[0].clientY); +}, { passive: true }); +window.addEventListener('touchend', () => { active = false; }); +window.addEventListener('resize', build); + +build(); + +const BASE_RGB = '230, 230, 230'; +const ACCENT_RGB = '166, 227, 161'; +const BASE_ALPHA = 0.11; +const PEAK_ALPHA = 0.9; + +function frame() { + // smooth cursor lerp + if (!active) { + // ease cursor off-screen so dots settle smoothly when mouse leaves + targetX = -9999; + targetY = -9999; + } + curX += (targetX - curX) * 0.18; + curY += (targetY - curY) * 0.18; + + ctx.clearRect(0, 0, w, h); + + const r2 = RADIUS * RADIUS; + + for (let i = 0; i < dots.length; i++) { + const d = dots[i]; + const dx = d.bx - curX; + const dy = d.by - curY; + const d2 = dx * dx + dy * dy; + + let tx = 0, ty = 0; + let strength = 0; + + if (d2 < r2) { + const dist = Math.sqrt(d2) || 0.0001; + // smooth falloff (ease-out-cubic style) + const t = 1 - dist / RADIUS; + strength = t * t; + // push dots away from cursor + tx = (dx / dist) * strength * PUSH; + ty = (dy / dist) * strength * PUSH; + } + + // smooth toward target offset + d.ox += (tx - d.ox) * 0.18; + d.oy += (ty - d.oy) * 0.18; + + const px = d.bx + d.ox; + const py = d.by + d.oy; + + // alpha + color blend + const alpha = BASE_ALPHA + (PEAK_ALPHA - BASE_ALPHA) * strength; + // size also grows slightly near cursor + const size = 1 + strength * 1.6; + + if (strength > 0.04) { + // blend toward accent near cursor + const blend = strength; + const r = Math.round(230 * (1 - blend) + 166 * blend); + const g = Math.round(230 * (1 - blend) + 227 * blend); + const b = Math.round(230 * (1 - blend) + 161 * blend); + ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`; + } else { + ctx.fillStyle = `rgba(${BASE_RGB}, ${alpha})`; + } + + // draw a tiny square — sharper than circles at this size, also faster + ctx.fillRect(px - size / 2, py - size / 2, size, size); + } + + // a single soft glow that follows the cursor — adds presence + if (active) { + const grad = ctx.createRadialGradient(curX, curY, 0, curX, curY, RADIUS); + grad.addColorStop(0, `rgba(${ACCENT_RGB}, 0.10)`); + grad.addColorStop(1, `rgba(${ACCENT_RGB}, 0)`); + ctx.fillStyle = grad; + ctx.fillRect(curX - RADIUS, curY - RADIUS, RADIUS * 2, RADIUS * 2); + } + + requestAnimationFrame(frame); +} + +if (!reduceMotion) { + requestAnimationFrame(frame); +} else { + // static render: just dim dots, no displacement + ctx.fillStyle = `rgba(${BASE_RGB}, ${BASE_ALPHA})`; + for (const d of dots) ctx.fillRect(d.bx, d.by, 1, 1); +} + +// keep tsc happy about caretEl import (used via DOM) +void caretEl; diff --git a/src/style.css b/src/style.css index ec61da1..9aa993f 100644 --- a/src/style.css +++ b/src/style.css @@ -4,7 +4,7 @@ --dim: #6a6a72; --line: #1c1c20; --accent: #a6e3a1; - --accent-soft: rgba(166, 227, 161, 0.18); + --accent-soft: rgba(166, 227, 161, 0.22); --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; } @@ -14,9 +14,7 @@ box-sizing: border-box; } -html, body { - height: 100%; -} +html, body { height: 100%; } body { font-family: var(--mono); @@ -26,24 +24,16 @@ body { text-rendering: optimizeLegibility; overflow: hidden; position: relative; - /* very subtle dot grid — static, no JS */ - background-image: - radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.045) 1px, transparent 0); - background-size: 22px 22px; } -/* a single, static, very soft glow behind the content — gives depth without movement */ -body::before { - content: ""; +#field { position: fixed; inset: 0; - background: radial-gradient( - 600px 400px at 50% 45%, - rgba(166, 227, 161, 0.06), - transparent 70% - ); - pointer-events: none; + width: 100%; + height: 100%; z-index: 0; + pointer-events: none; + display: block; } .stage { @@ -52,43 +42,30 @@ body::before { min-height: 100vh; display: flex; flex-direction: column; - align-items: flex-start; + align-items: center; justify-content: center; gap: 1.25rem; - max-width: 560px; + max-width: 580px; margin: 0 auto; padding: 2rem 1.75rem; opacity: 0; - animation: fade 600ms ease-out 80ms forwards; + animation: fade 500ms ease-out 60ms forwards; } @keyframes fade { to { opacity: 1; } } -.hello { - color: var(--dim); - font-size: 0.9rem; - letter-spacing: 0.01em; -} - -.command-row { - width: 100%; - display: flex; - align-items: center; - gap: 0.85rem; - flex-wrap: wrap; -} - .command-box { - flex: 1 1 auto; - min-width: 0; - background: rgba(10, 10, 12, 0.6); + width: 100%; + background: rgba(10, 10, 12, 0.55); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); border: 1px solid var(--line); border-radius: 4px; - padding: 0.95rem 1.1rem; - font-size: 1rem; + padding: 1.05rem 1.15rem; + font-size: 1.05rem; font-weight: 500; color: var(--fg); display: flex; @@ -96,7 +73,7 @@ body::before { gap: 0.6rem; cursor: pointer; user-select: none; - transition: border-color 180ms ease, background 180ms ease; + transition: border-color 180ms ease; position: relative; white-space: nowrap; overflow: hidden; @@ -105,14 +82,9 @@ body::before { .command-box:hover, .command-box:focus-visible { border-color: var(--accent-soft); - background: rgba(12, 14, 12, 0.8); outline: none; } -.command-box.copied { - border-color: var(--accent); -} - .dollar { color: var(--accent); font-weight: 600; @@ -123,7 +95,6 @@ body::before { color: var(--fg); white-space: pre; overflow: hidden; - text-overflow: ellipsis; } .caret { @@ -152,28 +123,16 @@ body::before { display: inline-flex; align-items: center; justify-content: center; - transition: color 160ms ease; flex-shrink: 0; } .copy-btn:hover { color: var(--fg); } -.copy-btn.copied { color: var(--accent); } .copy-btn svg { width: 16px; height: 16px; } -.hint { - color: var(--dim); - font-size: 0.78rem; - transition: color 160ms ease, opacity 200ms ease; -} - -.hint.copied { - color: var(--accent); -} - .docs-link { color: var(--dim); text-decoration: none; @@ -181,6 +140,7 @@ body::before { padding: 0.2rem 0; border-bottom: 1px solid transparent; transition: color 160ms ease, border-color 160ms ease; + align-self: center; } .docs-link:hover { @@ -188,33 +148,15 @@ body::before { border-bottom-color: var(--line); } -.byline { - position: fixed; - left: 0; - right: 0; - bottom: 1.25rem; - text-align: center; - color: var(--dim); - font-size: 0.72rem; - letter-spacing: 0.02em; - z-index: 1; - opacity: 0; - animation: fade 600ms ease-out 1100ms forwards; -} - @media (max-width: 640px) { - .stage { - padding: 1.5rem 1.25rem; - gap: 1rem; - } + .stage { padding: 1.5rem 1.25rem; } .command-box { - font-size: 0.9rem; - padding: 0.85rem 0.95rem; + font-size: 0.92rem; + padding: 0.9rem 1rem; } - .hint { display: none; } } @media (prefers-reduced-motion: reduce) { - .stage, .byline { animation: none; opacity: 1; } + .stage { animation: none; opacity: 1; } .caret { animation: none; } }