mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Add Crosshair animation component
This commit is contained in:
249
src/content/Animations/Crosshair/Crosshair.vue
Normal file
249
src/content/Animations/Crosshair/Crosshair.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div
|
||||
ref="cursorRef"
|
||||
:style="{
|
||||
position: containerRef ? 'absolute' : 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10000
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}"
|
||||
>
|
||||
<defs>
|
||||
<filter id="filter-noise-x">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.000001"
|
||||
numOctaves="1"
|
||||
ref="filterXRef"
|
||||
/>
|
||||
<feDisplacementMap in="SourceGraphic" scale="40" />
|
||||
</filter>
|
||||
<filter id="filter-noise-y">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.000001"
|
||||
numOctaves="1"
|
||||
ref="filterYRef"
|
||||
/>
|
||||
<feDisplacementMap in="SourceGraphic" scale="40" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
<div
|
||||
ref="lineHorizontalRef"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: color,
|
||||
pointerEvents: 'none',
|
||||
transform: 'translateY(50%)',
|
||||
opacity: 0
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
ref="lineVerticalRef"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '1px',
|
||||
background: color,
|
||||
pointerEvents: 'none',
|
||||
transform: 'translateX(50%)',
|
||||
opacity: 0
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, useTemplateRef, watchEffect, type Ref } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
containerRef?: HTMLElement | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: 'white',
|
||||
containerRef: null
|
||||
});
|
||||
|
||||
const cursorRef = useTemplateRef<HTMLDivElement>('cursorRef');
|
||||
const lineHorizontalRef = useTemplateRef<HTMLDivElement>('lineHorizontalRef');
|
||||
const lineVerticalRef = useTemplateRef<HTMLDivElement>('lineVerticalRef');
|
||||
const filterXRef = useTemplateRef<SVGFETurbulenceElement>('filterXRef');
|
||||
const filterYRef = useTemplateRef<SVGFETurbulenceElement>('filterYRef');
|
||||
|
||||
const lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b;
|
||||
|
||||
const getMousePos = (e: MouseEvent, container: HTMLElement | null) => {
|
||||
if (container) {
|
||||
const bounds = container.getBoundingClientRect();
|
||||
return {
|
||||
x: e.clientX - bounds.left,
|
||||
y: e.clientY - bounds.top
|
||||
};
|
||||
}
|
||||
return { x: e.clientX, y: e.clientY };
|
||||
};
|
||||
|
||||
let mouse = { x: 0, y: 0 };
|
||||
let animationId: number | null = null;
|
||||
let cleanup: (() => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
watchEffect(() => {
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
const handleMouseMove = (ev: Event) => {
|
||||
const mouseEvent = ev as MouseEvent;
|
||||
mouse = getMousePos(mouseEvent, props.containerRef);
|
||||
|
||||
if (props.containerRef) {
|
||||
const bounds = props.containerRef.getBoundingClientRect();
|
||||
if (
|
||||
mouseEvent.clientX < bounds.left ||
|
||||
mouseEvent.clientX > bounds.right ||
|
||||
mouseEvent.clientY < bounds.top ||
|
||||
mouseEvent.clientY > bounds.bottom
|
||||
) {
|
||||
gsap.to([lineHorizontalRef.value, lineVerticalRef.value], { opacity: 0 });
|
||||
} else {
|
||||
gsap.to([lineHorizontalRef.value, lineVerticalRef.value], { opacity: 1 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const target = props.containerRef || window;
|
||||
target.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
const renderedStyles: {
|
||||
[key: string]: { previous: number; current: number; amt: number };
|
||||
} = {
|
||||
tx: { previous: 0, current: 0, amt: 0.15 },
|
||||
ty: { previous: 0, current: 0, amt: 0.15 }
|
||||
};
|
||||
|
||||
gsap.set([lineHorizontalRef.value, lineVerticalRef.value], { opacity: 0 });
|
||||
|
||||
const onMouseMove = () => {
|
||||
renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x;
|
||||
renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y;
|
||||
|
||||
gsap.to([lineHorizontalRef.value, lineVerticalRef.value], {
|
||||
duration: 0.9,
|
||||
ease: 'Power3.easeOut',
|
||||
opacity: 1
|
||||
});
|
||||
|
||||
if (animationId === null) {
|
||||
animationId = requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
target.removeEventListener('mousemove', onMouseMove);
|
||||
};
|
||||
|
||||
target.addEventListener('mousemove', onMouseMove);
|
||||
|
||||
const primitiveValues = { turbulence: 0 };
|
||||
|
||||
const tl = gsap
|
||||
.timeline({
|
||||
paused: true,
|
||||
onStart: () => {
|
||||
if (lineHorizontalRef.value && lineVerticalRef.value) {
|
||||
lineHorizontalRef.value.style.filter = 'url(#filter-noise-x)';
|
||||
lineVerticalRef.value.style.filter = 'url(#filter-noise-y)';
|
||||
}
|
||||
},
|
||||
onUpdate: () => {
|
||||
if (filterXRef.value && filterYRef.value) {
|
||||
filterXRef.value.setAttribute('baseFrequency', primitiveValues.turbulence.toString());
|
||||
filterYRef.value.setAttribute('baseFrequency', primitiveValues.turbulence.toString());
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
if (lineHorizontalRef.value && lineVerticalRef.value) {
|
||||
lineHorizontalRef.value.style.filter = 'none';
|
||||
lineVerticalRef.value.style.filter = 'none';
|
||||
}
|
||||
}
|
||||
})
|
||||
.to(primitiveValues, {
|
||||
duration: 0.5,
|
||||
ease: 'power1',
|
||||
startAt: { turbulence: 1 },
|
||||
turbulence: 0
|
||||
});
|
||||
|
||||
const enter = () => tl.restart();
|
||||
const leave = () => tl.progress(1).kill();
|
||||
|
||||
const render = () => {
|
||||
renderedStyles.tx.current = mouse.x;
|
||||
renderedStyles.ty.current = mouse.y;
|
||||
|
||||
for (const key in renderedStyles) {
|
||||
renderedStyles[key].previous = lerp(
|
||||
renderedStyles[key].previous,
|
||||
renderedStyles[key].current,
|
||||
renderedStyles[key].amt
|
||||
);
|
||||
}
|
||||
|
||||
if (lineHorizontalRef.value && lineVerticalRef.value) {
|
||||
gsap.set(lineVerticalRef.value, { x: renderedStyles.tx.previous });
|
||||
gsap.set(lineHorizontalRef.value, { y: renderedStyles.ty.previous });
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
const links = props.containerRef
|
||||
? props.containerRef.querySelectorAll('a')
|
||||
: document.querySelectorAll('a');
|
||||
|
||||
links.forEach((link: HTMLAnchorElement) => {
|
||||
link.addEventListener('mouseenter', enter);
|
||||
link.addEventListener('mouseleave', leave);
|
||||
});
|
||||
|
||||
cleanup = () => {
|
||||
target.removeEventListener('mousemove', handleMouseMove);
|
||||
target.removeEventListener('mousemove', onMouseMove);
|
||||
|
||||
if (animationId !== null) {
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
|
||||
links.forEach((link: HTMLAnchorElement) => {
|
||||
link.removeEventListener('mouseenter', enter);
|
||||
link.removeEventListener('mouseleave', leave);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user