更新于 

Custom Mouse 自定义Cursor

项目介绍

项目地址

自定义Cursor

参考CodePen

在光标移动的父容器Page中,创建自定义光标容器:

1
2
3
4
5
6
<body class="body-content">
<!-- 光标外环 -->
<div class="cursor-dot-outline"></div>
<!-- 光标焦点 -->
<div class="cursor-dot"></div>
</body>

加上随意样式,最好是轮廓线条流畅的形状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
:root{
--primary:#00ff2a;
}
body{
width:50vw;
height:50vh;
overflow:hidden;
position:relative;
cursor:none
}
.cursor-dot-outline,.cursor-dot{
position:absolute;
pointer-events: none;
top:0;
left:0;
border-radius:50%;
transform:translate(-50%, -50%);
opacity: 0;
transition:
transform 0.3s ease-in-out,
opacity 0.3s ease-in-out;
}
.cursor-dot-outline{
width:50px;
height: 50px;
background:rgb(from var(--primary) r g b / 0.2);
}
.cursor-dot{
width:8px;
height:8px;
background:var(--primary);
}

设计控制光标的类,需要注意一些交互事件的触发时机:

  • 鼠标点击事件
    • 效果:点击轮廓变大,焦点变小,松开恢复原样
    • 绑定事件:mousedown/mouseup
  • 鼠标进入和交互元素事件
    • 效果:移入轮廓变大,焦点变小,移出恢复原样
    • 绑定事件:交互元素的mouseover/mouseleave
  • 鼠标进入/离开屏幕事件
    • 效果:进入屏幕透明度为1,离开屏幕透明度为0
    • 绑定事件:mouseenter/mouseleave
  • 鼠标移动事件
    • 效果:移动鼠标,焦点定位,轮廓跟随
    • 绑定事件:mousemove
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
class Cursor{
/**
* contentDom 容器dom
* dotDom 焦点dom
* outlineDom 轮廓dom
*/
constructor(contentDom, dotDom, outlineDom){
/**
* _x 轮廓圆的x坐标
*/
this. _x = 0
/**
* _y 轮廓圆的y坐标
*/
this._y = 0
/**
* endX 鼠标焦点的x坐标
*/
this.endX = window.innerWidth / 2
/**
* endY 鼠标焦点的y坐标
*/
this.endY = window.innerHeight / 2
/**
* delay 延迟的步数
*/
this.delay = 8
/**
* cursorVisible 自定义光标可见度
*/
this.cursorVisible = true
/**
* cursorEnlarged 自定义光标是否放大
*/
this.cursorEnlarged = false;
/**
* $content 鼠标容器dom
*/
this.$content = contentDom
/**
* $dot 焦点dom
*/
this.$dot = dotDom
/**
* $outline 外轮廓dom
*/
this.$outline = outlineDom
/**
* dotSize 焦点宽度
*/
this.dotSize = dotDom.offsetWidth
/**
* outlineSize 外轮廓宽度
*/
this.outlineSize = outlineDom.offsetWidth

this.setupEventListeners()
this.updateOutlineDot()
}
/**
* 绑定注册事件
*/
setupEventListeners(){
this.$content.querySelectorAll("a").forEach(el=>{
el.addEventListener('mouseover', ()=>{
this.cursorEnlarged = true
this.toggleCursorSize()
})
el.addEventListener('mouseout',()=>{
this.cursorEnlarged = false
this.toggleCursorSize()
})
})
this.$content.addEventListener("mousedown",()=>{
this.cursorEnlarged = true
this.toggleCursorSize()
})
this.$content.addEventListener("mouseup",()=>{
this.cursorEnlarged = false
this.toggleCursorSize()
})
this.$content.addEventListener("mouseenter",()=>{
this.cursorVisible = true
this.toggleCursorVisibility()
})
this.$content.addEventListener("mouseleave",()=>{
this.cursorVisible = false
this.toggleCursorVisibility()
})
this.$content.addEventListener("mousemove",e=>{
const rect = e.target.getBoundingClientRect()
this.endX = rect.left + e.offsetX
this.endY = rect.top + e.offsetY
// console.log(`RectLeft=${rect.left} RectTop=${rect.top} OffsetX=${e.offsetX} OffsetY=${e.offsetY}`)
this.$dot.style.top = `${this.endY}px`
this.$dot.style.left = `${this.endX}px`
})
}
/**
* 更新OutlineDot的位置
*/
updateOutlineDot(){
this._x += (this.endX - this._x) / this.delay
this._y += (this.endY - this._y) / this.delay
this.$outline.style.top = `${this._y}px`
this.$outline.style.left = `${this._x}px`
requestAnimationFrame(this.updateOutlineDot.bind(this))
}
/**
* 切换cursor的尺寸
*/
toggleCursorSize(){
if(this.cursorEnlarged){
this.$dot.style.transform = `translate(-50%, -50%) scale(0.7)`
this.$outline.style.transform = `translate(-50%, -50%) scale(1.5)`
}else{
this.$dot.style.transform = `translate(-50%, -50%) scale(1)`
this.$outline.style.transform = `translate(-50%, -50%) scale(1)`
}
}
/**
* 切换cursor的显隐
*/
toggleCursorVisibility(){
if(this.cursorVisible){
this.$dot.style.opacity = '1'
this.$outline.style.opacity = '1'
}else{
this.$dot.style.opacity = '0'
this.$outline.style.opacity = '0'
}
}
}

背景跟随鼠标晃动

参考CodePen

背景图片dom:

1
2
3
<body>
<div class="background-image"></div>
</body>

添加样式,注意背景图片需要比父容器稍微大一些,
这样即便在上下左右晃动时图片也不会被晃出父容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
*{
margin:0;
padding:0;
background-repeat: no-repeat;
background-size:cover;
}
body{
width:100vw;
height:100vh;
overflow:hidden;
}
.background-image{
width:120%;
height:120%;
background-image: url(./mountains.jpg);
}

添加鼠标跟随晃动js脚本,
核心方法就是将鼠标相对屏幕的坐标
转换为背景图片的偏移量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const bg = document.querySelector('body')

let mouse_endX = 0, mouse_endY = 0
let mouse_x = 0, mouse_y = 0
let delay = 10
bg.addEventListener('mousemove', e=>{
// 将鼠标移动的绝对距离由 [0, 1] 映射到 [0, 10] 上
mouse_endX = e.clientX / window.innerWidth * 10
mouse_endY = e.clientY / window.innerHeight * 10
})
// 跟随效果
function updateBackPosition(){
mouse_x += (mouse_endX - mouse_x) / delay
mouse_y += (mouse_endY - mouse_y) / delay
bg.style.transform = `translate3d(${-mouse_x}%, ${-mouse_y}%, 0)`
requestAnimationFrame(updateBackPosition)
}
updateBackPosition()

其它

3D模型部分的实现

使用的是官方文档中,PCDLoader部分的Demo
官方Demo
PCDLoader

鼠标点击效果添加

鼠标点击出出现的波纹动画为是用Lottie实现的,
思路就是在获取鼠标点击处的坐标,
在点击数组中添加新的点位,
页面根据点击数组响应式渲染新Lottie组件,
监听Lottie组件的onComplete事件,确保在动画播放结束之后删除点击数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import Lottie from "lottie-react";
import Animation_Clicking from "@/assets/Animation/Animation_Clicking.json";
interface ConfettiVO {
/**
* 礼花唯一标识
*/
id:number,
/**
* x坐标
*/
x:number,
/**
* y坐标
*/
y:number
}
// 点击尺寸
const confetti_w = 100, confetti_h = 100;
let idCounter = 0;
export default function CustomDotCursorComp() {
/**
* 鼠标点击,新增效果
*/
const [confettiList, setConfettiList] = useState<ConfettiVO[]>([])
function addConfetti(event: any): any {
const newConfetti: ConfettiVO = {
id: idCounter++,
x: event.pageX,
y: event.pageY,
}
setConfettiList([...confettiList, newConfetti])
}
/**
* 动画播放结束,将点位清除
* @param confetti
*/
function animationComplete(confetti: ConfettiVO) {
setConfettiList(confettiList.filter(point => point.id !== confetti.id))
}
return (
<div onMouseDown={addConfetti}>
{confettiList.map((point) => {
return <Lottie
key={point.id}
animationData={Animation_Clicking}
className={styles['mouse-confetti']}
style={{
top: `${point.y}px`,
left: `${point.x}px`,
width: `${confetti_w}px`,
height: `${confetti_h}px`
}}
loop={false}
onComplete={() => animationComplete(point)}
/>
})}
<div/>
)
}
click事件bug

这部分效果的实现存在一个bug,就是自定义光标监听了焦点的mousedown和mouseup事件,
鼠标点下和抬起构成一个完整的触发效果,
但是同时在鼠标点下时创建了一个波纹效果,波纹效果dom遮盖住了鼠标的mouseup事件的触发,
所以导致鼠标点下的缩放效果无法正常实现。

解决方法:将波纹效果dom的css属性point-event设置为none