mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
7.4 KiB
JSON
1 line
7.4 KiB
JSON
{"name":"Crosshair","title":"Crosshair","description":"Custom crosshair cursor with tracking, and link hover effects.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div\n ref=\"cursorRef\"\n :style=\"{\n position: containerRef ? 'absolute' : 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n zIndex: 10000\n }\"\n >\n <svg\n :style=\"{\n position: 'absolute',\n left: 0,\n top: 0,\n width: '100%',\n height: '100%'\n }\"\n >\n <defs>\n <filter id=\"filter-noise-x\">\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.000001\" numOctaves=\"1\" ref=\"filterXRef\" />\n <feDisplacementMap in=\"SourceGraphic\" scale=\"40\" />\n </filter>\n <filter id=\"filter-noise-y\">\n <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.000001\" numOctaves=\"1\" ref=\"filterYRef\" />\n <feDisplacementMap in=\"SourceGraphic\" scale=\"40\" />\n </filter>\n </defs>\n </svg>\n <div\n ref=\"lineHorizontalRef\"\n :style=\"{\n position: 'absolute',\n width: '100%',\n height: '1px',\n background: color,\n pointerEvents: 'none',\n transform: 'translateY(50%)',\n opacity: 0\n }\"\n />\n <div\n ref=\"lineVerticalRef\"\n :style=\"{\n position: 'absolute',\n height: '100%',\n width: '1px',\n background: color,\n pointerEvents: 'none',\n transform: 'translateX(50%)',\n opacity: 0\n }\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, useTemplateRef, watchEffect, type Ref } from 'vue';\nimport { gsap } from 'gsap';\n\ninterface Props {\n color?: string;\n containerRef?: HTMLElement | null;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n color: 'white',\n containerRef: null\n});\n\nconst cursorRef = useTemplateRef<HTMLDivElement>('cursorRef');\nconst lineHorizontalRef = useTemplateRef<HTMLDivElement>('lineHorizontalRef');\nconst lineVerticalRef = useTemplateRef<HTMLDivElement>('lineVerticalRef');\nconst filterXRef = useTemplateRef<SVGFETurbulenceElement>('filterXRef');\nconst filterYRef = useTemplateRef<SVGFETurbulenceElement>('filterYRef');\n\nconst lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;\n\nconst getMousePos = (e: MouseEvent, container: HTMLElement | null) => {\n if (container) {\n const bounds = container.getBoundingClientRect();\n return {\n x: e.clientX - bounds.left,\n y: e.clientY - bounds.top\n };\n }\n return { x: e.clientX, y: e.clientY };\n};\n\nlet mouse = { x: 0, y: 0 };\nlet animationId: number | null = null;\nlet cleanup: (() => void) | null = null;\n\nonMounted(() => {\n watchEffect(() => {\n if (cleanup) {\n cleanup();\n }\n\n const handleMouseMove = (ev: Event) => {\n const mouseEvent = ev as MouseEvent;\n mouse = getMousePos(mouseEvent, props.containerRef);\n\n if (props.containerRef) {\n const bounds = props.containerRef.getBoundingClientRect();\n if (\n mouseEvent.clientX < bounds.left ||\n mouseEvent.clientX > bounds.right ||\n mouseEvent.clientY < bounds.top ||\n mouseEvent.clientY > bounds.bottom\n ) {\n gsap.to([lineHorizontalRef.value, lineVerticalRef.value], { opacity: 0 });\n } else {\n gsap.to([lineHorizontalRef.value, lineVerticalRef.value], { opacity: 1 });\n }\n }\n };\n\n const target = props.containerRef || window;\n target.addEventListener('mousemove', handleMouseMove);\n\n const renderedStyles: {\n [key: string]: { previous: number; current: number; amt: number };\n } = {\n tx: { previous: 0, current: 0, amt: 0.15 },\n ty: { previous: 0, current: 0, amt: 0.15 }\n };\n\n gsap.set([lineHorizontalRef.value, lineVerticalRef.value], { opacity: 0 });\n\n const onMouseMove = () => {\n renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y;\n\n gsap.to([lineHorizontalRef.value, lineVerticalRef.value], {\n duration: 0.9,\n ease: 'Power3.easeOut',\n opacity: 1\n });\n\n if (animationId === null) {\n animationId = requestAnimationFrame(render);\n }\n\n target.removeEventListener('mousemove', onMouseMove);\n };\n\n target.addEventListener('mousemove', onMouseMove);\n\n const primitiveValues = { turbulence: 0 };\n\n const tl = gsap\n .timeline({\n paused: true,\n onStart: () => {\n if (lineHorizontalRef.value && lineVerticalRef.value) {\n lineHorizontalRef.value.style.filter = 'url(#filter-noise-x)';\n lineVerticalRef.value.style.filter = 'url(#filter-noise-y)';\n }\n },\n onUpdate: () => {\n if (filterXRef.value && filterYRef.value) {\n filterXRef.value.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n filterYRef.value.setAttribute('baseFrequency', primitiveValues.turbulence.toString());\n }\n },\n onComplete: () => {\n if (lineHorizontalRef.value && lineVerticalRef.value) {\n lineHorizontalRef.value.style.filter = 'none';\n lineVerticalRef.value.style.filter = 'none';\n }\n }\n })\n .to(primitiveValues, {\n duration: 0.5,\n ease: 'power1',\n startAt: { turbulence: 1 },\n turbulence: 0\n });\n\n const enter = () => tl.restart();\n const leave = () => tl.progress(1).kill();\n\n const render = () => {\n renderedStyles.tx.current = mouse.x;\n renderedStyles.ty.current = mouse.y;\n\n for (const key in renderedStyles) {\n renderedStyles[key].previous = lerp(\n renderedStyles[key].previous,\n renderedStyles[key].current,\n renderedStyles[key].amt\n );\n }\n\n if (lineHorizontalRef.value && lineVerticalRef.value) {\n gsap.set(lineVerticalRef.value, { x: renderedStyles.tx.previous });\n gsap.set(lineHorizontalRef.value, { y: renderedStyles.ty.previous });\n }\n\n animationId = requestAnimationFrame(render);\n };\n\n const links = props.containerRef ? props.containerRef.querySelectorAll('a') : document.querySelectorAll('a');\n\n links.forEach((link: HTMLAnchorElement) => {\n link.addEventListener('mouseenter', enter);\n link.addEventListener('mouseleave', leave);\n });\n\n cleanup = () => {\n target.removeEventListener('mousemove', handleMouseMove);\n target.removeEventListener('mousemove', onMouseMove);\n\n if (animationId !== null) {\n cancelAnimationFrame(animationId);\n animationId = null;\n }\n\n links.forEach((link: HTMLAnchorElement) => {\n link.removeEventListener('mouseenter', enter);\n link.removeEventListener('mouseleave', leave);\n });\n };\n });\n\n onUnmounted(() => {\n if (cleanup) {\n cleanup();\n }\n });\n});\n</script>\n","path":"Crosshair/Crosshair.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["Animations"]} |