CSS实现判断鼠标进入方向

CSS实现判断鼠标进入方向

使用纯 CSS 判断鼠标进入方向执行不一样的方向动画,或搭配 JS 实现同时记录进入方向与移出方向

目录

  1. 纯 CSS 实现
  2. JS 实现
  3. 总结

纯 CSS 实现

实现纯 CSS 判断鼠标进入方向,主要是先通过将 DIV 以对角线切割为 4 个部分,然后即可为这 4 个部分写入:hover 选择器执行不同方向的动画。如果 DIV 是正方形的话,对角线切割就很简单,可以用伪元素通过 rotate(45deg)就可以实现。但是当 Div 是长方形的时候,就需要使用以下的方法了。

对角线切割DIV

目前可以通过 2 种 CSS3 的方式实现

  1. 使用 CSS3 的 clip-path 属性定向裁剪区域
  2. 使用 CSS3 的 transfrom 属性,将 div 倾斜再旋转一定角度实现,需要同时用到 rotate 和 skew2 种 2D 变换。

Clip-path

Demo

clip-path 方式实现主要代码逻辑

CSS

.box {
  width: 400px;
  height: 300px;
  background: #eee;
  position: relative;
  overflow: hidden;
  border: 2px solid #262626;
}
.top,
.right,
.bottom,
.left {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  transition: all 0.4s;
}
.top:after,
.right:after,
.bottom:after,
.left:after {
  position: absolute;
  content: "";
  width: 100%;
  height: 100%;
}
.top {
  top: -100%;
  background: rgb(255, 228, 177);
}
.right {
  right: -100%;
  left: auto;
  background: rgb(200, 177, 255);
}
.bottom {
  bottom: -100%;
  top: auto;
  background: rgb(177, 255, 225);
}
.left {
  left: -100%;
  background: rgb(177, 189, 255);
}
.top:after {
  clip-path: polygon(0 0, 50% 50%, 100% 0);
  top: 100%;
  left: 0;
}
.right:after {
  clip-path: polygon(100% 0, 50% 50%, 100% 100%);
  right: 100%;
  top: 0;
}
.bottom:after {
  clip-path: polygon(0 100%, 50% 50%, 100% 100%);
  bottom: 100%;
  left: 0;
}
.left:after {
  clip-path: polygon(0 0, 50% 50%, 0 100%);
  left: 100%;
  top: 0;
}
.top:hover {
  top: 0;
}
.right:hover {
  right: 0;
}
.bottom:hover {
  bottom: 0;
}
.left:hover {
  left: 0;
}
/* 解决层级阻断问题 */
.top:hover ~ .right,
.top:hover ~ .bottom,
.top:hover ~ .left,
.right:hover ~ .bottom,
.right:hover ~ .left,
.bottom:hover ~ .left {
  display: none;
}

HTML

<div class="box">
  <span>Hover Here</span>
  <div class="top">From Top</div>
  <div class="right">From Right</div>
  <div class="bottom">From Bottom</div>
  <div class="left">From Left</div>
</div>

为.top 的 Div 创建一个伪元素,然后使用 clip-path: polygon(0 0, 50% 50%, 100% 0) 定位左上角、中心、右上角切割出上部分的三角形,这时.top 实际区域是一个五边形(.top:hover 时其伪元素也会包含在其中)。

Transform

Demo

transform 实现方式与 clip-path 方式基本差不多,主要是先将伪类通过旋转加偏移变换成一个平行四边形,将变换顶点定位在中心,然后隐藏超出部分即可。

transform变换DIV

transform 方式实现主要代码逻辑 (仅列举与 clip-path 不一样的代码)

body {
  --angle: 37deg;
}

/*...省略部分...*/

.top:after,
.right:after,
.bottom:after,
.left:after {
  position: absolute;
  content: "";
  width: 100%;
  height: 100%;
  transform-origin: 0 0;
}
.top:after {
  top: 150%;
  left: 50%;
  transform: rotate(calc(var(--angle) - 180deg)) skew(
      calc((var(--angle) - 45deg) * 2)
    );
}
.right:after {
  top: 50%;
  left: -50%;
  transform: rotate(calc(0deg - var(--angle))) skew(
      calc((45deg - var(--angle)) * 2)
    );
}
.bottom:after {
  top: -50%;
  left: 50%;
  transform: rotate(var(--angle)) skew(calc((var(--angle) - 45deg) * 2));
}
.left:after {
  top: 50%;
  left: 150%;
  transform: rotate(calc(180deg - var(--angle))) skew(
      calc((45deg - var(--angle)) * 2)
    );
}

/*...省略部分...*/

transform 方式最大缺点就是需要计算角度,先将变换顶点改为 0 0(原为 50% 50%),就可以直接套用以上的变换公式,再使用 acrtan(高/宽)计算出角度,然后赋值给—angle 自定义 CSS 变量即可。

PS: calc 计算函数中值为 0 时也要带上单位,不然无法生效

JS 实现

Demo 展示

判断方向算法

以上的 Demo 中,为每个 DIV 加入了 mouseenter 和 mouseleave 的事件监听,通过判断方向后然后为其添加不同方向的移入动画和移出动画。

JS 实现判断进入方向,依然是将 div 按对角线切割成 4 个三角形,然后通过判断鼠标事件中移入的点是否在三角形内来确定方向。这里涉及到了一个如何判断点在区域内的算法实现。

判断点是否在同侧

以下提供一个函数判断点 P 和点 O 是否在直线 AB 的同一侧

/* 判断两点是否位于直线同一侧
 * @param {Object} p - P点坐标
 * @param {Object} o - O点坐标
 * @param {Object} a - A点坐标(直线AB)
 * @param {Object} b - B点坐标(直线AB)
 * @return {Boolean}
 */
function isSameSide(p, o, a, b) {
  return (
    ((p.x - a.x) * (b.y - a.y) - (p.y - a.y) * (b.x - a.x)) *
      ((o.x - a.x) * (b.y - a.y) - (o.y - a.y) * (b.x - a.x)) >=
    0
  );
}

该函数需要传入 4 个点坐标(含有 x,y 属性的对象),参数 1 和参数 2 是需要判断的两点,参数 3 和参数 4 是确定直线的两点。使用的是矢量法,过程无需用到三角函数,而且参数 1 和参数 2 顺序可互换,参数 3 和参数 4 顺序也可互换。判断点在三角形内,就需要用 3 次这个函数分别为三条边进行判断。

移入方向判断

移入方向

如图,点 P 为鼠标事件移入获取到的点,当点 P 与点 O 在直线 AB 同一侧、点 P 与点 A 在直线 OB 同一侧、点 P 与点 B 在直线 AO 同一侧,即可判断出点 P 在三角形 AOB 内,即鼠标从上方进入。转换为代码即可写成:

...
if (isSameSide(p,o,a,b) && isSameSide(p,a,o,b) && isSameSide(p,b,a,o)) {
  console.log('Slide From Top')
}
...

移出方向判断

同理,要判断移出方向,可以判断点 P 与点 O 不在直线 AB 同一侧、点 P 与点 A 是否在直线 OB 同一侧、点 P 与点 B 是否在直线 AO 同一侧,即可判断出鼠标从上方移出。转换为代码即可写成:

...
if (!isSameSide(p,o,a,b) && isSameSide(p,a,o,b) && isSameSide(p,b,a,o)) {
  console.log('Slide Out Top')
}
...



下面提供 Demo 实现的主要 JS 代码,由于 CSS 需要定义几个简单的 Slide 动画,代码比较简单,由于篇幅有限就不在本文列出,感兴趣的可以直接查看 Demo 源码。

var isSameSide = function (p, o, a, b) {
  return (
    ((p.x - a.x) * (b.y - a.y) - (p.y - a.y) * (b.x - a.x)) *
      ((o.x - a.x) * (b.y - a.y) - (o.y - a.y) * (b.x - a.x)) >=
    0
  );
};
var boxList = Array.prototype.slice.call(document.querySelectorAll(".box"));
var mouseenterEvent = function (e) {
  var el = e.currentTarget;
  var { top, left, width, height } = el.getBoundingClientRect();
  var a = { x: left, y: top };
  var b = { x: left + width, y: top };
  var c = { x: left + width, y: top + height };
  var d = { x: left, y: top + height };
  var o = { x: left + width / 2, y: top + height / 2 };
  var p = { x: e.x, y: e.y };
  if (
    isSameSide(p, o, a, b) &&
    isSameSide(p, a, o, b) &&
    isSameSide(p, b, o, a)
  ) {
    el.setAttribute("class", "box slide-from-top");
  } else if (
    isSameSide(p, o, b, c) &&
    isSameSide(p, b, o, c) &&
    isSameSide(p, c, b, o)
  ) {
    el.setAttribute("class", "box slide-from-right");
  } else if (
    isSameSide(p, o, c, d) &&
    isSameSide(p, c, o, d) &&
    isSameSide(p, d, o, c)
  ) {
    el.setAttribute("class", "box slide-from-bottom");
  } else if (
    isSameSide(p, o, a, d) &&
    isSameSide(p, a, o, d) &&
    isSameSide(p, d, o, a)
  ) {
    el.setAttribute("class", "box slide-from-left");
  } else {
    el.setAttribute("class", "box slide-from-top");
  }
};
var mouseleaveEvent = function (e) {
  var el = e.currentTarget;
  var { top, left, width, height } = el.getBoundingClientRect();
  var a = { x: left, y: top };
  var b = { x: left + width, y: top };
  var c = { x: left + width, y: top + height };
  var d = { x: left, y: top + height };
  var o = { x: left + width / 2, y: top + height / 2 };
  var p = { x: e.x, y: e.y };
  if (
    !isSameSide(p, o, a, b) &&
    isSameSide(p, a, o, b) &&
    isSameSide(p, b, o, a)
  ) {
    el.setAttribute("class", "box slide-out-top");
  } else if (
    !isSameSide(p, o, b, c) &&
    isSameSide(p, b, o, c) &&
    isSameSide(p, c, b, o)
  ) {
    el.setAttribute("class", "box slide-out-right");
  } else if (
    !isSameSide(p, o, c, d) &&
    isSameSide(p, c, o, d) &&
    isSameSide(p, d, o, c)
  ) {
    el.setAttribute("class", "box slide-out-bottom");
  } else if (
    !isSameSide(p, o, a, d) &&
    isSameSide(p, a, o, d) &&
    isSameSide(p, d, o, a)
  ) {
    el.setAttribute("class", "box slide-out-left");
  } else {
    el.setAttribute("class", "box slide-out-top");
  }
};
for (var i = 0; i < boxList.length; i++) {
  boxList[i].addEventListener("mouseenter", mouseenterEvent);
  boxList[i].addEventListener("mouseleave", mouseleaveEvent);
}

总结

使用纯 CSS 实现判断方向会有一个比较大的缺点,就是浏览器鼠标移动事件含有一定延迟,当鼠标速度很快的进入 div 时,有可能:hover 会延迟执行到后面的元素。 同时纯 CSS 方式只可判断移入方向,还未能实现可以同时判断移入和移出。需要在记录移入方向的同时,再记录移出方向目前想到的只可使用 JS 去辅助实现了。

使用 JS 实现最主要是要运用到判断两点位于直线同侧的算法,这个算法只涉及四则运算,也比较简洁。获取 DIV 四个顶点和中心的坐标可以用getBoundingClientRect()来获取元素的 top,left,width,height 计算出来。

以上内容未经授权请勿随意转载。

2021/10/01 更新, 找到了一个使用 Grid 布局实现的移入方向 Demo: CSS-only direction-aware hover effect