爱心鼠标跟随特效
特效原理
鼠标跟随特效的核心思路很简单:监听鼠标移动事件,在鼠标位置创建一个带动画的元素,动画结束后自动移除。
整个流程可以拆解为四个环节:
- 捕获鼠标位置 — 监听
mousemove事件,拿到clientX和clientY - 生成爱心元素 — 在对应坐标创建一个
<span>♥</span>,附加随机参数(大小、颜色、旋转角度) - 播放动画 — CSS
@keyframes驱动爱心从小变大再缩小、同时上飘并渐隐 - 清理元素 — 动画结束后从 DOM 中移除,避免内存泄漏
文件结构
本次实现涉及以下文件:
app/ ├── components/blog/ │ └── BlogHeartCursor.vue ← 新建:爱心特效组件 ├── app.vue ← 修改:注册组件 └── app.config.ts ← 修改:添加配置项
第一步:创建组件
新建文件 app/components/blog/BlogHeartCursor.vue,这是整个特效的核心。
模板部分
模板只需要一个容器和一个循环渲染的爱心列表:
<template>
<ClientOnly>
<div v-if="enabled" class="heart-cursor" aria-hidden="true">
<TransitionGroup name="heart" @after-leave="onAfterLeave">
<span
v-for="heart in hearts"
:key="heart.id"
:data-id="heart.id"
class="heart-item"
:style="{
left: `${heart.x}px`,
top: `${heart.y}px`,
fontSize: `${heart.size}px`,
color: heart.color,
transform: `rotate(${heart.rotation}deg)`,
}"
>♥</span>
</TransitionGroup>
</div>
</ClientOnly>
</template>
几个关键设计决策:
<ClientOnly>包裹,因为mousemove是浏览器事件,SSR 环境不存在aria-hidden="true",爱心是纯装饰,不应被屏幕阅读器读取<TransitionGroup>管理爱心的进入/离开动画,@after-leave在动画结束后清理数据- 每个爱心通过
:style动态设置位置、大小、颜色和旋转
脚本部分
<script setup lang="ts">
const appConfig = useAppConfig()
const enabled = computed(() => appConfig.heartCursor?.enabled !== false)
interface Heart {
id: number
x: number
y: number
size: number
color: string
rotation: number
}
const hearts = ref<Heart[]>([])
let heartId = 0
let lastTime = 0
const throttleMs = 50
const colors = [
'rgba(255, 107, 129, 0.85)',
'rgba(255, 138, 128, 0.85)',
'rgba(255, 167, 196, 0.85)',
'rgba(244, 143, 177, 0.85)',
'rgba(233, 30, 99, 0.75)',
'rgba(255, 82, 82, 0.8)',
'rgba(255, 128, 171, 0.85)',
]
function randomColor() {
return colors[Math.floor(Math.random() * colors.length)]
}
function spawnHeart(x: number, y: number) {
hearts.value.push({
id: heartId++,
x: x + (Math.random() - 0.5) * 20,
y: y + (Math.random() - 0.5) * 20,
size: 10 + Math.random() * 14,
color: randomColor(),
rotation: (Math.random() - 0.5) * 40,
})
if (hearts.value.length > 20) {
hearts.value.splice(0, hearts.value.length - 20)
}
}
function onMouseMove(e: MouseEvent) {
if (!enabled.value) return
const now = Date.now()
if (now - lastTime < throttleMs) return
lastTime = now
spawnHeart(e.clientX, e.clientY)
}
function onTouchMove(e: TouchEvent) {
if (!enabled.value) return
const touch = e.touches[0]
if (!touch) return
const now = Date.now()
if (now - lastTime < throttleMs) return
lastTime = now
spawnHeart(touch.clientX, touch.clientY)
}
function onAfterLeave(el: Element) {
const idx = hearts.value.findIndex(
h => h.id === Number((el as HTMLElement).dataset.id)
)
if (idx !== -1) hearts.value.splice(idx, 1)
}
onMounted(() => {
document.addEventListener('mousemove', onMouseMove, { passive: true })
document.addEventListener('touchmove', onTouchMove, { passive: true })
})
onBeforeUnmount(() => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('touchmove', onTouchMove)
})
</script>
逐段说明:
节流控制 — throttleMs = 50 表示每 50ms 最多生成一个爱心。鼠标移动事件触发频率极高(每秒可达数百次),不做节流会瞬间创建大量 DOM 元素,拖慢页面。
随机参数 — 每个爱心在鼠标位置基础上偏移 ±10px(避免完全重叠),大小 10-24px 随机,旋转 ±20° 随机,颜色从 7 种粉红色系中随机选取。
数量上限 — hearts.value.length > 20 时移除最早的爱心,防止数组无限增长。
passive: true — 告诉浏览器事件回调不会调用 preventDefault(),允许浏览器在滚动时并行处理鼠标事件,提升性能。
onAfterLeave — Vue 的 <TransitionGroup> 在离开动画结束后触发,此时从数组中移除对应爱心数据。
样式部分
<style lang="scss" scoped>
.heart-cursor {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.heart-item {
position: absolute;
line-height: 1;
will-change: transform, opacity;
animation: heart-float 1.2s ease-out forwards;
filter: drop-shadow(0 0 2px rgba(255, 107, 129, 0.4));
}
.heart-enter-active {
animation: heart-float 1.2s ease-out forwards;
}
.heart-leave-active {
display: none;
}
@keyframes heart-float {
0% {
opacity: 1;
transform: scale(0.3) translateY(0);
}
30% {
opacity: 1;
transform: scale(1.1) translateY(-10px);
}
100% {
opacity: 0;
transform: scale(0.6) translateY(-60px);
}
}
</style>
动画分三个阶段:
| 阶段 | 时间点 | 效果 |
|---|---|---|
| 弹出 | 0% → 30% | 从 0.3 倍放大到 1.1 倍,略微上移,完全不透明 |
| 飘散 | 30% → 100% | 缩小到 0.6 倍,上飘 60px,透明度降为 0 |
| 结束 | 100% | forwards 保持最终状态,随后被 onAfterLeave 清理 |
几个关键属性:
pointer-events: none— 爱心不会拦截点击,不影响页面交互will-change: transform, opacity— 提示浏览器这两个属性会变化,提前创建合成层filter: drop-shadow(...)— 给爱心加一层柔和的粉色光晕,增加质感overflow: hidden— 容器裁剪超出视窗的爱心,避免出现滚动条
第二步:注册组件
打开 app/app.vue,在 <BlogPetals /> 下方添加一行:
<!-- 文件:app/app.vue --> <!-- 在 <BlogPetals /> 后面添加 --> <BlogHeartCursor />
Nuxt 3 的自动导入机制会识别 app/components/blog/BlogHeartCursor.vue,无需手动 import。
第三步:添加配置项
打开 app/app.config.ts,在 petals 配置后面添加:
// 文件:app/app.config.ts
// 在 petals 配置项后面添加
heartCursor: {
enabled: true,
},
这样可以通过 useAppConfig().heartCursor.enabled 在运行时控制特效开关,设为 false 即可关闭。
性能优化要点
| 优化手段 | 说明 |
|---|---|
| 50ms 节流 | 鼠标移动每秒最多触发 20 次,而非数百次 |
| 数组上限 20 | 最多同时存在 20 个爱心 DOM 元素 |
will-change | 让浏览器为 transform 和 opacity 创建独立合成层,动画走 GPU |
passive: true | 不阻塞浏览器的滚动优化 |
pointer-events: none | 爱心不参与命中测试,减少事件分发开销 |
onAfterLeave 清理 | 动画结束后及时移除数据和 DOM |
触屏适配
组件同时监听了 touchmove 事件,在手机上滑动时也会产生爱心。touch.clientX/Y 和 mouse.clientX/Y 的坐标系一致,不需要额外转换。
自定义扩展
如果想调整特效风格,可以修改以下参数:
// 爱心颜色,改成你喜欢的色系 const colors = [ 'rgba(100, 200, 255, 0.85)', // 天蓝 'rgba(150, 255, 200, 0.85)', // 薄荷绿 ] // 生成频率,越小越密集(单位 ms) const throttleMs = 30 // 爱心大小范围(单位 px) size: 14 + Math.random() * 18, // 上飘距离(CSS 动画中的 translateY) translateY(-80px)
如果想把爱心换成其他符号(比如星星 ✦、樱花 ❀),只需修改模板中的 ♥ 字符即可。

评论区
评论加载中...