136 lines
4.1 KiB
Vue
136 lines
4.1 KiB
Vue
<script setup lang="ts">
|
|
import {motion, frame} from "motion-v";
|
|
|
|
const {
|
|
name,
|
|
img = undefined,
|
|
link = '/',
|
|
color = 'bg-white',
|
|
textTop = false,
|
|
moveFactor = undefined,
|
|
scaleFactor = 2,
|
|
initialWidth = undefined,
|
|
initialHeight = undefined,
|
|
topTextOffset = undefined,
|
|
bottomTextOffset = undefined,
|
|
textSize = undefined,
|
|
} = defineProps<{
|
|
name: string,
|
|
link?: string,
|
|
img?: string,
|
|
color?: string,
|
|
textTop?: boolean,
|
|
moveFactor?: number,
|
|
scaleFactor?: number,
|
|
initialWidth?: string,
|
|
initialHeight?: string,
|
|
topTextOffset?: string,
|
|
bottomTextOffset?: string,
|
|
textSize?: string,
|
|
}>()
|
|
|
|
const emit = defineEmits(['positionChanged'])
|
|
|
|
const isHoveringParent = ref(false)
|
|
|
|
const imageMaskClass = `avatar mask mask-squircle ${initialWidth ? initialWidth : 'w-30'} ${initialHeight ? initialHeight : 'h-30'}`
|
|
const roundedDivClass = `rounded-full absolute ${color} ${initialWidth ? initialWidth : 'w-30'} ${initialHeight ? initialHeight : 'h-30'}`
|
|
const topTextClass = `absolute w-full text-center ${textSize ? textSize : 'text-2xl'} ${topTextOffset ? topTextOffset : '-top-30'}`
|
|
const bottomTextClass = `absolute w-full text-center ${textSize ? textSize : 'text-2xl'} ${bottomTextOffset ? bottomTextOffset : '-bottom-30'}`
|
|
|
|
function getRandomArbitrary(min: number, max: number): number {
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
const moveFactorValue = moveFactor ? moveFactor : getRandomArbitrary(8.0, 30.0)
|
|
|
|
const spring = {damping: 5, stiffness: 50, restDelta: 0.001}
|
|
|
|
const elementRef = useTemplateRef('elementRef')
|
|
const xPoint = useMotionValue(0)
|
|
const yPoint = useMotionValue(0)
|
|
const x = useSpring(xPoint, spring)
|
|
const y = useSpring(yPoint, spring)
|
|
const handlePointerMove = ({clientX, clientY}: { clientX: number; clientY: number }) => {
|
|
const element = elementRef.value?.$el
|
|
if (!element) return
|
|
frame.read(() => {
|
|
xPoint.set((clientX - element.offsetLeft - element.offsetWidth / 2) / moveFactorValue)
|
|
yPoint.set((clientY - element.offsetTop - element.offsetHeight / 2) / moveFactorValue)
|
|
})
|
|
}
|
|
|
|
useRafFn(() => {
|
|
const element = elementRef.value?.$el
|
|
if (!element)
|
|
return
|
|
emit(
|
|
'positionChanged',
|
|
x.get() + element.offsetLeft + element.offsetWidth / 2,
|
|
y.get() + element.offsetTop + element.offsetHeight / 2,)
|
|
})
|
|
|
|
onMounted(() => {
|
|
window.addEventListener("pointermove", handlePointerMove)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener("pointermove", handlePointerMove)
|
|
})
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<motion.div
|
|
ref="elementRef"
|
|
class="w-60 h-24 absolute flex items-center justify-center"
|
|
:style="{ x, y }">
|
|
|
|
<motion.div
|
|
v-if="img !== undefined"
|
|
:class="imageMaskClass"
|
|
:while-hover="{ scale: scaleFactor }"
|
|
@hover-start="event => {isHoveringParent=true}"
|
|
@hover-end="event => {isHoveringParent=false}">
|
|
<NuxtLink :to="link">
|
|
<NuxtImg :src="img"/>
|
|
</NuxtLink>
|
|
</motion.div>
|
|
<motion.div
|
|
v-else
|
|
:class="roundedDivClass"
|
|
:while-hover="{ scale: scaleFactor }"
|
|
@hover-start="event => {isHoveringParent=true}"
|
|
@hover-end="event => {isHoveringParent=false}">
|
|
<NuxtLink :to="link"/>
|
|
</motion.div>
|
|
|
|
<AnimatePresence>
|
|
<motion.p
|
|
v-if="textTop"
|
|
v-show="isHoveringParent"
|
|
:class="topTextClass"
|
|
:initial="{ opacity: 0, scale: 0 }"
|
|
:animate="{ opacity: 1, scale: 1 }"
|
|
:exit="{ opacity: 0, scale: 0 }">
|
|
{{ name }}
|
|
</motion.p>
|
|
|
|
<motion.p
|
|
v-else
|
|
v-show="isHoveringParent"
|
|
:class="bottomTextClass"
|
|
:initial="{ opacity: 0, scale: 0 }"
|
|
:animate="{ opacity: 1, scale: 1 }"
|
|
:exit="{ opacity: 0, scale: 0 }">
|
|
{{ name }}
|
|
</motion.p>
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
* {
|
|
overflow: visible;
|
|
}
|
|
</style> |