laconic.com/src/components/common/cursor/index.tsx
2022-03-28 15:00:11 -03:00

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