-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathGhostCursor-JS-CSS.json
More file actions
23 lines (23 loc) · 17.2 KB
/
GhostCursor-JS-CSS.json
File metadata and controls
23 lines (23 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "GhostCursor-JS-CSS",
"title": "GhostCursor",
"description": "Semi-transparent ghost cursor that smoothly follows the real cursor with a trailing effect.",
"type": "registry:component",
"files": [
{
"type": "registry:component",
"path": "GhostCursor/GhostCursor.css",
"content": ".ghost-cursor {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.ghost-cursor > canvas {\n display: block;\n width: 100%;\n height: 100%;\n background: transparent;\n}\n"
},
{
"type": "registry:component",
"path": "GhostCursor/GhostCursor.jsx",
"content": "import { useEffect, useMemo, useRef } from 'react';\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport './GhostCursor.css';\n\nconst GhostCursor = ({\n className,\n style,\n trailLength = 50,\n inertia = 0.5,\n grainIntensity = 0.05,\n bloomStrength = 0.1,\n bloomRadius = 1.0,\n bloomThreshold = 0.025,\n\n brightness = 1,\n color = '#B19EEF',\n mixBlendMode = 'screen',\n edgeIntensity = 0,\n\n maxDevicePixelRatio = 0.5,\n targetPixels,\n\n fadeDelayMs,\n fadeDurationMs,\n zIndex = 10\n}) => {\n const containerRef = useRef(null);\n const rendererRef = useRef(null);\n const composerRef = useRef(null);\n const materialRef = useRef(null);\n const bloomPassRef = useRef(null);\n const filmPassRef = useRef(null);\n\n const trailBufRef = useRef([]);\n const headRef = useRef(0);\n\n const rafRef = useRef(null);\n const resizeObsRef = useRef(null);\n const currentMouseRef = useRef(new THREE.Vector2(0.5, 0.5));\n const velocityRef = useRef(new THREE.Vector2(0, 0));\n const fadeOpacityRef = useRef(1.0);\n const lastMoveTimeRef = useRef(typeof performance !== 'undefined' ? performance.now() : Date.now());\n const pointerActiveRef = useRef(false);\n const runningRef = useRef(false);\n const hasValidSizeRef = useRef(false);\n\n const isTouch = useMemo(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0),\n []\n );\n\n const pixelBudget = targetPixels ?? (isTouch ? 0.9e6 : 1.3e6);\n const fadeDelay = fadeDelayMs ?? (isTouch ? 500 : 1000);\n const fadeDuration = fadeDurationMs ?? (isTouch ? 1000 : 1500);\n\n const baseVertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n uniform float iTime;\n uniform vec3 iResolution;\n uniform vec2 iMouse;\n uniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\n uniform float iOpacity;\n uniform float iScale;\n uniform vec3 iBaseColor;\n uniform float iBrightness;\n uniform float iEdgeIntensity;\n varying vec2 vUv;\n\n float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\n float noise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f *= f * (3. - 2. * f);\n return mix(mix(hash(i + vec2(0.,0.)), hash(i + vec2(1.,0.)), f.x),\n mix(hash(i + vec2(0.,1.)), hash(i + vec2(1.,1.)), f.x), f.y);\n }\n float fbm(vec2 p){\n float v = 0.0;\n float a = 0.5;\n mat2 m = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));\n for(int i=0;i<5;i++){\n v += a * noise(p);\n p = m * p * 2.0;\n a *= 0.5;\n }\n return v;\n }\n vec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\n vec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\n vec4 blob(vec2 p, vec2 mousePos, float intensity, float activity) {\n vec2 q = vec2(fbm(p * iScale + iTime * 0.1), fbm(p * iScale + vec2(5.2,1.3) + iTime * 0.1));\n vec2 r = vec2(fbm(p * iScale + q * 1.5 + iTime * 0.15), fbm(p * iScale + q * 1.5 + vec2(8.3,2.8) + iTime * 0.15));\n\n float smoke = fbm(p * iScale + r * 0.8);\n float radius = 0.5 + 0.3 * (1.0 / iScale);\n float distFactor = 1.0 - smoothstep(0.0, radius * activity, length(p - mousePos));\n float alpha = pow(smoke, 2.5) * distFactor;\n\n vec3 c1 = tint1(iBaseColor);\n vec3 c2 = tint2(iBaseColor);\n vec3 color = mix(c1, c2, sin(iTime * 0.5) * 0.5 + 0.5);\n\n return vec4(color * alpha * intensity, alpha * intensity);\n }\n\n void main() {\n vec2 uv = (gl_FragCoord.xy / iResolution.xy * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n vec2 mouse = (iMouse * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n\n vec3 colorAcc = vec3(0.0);\n float alphaAcc = 0.0;\n\n vec4 b = blob(uv, mouse, 1.0, iOpacity);\n colorAcc += b.rgb;\n alphaAcc += b.a;\n\n for (int i = 0; i < MAX_TRAIL_LENGTH; i++) {\n vec2 pm = (iPrevMouse[i] * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);\n float t = 1.0 - float(i) / float(MAX_TRAIL_LENGTH);\n t = pow(t, 2.0);\n if (t > 0.01) {\n vec4 bt = blob(uv, pm, t * 0.8, iOpacity);\n colorAcc += bt.rgb;\n alphaAcc += bt.a;\n }\n }\n\n colorAcc *= iBrightness;\n\n vec2 uv01 = gl_FragCoord.xy / iResolution.xy;\n float edgeDist = min(min(uv01.x, 1.0 - uv01.x), min(uv01.y, 1.0 - uv01.y));\n float distFromEdge = clamp(edgeDist * 2.0, 0.0, 1.0);\n float k = clamp(iEdgeIntensity, 0.0, 1.0);\n float edgeMask = mix(1.0 - k, 1.0, distFromEdge);\n\n float outAlpha = clamp(alphaAcc * iOpacity * edgeMask, 0.0, 1.0);\n gl_FragColor = vec4(colorAcc, outAlpha);\n }\n `;\n\n const FilmGrainShader = useMemo(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: grainIntensity }\n },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float iTime;\n uniform float intensity;\n varying vec2 vUv;\n\n float hash1(float n){ return fract(sin(n)*43758.5453); }\n\n void main(){\n vec4 color = texture2D(tDiffuse, vUv);\n float n = hash1(vUv.x*1000.0 + vUv.y*2000.0 + iTime) * 2.0 - 1.0;\n color.rgb += n * intensity * color.rgb;\n gl_FragColor = color;\n }\n `\n };\n }, [grainIntensity]);\n\n const UnpremultiplyPass = useMemo(\n () =>\n new ShaderPass({\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `\n varying vec2 vUv;\n void main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n varying vec2 vUv;\n void main(){\n vec4 c = texture2D(tDiffuse, vUv);\n float a = max(c.a, 1e-5);\n vec3 straight = c.rgb / a;\n gl_FragColor = vec4(clamp(straight, 0.0, 1.0), c.a);\n }\n `\n }),\n []\n );\n\n function calculateScale(el) {\n const r = el.getBoundingClientRect();\n const base = 600;\n const current = Math.min(Math.max(1, r.width), Math.max(1, r.height));\n return Math.max(0.5, Math.min(2.0, current / base));\n }\n\n useEffect(() => {\n const host = containerRef.current;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n let active = true;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = new THREE.WebGLRenderer({\n antialias: !isTouch,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n });\n renderer.setClearColor(0x000000, 0);\n rendererRef.current = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n host.appendChild(renderer.domElement);\n\n const scene = new THREE.Scene();\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n const geom = new THREE.PlaneGeometry(2, 2);\n\n const maxTrail = Math.max(1, Math.floor(trailLength));\n trailBufRef.current = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.current = 0;\n\n const baseColor = new THREE.Color(color);\n\n const material = new THREE.ShaderMaterial({\n defines: { MAX_TRAIL_LENGTH: maxTrail },\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new THREE.Vector3(1, 1, 1) },\n iMouse: { value: new THREE.Vector2(0.5, 0.5) },\n iPrevMouse: { value: trailBufRef.current.map(v => v.clone()) },\n iOpacity: { value: 1.0 },\n iScale: { value: 1.0 },\n iBaseColor: { value: new THREE.Vector3(baseColor.r, baseColor.g, baseColor.b) },\n iBrightness: { value: brightness },\n iEdgeIntensity: { value: edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n });\n materialRef.current = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = new EffectComposer(renderer);\n composerRef.current = composer;\n\n const renderPass = new RenderPass(scene, camera);\n composer.addPass(renderPass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(1, 1), bloomStrength, bloomRadius, bloomThreshold);\n bloomPassRef.current = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = new ShaderPass(FilmGrainShader);\n filmPassRef.current = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass);\n\n const resize = () => {\n if (!active) return;\n\n const rect = host.getBoundingClientRect();\n const cssW = Math.floor(rect.width);\n const cssH = Math.floor(rect.height);\n\n if (cssW <= 0 || cssH <= 0) {\n hasValidSizeRef.current = false;\n return;\n }\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale = need <= pixelBudget ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget / Math.max(1, need))));\n const pixelRatio = currentDPR * scale;\n\n renderer.setPixelRatio(pixelRatio);\n renderer.setSize(cssW, cssH, false);\n\n composer.setPixelRatio?.(pixelRatio);\n composer.setSize(cssW, cssH);\n\n const wpx = Math.max(1, Math.floor(cssW * pixelRatio));\n const hpx = Math.max(1, Math.floor(cssH * pixelRatio));\n material.uniforms.iResolution.value.set(wpx, hpx, 1);\n material.uniforms.iScale.value = calculateScale(host);\n bloomPass.setSize(wpx, hpx);\n\n hasValidSizeRef.current = true;\n };\n\n resize();\n const ro = new ResizeObserver(() => {\n if (!active) return;\n resize();\n });\n resizeObsRef.current = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n if (!active) return;\n\n if (!hasValidSizeRef.current) {\n rafRef.current = requestAnimationFrame(animate);\n return;\n }\n\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.current;\n const comp = composerRef.current;\n\n if (pointerActiveRef.current) {\n velocityRef.current.set(\n currentMouseRef.current.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.current.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.current);\n fadeOpacityRef.current = 1.0;\n } else {\n velocityRef.current.multiplyScalar(inertia);\n if (velocityRef.current.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.current);\n }\n const dt = now - lastMoveTimeRef.current;\n if (dt > fadeDelay) {\n const k = Math.min(1, (dt - fadeDelay) / fadeDuration);\n fadeOpacityRef.current = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.current.length;\n headRef.current = (headRef.current + 1) % N;\n trailBufRef.current[headRef.current].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value;\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.current - i + N) % N;\n arr[i].copy(trailBufRef.current[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.current;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.current?.uniforms?.iTime) {\n filmPassRef.current.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.current && fadeOpacityRef.current <= 0.001) {\n runningRef.current = false;\n rafRef.current = null;\n return;\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.current) {\n runningRef.current = true;\n rafRef.current = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = e => {\n const rect = parent.getBoundingClientRect();\n const x = THREE.MathUtils.clamp((e.clientX - rect.left) / Math.max(1, rect.width), 0, 1);\n const y = THREE.MathUtils.clamp(1 - (e.clientY - rect.top) / Math.max(1, rect.height), 0, 1);\n currentMouseRef.current.set(x, y);\n pointerActiveRef.current = true;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.current = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.current = false;\n lastMoveTimeRef.current = performance.now();\n ensureLoop();\n };\n\n parent.addEventListener('pointermove', onPointerMove, { passive: true });\n parent.addEventListener('pointerenter', onPointerEnter, { passive: true });\n parent.addEventListener('pointerleave', onPointerLeave, { passive: true });\n\n ensureLoop();\n\n return () => {\n active = false;\n hasValidSizeRef.current = false;\n\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n runningRef.current = false;\n rafRef.current = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.current?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n materialRef.current = null;\n composer.dispose();\n composerRef.current = null;\n renderer.dispose();\n renderer.forceContextLoss();\n rendererRef.current = null;\n\n if (renderer.domElement && renderer.domElement.parentElement) {\n renderer.domElement.parentElement.removeChild(renderer.domElement);\n }\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = prevParentPos;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n trailLength,\n inertia,\n grainIntensity,\n bloomStrength,\n bloomRadius,\n bloomThreshold,\n pixelBudget,\n fadeDelay,\n fadeDuration,\n isTouch,\n color,\n brightness,\n mixBlendMode,\n edgeIntensity\n ]);\n\n useEffect(() => {\n if (materialRef.current) {\n const c = new THREE.Color(color);\n materialRef.current.uniforms.iBaseColor.value.set(c.r, c.g, c.b);\n }\n }, [color]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iBrightness.value = brightness;\n }\n }, [brightness]);\n\n useEffect(() => {\n if (materialRef.current) {\n materialRef.current.uniforms.iEdgeIntensity.value = edgeIntensity;\n }\n }, [edgeIntensity]);\n\n useEffect(() => {\n if (filmPassRef.current?.uniforms?.intensity) {\n filmPassRef.current.uniforms.intensity.value = grainIntensity;\n }\n }, [grainIntensity]);\n\n useEffect(() => {\n const el = rendererRef.current?.domElement;\n if (!el) return;\n if (mixBlendMode) {\n el.style.mixBlendMode = String(mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }, [mixBlendMode]);\n\n const mergedStyle = useMemo(() => ({ zIndex, ...style }), [zIndex, style]);\n\n return <div ref={containerRef} className={`ghost-cursor ${className ?? ''}`} style={mergedStyle} />;\n};\n\nexport default GhostCursor;\n"
}
],
"registryDependencies": [],
"dependencies": [
"three@^0.167.1"
]
}