From 9c878fb1d0610f6a0b86a2e0886b4642efa57f25 Mon Sep 17 00:00:00 2001 From: keshavananddev <53607429-keshavananddev@users.noreply.replit.com> Date: Fri, 24 Apr 2026 03:42:49 +0000 Subject: [PATCH] Enhance typing animation and background interaction with subtle new elements Speed up the typing animation, introduce click-based ripple effects on the background dots, and replace the "how it works" link with a minimal "-h" flag and a faint byline. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 6dcd9377-c004-4fa1-82a2-b1e55f7e9e41 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/iilSvgd Replit-Helium-Checkpoint-Created: true --- index.html | 5 +-- src/main.ts | 94 +++++++++++++++++++++++++++++++++++++++++---------- src/style.css | 49 +++++++++++++++++++++------ 3 files changed, 118 insertions(+), 30 deletions(-) diff --git a/index.html b/index.html index a2b0ec6..43b008d 100644 --- a/index.html +++ b/index.html @@ -21,10 +21,11 @@ - - how it works → + -h + // vibe coded to present human code + diff --git a/src/main.ts b/src/main.ts index e264bdb..8f0f130 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,19 +24,19 @@ function typeWithScramble(target: string, onDone?: () => void) { onDone?.(); return; } - let cycles = target[i] === ' ' ? 0 : 2 + Math.floor(Math.random() * 2); + let cycles = target[i] === ' ' ? 0 : 1 + Math.floor(Math.random() * 2); function flicker() { if (cycles <= 0) { resolved += target[i]; typedEl.textContent = resolved; i++; - setTimeout(nextChar, 22 + Math.random() * 30); + setTimeout(nextChar, 8 + Math.random() * 18); return; } const ch = SCRAMBLE[Math.floor(Math.random() * SCRAMBLE.length)]; typedEl.textContent = resolved + ch; cycles--; - setTimeout(flicker, 28); + setTimeout(flicker, 14); } flicker(); } @@ -46,7 +46,7 @@ function typeWithScramble(target: string, onDone?: () => void) { if (reduceMotion) { typedEl.textContent = COMMAND; } else { - setTimeout(() => typeWithScramble(COMMAND), 280); + setTimeout(() => typeWithScramble(COMMAND), 180); } /* ---------- Silent copy (no flashy feedback, just a brief icon swap) ---------- */ @@ -112,6 +112,14 @@ let curX = -9999; let curY = -9999; let active = false; +// Click ripples — expanding rings that push dots outward as they pass. +type Ripple = { x: number; y: number; born: number }; +const ripples: Ripple[] = []; +const RIPPLE_LIFE = 900; // ms +const RIPPLE_MAX = 520; // max radius in px +const RIPPLE_BAND = 70; // ring thickness +const RIPPLE_PUSH = 36; // peak displacement + function build() { dpr = Math.max(1, window.devicePixelRatio || 1); w = window.innerWidth; @@ -138,11 +146,21 @@ function onMove(x: number, y: number) { active = true; } +function spawnRipple(x: number, y: number) { + ripples.push({ x, y, born: performance.now() }); + // cap to keep things light + if (ripples.length > 8) ripples.shift(); +} + window.addEventListener('mousemove', (e) => onMove(e.clientX, e.clientY), { passive: true }); window.addEventListener('mouseleave', () => { active = false; }); +window.addEventListener('click', (e) => spawnRipple(e.clientX, e.clientY)); window.addEventListener('touchmove', (e) => { if (e.touches[0]) onMove(e.touches[0].clientX, e.touches[0].clientY); }, { passive: true }); +window.addEventListener('touchstart', (e) => { + if (e.touches[0]) spawnRipple(e.touches[0].clientX, e.touches[0].clientY); +}, { passive: true }); window.addEventListener('touchend', () => { active = false; }); window.addEventListener('resize', build); @@ -154,21 +172,37 @@ const BASE_ALPHA = 0.11; const PEAK_ALPHA = 0.9; function frame() { + const now = performance.now(); + // 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; + // expire dead ripples + for (let i = ripples.length - 1; i >= 0; i--) { + if (now - ripples[i].born > RIPPLE_LIFE) ripples.splice(i, 1); + } + + // precompute ripple state + const rippleState = ripples.map(rp => { + const p = (now - rp.born) / RIPPLE_LIFE; // 0..1 + const radius = p * RIPPLE_MAX; + const fade = 1 - p; // weakens over time + return { x: rp.x, y: rp.y, radius, fade }; + }); + ctx.clearRect(0, 0, w, h); const r2 = RADIUS * RADIUS; for (let i = 0; i < dots.length; i++) { const d = dots[i]; + + // --- cursor displacement --- const dx = d.bx - curX; const dy = d.by - curY; const d2 = dx * dx + dy * dy; @@ -178,29 +212,43 @@ function frame() { 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; } + // --- ripple displacement (additive) --- + let rippleStrength = 0; + for (let k = 0; k < rippleState.length; k++) { + const rp = rippleState[k]; + const rdx = d.bx - rp.x; + const rdy = d.by - rp.y; + const rdist = Math.sqrt(rdx * rdx + rdy * rdy) || 0.0001; + const delta = Math.abs(rdist - rp.radius); + if (delta < RIPPLE_BAND) { + const ringT = 1 - delta / RIPPLE_BAND; // 0..1 across band + const s = ringT * ringT * rp.fade; + tx += (rdx / rdist) * s * RIPPLE_PUSH; + ty += (rdy / rdist) * s * RIPPLE_PUSH; + if (s > rippleStrength) rippleStrength = s; + } + } + // smooth toward target offset - d.ox += (tx - d.ox) * 0.18; - d.oy += (ty - d.oy) * 0.18; + d.ox += (tx - d.ox) * 0.22; + d.oy += (ty - d.oy) * 0.22; 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; + // combined visual strength (cursor + ripple, capped) + const visStrength = Math.min(1, strength + rippleStrength); + const alpha = BASE_ALPHA + (PEAK_ALPHA - BASE_ALPHA) * visStrength; + const size = 1 + visStrength * 1.6; - if (strength > 0.04) { - // blend toward accent near cursor - const blend = strength; + if (visStrength > 0.04) { + const blend = visStrength; 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); @@ -209,11 +257,10 @@ function frame() { 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 + // soft cursor glow if (active) { const grad = ctx.createRadialGradient(curX, curY, 0, curX, curY, RADIUS); grad.addColorStop(0, `rgba(${ACCENT_RGB}, 0.10)`); @@ -222,6 +269,17 @@ function frame() { ctx.fillRect(curX - RADIUS, curY - RADIUS, RADIUS * 2, RADIUS * 2); } + // ripple ring outlines — very faint, just a hint + for (let k = 0; k < rippleState.length; k++) { + const rp = rippleState[k]; + if (rp.radius < 4) continue; + ctx.strokeStyle = `rgba(${ACCENT_RGB}, ${0.16 * rp.fade})`; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(rp.x, rp.y, rp.radius, 0, Math.PI * 2); + ctx.stroke(); + } + requestAnimationFrame(frame); } diff --git a/src/style.css b/src/style.css index 9aa993f..72b2b67 100644 --- a/src/style.css +++ b/src/style.css @@ -133,19 +133,45 @@ body { height: 16px; } -.docs-link { - color: var(--dim); +/* tiny "-h" flag — shows up late, sits in the bottom-left */ +.help-flag { + position: fixed; + left: 1rem; + bottom: 1rem; + z-index: 1; + font-family: var(--mono); + font-size: 0.7rem; + letter-spacing: 0.02em; + color: rgba(106, 106, 114, 0.55); text-decoration: none; - font-size: 0.85rem; - padding: 0.2rem 0; - border-bottom: 1px solid transparent; - transition: color 160ms ease, border-color 160ms ease; - align-self: center; + padding: 0.15rem 0.3rem; + opacity: 0; + transition: color 200ms ease, opacity 200ms ease; + animation: fadeFaint 700ms ease-out 2200ms forwards; } -.docs-link:hover { - color: var(--fg); - border-bottom-color: var(--line); +.help-flag:hover { color: var(--fg); } + +/* even fainter — bottom-right */ +.byline { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 1; + font-family: var(--mono); + font-size: 0.62rem; + letter-spacing: 0.02em; + color: rgba(106, 106, 114, 0.28); + pointer-events: none; + opacity: 0; + animation: fadeWhisper 900ms ease-out 2700ms forwards; +} + +@keyframes fadeFaint { + to { opacity: 1; } +} +@keyframes fadeWhisper { + to { opacity: 1; } } @media (max-width: 640px) { @@ -154,9 +180,12 @@ body { font-size: 0.92rem; padding: 0.9rem 1rem; } + .help-flag { font-size: 0.65rem; } + .byline { font-size: 0.56rem; } } @media (prefers-reduced-motion: reduce) { .stage { animation: none; opacity: 1; } .caret { animation: none; } + .help-flag, .byline { animation: none; opacity: 1; } }