最近在浏览掘金网站的时候发现新上线的扣子平台,它是一款无需代码,即刻开发新一代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的旋转效果,我们先在页面上将卡片的布局排列好:
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
| <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
文件:
|
export function useMouse(width, height, cardEl) { const styles = ref('');
const mouseMove = (ev) => {} const mouseOut = (ev) => {} return { styles, mouseMove, mouseOut, }; }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
导出我们需要用到的styles样式、mouseMove鼠标移动函数、mouseOut鼠标移出函数;使用方式也很简单,导入组合式函数,传入宽高以及卡片的节点,卡片节点在后面要用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <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的代码了;我们想象一下,需要在鼠标移动的时候,让卡片旋转向前倾斜对应的角度,而在鼠标离开的时候,则清空角度样式:
| export function useMouse(width, height, cardEl) { const styles = ref(''); const halfWidth = width / 2; const halfHeight = height / 2; const mouseMove = (ev) => { } const mouseOut = (ev) => { styles.value = ''; } }
|
我们想象一下,鼠标某一时刻的位置假设在卡片的左上方某个点,获取鼠标距离卡片左上角的offsetX和offsetY,旋转角度需要计算出蓝色框的长宽:
绕着Y轴旋转的角度就是蓝色框的长度除以一半的宽度,绕着X轴旋转的角度就是蓝色框的高度除以一半的宽度。
我们定义一个最大的旋转角度MAX_DEG,然后用一个笨办法,在四个区域分别计算出不同的角度:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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的计算方式,上面代码最终可以优化成两行:
| const mouseMove = (ev) => { rotateX = -((halfHeight - offsetY) / halfHeight) * MAX_DEG; rotateY = ((halfWidth - offsetX) / halfWidth) * MAX_DEG; }
|
辉光效果
接下来就是鼠标移动时候的背景辉光效果了,我们首先向卡片下插入一个dom节点,用来渲染辉光:
| export function useMouse(width, height, cardEl) { let hoverEl = null;
onMounted(() => { hoverEl = document.createElement("div"); hoverEl.className = "hover-element"; cardEl.value.append(hoverEl); }); }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
给hover-element元素
设置样式,在卡片悬浮的时候才让元素显示出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| .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; } }
|
在鼠标移动时,去设置悬浮元素的位置偏移:
| 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元素的上方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <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上,即可修正偏差:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
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;
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文件中,方便复用逻辑;最后我们发现某些元素下鼠标悬浮的效果会有偏差,排查原因后对偏差进行修正。
本文所有源码敬请关注公众号【前端壹读】,后台回复关键词【辉光卡片】即可获取。