From 539ebfa102dd80b7187b916d3401ef8081fa02f2 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Sun, 26 Oct 2025 17:49:59 -0400 Subject: [PATCH] making changes to cursor glide over text changing caret cursor behavior and overall making cursor smoother to make typing feel better --- client/src/components/Typing.css | 62 ++++- client/src/components/Typing.jsx | 81 ++++++- public/css/styles.css | 62 ++++- public/js/cursor.js | 388 +++++++++++++++++++++++++++++-- 4 files changed, 563 insertions(+), 30 deletions(-) diff --git a/client/src/components/Typing.css b/client/src/components/Typing.css index b3cded4d..c52770c9 100644 --- a/client/src/components/Typing.css +++ b/client/src/components/Typing.css @@ -324,6 +324,29 @@ animation: blink 1s infinite; } +:root[data-cursor='caret'] .snippet-display .current { + background-color: transparent !important; + color: inherit !important; + font-weight: inherit; + animation: none !important; + text-shadow: none; + transition: color 120ms ease; +} + +:root[data-cursor='caret'] .snippet-display .current.error { + color: var(--error-color); + text-shadow: none; +} + +:root[data-cursor='caret'][data-caret-blink='blink'] .snippet-display .current::before { + animation: caretBlink 1.2s ease-in-out infinite; +} + +:root[data-cursor='caret'][data-caret-blink='solid'] .snippet-display .current::before { + animation: none !important; + opacity: 1; +} + /* Default caret rendering (line cursor) when block cursor is disabled */ .current::before { visibility: var(--line-cursor); @@ -369,6 +392,16 @@ display: none !important; } +:root[data-cursor='caret'] .snippet-display.glide-on .current { + color: inherit !important; + background-color: transparent !important; + animation: none !important; +} + +:root[data-cursor='caret'] .snippet-display.glide-on .current::before { + display: none !important; +} + .snippet-display { position: relative; } @@ -382,9 +415,9 @@ pointer-events: none; z-index: 0; /* behind the text */ transition: - transform var(--cursor-glide-duration, 95ms) cubic-bezier(0.2, 0.8, 0.2, 1), - width var(--cursor-glide-duration, 95ms) cubic-bezier(0.2, 0.8, 0.2, 1), - height var(--cursor-glide-duration, 95ms) cubic-bezier(0.2, 0.8, 0.2, 1); + transform var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1), + width var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1), + height var(--cursor-glide-duration, 90ms) cubic-bezier(0.24, 0.92, 0.35, 1); opacity: calc(var(--glide-cursor-enabled, 0)); /* 0 or 1 set by Settings */ border-radius: 2px; backface-visibility: hidden; @@ -402,6 +435,29 @@ z-index: 2; /* above text so thin caret remains visible */ animation: caretBlink 1.2s ease-in-out infinite; } +:root[data-cursor='caret'][data-caret-blink='solid'] .cursor-overlay.caret { + animation: none !important; + opacity: 1 !important; +} +.cursor-overlay.caret.typing-active { + animation: none !important; + opacity: 1 !important; +} + +.snippet-display.caret-solid .cursor-overlay.caret { + animation: none !important; + opacity: 1 !important; +} + +.snippet-display.caret-solid .current, +.snippet-display.caret-solid .current::before { + animation: none !important; + opacity: 1 !important; +} + +.snippet-display.caret-solid .current::before { + background: var(--caret-color) !important; +} /* Non-glide caret (pseudo) also blinks smoothly */ .current::before { diff --git a/client/src/components/Typing.jsx b/client/src/components/Typing.jsx index aa2fc12a..d4a74d52 100644 --- a/client/src/components/Typing.jsx +++ b/client/src/components/Typing.jsx @@ -1,6 +1,6 @@ // [AI DISCLAIMER: AI was used to help debug socket emit for timed tests; lines 394-408] -import { useState, useEffect, useRef, useLayoutEffect } from 'react'; +import { useState, useEffect, useRef, useLayoutEffect, useCallback } from 'react'; import { useRace } from '../context/RaceContext'; import { useSocket } from '../context/SocketContext'; import playKeySound from './Sound.jsx'; @@ -65,6 +65,57 @@ function Typing({ return (getComputedStyle(document.documentElement).getPropertyValue('--glide-cursor-enabled') || '0').trim() === '1'; }); const initialCursorSetRef = useRef(false); + const initialCursorStyle = typeof document !== 'undefined' + ? (document.documentElement.getAttribute('data-cursor') || 'block') + : 'block'; + const cursorStyleRef = useRef(initialCursorStyle); + const [cursorStyle, setCursorStyle] = useState(initialCursorStyle); + const typingActiveRef = useRef(false); + const [typingActive, setTypingActive] = useState(false); + + const setTypingActiveState = useCallback((active) => { + if (typingActiveRef.current === active) return; + typingActiveRef.current = active; + setTypingActive(active); + if (typeof document !== 'undefined') { + const overlay = cursorRef.current; + if (overlay) { + if (cursorStyleRef.current === 'caret') { + overlay.classList.toggle('typing-active', active); + } else { + overlay.classList.remove('typing-active'); + } + } + const mode = cursorStyleRef.current === 'caret' && active ? 'solid' : 'blink'; + document.documentElement.setAttribute('data-caret-blink', mode); + } + }, []); + + useEffect(() => { + if (typeof document === 'undefined') return undefined; + return () => { + document.documentElement.removeAttribute('data-caret-blink'); + }; + }, []); + + useEffect(() => { + if (typeof document === 'undefined') return undefined; + const root = document.documentElement; + const syncCursorStyle = () => { + const next = root.getAttribute('data-cursor') || 'block'; + cursorStyleRef.current = next; + setCursorStyle(next); + if (next !== 'caret') { + setTypingActiveState(false); + } else if (!typingActiveRef.current) { + document.documentElement.setAttribute('data-caret-blink', 'blink'); + } + }; + syncCursorStyle(); + const observer = new MutationObserver(syncCursorStyle); + observer.observe(root, { attributes: true, attributeFilter: ['data-cursor'] }); + return () => observer.disconnect(); + }, [setTypingActiveState]); // Use testMode and testDuration for timed tests if provided useEffect(() => { @@ -200,6 +251,15 @@ function Typing({ }, [typingState.position]); // Track snippet changes to reset input + useEffect(() => { + const active = ((raceState.inProgress || raceState.type === 'practice') && input.length > 0 && !typingState.completed); + setTypingActiveState(active); + }, [input.length, raceState.inProgress, raceState.type, typingState.completed, setTypingActiveState]); + + useEffect(() => { + return () => setTypingActiveState(false); + }, [setTypingActiveState]); + useEffect(() => { if (raceState.snippet && raceState.snippet.id !== snippetId) { setSnippetId(raceState.snippet.id); @@ -751,17 +811,26 @@ function Typing({ const y = Math.round((rect.top - containerRect.top) + scrollY); // Determine caret vs block based on Settings-managed CSS var - const useCaret = (document.documentElement.getAttribute('data-cursor') === 'caret'); + const useCaret = (cursorStyleRef.current === 'caret'); // Size overlay to target element const height = rect.height; const width = useCaret ? 0 : rect.width; // caret drawn via border-left for crispness overlay.style.height = `${height}px`; overlay.style.width = `${width}px`; - overlay.className = `cursor-overlay ${useCaret ? 'caret' : 'block'}`; + overlay.className = 'cursor-overlay'; + if (useCaret) { + overlay.classList.add('caret'); + overlay.classList.remove('block'); + overlay.classList.toggle('typing-active', typingActiveRef.current); + } else { + overlay.classList.add('block'); + overlay.classList.remove('caret'); + overlay.classList.remove('typing-active'); + } // Cursor-specific duration (caret snappier) - overlay.style.setProperty('--cursor-glide-duration', useCaret ? '95ms' : '95ms'); + overlay.style.setProperty('--cursor-glide-duration', useCaret ? '85ms' : '110ms'); // First placement should not animate from origin if (!initialCursorSetRef.current) { @@ -1019,6 +1088,8 @@ function Typing({ }; }, []); + const caretSolid = typingActive && cursorStyle === 'caret'; + return ( <>
{getStatsContent()}
@@ -1026,7 +1097,7 @@ function Typing({ {/* Only show typing area (snippet + input) if race is NOT completed */} {!raceState.completed && (
-
+
{/* Smooth-glide overlay cursor (rendered behind text) */}