XCJ's Blog

XCJ

使用Vue3实现鼠标跟随效果

81
2024-09-13

F56B822D-161C-4DAD-B4D9-F511267A7759.gif

1. 创建组件基本结构

首先,创建一个 Vue3 组件,我们把它命名为 PageCursor.vue。基本结构如下:

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const props = withDefaults(defineProps<{
  hideCursorSelector?: string | string[]
}>(), {
  hideCursorSelector: '.hide-page-cursor'
})

const cursor = ref<HTMLElement | null>(null)
const cursorType = ref('auto')
const cursorState = ref('')

onMounted(() => {})

onUnmounted(() => {})
</script>

<template>
  <div
    ref="cursor"
    class="page-cursor"
    :class="[cursorType, cursorState]"
  ></div>
</template>

<style lang="scss" scoped>
.page-cursor {
  --cursor-size: 20px;
  position: fixed;
  z-index: 9999;
  top: calc(-1 * var(--cursor-size) / 2);
  left: calc(-1 * var(--cursor-size) / 2);
  width: var(--cursor-size);
  height: var(--cursor-size);
  border-radius: 50%;
  backdrop-filter: invert(100%);
  pointer-events: none;
  opacity: 0;
}
</style>

在组件中,我们定义了 props 对象、三个响应式对象和一个 page-cursor 样式类

props 对象用于接收参数。使用 props 传参可以在外部引用组件时控制组件的样式或行为,这里我们只定义了一个 hideCursorSelector 参数用于设置隐藏光标这个行为。

三个响应式对象分别是:

  • cursor:跟随鼠标运动的光标元素。

  • cursorType:光标的类型。

  • cursorState:光标的状态。

page-cursor 样式类:

  • --cursor-size:主要是设置光标的大小,后续有多个地方会用到,所以将其定义为 CSS 变量。

  • topleftwidthheight:基于 --cursor-size 变量进行位置和大小的设置。

  • backdrop-filter:将其值设置为 invert(100%) 为光标后面区域添加反色效果。

  • pointer-events:将其值设置为 none 来禁用光标的指针事件,使其不会影响页面上其他元素的交互。

2. 添加鼠标响应事件

添加鼠标响应事件(移动、按下、弹起)并在组件挂载时注册事件,在组件卸载时移除事件:

function onMousemove() {}

function onMousedown() {}

function onMouseup() {}

onMounted(() => {
  document.addEventListener('mousemove', onMousemove)
  document.addEventListener('mousedown', onMousedown)
  document.addEventListener('mouseup', onMouseup)
})

onUnmounted(() => {
  document.removeEventListener('mousemove', onMousemove)
  document.removeEventListener('mousedown', onMousedown)
  document.removeEventListener('mouseup', onMouseup)
})

3. 实现具体功能

在 onMousedown 和 onMouseup 中修改光标状态:

function onMousedown() {
  cursorState.value = 'pressed'
}

function onMouseup() {
  cursorState.value = ''
}

在 onMousemove 事件中获取鼠标位置,并在 requestAnimationFrame 方法中进行更新位置:

let myReq: number = 0

function onMousemove(event: MouseEvent) {
  if(!cursor.value) return

  cancelAnimationFrame(myReq)

  const { clientX, clientY } = event
  const target = event.target as HTMLElement

  myReq = requestAnimationFrame(() => {
    const style = cursor.value!.style
    style.transform = `translate3d(${clientX}px, ${clientY}px, 0)`
    cursorType.value = getComputedStyle(target)?.cursor || 'auto'

    const hideCursorSelectorList = Array.isArray(props.hideCursorSelector)
      ? props.hideCursorSelector
      : [props.hideCursorSelector]
    const hideCursor = hideCursorSelectorList.some(item => target.closest(item) !== null)
    style.opacity = hideCursor ? '0' : '1'
    style.transition = hideCursor ? '0.2s ease-out' : '0.125s ease-out'
  })
}

在这段代码中,首先使用 cancelAnimationFrame 方法关闭之前创建的动画帧任务。然后获取当前鼠标的坐标和指向的元素。利用 requestAnimationFrame 方法在下一帧渲染前进行样式设置,以防止在同一帧内执行多次样式设置。并使用 getComputedStyle 方法获取当前鼠标指向元素的 CSS 属性,并从中获取鼠标指针的类型。

最后,将 hideCursorSelector 格式化为 hideCursorSelectorList,通过检查鼠标指向元素与 hideCursorSelectorList 匹配特定选择器且离当前元素最近的祖先元素是否存在来判断是否隐藏光标。

4. 光标的状态设置

.page-cursor {
  // 其他 page-cursor 样式

  // 鼠标光标类型为指针时
  &.pointer {
    --cursor-size: 40px;

    // 指针类型并且按下时
    &.pressed {
      --cursor-size: 20px;
    }
  }

  // 默认类型按下时
  &.pressed {
    --cursor-size: 10px;
  }
}

你还可以在不同的鼠标事件中对 cursorStatecursorType 进行赋值,并对 page-cursor 样式类进行更多的定义,来实现更多光标形态的展示。

5. 完整代码

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const props = withDefaults(defineProps<{
  hideCursorSelector?: string | string[]
}>(), {
  hideCursorSelector: '.hide-page-cursor'
})

const cursor = ref<HTMLElement | null>(null)
const cursorType = ref('auto')
const cursorState = ref('')

let myReq: number = 0

function onMousemove(event: MouseEvent) {
  if(!cursor.value) return

  cancelAnimationFrame(myReq)

  const { clientX, clientY } = event
  const target = event.target as HTMLElement

  myReq = requestAnimationFrame(() => {
    const style = cursor.value!.style
    style.transform = `translate3d(${clientX}px, ${clientY}px, 0)`
    cursorType.value = getComputedStyle(target)?.cursor || 'auto'

    const hideCursorSelectorList = Array.isArray(props.hideCursorSelector)
      ? props.hideCursorSelector
      : [props.hideCursorSelector]
    const hideCursor = hideCursorSelectorList.some(item => target.closest(item) !== null)
    style.opacity = hideCursor ? '0' : '1'
    style.transition = hideCursor ? '0.2s ease-out' : '0.125s ease-out'
  })
}

function onMousedown() {
  cursorState.value = 'pressed'
}

function onMouseup() {
  cursorState.value = ''
}

onMounted(() => {
  globalThis.document.addEventListener('mousemove', onMousemove)
  globalThis.document.addEventListener('mousedown', onMousedown)
  globalThis.document.addEventListener('mouseup', onMouseup)
})

onUnmounted(() => {
  globalThis.document.removeEventListener('mousemove', onMousemove)
  globalThis.document.removeEventListener('mousedown', onMousedown)
  globalThis.document.removeEventListener('mouseup', onMouseup)
})
</script>

<template>
  <div
    ref="cursor"
    class="page-cursor"
    :class="[cursorType, cursorState]"
  ></div>
</template>

<style lang="scss" scoped>
.page-cursor {
  --cursor-size: 20px;
  position: fixed;
  z-index: 9999;
  top: calc(-1 * var(--cursor-size) / 2);
  left: calc(-1 * var(--cursor-size) / 2);
  width: var(--cursor-size);
  height: var(--cursor-size);
  border-radius: 50%;
  backdrop-filter: invert(100%);
  pointer-events: none;
  opacity: 0;

  &.pointer {
    --cursor-size: 40px;

    &.pressed {
      --cursor-size: 20px;
    }
  }

  &.pressed {
    --cursor-size: 10px;
  }
}
</style>