看看效果:
可以在 TIY  中调试:
也可以在主页预览 该鼠标指针 样式及配色  来自一个我非常喜欢的博客 . 在此基础上,我对其优化和扩展,代码使用纯 js
0x01 设置实体指针“实体指针” 就是那个深色的小点啦
为了让页面所有的元素都有这个效果,直接对 * 设置就好了.
CSS 代码如下:
1 2 3 * {     cursor : url ("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8px' height='8px'><circle cx='4' cy='4' r='4' opacity='.5'/></svg>" ) 4  4 , auto } 
这里的 url 是个内置的 svg 图片:
长宽 8 像素,颜色为 #000 再加上 0.5 的透明度.
实际上,这一步会在后面有更改,我们不采用直接 css 文件的方式引入指针
0x02 生成跟随的圆圈“圆圈” 指浅色大一点的圆,采用 DOM 渲染.
我们用一个类来封装这个指针:
在这个类里面创建好这个圆圈:
1 2 3 4 5 6 if  (!this .cursor ) {    this .cursor  = document .createElement ("div" );     this .cursor .id  = "cursor" ;     this .cursor .classList .add ("hidden" );      document .body .append (this .cursor ); } 
然后,我们要确定用户会有哪些鼠标操作:
移上元素:用 document.onmouseover 实现 移出元素:用 document.onmouseout 实现 移动鼠标:用 document.onmousemove 实现 移入页面:用 document.onmouseenter 实现 移出页面:用 document.onmouseleave 实现 按下左键:用 document.onmousedown 实现 松开左键:用 document.onmouseup 实现 给圆圈设置好一些样式,以供使用:
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 #cursor  {    position : fixed;     width : 16px ;     height : 16px ;     background : #000 ;     border-radius : 8px ;     opacity : 0.25 ;     z-index : 10086 ;     pointer-events : none;     transition : 0.2s  ease-in-out;     transition-property : background, opacity, transform; } #cursor .hidden  {     opacity : 0 ; } #cursor .hover  {     opacity : 0.1 ;     transform : scale (2.5 ); } #cursor .active  {     opacity : 0.5 ;     transform : scale (0.5 ); } 
然后,我按照难易程度依次说一下各鼠标操作的实现方法
按下 / 松开左键 
两行代码搞定.
1 2 document .onmousedown   = e  =>this .cursor .classList .add ("active" );document .onmouseup     = e  =>this .cursor .classList .remove ("active" );
移入 / 移出页面 
(啊……上面灰色的是浏览器搜索框,鼠标移出去,圆圈自动消失)
1 2 document .onmouseenter  = e  =>this .cursor .classList .remove ("hidden" );document .onmouseleave  = e  =>this .cursor .classList .add ("hidden" );
移上 / 移出元素 
首先明确一个概念:”元素” 是什么?
在这里 “元素” 指 “可以点击的 DOM 对象 “.
那到底怎么教会计算机区分 “可以点击的” 和 “不可以点击的” 呢?
思考这个问题的时候,我第一个想到的是判断元素标签是不是 <a>、<button> 等,但这样做有个很大的弊端,请考虑下面这个元素:
1 <span  onclick ="window.location='/'"  style ="cursor:pointer" > Click ME!</span > 
这种设计的 “按钮” 在前端十分常见,但刚刚的方法不能起作用,这样我们不得不换一个思路:
一般而言,网页设计者会把能点击的元素加上 cursor: pointer 的样式,来提醒用户 “这是个按钮”,所以,只需要检测这个元素是否有 cursor: pointer 属性即可.
显然,我们不能用 el.style["cursor"] == pointer 来判断,因为有些元素是 “天生自带” pointer 样式的,网页设计者不会再加额外的 CSS 属性(例如 <a> 标签).
所以,我们要使出必杀技,window.getComputedStyle() 函数(MDN 文档 ). 这个函数返回元素最终渲染的样式 .
考虑到低版本浏览器的兼容性,和一些奇奇怪怪的报错,我写了这样一个函数:
1 2 3 4 5 6 7 8 const  getStyle  = (el, attr ) => {    try  {         return  window .getComputedStyle              ? window .getComputedStyle (el)[attr]             : el.currentStyle [attr];     } catch  (e) {}     return  "" ; }; 
这样,getStyle(el, "cursor") == "pointer" 就可以判断是否该有 hover 效果了.
只不过,这样又引发了另一个问题:我既然已经设置好了全局 cursor,再 getStyle() 得到的不就是刚刚设置的 cursor 属性了吗?
的确,所以我们得换一个方式:页面加载完成后,在更改全局 cursor 前,先枚举每一个 DOM 元素,看看是否满足 cursor: pointer,如果满足,加入列表当中. 然后再通过 js 插入 CSS 代码的方式设置全局 cursor.
这样做有个意外的收获,考虑这样一个结构:
1 2 3 4 <a  href ="/" >     <div > I'm a block</div >      <div > I'm also a block</div >  </a > 
鼠标移上第一个 <div> 的时候,浏览器会认为移上的元素标签是 <div>,而不是 <a>,如果用 <a> 标签的形式判断,这样无法引发 hover 特效.
采用 getStyle() 的方式判断,<a> 标签里面的所有元素都会有 cursor: pointer 属性,所以不会出现问题.
预处理部分的代码:
1 2 3 4 5 6 7 8 9 var  el = document .getElementsByTagName ('*' );for  (let  i = 0 ; i < el.length ; i++)    if  (getStyle (el[i], "cursor" ) == "pointer" )         this .pt .push (el[i].outerHTML );       this .scr  = document .createElement ("style" );document .body .appendChild (this .scr );this .scr .innerHTML  = `* {cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8px' height='8px'><circle cx='4' cy='4' r='4' opacity='.5'/></svg>") 4 4, auto !important}` ;
最终移上 / 移出特效的代码:
1 2 3 document .onmouseover   = e  =>this .pt .includes (e.target .outerHTML ) && this .cursor .classList .add ("hover" );document .onmouseout    = e  =>this .pt .includes (e.target .outerHTML ) && this .cursor .classList .remove ("hover" );
移动鼠标 
浅色圆圈总是 “跟不上” 深色指针,这里我们简单地使用插值 来实现,记录指针 “上一步” 的位置 this.pos.prev 和 “这一步” 的位置 this.pos.curr,在其间使用线性插值,实现一种 “滞后” 的效果.
线性插值函数:
1 Math .lerp  = (a, b, n ) =>  (1  - n) * a + n * b;
(为了好看直接给它写道 Math 库里去了,好孩子别学)
它返回从数字 a 到数字 b 之间比例为 n 的那个值(是不是异常简单).
然后我们还要注意一个点,cursor 默认位置是页面左上角,执行第一次鼠标移动时,当然不希望圆圈从左上角移动到鼠标的位置,所以要特殊判断一下:
1 2 3 4 5 6 document .onmousemove  = e  =>    if  (this .pos .curr  == null )         this .move (e.clientX  - 8 , e.clientY  - 8 );      this .pos .curr  = {x : e.clientX  - 8 , y : e.clientY  - 8 };     this .cursor .classList .remove ("hidden" ); }; 
其中的 move() 函数(就是拿来移动 cursor 元素的):
1 2 3 4 move (left, top ) {    this .cursor .style ["left" ] = `${left} px` ;     this .cursor .style ["top" ] = `${top} px` ; } 
整个动画的核心是 render() 函数:
1 2 3 4 5 6 7 8 9 10 render (    if  (this .pos .prev ) {         this .pos .prev .x  = Math .lerp (this .pos .prev .x , this .pos .curr .x , 0.15 );         this .pos .prev .y  = Math .lerp (this .pos .prev .y , this .pos .curr .y , 0.15 );         this .move (this .pos .prev .x , this .pos .prev .y );     } else  {         this .pos .prev  = this .pos .curr ;     }     requestAnimationFrame (() =>  this .render ()); } 
整合一下 用 init() 函数初始化鼠标事件:
1 2 3 4 5 6 7 8 9 init (    document .onmouseover   = e  =>this .pt .includes (e.target .outerHTML ) && this .cursor .classList .add ("hover" );     document .onmouseout    = e  =>this .pt .includes (e.target .outerHTML ) && this .cursor .classList .remove ("hover" );     document .onmousemove   = e  =>this .pos .curr  == null ) && this .move (e.clientX  - 8 , e.clientY  - 8 ); this .pos .curr  = {x : e.clientX  - 8 , y : e.clientY  - 8 }; this .cursor .classList .remove ("hidden" );};     document .onmouseenter  = e  =>this .cursor .classList .remove ("hidden" );     document .onmouseleave  = e  =>this .cursor .classList .add ("hidden" );     document .onmousedown   = e  =>this .cursor .classList .add ("active" );     document .onmouseup     = e  =>this .cursor .classList .remove ("active" ); } 
一直压行一直爽,后期维护火葬场
0x03 加一些功能有些时候,页面的 DOM 被重新渲染了一遍(例如说 vue router 的页面跳转)或加了些元素,这样的 hover 效果就不起作用了,这时候,我们需要使用 refresh() 函数来重新获取要有 hover 效果的列表:
1 2 3 4 5 6 7 8 9 10 11 refresh (    this .scr .remove ();     this .cursor .classList .remove ("hover" );     this .cursor .classList .remove ("active" );     this .pos  = {curr : null , prev : null };     this .pt  = [];     this .create ();      this .init ();        this .render ();  } 
0x04 最终代码Javascript 部分:
cursor.js 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 var  CURSOR ;Math .lerp  = (a, b, n ) =>  (1  - n) * a + n * b;const  getStyle  = (el, attr ) => {    try  {         return  window .getComputedStyle              ? window .getComputedStyle (el)[attr]             : el.currentStyle [attr];     } catch  (e) {}     return  "" ; }; class  Cursor  {    constructor (         this .pos  = {curr : null , prev : null };         this .pt  = [];         this .create ();         this .init ();         this .render ();     }     move (left, top ) {         this .cursor .style ["left" ] = `${left} px` ;         this .cursor .style ["top" ] = `${top} px` ;     }     create (         if  (!this .cursor ) {             this .cursor  = document .createElement ("div" );             this .cursor .id  = "cursor" ;             this .cursor .classList .add ("hidden" );             document .body .append (this .cursor );         }         var  el = document .getElementsByTagName ('*' );         for  (let  i = 0 ; i < el.length ; i++)             if  (getStyle (el[i], "cursor" ) == "pointer" )                 this .pt .push (el[i].outerHTML );         document .body .appendChild ((this .scr  = document .createElement ("style" )));         this .scr .innerHTML  = `* {cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8px' height='8px'><circle cx='4' cy='4' r='4' opacity='.5'/></svg>") 4 4, auto}` ;     }     refresh (         this .scr .remove ();         this .cursor .classList .remove ("hover" );         this .cursor .classList .remove ("active" );         this .pos  = {curr : null , prev : null };         this .pt  = [];         this .create ();         this .init ();         this .render ();     }     init (         document .onmouseover   = e  =>this .pt .includes (e.target .outerHTML ) && this .cursor .classList .add ("hover" );         document .onmouseout    = e  =>this .pt .includes (e.target .outerHTML ) && this .cursor .classList .remove ("hover" );         document .onmousemove   = e  =>this .pos .curr  == null ) && this .move (e.clientX  - 8 , e.clientY  - 8 ); this .pos .curr  = {x : e.clientX  - 8 , y : e.clientY  - 8 }; this .cursor .classList .remove ("hidden" );};         document .onmouseenter  = e  =>this .cursor .classList .remove ("hidden" );         document .onmouseleave  = e  =>this .cursor .classList .add ("hidden" );         document .onmousedown   = e  =>this .cursor .classList .add ("active" );         document .onmouseup     = e  =>this .cursor .classList .remove ("active" );     }     render (         if  (this .pos .prev ) {             this .pos .prev .x  = Math .lerp (this .pos .prev .x , this .pos .curr .x , 0.15 );             this .pos .prev .y  = Math .lerp (this .pos .prev .y , this .pos .curr .y , 0.15 );             this .move (this .pos .prev .x , this .pos .prev .y );         } else  {             this .pos .prev  = this .pos .curr ;         }         requestAnimationFrame (() =>  this .render ());     } } (() =>  {     CURSOR  = new  Cursor ();      })(); 
CSS 部分:
cursor.css 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 #cursor  {    position : fixed;     width : 16px ;     height : 16px ;     background : #000 ;     border-radius : 8px ;     opacity : 0.25 ;     z-index : 10086 ;     pointer-events : none;     transition : 0.2s  ease-in-out;     transition-property : background, opacity, transform; } #cursor .hidden  {    opacity : 0 ; } #cursor .hover  {    opacity : 0.1 ;     transform : scale (2.5 ); } #cursor .active  {    opacity : 0.5 ;     transform : scale (0.5 ); } 
本代码遵循 Unlicense  协议进行许可,这意味着任何人可以在无声明的前提下使用这段代码.
但是请注意,遵循 Unlicense  协议的只是这段代码,本篇文章仍然遵循 CC BY-NC-SA 4.0  协议.
评论