Files
vue-bits/public/r/GhostCursor.json
David Haz e621971723 jsrepo v3
2025-12-15 23:50:24 +02:00

1 line
17 KiB
JSON

{"name":"GhostCursor","title":"GhostCursor","description":"Semi-transparent ghost cursor that smoothly follows the real cursor with a trailing effect.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport * as THREE from 'three';\nimport { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';\nimport { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';\nimport { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';\nimport { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';\nimport { computed, markRaw, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, type CSSProperties } from 'vue';\n\ntype GhostCursorProps = {\n className?: string;\n style?: CSSProperties;\n\n trailLength?: number;\n inertia?: number;\n grainIntensity?: number;\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n\n brightness?: number;\n color?: string;\n mixBlendMode?: CSSProperties['mixBlendMode'];\n edgeIntensity?: number;\n\n maxDevicePixelRatio?: number;\n targetPixels?: number;\n fadeDelayMs?: number;\n fadeDurationMs?: number;\n zIndex?: number;\n};\n\nconst props = withDefaults(defineProps<GhostCursorProps>(), {\n trailLength: 50,\n inertia: 0.5,\n grainIntensity: 0.05,\n bloomStrength: 0.1,\n bloomRadius: 1.0,\n bloomThreshold: 0.025,\n brightness: 1,\n color: '#A0FFBC',\n mixBlendMode: 'screen',\n edgeIntensity: 0,\n maxDevicePixelRatio: 0.5,\n zIndex: 10\n});\n\nconst containerRef = useTemplateRef('containerRef');\nconst rendererRef = ref<THREE.WebGLRenderer | null>(null);\nconst composerRef = ref<EffectComposer | null>(null);\nconst materialRef = ref<THREE.ShaderMaterial | null>(null);\nconst bloomPassRef = ref<UnrealBloomPass | null>(null);\nconst filmPassRef = ref<ShaderPass | null>(null);\n\nconst trailBufRef = ref<THREE.Vector2[]>([]);\nconst headRef = ref(0);\n\nconst rafRef = ref<number | null>(null);\nconst resizeObsRef = ref<ResizeObserver | null>(null);\nconst currentMouseRef = ref(new THREE.Vector2(0.5, 0.5));\nconst velocityRef = ref(new THREE.Vector2(0, 0));\nconst fadeOpacityRef = ref(1);\nconst lastMoveTimeRef = ref(typeof performance !== 'undefined' ? performance.now() : Date.now());\nconst pointerActiveRef = ref(false);\nconst runningRef = ref(false);\n\nconst isTouch = computed(\n () => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0)\n);\n\nconst pixelBudget = computed(() => props.targetPixels ?? (isTouch.value ? 0.9e6 : 1.3e6));\nconst fadeDelay = computed(() => props.fadeDelayMs ?? (isTouch.value ? 500 : 1000));\nconst fadeDuration = computed(() => props.fadeDurationMs ?? (isTouch.value ? 1000 : 1500));\n\nconst baseVertexShader = `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n}\n`;\n\nconst fragmentShader = `\nuniform float iTime;\nuniform vec3 iResolution;\nuniform vec2 iMouse;\nuniform vec2 iPrevMouse[MAX_TRAIL_LENGTH];\nuniform float iOpacity;\nuniform float iScale;\nuniform vec3 iBaseColor;\nuniform float iBrightness;\nuniform float iEdgeIntensity;\nvarying vec2 vUv;\n\nfloat hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7))) * 43758.5453123); }\nfloat 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}\nfloat 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}\nvec3 tint1(vec3 base){ return mix(base, vec3(1.0), 0.15); }\nvec3 tint2(vec3 base){ return mix(base, vec3(0.8, 0.9, 1.0), 0.25); }\n\nvec4 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\nvoid 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\nconst FilmGrainShader = computed(() => {\n return {\n uniforms: {\n tDiffuse: { value: null },\n iTime: { value: 0 },\n intensity: { value: props.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});\n\nconst UnpremultiplyPass = computed(\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\nfunction calculateScale(el: HTMLElement) {\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\nlet cleanup: (() => void) | null = null;\nconst setup = () => {\n const host = containerRef.value;\n const parent = host?.parentElement;\n if (!host || !parent) return;\n\n const prevParentPos = parent.style.position;\n if (!prevParentPos || prevParentPos === 'static') {\n parent.style.position = 'relative';\n }\n\n const renderer = markRaw(\n new THREE.WebGLRenderer({\n antialias: !isTouch.value,\n alpha: true,\n depth: false,\n stencil: false,\n powerPreference: isTouch.value ? 'low-power' : 'high-performance',\n premultipliedAlpha: false,\n preserveDrawingBuffer: false\n })\n );\n renderer.setClearColor(0x000000, 0);\n rendererRef.value = renderer;\n\n renderer.domElement.style.pointerEvents = 'none';\n if (props.mixBlendMode) {\n renderer.domElement.style.mixBlendMode = String(props.mixBlendMode);\n } else {\n renderer.domElement.style.removeProperty('mix-blend-mode');\n }\n\n renderer.domElement.style.display = 'block';\n renderer.domElement.style.width = '100%';\n renderer.domElement.style.height = '100%';\n renderer.domElement.style.background = 'transparent';\n\n host.appendChild(renderer.domElement);\n\n const scene = markRaw(new THREE.Scene());\n const camera = markRaw(new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1));\n\n const geom = markRaw(new THREE.PlaneGeometry(2, 2));\n\n const maxTrail = Math.max(1, Math.floor(props.trailLength));\n trailBufRef.value = Array.from({ length: maxTrail }, () => new THREE.Vector2(0.5, 0.5));\n headRef.value = 0;\n\n const baseColor = new THREE.Color(props.color);\n\n const material = markRaw(\n 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.value.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: props.brightness },\n iEdgeIntensity: { value: props.edgeIntensity }\n },\n vertexShader: baseVertexShader,\n fragmentShader,\n transparent: true,\n depthTest: false,\n depthWrite: false\n })\n );\n materialRef.value = material;\n\n const mesh = new THREE.Mesh(geom, material);\n scene.add(mesh);\n\n const composer = markRaw(new EffectComposer(renderer));\n composerRef.value = composer;\n\n const renderPass = markRaw(new RenderPass(scene, camera));\n composer.addPass(renderPass);\n\n const bloomPass = markRaw(\n new UnrealBloomPass(new THREE.Vector2(1, 1), props.bloomStrength, props.bloomRadius, props.bloomThreshold)\n );\n bloomPassRef.value = bloomPass;\n composer.addPass(bloomPass);\n\n const filmPass = markRaw(new ShaderPass(FilmGrainShader.value as ConstructorParameters<typeof ShaderPass>[0]));\n filmPassRef.value = filmPass;\n composer.addPass(filmPass);\n\n composer.addPass(UnpremultiplyPass.value);\n\n const resize = () => {\n const rect = host.getBoundingClientRect();\n const cssW = Math.max(1, Math.floor(rect.width));\n const cssH = Math.max(1, Math.floor(rect.height));\n\n const currentDPR = Math.min(\n typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n props.maxDevicePixelRatio\n );\n const need = cssW * cssH * currentDPR * currentDPR;\n const scale =\n need <= pixelBudget.value ? 1 : Math.max(0.5, Math.min(1, Math.sqrt(pixelBudget.value / 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\n resize();\n const ro = new ResizeObserver(resize);\n resizeObsRef.value = ro;\n ro.observe(parent);\n ro.observe(host);\n\n const start = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const animate = () => {\n const now = performance.now();\n const t = (now - start) / 1000;\n\n const mat = materialRef.value!;\n const comp = composerRef.value!;\n\n if (pointerActiveRef.value) {\n velocityRef.value.set(\n currentMouseRef.value.x - mat.uniforms.iMouse.value.x,\n currentMouseRef.value.y - mat.uniforms.iMouse.value.y\n );\n mat.uniforms.iMouse.value.copy(currentMouseRef.value);\n fadeOpacityRef.value = 1.0;\n } else {\n velocityRef.value.multiplyScalar(props.inertia);\n if (velocityRef.value.lengthSq() > 1e-6) {\n mat.uniforms.iMouse.value.add(velocityRef.value);\n }\n const dt = now - lastMoveTimeRef.value;\n if (dt > fadeDelay.value) {\n const k = Math.min(1, (dt - fadeDelay.value) / fadeDuration.value);\n fadeOpacityRef.value = Math.max(0, 1 - k);\n }\n }\n\n const N = trailBufRef.value.length;\n headRef.value = (headRef.value + 1) % N;\n trailBufRef.value[headRef.value].copy(mat.uniforms.iMouse.value);\n const arr = mat.uniforms.iPrevMouse.value as THREE.Vector2[];\n for (let i = 0; i < N; i++) {\n const srcIdx = (headRef.value - i + N) % N;\n arr[i].copy(trailBufRef.value[srcIdx]);\n }\n\n mat.uniforms.iOpacity.value = fadeOpacityRef.value;\n mat.uniforms.iTime.value = t;\n\n if (filmPassRef.value?.uniforms?.iTime) {\n filmPassRef.value.uniforms.iTime.value = t;\n }\n\n comp.render();\n\n if (!pointerActiveRef.value && fadeOpacityRef.value <= 0.001) {\n runningRef.value = false;\n rafRef.value = null;\n return;\n }\n\n rafRef.value = requestAnimationFrame(animate);\n };\n\n const ensureLoop = () => {\n if (!runningRef.value) {\n runningRef.value = true;\n rafRef.value = requestAnimationFrame(animate);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\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.value.set(x, y);\n pointerActiveRef.value = true;\n lastMoveTimeRef.value = performance.now();\n ensureLoop();\n };\n const onPointerEnter = () => {\n pointerActiveRef.value = true;\n ensureLoop();\n };\n const onPointerLeave = () => {\n pointerActiveRef.value = false;\n lastMoveTimeRef.value = 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 cleanup = () => {\n if (rafRef.value) cancelAnimationFrame(rafRef.value);\n runningRef.value = false;\n rafRef.value = null;\n\n parent.removeEventListener('pointermove', onPointerMove);\n parent.removeEventListener('pointerenter', onPointerEnter);\n parent.removeEventListener('pointerleave', onPointerLeave);\n resizeObsRef.value?.disconnect();\n\n scene.clear();\n geom.dispose();\n material.dispose();\n composer.dispose();\n renderer.dispose();\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};\n\nonMounted(() => {\n setup();\n});\n\nonBeforeUnmount(() => {\n cleanup?.();\n});\n\nwatch(\n () => [\n props.trailLength,\n props.inertia,\n props.grainIntensity,\n props.bloomStrength,\n props.bloomRadius,\n props.bloomThreshold,\n pixelBudget.value,\n fadeDelay.value,\n fadeDuration.value,\n isTouch.value,\n props.color,\n props.brightness,\n props.mixBlendMode,\n props.edgeIntensity\n ],\n () => {\n cleanup?.();\n setup();\n },\n { deep: true }\n);\n\nwatch(\n () => props.color,\n () => {\n if (materialRef.value) {\n const c = new THREE.Color(props.color);\n (materialRef.value.uniforms.iBaseColor.value as THREE.Vector3).set(c.r, c.g, c.b);\n }\n }\n);\n\nwatch(\n () => props.brightness,\n () => {\n if (materialRef.value) {\n materialRef.value.uniforms.iBrightness.value = props.brightness;\n }\n }\n);\n\nwatch(\n () => props.edgeIntensity,\n () => {\n if (materialRef.value) {\n materialRef.value.uniforms.iEdgeIntensity.value = props.edgeIntensity;\n }\n }\n);\n\nwatch(\n () => props.grainIntensity,\n () => {\n if (filmPassRef.value?.uniforms?.intensity) {\n filmPassRef.value.uniforms.intensity.value = props.grainIntensity;\n }\n }\n);\n\nwatch(\n () => props.mixBlendMode,\n () => {\n const el = rendererRef.value?.domElement;\n if (!el) return;\n if (props.mixBlendMode) {\n el.style.mixBlendMode = String(props.mixBlendMode);\n } else {\n el.style.removeProperty('mix-blend-mode');\n }\n }\n);\n\nconst mergedStyle = computed(() => ({\n zIndex: props.zIndex,\n ...props.style\n}));\n</script>\n\n<template>\n <div ref=\"containerRef\" :class=\"['pointer-events-none absolute inset-0', className]\" :style=\"mergedStyle\" />\n</template>\n","path":"GhostCursor/GhostCursor.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"three","version":"^0.178.0"}],"devDependencies":[],"categories":["Animations"]}