使用Vue3实现鼠标跟随效果

1. 创建组件基本结构

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

html 复制代码
<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. 添加鼠标响应事件

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

js 复制代码
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. 实现具体功能

onMousedownonMouseup 中修改光标状态:

js 复制代码
function onMousedown() {
  cursorState.value = 'pressed'
}

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

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

js 复制代码
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. 光标的状态设置

scss 复制代码
.page-cursor {
  // 其他 page-cursor 样式

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

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

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

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

5. 完整代码

html 复制代码
<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>
相关推荐
林九生3 分钟前
【Flutter】Flutter 拍照/相册选择后无法显示对话框问题解决方案
前端·javascript·flutter
程序员小寒10 分钟前
JavaScript设计模式(四):发布-订阅模式实现与应用
开发语言·前端·javascript·设计模式
Highcharts.js10 分钟前
Highcharts Gantt 实战:从框架集成到高级功能应用-打造现代化、交互式项目进度管理图表
前端·javascript·vue.js·信息可视化·免费
程序猿的程12 分钟前
把股票数据能力接进 AI:stock-sdk-mcp 的实践整理
前端·javascript·node.js
终端鹿19 分钟前
setup 语法糖从 0 到 1 实战教程
前端·javascript·vue.js
周淳APP20 分钟前
【React Fiber架构+React18知识点+浏览器原生帧流程和React阶段流程相串】
前端·javascript·react.js·架构
reasonsummer22 分钟前
【白板类-01-01】20260326水果连连看01(html+希沃白板)
前端·html
HelloReader23 分钟前
Qt Quick 视觉元素、交互与自定义组件(七)
前端
We་ct25 分钟前
LeetCode 153. 旋转排序数组找最小值:二分最优思路
前端·算法·leetcode·typescript·二分·数组
程序员阿峰27 分钟前
前端3D·Three.js一学就会系列:第二 画线
前端·three.js