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
This commit is contained in:
@@ -21,10 +21,11 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/docs.html" class="docs-link">how it works →</a>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<a href="/docs.html" class="help-flag" id="helpFlag" aria-label="how it works">-h</a>
|
||||||
|
<span class="byline" id="byline">// vibe coded to present human code</span>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
94
src/main.ts
94
src/main.ts
@@ -24,19 +24,19 @@ function typeWithScramble(target: string, onDone?: () => void) {
|
|||||||
onDone?.();
|
onDone?.();
|
||||||
return;
|
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() {
|
function flicker() {
|
||||||
if (cycles <= 0) {
|
if (cycles <= 0) {
|
||||||
resolved += target[i];
|
resolved += target[i];
|
||||||
typedEl.textContent = resolved;
|
typedEl.textContent = resolved;
|
||||||
i++;
|
i++;
|
||||||
setTimeout(nextChar, 22 + Math.random() * 30);
|
setTimeout(nextChar, 8 + Math.random() * 18);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ch = SCRAMBLE[Math.floor(Math.random() * SCRAMBLE.length)];
|
const ch = SCRAMBLE[Math.floor(Math.random() * SCRAMBLE.length)];
|
||||||
typedEl.textContent = resolved + ch;
|
typedEl.textContent = resolved + ch;
|
||||||
cycles--;
|
cycles--;
|
||||||
setTimeout(flicker, 28);
|
setTimeout(flicker, 14);
|
||||||
}
|
}
|
||||||
flicker();
|
flicker();
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ function typeWithScramble(target: string, onDone?: () => void) {
|
|||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
typedEl.textContent = COMMAND;
|
typedEl.textContent = COMMAND;
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => typeWithScramble(COMMAND), 280);
|
setTimeout(() => typeWithScramble(COMMAND), 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Silent copy (no flashy feedback, just a brief icon swap) ---------- */
|
/* ---------- Silent copy (no flashy feedback, just a brief icon swap) ---------- */
|
||||||
@@ -112,6 +112,14 @@ let curX = -9999;
|
|||||||
let curY = -9999;
|
let curY = -9999;
|
||||||
let active = false;
|
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() {
|
function build() {
|
||||||
dpr = Math.max(1, window.devicePixelRatio || 1);
|
dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
w = window.innerWidth;
|
w = window.innerWidth;
|
||||||
@@ -138,11 +146,21 @@ function onMove(x: number, y: number) {
|
|||||||
active = true;
|
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('mousemove', (e) => onMove(e.clientX, e.clientY), { passive: true });
|
||||||
window.addEventListener('mouseleave', () => { active = false; });
|
window.addEventListener('mouseleave', () => { active = false; });
|
||||||
|
window.addEventListener('click', (e) => spawnRipple(e.clientX, e.clientY));
|
||||||
window.addEventListener('touchmove', (e) => {
|
window.addEventListener('touchmove', (e) => {
|
||||||
if (e.touches[0]) onMove(e.touches[0].clientX, e.touches[0].clientY);
|
if (e.touches[0]) onMove(e.touches[0].clientX, e.touches[0].clientY);
|
||||||
}, { passive: true });
|
}, { 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('touchend', () => { active = false; });
|
||||||
window.addEventListener('resize', build);
|
window.addEventListener('resize', build);
|
||||||
|
|
||||||
@@ -154,21 +172,37 @@ const BASE_ALPHA = 0.11;
|
|||||||
const PEAK_ALPHA = 0.9;
|
const PEAK_ALPHA = 0.9;
|
||||||
|
|
||||||
function frame() {
|
function frame() {
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
// smooth cursor lerp
|
// smooth cursor lerp
|
||||||
if (!active) {
|
if (!active) {
|
||||||
// ease cursor off-screen so dots settle smoothly when mouse leaves
|
|
||||||
targetX = -9999;
|
targetX = -9999;
|
||||||
targetY = -9999;
|
targetY = -9999;
|
||||||
}
|
}
|
||||||
curX += (targetX - curX) * 0.18;
|
curX += (targetX - curX) * 0.18;
|
||||||
curY += (targetY - curY) * 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);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
const r2 = RADIUS * RADIUS;
|
const r2 = RADIUS * RADIUS;
|
||||||
|
|
||||||
for (let i = 0; i < dots.length; i++) {
|
for (let i = 0; i < dots.length; i++) {
|
||||||
const d = dots[i];
|
const d = dots[i];
|
||||||
|
|
||||||
|
// --- cursor displacement ---
|
||||||
const dx = d.bx - curX;
|
const dx = d.bx - curX;
|
||||||
const dy = d.by - curY;
|
const dy = d.by - curY;
|
||||||
const d2 = dx * dx + dy * dy;
|
const d2 = dx * dx + dy * dy;
|
||||||
@@ -178,29 +212,43 @@ function frame() {
|
|||||||
|
|
||||||
if (d2 < r2) {
|
if (d2 < r2) {
|
||||||
const dist = Math.sqrt(d2) || 0.0001;
|
const dist = Math.sqrt(d2) || 0.0001;
|
||||||
// smooth falloff (ease-out-cubic style)
|
|
||||||
const t = 1 - dist / RADIUS;
|
const t = 1 - dist / RADIUS;
|
||||||
strength = t * t;
|
strength = t * t;
|
||||||
// push dots away from cursor
|
|
||||||
tx = (dx / dist) * strength * PUSH;
|
tx = (dx / dist) * strength * PUSH;
|
||||||
ty = (dy / 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
|
// smooth toward target offset
|
||||||
d.ox += (tx - d.ox) * 0.18;
|
d.ox += (tx - d.ox) * 0.22;
|
||||||
d.oy += (ty - d.oy) * 0.18;
|
d.oy += (ty - d.oy) * 0.22;
|
||||||
|
|
||||||
const px = d.bx + d.ox;
|
const px = d.bx + d.ox;
|
||||||
const py = d.by + d.oy;
|
const py = d.by + d.oy;
|
||||||
|
|
||||||
// alpha + color blend
|
// combined visual strength (cursor + ripple, capped)
|
||||||
const alpha = BASE_ALPHA + (PEAK_ALPHA - BASE_ALPHA) * strength;
|
const visStrength = Math.min(1, strength + rippleStrength);
|
||||||
// size also grows slightly near cursor
|
const alpha = BASE_ALPHA + (PEAK_ALPHA - BASE_ALPHA) * visStrength;
|
||||||
const size = 1 + strength * 1.6;
|
const size = 1 + visStrength * 1.6;
|
||||||
|
|
||||||
if (strength > 0.04) {
|
if (visStrength > 0.04) {
|
||||||
// blend toward accent near cursor
|
const blend = visStrength;
|
||||||
const blend = strength;
|
|
||||||
const r = Math.round(230 * (1 - blend) + 166 * blend);
|
const r = Math.round(230 * (1 - blend) + 166 * blend);
|
||||||
const g = Math.round(230 * (1 - blend) + 227 * blend);
|
const g = Math.round(230 * (1 - blend) + 227 * blend);
|
||||||
const b = Math.round(230 * (1 - blend) + 161 * blend);
|
const b = Math.round(230 * (1 - blend) + 161 * blend);
|
||||||
@@ -209,11 +257,10 @@ function frame() {
|
|||||||
ctx.fillStyle = `rgba(${BASE_RGB}, ${alpha})`;
|
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);
|
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) {
|
if (active) {
|
||||||
const grad = ctx.createRadialGradient(curX, curY, 0, curX, curY, RADIUS);
|
const grad = ctx.createRadialGradient(curX, curY, 0, curX, curY, RADIUS);
|
||||||
grad.addColorStop(0, `rgba(${ACCENT_RGB}, 0.10)`);
|
grad.addColorStop(0, `rgba(${ACCENT_RGB}, 0.10)`);
|
||||||
@@ -222,6 +269,17 @@ function frame() {
|
|||||||
ctx.fillRect(curX - RADIUS, curY - RADIUS, RADIUS * 2, RADIUS * 2);
|
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);
|
requestAnimationFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,19 +133,45 @@ body {
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-link {
|
/* tiny "-h" flag — shows up late, sits in the bottom-left */
|
||||||
color: var(--dim);
|
.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;
|
text-decoration: none;
|
||||||
font-size: 0.85rem;
|
padding: 0.15rem 0.3rem;
|
||||||
padding: 0.2rem 0;
|
opacity: 0;
|
||||||
border-bottom: 1px solid transparent;
|
transition: color 200ms ease, opacity 200ms ease;
|
||||||
transition: color 160ms ease, border-color 160ms ease;
|
animation: fadeFaint 700ms ease-out 2200ms forwards;
|
||||||
align-self: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-link:hover {
|
.help-flag:hover { color: var(--fg); }
|
||||||
color: var(--fg);
|
|
||||||
border-bottom-color: var(--line);
|
/* 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) {
|
@media (max-width: 640px) {
|
||||||
@@ -154,9 +180,12 @@ body {
|
|||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.9rem 1rem;
|
||||||
}
|
}
|
||||||
|
.help-flag { font-size: 0.65rem; }
|
||||||
|
.byline { font-size: 0.56rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.stage { animation: none; opacity: 1; }
|
.stage { animation: none; opacity: 1; }
|
||||||
.caret { animation: none; }
|
.caret { animation: none; }
|
||||||
|
.help-flag, .byline { animation: none; opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user