谁会拒绝一款酷炫流畅的辉光卡片动画呢?

最近在浏览掘金网站的时候发现新上线的扣子平台,它是一款无需代码,即刻开发新一代AI Chat Bot的应用编辑平台,可以通过这个平台快速创建各种类型的Chat Bot;不过让笔者更感兴趣的是它首页酷炫的卡片辉光效果是如何实现的,本文就来探讨一下它的实现过程。

我们看下动画效果如下:

如果我们打开控制台去看元素上面的属性,我们会发现,这样的效果其实主要是两种动画效果叠加:卡片3D旋转和后面的辉光层的效果,我们逐一来看下实现的逻辑。

本文的实现代码实现基于Vue3,对Vue3语法不了解的可以先看一下这篇文章;想看最终的实现效果的小伙伴可以直接戳这里查看

鼠标属性的区别

在实现效果之前,我们先来回顾一下,我们在打印鼠标事件的时候,经常会看到事件对象event上面有各种属性,offsetX、clientX、pageX和screenX等等,那么这些属性有什么区别呢?我们在使用时到底该用哪个属性呢?

为了简化流程,下面主要介绍X轴方向上的属性,Y轴方向属性同理。

offsetX

我们按照从小范围到大范围的原则,先来看下offsetX属性,根据MDN上的介绍,只读属性offsetX规定了事件对象与目标节点的内填充边(padding edge)在X轴方向上的偏移量;从offset单词也能看出,它是相对于目标div的偏移量,我们用图形通俗的表示如下:

clientX

然后是clientX,client有客户端的意思,因此它表示事件发生时的浏览器客户端区域的水平坐标,是相对于浏览器窗口的边距,会随页面滚动而改变,用图形表示如下:

注:clientY是不包含浏览器的书签、地址栏和标签栏等的高度。

pageX

再是pageX属性,它是相对于整个文档的水平坐标,不随着页面滚动而改变;它和上面clientX看似是相同的,大多数情况下它们的值也是相同的;但pageX会考虑页面的水平方向上的滚动,因此有下面的公式:

ev.pageX = ev.clientX + window.scrollX

通过图形我们也能看出,当文档大小超出浏览器区域时,pageX明显是要大于clientX的值:

screenX

最后是screenX,表示范围是最大的,使用频率也是最低的,表示距离电脑屏幕边缘的水平坐标偏移量:

除此之外,还有一个属性是movementX,它提供了当前鼠标移动事件上一次鼠标移动事件之间鼠标在水平方向的移动值;在处理移动元素等场景时就不需要我们记录上次的位置数据了,因此它的计算公式如下:

currentEvent.movementX = currentEvent.screenX - previousEvent.screenX

卡片3D旋转

首先我们来看下第一个实现效果,首先是让卡片实现3D的旋转效果,我们先在页面上将卡片的布局排列好:

vue 复制代码
<template>
  <div class="clip-box">
    <div class="title">如何实现一款酷炫流畅的辉光卡片动画?</div>
    <div class="clip-cont">
      <div class="card card1">Card1</div>
      <div class="card card2">Card2</div>
      <div class="card card3">Card3</div>
      <div class="card card4">Card4</div>
      <div class="card card5">Card5</div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.clip-cont {
  width: 1204px;
  position: relative;
}
.card {
  background: #161616;
  position: absolute;
}
.card1 {
  width: 434px;
  height: 450px;
}
.card2 {
  width: 434px;
  height: 290px;
  top: 460px;
}
.card3 {
  width: 760px;
  height: 290px;
  left: 444px;
}
.card4 {
  width: 375px;
  height: 450px;
  left: 444px;
  top: 300px;
}
.card5 {
  width: 375px;
  height: 450px;
  left: 829px;
  top: 300px;
}
</style>

我们给每个卡片设置一个固定的宽高和位置,使用绝对布局让其堆叠排列;

接下来,我们就需要在dom节点上监听鼠标事件后对5个卡片进行样式的修改;但是这里,如果写五遍监听函数显然是不合适的;想要在多个节点元素上复用相同的逻辑,可以把这个逻辑以组合式函数的形式提取,我们单独新建一个useMouse.js文件:

javascript 复制代码
// useMouse.js
// width:卡片宽度 
// height:卡片高度
// cardEl:卡片的dom节点
export function useMouse(width, height, cardEl) {
  const styles = ref('');

  const mouseMove = (ev) => {}
  const mouseOut = (ev) => {}
  return {
    styles,
    mouseMove,
    mouseOut,
  };
}

导出我们需要用到的styles样式、mouseMove鼠标移动函数、mouseOut鼠标移出函数;使用方式也很简单,导入组合式函数,传入宽高以及卡片的节点,卡片节点在后面要用到。

javascript 复制代码
<template>
  <div class="card card1" 
    ref="card1" 
    :style="styles1" 
    @mousemove="mouseMove1" 
    @mouseout="mouseOut1"
  >Card1</div>
</template>
<script setup>
import { ref } from "vue";
import { useMouse } from "./useMouse.js";
const card1 = ref(null);
const { 
  styles: styles1, 
  mouseMove: mouseMove1, 
  mouseOut: mouseOut1 
} = useMouse(434, 450, card1); // 传入卡片宽高
</script>

接下来就是最最最最核心的useMouse的代码了;我们想象一下,需要在鼠标移动的时候,让卡片旋转向前倾斜对应的角度,而在鼠标离开的时候,则清空角度样式:

javascript 复制代码
export function useMouse(width, height, cardEl) {
  const styles = ref('');
  // 一半的宽度
  const halfWidth = width / 2;
  // 一半的高度
  const halfHeight = height / 2;
  const mouseMove = (ev) => {
    // todo 设置styles
  }
  const mouseOut = (ev) => {
    styles.value = '';
  }
}

我们想象一下,鼠标某一时刻的位置假设在卡片的左上方某个点,获取鼠标距离卡片左上角的offsetX和offsetY,旋转角度需要计算出蓝色框的长宽:

绕着Y轴旋转的角度就是蓝色框的长度除以一半的宽度,绕着X轴旋转的角度就是蓝色框的高度除以一半的宽度。

我们定义一个最大的旋转角度MAX_DEG,然后用一个笨办法,在四个区域分别计算出不同的角度:

javascript 复制代码
// X轴和Y轴最大的翻转角度
const MAX_DEG = 10;

const mouseMove = (ev) => {
  const { offsetX, offsetY } = ev;

  let rotateX = 0;
  let rotateY = 0;
  if (offsetX < halfWidth && offsetY < halfHeight) {
    rotateY = ((halfWidth - offsetX) / halfWidth) * MAX_DEG;
    rotateX = -((halfHeight - offsetY) / halfHeight) * MAX_DEG;
  } else if (offsetX >= halfWidth && offsetY < halfHeight) {
    rotateY = -((offsetX - halfWidth) / halfWidth) * MAX_DEG;
    rotateX = -((halfHeight - offsetY) / halfHeight) * MAX_DEG;
  } else if (offsetX < halfWidth && offsetY >= halfHeight) {
    rotateY = ((halfWidth - offsetX) / halfWidth) * MAX_DEG;
    rotateX = ((offsetY - halfHeight) / halfHeight) * MAX_DEG;
  } else if (offsetX >= halfWidth && offsetY >= halfHeight) {
    rotateY = -((offsetX - halfWidth) / halfWidth) * MAX_DEG;
    rotateX = ((offsetY - halfHeight) / halfHeight) * MAX_DEG;
  }
  styles.value = `transform:perspective(1400px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
}

这里的CSS3的perspective指定了观察者与平面的距离,使具有三维位置变换的元素产生透视效果,如果不设置就没有透视的效果;我们看下鼠标的效果基本就可以实现3D倾斜的效果了。

继续对代码进行优化,四个区域太麻烦了,我们只考虑X轴和Y轴方向上的rotateX和rotateY的计算方式,上面代码最终可以优化成两行:

javascript 复制代码
const mouseMove = (ev) => {
  // 省略其他代码
  rotateX = -((halfHeight - offsetY) / halfHeight) * MAX_DEG;
  rotateY = ((halfWidth - offsetX) / halfWidth) * MAX_DEG;
}

辉光效果

接下来就是鼠标移动时候的背景辉光效果了,我们首先向卡片下插入一个dom节点,用来渲染辉光:

javascript 复制代码
export function useMouse(width, height, cardEl) {
  let hoverEl = null;

  onMounted(() => {
    hoverEl = document.createElement("div");
    hoverEl.className = "hover-element";
    cardEl.value.append(hoverEl);
  });
}

hover-element元素设置样式,在卡片悬浮的时候才让元素显示出来:

css 复制代码
.card {
  &:hover {
    color: #fff;
    .hover-element {
      visibility: visible;
    }
  }
  .hover-element {
    background: rgba(77, 82, 232, 0.6);
    border-radius: 50%;
    -webkit-filter: blur(64px);
    filter: blur(64px);
    width: 300px;
    height: 300px;
    left: 0px;
    top: 0px;
    position: absolute;
    visibility: hidden;
    z-index: -1;
  }
}

在鼠标移动时,去设置悬浮元素的位置偏移:

javascript 复制代码
// 悬浮高亮元素的尺寸
const HOVER_SIZE = 300;

const mouseMove = (ev) => {
  if (hoverEl) {
    const hX = offsetX - HOVER_SIZE / 2;
    const hY = offsetY - HOVER_SIZE / 2;
    hoverEl.style = `transform: translate(${hX}px, ${hY}px);`;
  }
}

但是我们这样设置之后会发现,后面的hover-element元素在鼠标移动的时候,一直在不断的闪现:

这是因为鼠标在卡片上移动的时候,我们给hover-element元素设置了偏移,偏移的hover-element元素阻断了mousemove事件,让hover-element元素又回到了原点,在鼠标不断移动时,导致了辉光呈现出了闪现效果。

我们可以在卡片下面再嵌套一层div作为卡片内容,让它始终位于hover-element元素的上方:

vue 复制代码
<template>
<div class="card card1" ref="card1" :style="styles1">
  <div class="card-cont" 
    @mousemove="mouseMove1" 
    @mouseout="mouseOut1">
    卡片内容
  </div>
</div>
</template>
<style lang="scss">
.card-cont {
  position: absolute;
  z-index: 1;
  width: 100%;
  height: 100%;
}
</style>

这样我们的辉光效果就很流畅了。

修正偏差

我们将文案和图片添加到卡片内容后会发现,当鼠标悬浮到图片元素上时,辉光元素会出现偏差;由于笔者这边是将图片元素设置成absolute的,而offsetX/offsetY是相对偏差,当悬浮到目标图片上是,offset是相对于图片的位移,而不是相对于外层元素,这样就会导致错位。

我们在mouse移动时,获取图片的left和top,将其数据加到offsetX/offsetY上,即可修正偏差:

javascript 复制代码
/**
 * 解析字符串中的数值 例如auto、120px
 */
const parseNumber = (str) => {
  if (!str) return 0;
  const mt = str.match(/(\d*)px/);
  if (mt) {
    return parseFloat(mt[1]);
  }
  return 0;
};

const mouseMove = (ev) => {
  let { offsetX, offsetY } = ev;

  // 修正offset偏差
  if (ev.target && ev.target.nodeName === "IMG") {
    let style = getComputedStyle(ev.target);
    offsetX += parseNumber(style.left);
    offsetY += parseNumber(style.top);
  }
}

这里getComputedStyle实时获取图片的left和top会比较耗费性能,这里的优化点,可以在mounted的时候提前获取图片的left/top,提前将其存储起来。

本文最终的实现效果可以戳这里查看

总结

本文首先总结了鼠标事件中offsetX/clientX/pageX/screenX每个属性的用法,了解了每个属性的用法和区别;然后实现了3D倾斜旋转和辉光元素的效果,将具体的实现逻辑抽取到了独立的js文件中,方便复用逻辑;最后我们发现某些元素下鼠标悬浮的效果会有偏差,排查原因后对偏差进行修正。

本文所有源码敬请关注公众号【前端壹读】,后台回复关键词【辉光卡片】即可获取。

如果觉得写得还不错,敬请关注我的掘金主页。更多文章请访问谢小飞的博客

相关推荐
(⊙o⊙)~哦1 小时前
JavaScript substring() 方法
前端
无心使然云中漫步1 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者1 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_2 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
罗政2 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
麒麟而非淇淋3 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120533 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢3 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写4 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
史努比.5 小时前
redis群集三种模式:主从复制、哨兵、集群
前端·bootstrap·html