Browse Source

添加一个全局的鼠标效果

master
秦汉 1 week ago
parent
commit
e6b658f821
  1. 3
      Build_God_Game/src/App.vue
  2. 20
      Build_God_Game/src/components/GooeyNav/GooeyNav.vue
  3. 188
      Build_God_Game/src/components/TextCursor/TextCursor.vue
  4. 27
      Build_God_Game/src/views/CatalogView.vue
  5. 4
      Build_God_Game/src/views/GameView.vue

3
Build_God_Game/src/App.vue

@ -1,6 +1,9 @@
<script setup lang="ts">
import TextCursor from './components/TextCursor/TextCursor.vue';
</script>
<template>
<TextCursor class="text-cursor-overlay" text="💀" :delay="0.01" :spacing="100" :exit-duration="0.5"
:removal-interval="30" :max-points="8" />
<router-view />
</template>

20
Build_God_Game/src/components/GooeyNav/GooeyNav.vue

@ -1,5 +1,5 @@
<template>
<div class="gooey-nav-root">
<div class="gooey-nav-root" :class="`size-${props.size}`">
<div class="relative" ref="containerRef">
<nav class="flex relative" :style="{ transform: 'translate3d(0,0,0.01px)' }">
<ul
@ -47,6 +47,7 @@ interface GooeyNavItem {
interface GooeyNavProps {
items: GooeyNavItem[];
size?: 'small' | 'medium' | 'large';
animationTime?: number;
particleCount?: number;
particleDistances?: [number, number];
@ -62,6 +63,7 @@ const emit = defineEmits<{
}>();
const props = withDefaults(defineProps<GooeyNavProps>(), {
size: 'medium',
animationTime: 600,
particleCount: 15,
particleDistances: () => [90, 10],
@ -225,11 +227,25 @@ onUnmounted(() => {
.gooey-nav-root .gooey-nav-link {
display: inline-block;
outline: none;
padding: 12px 26px;
line-height: 1.3;
box-sizing: border-box;
}
.gooey-nav-root.size-small .gooey-nav-link {
font-size: 11px;
padding: 6px 14px;
}
.gooey-nav-root.size-medium .gooey-nav-link {
font-size: 14px;
padding: 12px 26px;
}
.gooey-nav-root.size-large .gooey-nav-link {
font-size: 16px;
padding: 14px 32px;
}
:root {
--linear-ease: linear(
0,

188
Build_God_Game/src/components/TextCursor/TextCursor.vue

@ -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>

27
Build_God_Game/src/views/CatalogView.vue

@ -125,6 +125,7 @@ onMounted(() => {
<div class="catalog-nav-wrap">
<GooeyNav :items="gooeyNavItems" :initial-active-index="0" @select="onGooeyNavSelect" :animation-time="600"
:size="'small'"
:particle-count="15" :particle-distances="[90, 10]" :particle-r="100" :time-variance="300"
:colors="[1, 2, 3, 1, 2, 3, 1, 4]" />
</div>
@ -307,32 +308,6 @@ onMounted(() => {
padding: 6px 0 14px;
}
/* .catalog-nav-wrap :deep(ul) {
flex-wrap: wrap;
justify-content: center;
row-gap: 0.35rem;
column-gap: 0.45rem !important;
padding-left: 0.25rem !important;
padding-right: 0.25rem !important;
max-width: 100%;
box-sizing: border-box;
text-shadow: none !important;
}
.catalog-nav-wrap :deep(li a),
.catalog-nav-wrap :deep(li.active a) {
text-shadow: none !important;
}
.catalog-nav-wrap :deep(.effect.text) {
text-shadow: none !important;
}
.catalog-nav-wrap :deep(a) {
font-size: 0.82rem;
padding: 0.55em 1.25em !important;
} */
.state-msg {
text-align: center;
color: #888;

4
Build_God_Game/src/views/GameView.vue

@ -328,7 +328,7 @@ onMounted(async () => {
}
.character-sidebar {
flex: 0 0 280px;
flex: 0 0 320px;
min-width: 0;
display: flex;
flex-direction: column;
@ -395,7 +395,7 @@ onMounted(async () => {
}
.game-main {
flex: 0 0 400px;
flex: 0 0 480px;
min-width: 0;
height: 100%;
display: flex;

Loading…
Cancel
Save