JavaScript实现鼠标跟随运动的三种典型效果:眼球追踪、菜单缩放与方向引导

作者:函数茶馆

时间:2026-10-30

浏览:321

本文详细解析JavaScript中鼠标跟随运动的三种实现方式。通过三角函数计算角度,实现眼球随鼠标转动的视觉效果;利用距离与宽高反比关系,打造类似苹果菜单的近大远小交互;基于象限角度判断,实现元素根据鼠标移入移出方向的引导动画。包含完整公式推导与代码逻辑说明。

文章目录

鼠标跟随运动概述

除了常见的直线与曲线运动,前端交互中还广泛存在一种鼠标跟随运动。这类效果通常依赖于三角函数的角度计算或比例运算。本文将通过几个具体案例,深入讲解角度运动在鼠标跟随场景中的应用。

眼球转动效果实现

眼球转动效果

许多网页设计中包含眼球跟随鼠标的交互。当鼠标在页面移动时,眼球会实时朝向鼠标所在的方向转动,增强页面的生动感。

鼠标跟随运动概述

如图所示,设眼球中心坐标为(x0, y0),鼠标当前位置为(x, y)。若以圆心为原点(0,0),直线与垂直方向的夹角为a,可建立如下几何关系。

tan(a) = x/y = x0/y0 x0 = r*sin(a) y0 = r*cos(a)

在mousemove事件监听中,获取鼠标坐标(x, y)后,即可计算出夹角a。利用上述公式,进一步推导出眼球中心的坐标位置,从而实现眼球跟随。

以左右眼为例,左眼ball1圆心坐标为(39, 72),右眼ball2圆心坐标为(106, 68)。眼球移动的半径限制为12px,需在此范围内计算新坐标。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
#test{position: absolute;top: 100px;left: 200px;}
#ball1{position: absolute;top: 62px;left: 28px;}
#ball2{position: absolute;top: 58px;left: 96px;}
</style>
</head>
<body>
<div id="test">
  <img src="head.png" alt="body">  
  <img id="ball1" src="ball.png" alt="ball">
  <img id="ball2" src="ball.png" alt="ball">  
</div>
<script>
//声明脑袋的默认偏移
var offsetLeft = test.offsetLeft;
var offsetTop = test.offsetTop;
//声明左眼夹角a1、右眼夹角a2
var a1,a2;
//声明左眼圆心(X1,Y1)、右眼圆心(X2,Y2)
var X1 = 38,Y1 = 72,X2 = 106,Y2 = 68;
//声明半径
var R = 12;
document.onmousemove = function(e){
  e = e || event;
  //获取鼠标坐标
  var x = e.clientX;
  var y = e.clientY;
  //更新夹角a1、a2
  a1 = Math.atan2(x-X1-offsetLeft,y-Y1-offsetTop);
  a2 = Math.atan2(x-X2-offsetLeft,y-Y2-offsetTop);
  //更新左眼、右眼的left、top值
  ball1.style.left = R*Math.sin(a1) + X1 -10 + 'px';
  ball1.style.top = R*Math.cos(a1) + Y1 -10+ 'px';
  ball2.style.left = R*Math.sin(a2) + X2 -10 + 'px';
  ball2.style.top = R*Math.cos(a2) + Y2 -10 + 'px';
}
</script>
</body>
</html>

苹果菜单缩放效果

苹果菜单风格效果

此类菜单交互中,菜单项的尺寸随鼠标距离变化:距离鼠标越近的菜单项,其宽高越大;距离越远,宽高越小,形成视觉聚焦效果。

鼠标跟随运动概述

通过mousemove事件的clientX和clientY获取鼠标坐标。已知各菜单项的坐标,计算鼠标与菜单项的距离。该距离与菜单项的宽高呈反比关系,据此动态调整样式。

[注意]在代码实现中,避免将元素的自定义属性命名为x,因为x已被浏览器原生属性占用,可能导致冲突。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
body{margin: 0;}
#test{position: absolute;bottom:0;width: 100%;text-align: center;}
img{width: 64px;height: 64px;}
</style>
</head>
<body>
<div id="test">
  <img id="img1" src="img/1.png">
  <img src="img/2.png">
  <img src="img/3.png">
  <img src="img/4.png">
  <img src="img/5.png"> 
</div>
<script>
//声明菜单项的宽高值
var offsetWidth = img1.offsetWidth;
var offsetHeight = img1.offsetHeight;
//声明外层盒子的left、top值
var offsetLeft = test.offsetLeft;
var offsetTop = test.offsetTop;
//获取菜单项
var imgs = test.getElementsByTagName('img');
document.onmousemove = function(e){
  e = e || event;
  //更新鼠标位置
  var x = e.clientX;
  var y = e.clientY;
  for(var i = 0; i < imgs.length; i++){
    //获取菜单项的坐标
    imgs[i].x0= imgs[i].offsetLeft+offsetLeft+imgs[i].offsetWidth/2;
    imgs[i].y0 = imgs[i].offsetTop + offsetTop + imgs[i].offsetHeight/2;
    //更新鼠标与菜单项的距离
    imgs[i].len =Math.sqrt((x-imgs[i].x0)*(x-imgs[i].x0) + (y-imgs[i].y0)*(y-imgs[i].y0));
    //限制范围
    if(imgs[i].len > 150){
      imgs[i].len = 150;
    }
    //更新菜单项的宽高
    imgs[i].style.width = (1-imgs[i].len/300)*2*offsetWidth + 'px';
    imgs[i].style.height = (1-imgs[i].len/300)*2*offsetHeight + 'px';
    
  }
}
</script>
</body>
</html>

方向跟随动画逻辑

方向跟随效果

部分网页实现元素根据鼠标移入或移出方向进行引导动画。即鼠标从哪个方向接近,元素便从该方向移入;移出时同理,元素沿对应方向离开。

此类移入移出动画可采用匀速直线运动实现,核心难点在于准确判断鼠标的进入或移出方向。

鼠标跟随运动概述

如示意图所示,可将正方形元素划分为八个区域:上-右、上-左、左-上、左-下、下-右、下-左、右-上、右-下。每个区域为等腰直角三角形,元素进入特定区域时,该区域名称的前半部分即代表元素运动的方向。

设正方形中心坐标为(x0, y0),动态元素move进入时的坐标为(x, y)。以这两点连线与水平正方向的夹角a为基准,通过判断a的取值范围,即可确定move进入的具体方向。

-45<a<45时,进入方向为右 45<a<135时,进入方向为上 a>135或a<-135时,进入方向为左 -135<a<-45时,进入方向为下

确定move进入方向后,需先将move瞬间移动至对应方向的边界位置,随后启动匀速直线运动,最终使move停止在与静态元素test重合的位置。

当动态元素move移出静态元素test范围时,需注意:此时鼠标仍位于move上方,因此不会触发test的mouseout事件,而是触发move自身的mouseout事件。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
#box{overflow: hidden;position: relative;left: 100px;top: 100px;height: 100px;width: 300px;}
.test{width: 100px;height: 100px;position: absolute;font:20px/100px '宋体';text-align: center;} 
</style>
</head>
<body>
<div id="box">
  <div class="test" style="top: 0px;left: 0px;background-color: pink;">1</div>
  <div class="test" style="top: 0px;left: 100px;background-color: lightcoral;">2</div>
  <div class="test" style="top: 0px;left: 200px;background-color: lightgreen;">3</div> 
  <div id="move" style="width: 100px;height: 100px;background-color: lightblue;position: absolute;top: -100px;left: -100px;"></div> 
</div>
<script>
var tests = box.getElementsByTagName('div');
for(var i = 0; i < tests.length; i++){
  tests[i].onmouseover = fn;
}
//鼠标移出动态元素(move)时,再将fn()函数置于所有静态元素上
move.onmouseout = fn;
function fn(e){
  e = e || event;
  //阻止冒泡
  if(e.stopPropagation){
    e.stopPropagation();
  }else{
    e.cancelBubble = true;
  }
  for(var i = 0; i < tests.length; i++){
    tests[i].onmouseover = fn;
  }
  var _this = this;
  //鼠标移入动态元素(move)时,将静态元素上的mouseover事件置空
  move.onmouseover = function(){
    move.innerHTML = _this.innerHTML;
    _this.onmouseover = null;
  }
  //声明坐标
  var x = e.clientX;
  var y = e.clientY;
  //声明静态元素(test)左上角坐标(相对于父级)
  var x11 = this.offsetLeft;
  var y11 = this.offsetTop;  
   //声明静态元素(test)中心点坐标(相对于父级)
  var x10 = x11 + this.offsetWidth/2;
  var y10 = y11 + this.offsetHeight/2;  
  //声明静态元素(test)左上角坐标(相对于文档)
  var x21 = this.parentNode.offsetLeft + x11;
  var y21 = this.parentNode.offsetTop + y11;
  //声明静态元素(test)中心点坐标(相对于文档)
  var x20 = x21 + this.offsetWidth/2;
  var y20 = y21 + this.offsetHeight/2;
  //声明静态元素宽高
  var height = this.offsetHeight;
  var width = this.offsetWidth;
  //声明并计算夹角
  var a = Math.atan2(y20-y,x-x20)*180/Math.PI;
  //声明并计算方向
  var dir;
  if(a > -45 && a < 45){
    dir = 'right';
  }else if(a > 45 && a < 135){
    dir = 'top';
  }else if(a > -135 && a < 45){
    dir = 'bottom';
  }else{
    dir = 'left';
  }
  //鼠标移入时
  if(e.type == 'mouseover'){
    //更新动态元素(move)的初始位置
    //移动动态元素(move)直到完全覆盖静态元素(test)
    if(dir == 'right'){
      move.style.left = x10 + width/2 + 'px';
      move.style.top = y10 - height/2 + 'px';
      fnMove(move,'left',x11)
    }else if(dir == 'top'){
      move.style.left = x10 - width/2 + 'px';
      move.style.top = y10 - height/2 - height + 'px'; 
      fnMove(move,'top',y11)
    }else if(dir == 'left'){
      move.style.left = x10 - width/2 - width + 'px';
      move.style.top = y10 - height/2 + 'px';
      fnMove(move,'left',x11)     
    }else{
      move.style.left = x10 - width/2 + 'px';
      move.style.top = y10 - height/2 + height + 'px'; 
      fnMove(move,'top',y11)    
    }     
  }
  if(e.type == 'mouseout'){
    //鼠标移出时
    if(dir == 'right'){
      fnMove(move,'left',x11 + width);
    }else if(dir == 'top'){
      fnMove(move,'top',y11 - height);
    }else if(dir == 'left'){
      fnMove(move,'left',x11 - width);
    }else{
      fnMove(move,'top',y11 + height);
    }     
  }
}
function getCSS(obj,style){
  if(window.getComputedStyle){
    return getComputedStyle(obj)[style];
  }
  return obj.currentStyle[style];
}
function fnMove(obj,attr,target){
  var H = obj.offsetHeight;
  if(obj.timer) return;
  var cur = parseFloat(getCSS(obj,attr));
  if(target > cur){
    var step = H/4;
  }else{
    var step = -H/4;
  }
  obj.timer = setInterval(function(){
    cur = parseFloat(getCSS(obj,attr));
    cur = cur + step;
    if((cur -target) * step >= 0){
      cur = target; 
    }
      obj.style[attr] = cur + 'px'; 
    if(cur == target){
      clearInterval(obj.timer);
      obj.timer = 0;
    }
  },20);
} 
</script>  
</body>
</html>

以上为本文全部内容,希望这些示例能对您的学习有所帮助,欢迎继续支持本站获取更多技术资源。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章