5 changed files with 212 additions and 30 deletions
@ -1,6 +1,9 @@ |
|||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||
|
import TextCursor from './components/TextCursor/TextCursor.vue'; |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
|
<TextCursor class="text-cursor-overlay" text="💀" :delay="0.01" :spacing="100" :exit-duration="0.5" |
||||
|
:removal-interval="30" :max-points="8" /> |
||||
<router-view /> |
<router-view /> |
||||
</template> |
</template> |
||||
|
|||||
@ -0,0 +1,188 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { ref, onMounted, onUnmounted } from 'vue'; |
||||
|
import { Motion } from 'motion-v'; |
||||
|
|
||||
|
interface TextCursorProps { |
||||
|
text?: string; |
||||
|
delay?: number; |
||||
|
spacing?: number; |
||||
|
followMouseDirection?: boolean; |
||||
|
randomFloat?: boolean; |
||||
|
exitDuration?: number; |
||||
|
removalInterval?: number; |
||||
|
maxPoints?: number; |
||||
|
} |
||||
|
|
||||
|
interface TrailItem { |
||||
|
id: number; |
||||
|
x: number; |
||||
|
y: number; |
||||
|
angle: number; |
||||
|
randomX?: number; |
||||
|
randomY?: number; |
||||
|
randomRotate?: number; |
||||
|
} |
||||
|
|
||||
|
const props = withDefaults(defineProps<TextCursorProps>(), { |
||||
|
text: '⚛️', |
||||
|
delay: 0.01, |
||||
|
spacing: 100, |
||||
|
followMouseDirection: true, |
||||
|
randomFloat: true, |
||||
|
exitDuration: 0.5, |
||||
|
removalInterval: 30, |
||||
|
maxPoints: 5 |
||||
|
}); |
||||
|
|
||||
|
const trail = ref<TrailItem[]>([]); |
||||
|
const lastMoveTime = ref(Date.now()); |
||||
|
const idCounter = ref(0); |
||||
|
|
||||
|
let removalIntervalId: ReturnType<typeof setInterval> | null = null; |
||||
|
|
||||
|
const handleMouseMove = (e: MouseEvent) => { |
||||
|
// 使用 window 监听:覆盖层为 pointer-events:none 时根节点收不到事件 |
||||
|
const mouseX = e.clientX; |
||||
|
const mouseY = e.clientY; |
||||
|
|
||||
|
let newTrail = [...trail.value]; |
||||
|
|
||||
|
if (newTrail.length === 0) { |
||||
|
newTrail.push({ |
||||
|
id: idCounter.value++, |
||||
|
x: mouseX, |
||||
|
y: mouseY, |
||||
|
angle: 0, |
||||
|
...(props.randomFloat && { |
||||
|
randomX: Math.random() * 10 - 5, |
||||
|
randomY: Math.random() * 10 - 5, |
||||
|
randomRotate: Math.random() * 10 - 5 |
||||
|
}) |
||||
|
}); |
||||
|
} else { |
||||
|
const last = newTrail[newTrail.length - 1]; |
||||
|
const dx = mouseX - last.x; |
||||
|
const dy = mouseY - last.y; |
||||
|
const distance = Math.sqrt(dx * dx + dy * dy); |
||||
|
|
||||
|
if (distance >= props.spacing) { |
||||
|
let rawAngle = (Math.atan2(dy, dx) * 180) / Math.PI; |
||||
|
if (rawAngle > 90) rawAngle -= 180; |
||||
|
else if (rawAngle < -90) rawAngle += 180; |
||||
|
const computedAngle = props.followMouseDirection ? rawAngle : 0; |
||||
|
const steps = Math.floor(distance / props.spacing); |
||||
|
|
||||
|
for (let i = 1; i <= steps; i++) { |
||||
|
const t = (props.spacing * i) / distance; |
||||
|
const newX = last.x + dx * t; |
||||
|
const newY = last.y + dy * t; |
||||
|
newTrail.push({ |
||||
|
id: idCounter.value++, |
||||
|
x: newX, |
||||
|
y: newY, |
||||
|
angle: computedAngle, |
||||
|
...(props.randomFloat && { |
||||
|
randomX: Math.random() * 10 - 5, |
||||
|
randomY: Math.random() * 10 - 5, |
||||
|
randomRotate: Math.random() * 10 - 5 |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (newTrail.length > props.maxPoints) { |
||||
|
newTrail = newTrail.slice(newTrail.length - props.maxPoints); |
||||
|
} |
||||
|
|
||||
|
trail.value = newTrail; |
||||
|
lastMoveTime.value = Date.now(); |
||||
|
}; |
||||
|
|
||||
|
const startRemovalInterval = () => { |
||||
|
if (removalIntervalId) { |
||||
|
clearInterval(removalIntervalId); |
||||
|
} |
||||
|
|
||||
|
removalIntervalId = setInterval(() => { |
||||
|
if (Date.now() - lastMoveTime.value > 100) { |
||||
|
if (trail.value.length > 0) { |
||||
|
trail.value = trail.value.slice(1); |
||||
|
} |
||||
|
} |
||||
|
}, props.removalInterval); |
||||
|
}; |
||||
|
|
||||
|
onMounted(() => { |
||||
|
window.addEventListener('mousemove', handleMouseMove, { passive: true }); |
||||
|
startRemovalInterval(); |
||||
|
}); |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
window.removeEventListener('mousemove', handleMouseMove); |
||||
|
if (removalIntervalId) { |
||||
|
clearInterval(removalIntervalId); |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="text-cursor-container"> |
||||
|
<div class="text-cursor-content"> |
||||
|
<Motion |
||||
|
v-for="item in trail" |
||||
|
:key="item.id" |
||||
|
:initial="{ opacity: 0, scale: 0.5, rotate: item.angle }" |
||||
|
:animate="{ |
||||
|
opacity: 1, |
||||
|
scale: 1, |
||||
|
x: props.randomFloat ? [0, item.randomX || 0, 0] : 0, |
||||
|
y: props.randomFloat ? [0, item.randomY || 0, 0] : 0, |
||||
|
rotate: props.randomFloat ? [item.angle, item.angle + (item.randomRotate || 0), item.angle] : item.angle |
||||
|
}" |
||||
|
:transition="{ |
||||
|
duration: props.randomFloat ? 2 : props.exitDuration, |
||||
|
repeat: props.randomFloat ? Infinity : 0, |
||||
|
repeatType: props.randomFloat ? 'mirror' : 'loop' |
||||
|
}" |
||||
|
class="text-cursor-item" |
||||
|
:style="{ left: `${item.x}px`, top: `${item.y}px` }" |
||||
|
> |
||||
|
{{ props.text }} |
||||
|
</Motion> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.text-cursor-container { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100vw; |
||||
|
height: 100vh; |
||||
|
pointer-events: none; |
||||
|
z-index: 9999; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.text-cursor-content { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.text-cursor-item { |
||||
|
position: absolute; |
||||
|
font-size: 24px; |
||||
|
white-space: nowrap; |
||||
|
user-select: none; |
||||
|
transform: translate(-50%, -50%); |
||||
|
} |
||||
|
|
||||
|
.text-cursor-item:hover { |
||||
|
cursor: none; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue