You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
196 lines
5.2 KiB
TypeScript
196 lines
5.2 KiB
TypeScript
import { smooth } from '@/utils/smooth';
|
|
import useFrame from '@/utils/useFrame';
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
type CursorEffectProps = {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
effectDistance?: number;
|
|
effectForce?: number;
|
|
cursorPadding?: number;
|
|
cursorBorderRadius?: number;
|
|
};
|
|
|
|
const clamp = (value: number, min: number, max: number) => {
|
|
const minOffset = Math.max(value - min, 0);
|
|
|
|
return Math.min(minOffset / (max - min), 1);
|
|
};
|
|
|
|
const inRect = (x: number, y: number, rect: DOMRect, gap: number = 0) => {
|
|
return (
|
|
rect.left - gap < x &&
|
|
x < rect.right + gap &&
|
|
rect.top - gap < y &&
|
|
y < rect.bottom + gap
|
|
);
|
|
};
|
|
|
|
export function CursorEffect(props: CursorEffectProps) {
|
|
const {
|
|
children,
|
|
className,
|
|
effectDistance = 128,
|
|
effectForce = 6,
|
|
cursorPadding = 16,
|
|
cursorBorderRadius = 0,
|
|
} = props;
|
|
const cursorAffectorRef = useRef<HTMLDivElement>(null);
|
|
const cursorAffectoContainerrRef = useRef<HTMLDivElement>(null);
|
|
const mousePosition = useRef<{
|
|
x: number;
|
|
y: number;
|
|
targetRect: DOMRect | null;
|
|
}>({
|
|
x: 0,
|
|
y: 0,
|
|
targetRect: null,
|
|
});
|
|
const prevCursorState = useRef<{
|
|
x: number;
|
|
y: number;
|
|
}>({
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
let isTouch = false;
|
|
|
|
const handleMouseMove = (event: MouseEvent) => {
|
|
if (isTouch) return;
|
|
|
|
let targetRect =
|
|
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
|
|
|
|
mousePosition.current = {
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
targetRect,
|
|
};
|
|
};
|
|
|
|
const handleScroll = () => {
|
|
if (isTouch) return;
|
|
|
|
let targetRect =
|
|
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
|
|
|
|
mousePosition.current = {
|
|
...mousePosition.current,
|
|
targetRect,
|
|
};
|
|
};
|
|
|
|
const handlePointerDown = (event: PointerEvent) => {
|
|
isTouch = event.pointerType === 'touch';
|
|
|
|
let targetRect =
|
|
cursorAffectoContainerrRef.current?.getBoundingClientRect() || null;
|
|
|
|
mousePosition.current = {
|
|
...mousePosition.current,
|
|
targetRect,
|
|
x: -9999999,
|
|
y: -9999999,
|
|
};
|
|
};
|
|
|
|
window.addEventListener('pointerdown', handlePointerDown);
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
window.addEventListener('scroll', handleScroll);
|
|
|
|
return () => {
|
|
window.removeEventListener('pointerdown', handlePointerDown);
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
window.removeEventListener('scroll', handleScroll);
|
|
};
|
|
}, []);
|
|
|
|
useFrame((delta) => {
|
|
let x = 0;
|
|
let y = 0;
|
|
|
|
const gap = effectDistance;
|
|
const offsetMax = effectForce;
|
|
|
|
const mousePos = mousePosition.current;
|
|
const rect = mousePosition.current.targetRect;
|
|
|
|
const isActive = rect && inRect(mousePos.x, mousePos.y, rect, gap);
|
|
|
|
if (isActive) {
|
|
const halfWidth = rect.width / 2;
|
|
const targetCenterX = rect.left + halfWidth;
|
|
const leftStart = rect.left - gap;
|
|
const leftMiddle = rect.left - offsetMax;
|
|
const rightMiddle = rect.right + offsetMax;
|
|
const rightEnd = rect.right + gap;
|
|
|
|
const affectX =
|
|
clamp(mousePos.y, rect.top - gap, rect.top) *
|
|
(1 - clamp(mousePos.y, rect.bottom, rect.bottom + gap));
|
|
|
|
if (mousePos.x < leftMiddle) {
|
|
x = -clamp(mousePos.x, leftStart, leftMiddle);
|
|
} else if (mousePos.x < targetCenterX) {
|
|
x = -(1 - clamp(mousePos.x, leftMiddle, targetCenterX));
|
|
} else if (mousePos.x < rightMiddle) {
|
|
x = clamp(mousePos.x, targetCenterX, rightMiddle);
|
|
} else if (mousePos.x < rightEnd) {
|
|
x = 1 - clamp(mousePos.x, rightMiddle, rightEnd);
|
|
}
|
|
|
|
x = x * affectX;
|
|
|
|
const halfHeight = rect.height / 2;
|
|
const targetCenterY = rect.top + halfHeight;
|
|
const topStart = rect.top - gap;
|
|
const topMiddle = rect.top - offsetMax;
|
|
const bottomMiddle = rect.bottom + offsetMax;
|
|
const bottomEnd = rect.bottom + gap;
|
|
|
|
const affectY =
|
|
clamp(mousePos.x, rect.left - gap, rect.left) *
|
|
(1 - clamp(mousePos.x, rect.right, rect.right + gap));
|
|
|
|
if (mousePos.y < topMiddle) {
|
|
y = -clamp(mousePos.y, topStart, topMiddle);
|
|
} else if (mousePos.y < targetCenterY) {
|
|
y = -(1 - clamp(mousePos.y, topMiddle, targetCenterY));
|
|
} else if (mousePos.y < bottomMiddle) {
|
|
y = clamp(mousePos.y, targetCenterY, bottomMiddle);
|
|
} else if (mousePos.y < bottomEnd) {
|
|
y = 1 - clamp(mousePos.y, bottomMiddle, bottomEnd);
|
|
}
|
|
|
|
y = y * affectY;
|
|
|
|
x = x * offsetMax;
|
|
y = y * offsetMax;
|
|
}
|
|
|
|
x = smooth(prevCursorState.current.x, x, 0.02 * delta);
|
|
y = smooth(prevCursorState.current.y, y, 0.02 * delta);
|
|
|
|
prevCursorState.current = { x, y };
|
|
|
|
if (cursorAffectorRef.current) {
|
|
cursorAffectorRef.current.style.transform = `translate3D(${x}px, ${y}px, 0)`;
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div ref={cursorAffectoContainerrRef} className={className}>
|
|
<div
|
|
ref={cursorAffectorRef}
|
|
style={{ width: '100%', height: '100%' }}
|
|
data-cursor-effect
|
|
data-cursor-padding={cursorPadding}
|
|
data-cursor-border-radius={cursorBorderRadius}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |