mirror of
https://github.com/LaconicNetwork/laconic.com.git
synced 2026-02-28 05:54:09 +00:00
157 lines
4.4 KiB
TypeScript
157 lines
4.4 KiB
TypeScript
import gsap from 'gsap'
|
|
import Head from 'next/head'
|
|
import * as React from 'react'
|
|
|
|
import { useDeviceDetect } from '~/hooks/use-device-detect'
|
|
|
|
import defaultSrc from '../../../../public/images/cursor/default.svg'
|
|
import defaultActiveSrc from '../../../../public/images/cursor/default-active.svg'
|
|
import pointerSrc from '../../../../public/images/cursor/pointer.svg'
|
|
import pointerActiveSrc from '../../../../public/images/cursor/pointer-active.svg'
|
|
import s from './cursor.module.scss'
|
|
|
|
type CursorType = 'pointer' | 'default' | undefined
|
|
|
|
const CursorContext = React.createContext<
|
|
{ setType: React.Dispatch<React.SetStateAction<CursorType>> } | undefined
|
|
>(undefined)
|
|
|
|
const Cursor = ({ children }: { children?: React.ReactNode }) => {
|
|
const cursorRef = React.useRef<HTMLDivElement>(null)
|
|
const [type, setType] = React.useState<CursorType>()
|
|
const { isMobile } = useDeviceDetect()
|
|
|
|
React.useEffect(() => {
|
|
if (!cursorRef.current) return
|
|
gsap.set(cursorRef.current, { xPercent: -50, yPercent: -50 })
|
|
|
|
const pos = { x: window.innerWidth / 2, y: window.innerHeight / 2 }
|
|
const mouse = { x: pos.x, y: pos.y }
|
|
const speed = 0.2
|
|
|
|
const xSet = gsap.quickSetter(cursorRef.current, 'x', 'px')
|
|
const ySet = gsap.quickSetter(cursorRef.current, 'y', 'px')
|
|
|
|
function handleMouseMove(e: MouseEvent) {
|
|
mouse.x = e.x
|
|
mouse.y = e.y
|
|
if (e.target instanceof HTMLElement || e.target instanceof SVGElement) {
|
|
if (e.target.dataset.cursor) {
|
|
setType(e.target.dataset.cursor as any)
|
|
return
|
|
}
|
|
if (e.target.closest('button') || e.target.closest('a')) {
|
|
setType('pointer')
|
|
return
|
|
} else if (
|
|
e.target.closest('p') ||
|
|
e.target.closest('span') ||
|
|
e.target.closest('h1') ||
|
|
e.target.closest('h2') ||
|
|
e.target.closest('h3') ||
|
|
e.target.closest('h4') ||
|
|
e.target.closest('h5') ||
|
|
e.target.closest('h5') ||
|
|
e.target.closest('input') ||
|
|
e.target.closest('textarea')
|
|
) {
|
|
setType('default') // this would be for text, if we'd have any text cursor
|
|
return
|
|
}
|
|
}
|
|
setType(undefined)
|
|
}
|
|
|
|
function handleTick() {
|
|
const dt = 1.0 - Math.pow(0.6 - speed, gsap.ticker.deltaRatio())
|
|
|
|
pos.x += (mouse.x - pos.x) * dt
|
|
pos.y += (mouse.y - pos.y) * dt
|
|
xSet(pos.x)
|
|
ySet(pos.y)
|
|
}
|
|
|
|
window.addEventListener('mousemove', handleMouseMove, { passive: true })
|
|
gsap.ticker.add(handleTick)
|
|
|
|
return () => {
|
|
window.removeEventListener('mousemove', handleMouseMove)
|
|
gsap.ticker.remove(handleTick)
|
|
}
|
|
}, [isMobile])
|
|
|
|
return (
|
|
<>
|
|
{isMobile === false && <CursorFollower ref={cursorRef} type={type} />}
|
|
<CursorContext.Provider value={{ setType }}>
|
|
{children}
|
|
</CursorContext.Provider>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const CursorFollower = React.forwardRef<HTMLDivElement, { type: CursorType }>(
|
|
({ type }, ref) => {
|
|
const { src, adjustments } = React.useMemo(() => {
|
|
switch (type) {
|
|
case 'pointer':
|
|
return {
|
|
src: pointerSrc,
|
|
adjustments: { x: '4px', y: '22px' }
|
|
}
|
|
default:
|
|
return {
|
|
src: defaultSrc,
|
|
adjustments: { x: '10px', y: '17px' }
|
|
}
|
|
}
|
|
}, [type])
|
|
|
|
React.useEffect(() => {
|
|
document.documentElement.classList.add('has-custom-cursor')
|
|
|
|
return () => {
|
|
document.documentElement.classList.remove('has-custom-cursor')
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<div ref={ref} className={s['cursor']}>
|
|
<Head>
|
|
{/* preload images */}
|
|
{[defaultSrc, defaultActiveSrc, pointerSrc, pointerActiveSrc].map(
|
|
(src) => {
|
|
return (
|
|
<link key={src.src} rel="preload" href={src.src} as="image" />
|
|
)
|
|
}
|
|
)}
|
|
</Head>
|
|
<span
|
|
style={{
|
|
transform: `translate(${adjustments.x}, ${adjustments.y})`
|
|
}}
|
|
>
|
|
<img
|
|
src={src.src}
|
|
alt={`cursor-${type}`}
|
|
width={src.width}
|
|
height={src.height}
|
|
loading="eager"
|
|
/>
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
|
|
export const useCursor = () => {
|
|
const context = React.useContext(CursorContext)
|
|
if (context === undefined) {
|
|
throw new Error('useCursor must be used within a CursorProvider')
|
|
}
|
|
return context
|
|
}
|
|
|
|
export default Cursor
|