Improve command input and "how it works" animation
Fixes cursor positioning errors by explicitly managing the space between the command prompt and user input, and enhances the "how it works" animation with glitch effects and a more dynamic visual sequence. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 6def8112-39d2-4641-b93b-f39108179f33 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9956cf72-7ee5-4600-91e5-0d22e4fbc583 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/42ae33dd-8759-4196-85a5-434465c72ece/6def8112-39d2-4641-b93b-f39108179f33/HyFeWjl Replit-Helium-Checkpoint-Created: true
This commit is contained in:
BIN
attached_assets/image_1777002696453.png
Normal file
BIN
attached_assets/image_1777002696453.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<main class="stage">
|
<main class="stage">
|
||||||
<div class="command-box" id="commandBox">
|
<div class="command-box" id="commandBox">
|
||||||
<span class="dollar">$</span><span class="typed" id="typed"></span><span class="user-input" id="userInput"></span><span class="caret" id="caret">█</span>
|
<span class="dollar">$</span><span class="typed" id="typed"></span><span class="prompt-space" aria-hidden="true"> </span><span class="user-input" id="userInput"></span><span class="caret" id="caret" aria-hidden="true"></span>
|
||||||
<button class="action-btn" id="actionBtn" aria-label="Copy to clipboard">
|
<button class="action-btn" id="actionBtn" aria-label="Copy to clipboard">
|
||||||
<svg id="actionIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg id="actionIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path id="iconPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
|
<path id="iconPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
|
||||||
|
|||||||
71
src/main.ts
71
src/main.ts
@@ -1,7 +1,6 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
const COMMAND = 'ssh portfolio@keshavanand.net';
|
const COMMAND = 'ssh portfolio@keshavanand.net';
|
||||||
const PROMPT_TAIL = ' '; // single space between prefix and user input
|
|
||||||
|
|
||||||
const typedEl = document.getElementById('typed') as HTMLSpanElement;
|
const typedEl = document.getElementById('typed') as HTMLSpanElement;
|
||||||
const userEl = document.getElementById('userInput') as HTMLSpanElement;
|
const userEl = document.getElementById('userInput') as HTMLSpanElement;
|
||||||
@@ -58,7 +57,7 @@ function typeIntro(target: string, onDone?: () => void) {
|
|||||||
|
|
||||||
function nextChar() {
|
function nextChar() {
|
||||||
if (i >= target.length) {
|
if (i >= target.length) {
|
||||||
typedEl.textContent = resolved + PROMPT_TAIL;
|
typedEl.textContent = resolved;
|
||||||
onDone?.();
|
onDone?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -82,7 +81,7 @@ function typeIntro(target: string, onDone?: () => void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
typedEl.textContent = COMMAND + PROMPT_TAIL;
|
typedEl.textContent = COMMAND;
|
||||||
inputEnabled = true;
|
inputEnabled = true;
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => typeIntro(COMMAND, () => {
|
setTimeout(() => typeIntro(COMMAND, () => {
|
||||||
@@ -206,40 +205,68 @@ commandBox.addEventListener('click', (e) => {
|
|||||||
|
|
||||||
/* ---------- auto-type from "how it works" link ---------- */
|
/* ---------- auto-type from "how it works" link ---------- */
|
||||||
|
|
||||||
|
function rand<T>(a: ArrayLike<T>): T {
|
||||||
|
return a[Math.floor(Math.random() * a.length)];
|
||||||
|
}
|
||||||
|
|
||||||
async function autoTypeHelp() {
|
async function autoTypeHelp() {
|
||||||
if (!inputEnabled || isAutoTyping) return;
|
if (!inputEnabled || isAutoTyping) return;
|
||||||
isAutoTyping = true;
|
isAutoTyping = true;
|
||||||
|
|
||||||
// clear whatever the user had typed
|
// clear current input, arm the box
|
||||||
userInput = '';
|
userInput = '';
|
||||||
refreshUI();
|
refreshUI();
|
||||||
commandBox.classList.add('armed');
|
commandBox.classList.add('armed');
|
||||||
|
|
||||||
await sleep(120);
|
// ripple from where the user clicked the link
|
||||||
|
const linkRect = docsLink.getBoundingClientRect();
|
||||||
|
spawnRipple(linkRect.left + linkRect.width / 2, linkRect.top + linkRect.height / 2);
|
||||||
|
|
||||||
const text = '-h';
|
await sleep(90);
|
||||||
for (const ch of text) {
|
|
||||||
// small scramble for flair, then settle
|
// PHASE 1 — heavy glitch flicker across both character slots,
|
||||||
let cycles = 2;
|
// with a tiny box jitter for chromatic-aberration energy
|
||||||
while (cycles > 0) {
|
commandBox.classList.add('glitch');
|
||||||
const sc = SCRAMBLE[Math.floor(Math.random() * SCRAMBLE.length)];
|
const glitchSteps = 14;
|
||||||
userEl.textContent = userInput + sc;
|
for (let s = 0; s < glitchSteps; s++) {
|
||||||
await sleep(38);
|
const a = rand(SCRAMBLE);
|
||||||
cycles--;
|
const b = Math.random() > 0.4 ? rand(SCRAMBLE) : '';
|
||||||
|
userEl.textContent = a + b;
|
||||||
|
// micro-jitter the box
|
||||||
|
const jx = (Math.random() - 0.5) * 3;
|
||||||
|
const jy = (Math.random() - 0.5) * 1.5;
|
||||||
|
commandBox.style.transform = `translate(${jx.toFixed(2)}px, ${jy.toFixed(2)}px)`;
|
||||||
|
await sleep(28 + Math.random() * 18);
|
||||||
|
}
|
||||||
|
commandBox.style.transform = '';
|
||||||
|
commandBox.classList.remove('glitch');
|
||||||
|
|
||||||
|
// PHASE 2 — settle to "-h" with a brief micro-flicker on each char
|
||||||
|
userInput = '';
|
||||||
|
for (const ch of '-h') {
|
||||||
|
// 1-2 quick scramble flickers, then resolve
|
||||||
|
for (let c = 0; c < 2; c++) {
|
||||||
|
userEl.textContent = userInput + rand(SCRAMBLE);
|
||||||
|
await sleep(34);
|
||||||
}
|
}
|
||||||
userInput += ch;
|
userInput += ch;
|
||||||
refreshUI();
|
refreshUI();
|
||||||
await sleep(70);
|
await sleep(60);
|
||||||
}
|
}
|
||||||
|
|
||||||
// brief pause so the user reads it
|
// PHASE 3 — quick read pause + ripple from the box itself
|
||||||
await sleep(260);
|
await sleep(220);
|
||||||
|
const boxRect = commandBox.getBoundingClientRect();
|
||||||
|
spawnRipple(boxRect.right - 24, boxRect.top + boxRect.height / 2);
|
||||||
|
|
||||||
// visual "Enter pressed" — pulse the action button
|
// PHASE 4 — Enter "press": punch the action icon down + a confirming ripple
|
||||||
actionBtn.style.transform = 'scale(0.85)';
|
actionBtn.style.transition = 'transform 90ms ease-out';
|
||||||
setTimeout(() => { actionBtn.style.transform = ''; }, 140);
|
actionBtn.style.transform = 'scale(0.78)';
|
||||||
|
await sleep(110);
|
||||||
await sleep(180);
|
actionBtn.style.transform = 'scale(1)';
|
||||||
|
await sleep(140);
|
||||||
|
actionBtn.style.transition = '';
|
||||||
|
actionBtn.style.transform = '';
|
||||||
|
|
||||||
isAutoTyping = false;
|
isAutoTyping = false;
|
||||||
submit();
|
submit();
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ body {
|
|||||||
box-shadow: 0 0 0 1px var(--accent-soft), 0 0 28px rgba(166, 227, 161, 0.18);
|
box-shadow: 0 0 0 1px var(--accent-soft), 0 0 28px rgba(166, 227, 161, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* glitch state — subtle chromatic-aberration shadow on the typed text */
|
||||||
|
.command-box.glitch {
|
||||||
|
border-color: var(--accent-strong);
|
||||||
|
box-shadow: 0 0 0 1px rgba(166, 227, 161, 0.10), 0 0 32px rgba(166, 227, 161, 0.10);
|
||||||
|
}
|
||||||
|
.command-box.glitch .typed,
|
||||||
|
.command-box.glitch .user-input {
|
||||||
|
text-shadow:
|
||||||
|
-1px 0 rgba(243, 139, 168, 0.7),
|
||||||
|
1px 0 rgba(137, 180, 250, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.dollar {
|
.dollar {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -122,17 +134,23 @@ body {
|
|||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prompt-space {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
.user-input {
|
.user-input {
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* a real terminal block caret — exactly one monospace char wide, no offset */
|
||||||
.caret {
|
.caret {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--accent);
|
width: 0.6em;
|
||||||
font-size: 0.85em;
|
height: 1.05em;
|
||||||
transform: translateY(-1px);
|
background: var(--accent);
|
||||||
margin-left: 1px;
|
vertical-align: text-bottom;
|
||||||
|
margin-bottom: 0.05em;
|
||||||
animation: blink 1.05s steps(1, end) infinite;
|
animation: blink 1.05s steps(1, end) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user