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
This commit is contained in:
keshavananddev
2026-04-24 03:37:24 +00:00
parent b79bf1d4ca
commit d5fde28b15
4 changed files with 236 additions and 120 deletions

View File

@@ -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;