add: FancyCursor
This commit is contained in:
File diff suppressed because one or more lines are too long
128
build/assets/index-BR63qlJT.js
Normal file
128
build/assets/index-BR63qlJT.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-CKQM2HIs.css
Normal file
1
build/assets/index-CKQM2HIs.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
9
build/assets/three-1foLNQd8.js
Normal file
9
build/assets/three-1foLNQd8.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -10,10 +10,10 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-VO0wVEDp.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/three-eMwtdKkp.js">
|
||||
<script type="module" crossorigin src="/assets/index-BR63qlJT.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/three-1foLNQd8.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/motion-BIOHP8Ul.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BAFm1LaZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CKQM2HIs.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { LanguageProvider } from './i18n';
|
||||
import { Navbar, Footer, CustomCursor } from './components/layout';
|
||||
import { Navbar, Footer, FancyCursor } from './components/layout';
|
||||
import { Home, About, Contact } from './pages';
|
||||
import './styles/global.css';
|
||||
|
||||
@@ -8,7 +8,7 @@ export function App() {
|
||||
return (
|
||||
<LanguageProvider>
|
||||
<BrowserRouter>
|
||||
<CustomCursor />
|
||||
<FancyCursor />
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
.cursor {
|
||||
position: fixed;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
transform: translate(-50%, -50%);
|
||||
transition:
|
||||
width 0.15s ease-out,
|
||||
height 0.15s ease-out,
|
||||
border-color 0.15s ease-out,
|
||||
background-color 0.15s ease-out,
|
||||
opacity 0.15s ease-out;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.cursorDot {
|
||||
position: fixed;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
.cursor.pointer {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: rgba(127, 217, 152, 0.1);
|
||||
}
|
||||
|
||||
.cursor.clicking {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-color: rgba(127, 217, 152, 0.2);
|
||||
}
|
||||
|
||||
.cursor.hidden,
|
||||
.cursorDot.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Hide on touch devices and when reduced motion is preferred */
|
||||
@media (hover: none), (prefers-reduced-motion: reduce) {
|
||||
.cursor,
|
||||
.cursorDot {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add global cursor hide for devices with custom cursor */
|
||||
@media (hover: hover) {
|
||||
:global(body) {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
:global(a),
|
||||
:global(button) {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import styles from './CustomCursor.module.css';
|
||||
|
||||
export function CustomCursor() {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [isPointer, setIsPointer] = useState(false);
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
const [isClicking, setIsClicking] = useState(false);
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
setPosition({ x: e.clientX, y: e.clientY });
|
||||
setIsHidden(false);
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
const isClickable =
|
||||
target.tagName === 'A' ||
|
||||
target.tagName === 'BUTTON' ||
|
||||
!!target.closest('a') ||
|
||||
!!target.closest('button') ||
|
||||
window.getComputedStyle(target).cursor === 'pointer';
|
||||
|
||||
setIsPointer(isClickable);
|
||||
}, []);
|
||||
|
||||
const handleMouseDown = useCallback(() => setIsClicking(true), []);
|
||||
const handleMouseUp = useCallback(() => setIsClicking(false), []);
|
||||
const handleMouseLeave = useCallback(() => setIsHidden(true), []);
|
||||
const handleMouseEnter = useCallback(() => setIsHidden(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
// Don't show custom cursor on touch devices
|
||||
if (window.matchMedia('(hover: none)').matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mousedown', handleMouseDown);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.addEventListener('mouseleave', handleMouseLeave);
|
||||
document.addEventListener('mouseenter', handleMouseEnter);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mousedown', handleMouseDown);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.removeEventListener('mouseleave', handleMouseLeave);
|
||||
document.removeEventListener('mouseenter', handleMouseEnter);
|
||||
};
|
||||
}, [handleMouseMove, handleMouseDown, handleMouseUp, handleMouseLeave, handleMouseEnter]);
|
||||
|
||||
// Don't render on touch devices
|
||||
if (typeof window !== 'undefined' && window.matchMedia('(hover: none)').matches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${styles.cursor} ${isPointer ? styles.pointer : ''} ${isHidden ? styles.hidden : ''} ${isClicking ? styles.clicking : ''}`}
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.cursorDot} ${isHidden ? styles.hidden : ''}`}
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
77
src/components/layout/FancyCursor.css
Normal file
77
src/components/layout/FancyCursor.css
Normal file
@@ -0,0 +1,77 @@
|
||||
:root {
|
||||
--cursor-size: 20px;
|
||||
--cursor-dot-size: 8px;
|
||||
}
|
||||
|
||||
#fancy-cursor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: difference;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fancy-cursor-dot {
|
||||
width: var(--cursor-dot-size);
|
||||
height: var(--cursor-dot-size);
|
||||
background-color: var(--md-sys-color-primary);
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s ease, opacity 0.2s, width 0.2s, height 0.2s, border-radius 0.2s;
|
||||
}
|
||||
|
||||
#fancy-cursor svg {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
opacity: 0;
|
||||
transform: scale(0.5) rotate(0deg);
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
stroke: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* Link Hover State */
|
||||
#fancy-cursor.link-hover .fancy-cursor-dot {
|
||||
transform: scale(2.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Text Hover State */
|
||||
#fancy-cursor.text-hover .fancy-cursor-dot {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
border-radius: 1px;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Resize Hover State */
|
||||
#fancy-cursor.resize-hover .fancy-cursor-dot {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#fancy-cursor.resize-hover svg {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
/* Global cursor hiding */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
body,
|
||||
a,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
[role="button"] {
|
||||
cursor: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide custom cursor on touch/no-hover devices */
|
||||
@media (hover: none) {
|
||||
#fancy-cursor {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
75
src/components/layout/FancyCursor.tsx
Normal file
75
src/components/layout/FancyCursor.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import './FancyCursor.css';
|
||||
|
||||
// This is the new, more performant implementation for the custom cursor.
|
||||
// It uses requestAnimationFrame for smooth movement and direct DOM manipulation for speed.
|
||||
export const FancyCursor = memo(() => {
|
||||
const cursorRef = useRef<HTMLDivElement>(null);
|
||||
const [isTouch, setIsTouch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for touch devices once at the beginning. If it's a touch device,
|
||||
// the custom cursor will not be rendered.
|
||||
if (typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0)) {
|
||||
setIsTouch(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = cursorRef.current;
|
||||
if (!cursor) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
|
||||
// The mouse move handler is throttled with requestAnimationFrame to ensure
|
||||
// the animation is smooth and doesn't cause performance issues.
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = requestAnimationFrame(() => {
|
||||
// The offset issue was caused by positioning the cursor's top-left corner
|
||||
// at the mouse coordinates. To fix this, `translate(-50%, -50%)` is added.
|
||||
// This shifts the cursor element by half its own width and height,
|
||||
// which effectively centers it on the pointer without affecting the visuals.
|
||||
cursor.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) translate(-50%, -50%)`;
|
||||
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
const classList = cursor.classList;
|
||||
|
||||
// Coercing to boolean with `!!` is a micro-optimization.
|
||||
const isResize = !!el?.closest('#custom-resize-handle');
|
||||
const isText = !!el?.closest('input[type="text"], input[type="email"], textarea, [contenteditable="true"]');
|
||||
const isLink = !!el?.closest('a, button, [role="button"], input[type="submit"], input[type="button"]');
|
||||
|
||||
// These classes are toggled based on the hovered element.
|
||||
// The actual visual styles are defined in your CSS files.
|
||||
classList.toggle('resize-hover', isResize);
|
||||
classList.toggle('text-hover', isText && !isResize);
|
||||
classList.toggle('link-hover', isLink && !isText && !isResize);
|
||||
});
|
||||
};
|
||||
|
||||
// Using a passive event listener can improve scrolling performance.
|
||||
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
||||
|
||||
// The cleanup function removes the event listener when the component unmounts
|
||||
// to prevent memory leaks.
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []); // The empty dependency array ensures this effect runs only once.
|
||||
|
||||
if (isTouch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
// `will-change` is a hint to the browser to optimize for transform changes.
|
||||
<div ref={cursorRef} id="fancy-cursor" style={{ willChange: 'transform' }}>
|
||||
<div className="fancy-cursor-dot" />
|
||||
{/* SVG updated to use currentColor to inherit from CSS */}
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 19L19 5M5 19L9 19M5 19L5 15M19 5L15 5M19 5L19 9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
export { Navbar } from './Navbar';
|
||||
export { Footer } from './Footer';
|
||||
export { CustomCursor } from './CustomCursor';
|
||||
export { FancyCursor } from './FancyCursor';
|
||||
|
||||
Reference in New Issue
Block a user