|
最近发现 B 站的首页头图的交互效果非常有趣,如下图所示,当鼠标在画面中左右滑动时,海洋生物会栩栩如生地动起来:这是使用多张具有不同视角的图片,通过透视、位移等处理方式来实现的视差效果,在佩服 UI 与前端对网页交互效果方面的努力和探索之外,我也沉浸在这片“海洋”中疯狂摸鱼:尝试使用原生 JS 来复刻它,最终实现了非常还原的效果:本文将一步步介绍整个制作思路,讲解涉及的相关知识点,干货非常多,搬好你的小板凳,咱们话不多说直接开摸吧。准备工作打开浏览器控制台,查看B站头图的 HTML 结构:不难看出,我们接下来的思路就是把 banner 中所有的图片用一个?.layer?的 div 包住堆叠起来,然后编写鼠标事件对每张图片应用相应的变换(transform)操作,由于接下来的操作我们都用 JS 来完成,所以布局很简单,只需要一个 div 来充当容器:loading...把图片素材通过 JS 添加进容器中,我们创建一个数组来描述这些图片,数据的结构暂时如下所示:const?barnerImagesData?=?[??{????url:?'https://xxxx/abcdegfsa.webp',??},??{????url:?'https://xxxx/dsaasdsaasdds.webp',??},?...........]注:完整数据在 https://code.juejin.cn/api/raw/7267103634863702050?id=7267103634863751202 ,这里包含了后续全部代码所需的内容然后我们把?barnerImagesData?循环并添加到容器中:const?body?=?document.getElementById('app')let?layers?=?[]function?initItems()?{????body.style.display?=?'none'????for?(let?i?=?0;?i??initX?=?e.pageX)body.addEventListener('mousemove',?(e)?=>?{??moveX?=?e.pageX?-?initX})获取到偏移值后,我想你已经迫不及待地想要让画面跟随鼠标动起来了,我们先来尝试一下吧,CSS 变换属性为?transform,它可以接收多个值,其中?translate()?可以让元素发生偏移,从而改变显示位置,接下来我们即是要将偏移值应用到其中,我们定义一个?animate?方法用于执行动画,该方法中循环取出所有元素并应用变换://?动画执行function?animate()?{??for?(let?i?=?0;?i??{??moveX?=?e.pageX?-?initX??requestAnimationFrame(mouseMove)})function?mouseMove()?{?//?滑动操作??animate()}滑动就会看到如下效果:到这里都还没什么难度,虽然离最终效果相距甚远,但基本就只剩下对细节的亿点处理了,我们来具体看看B站是怎么做的。视差效果原理在视差效果中,通常会使用多张具有不同视角的图片或分层的图像,通过透视、位移等处理方式,让观察者感受到物体的前后关系和深度差异。我们打开控制台观察B站首页头图对应的 DOM 结构,会看到处理的对应变换包括了:平移(translate)、旋转(rotate)、缩放(scale)等,此外还有透明度可能也会随之改变。通过鼠标移动产生的偏移值,我们可以按一定比例设置对应的变换属性来达到最终效果,不过这里我并不打算使用跟B站一样的实现方式,让我们来上点强度,只使用矩阵变换?matrix?来实现?transform?中的三种变换。二维矩阵变换很多人可能对?matrix?感到陌生,实际上平时我们常使用的?translate、rotate?等变换操作都是语法糖,是为了更加符合开发直觉而设计出来的,最终它们都会被转化成矩阵进行二维变换,它的基本形式是这样的:matrix(a,?b,?c,?d,?e,?f)这里的 abcdef 共6个参数,默认值是?1,0,0,1,0,0,按顺序拆开来分别是:? x 轴系数:(1,0)? y 轴系数:(0,1)??偏移绝对值:(0,0)我们把第一个坐标点表示在如下的坐标轴上:第二个点是在?y?轴上:通过这两个点与原点我们可以确定一个图形:(注意这里是倍数,1就是保持原样的意思)如果我要把图形拉宽 2 倍那就是改变第一个坐标为?(2,0),同理,如果将图形变高 1.5 倍就是改变第二个坐标点为?(0,1.5),如下所示:如此变换过程编写为 CSS 就是:transform:?matrix(2,0,0,1.5,0,0);即等价于:transform:?scale(2,?1.5)学会了如上这一基础变换,后面我们实现等比缩放的操作就非常简单了,往这两个系数乘上一个缩放倍数(假设为?s)即可,公式表示如下:matrix(s?*?x,?0,?0,?s?*?y,?0,?0)而平移就更简单了,第三个坐标点即代表平移的?x?y?值,例如我们将图形向右平移?100?个像素:只需在?x?上增加?100?即可,前面两个点不需要动,CSS 编写为:transform:?matrix(1,0,0,1,100,0);等价于:transform:?translateX(100px);如果图形向左偏移,那么?x?就加上负的?100,如何上下平移相信也不用我多说了吧。关于矩阵变换先搞懂这些就行,旋转后面会讲到,我们接着回到正题中。调整初始位置修改第一步的数组结构,添加一些初始值,大致结构如下:const?barnerImagesData?=?[??{????url:?'https://xxxx/xxxxxxx.webp',????transform:?[1,?0,?0,?1,?0,?0],????width:?1950,????blur:?0??},?...........]注:之后我也会像这样增加一些“参数”,将不再赘述,完整数据在这里 https://code.juejin.cn/api/raw/7267103634863702050?id=7267103634863751202其中?transform?值就是为这些海洋生物所提供的初始位置了,我们在前面的?initItems?方法中编写相应代码:function?initItems()?{????.........??????layer.classList.add('layer')??????layer.style?=?'transform:'?+?new?DOMMatrix(item.transform)??????//?创建?imgage??????const?img?=?document.createElement('img')??????img.src?=?item.url??????img.style.filter?=?`blur(${item.blur}px)`??????img.style.width?=?`${item.width}px`????...........}我们用?new DOMMatrix?方法将数组实例化为?matrix,赋值给 CSS 的?transform?属性,同时我们也定义了一些图片的宽度和模糊值,这里使用 CSS?filter: blur()?来实现高斯模糊,给靠前面的水草等几个图层添加模糊值,使场景更真实,更符合人眼聚焦画面主体时的环境感受。代码编写完毕,对数据进行亿番调整后,画面已经基本和B站一致了:平移与缩放我们继续完善鼠标交互效果,让原本紧贴鼠标移动的图层按不同速度进行移动,以此实现最基本的视差效果,为此我添加了一个参数?a?用来代表加速度,不同图层有不同的加速度,加速度越快代表移动幅度越大,我们修改?animate?函数:function?animate()?{??for?(let?i?=?0;?i??0???lerp(item.opacity[1],?item.opacity[0],?progress)?:?lerp(item.opacity[0],?item.opacity[1],?moveX?/?window.innerWidth?*?2)????}????........}矩阵旋转上面其实我们已经完成了 90% 的效果了,但和B站的效果相比还是有点差距,通过观察我发现乌龟在前进的过程中还带有一点旋转的角度。再次为数据添加参数?deg?来表示每一帧的旋转角度,当存在角度时乘以新矩阵?[Math.cos(deg), Math.sin(deg), -Math.sin(deg), Math.cos(deg),0,0](旋转时不发生位移,所以第三个坐标应为?0,0),我们为?animate?函数增加如下代码:function?animate()?{??.........????if?(item.deg)?{?//?有旋转角度??????const?deg?=?item.deg?*?moveX??????m?=?m.multiply(new?DOMMatrix([Math.cos(deg),?Math.sin(deg),?-Math.sin(deg),?Math.cos(deg),?0,?0]))????}????.........}来感受下加入一定的旋转角度后是什么效果:画面更加灵动自然了,基本和B站的效果无差,感觉海洋生物们都栩栩如生起来了捏~矩阵旋转推导过程这里补充一下旋转的四个值是如何推导而来的,首先帮大家回忆一下中学时的三角函数,在如图所示的直角三角形中,我们有 A、B、C 三个角,每个角的对边我们记作小写 a、b、c:然后我们会有边长比值的公式:??正弦(sin):sin(A) = a / c,对边比斜边。??余弦(cos):cos(A) = b / c,邻边比斜边。??正切(tan):tan(A) = a / b,对边比领边。当旋转一定角度?θ?时,我们画出图形的变化,如下图,矩阵的第一个点?( x , y )?变为?( x‘ , y‘ ),要求得变化后的?x’?和?y‘,我们先把它与?θ?角围成的三角形画出来,并标记其三条边:代入参数可得:接着我们根据上面两条公式分别得出坐标点:x' = x * cos(θ)y' = x * sin(θ)前面我们讲矩阵的时候已经知道了,这个( x , y )点其实就是?( 1 , 0 ),代入?x = 1?我们得到?( x‘ , y‘ )?点坐标值为:?( cos(θ) , sin(θ) )。我们继续看另一个点,还是把变化与夹角的三角形画出来:同样地,得到下面的正余弦:然后得出坐标点:x' = y * sin(θ)y' = y * cos(θ)矩阵第二个坐标为?( 0 , 1 ),将?y = 1?代入得到这个点的坐标为:?( -sin(θ) , cos(θ) ),注意这个点的?x?是在负半轴上,所以要加上负号。以上,我们就推导出了二维矩阵的旋转变换为:matrix(cosθ, sinθ, -sinθ, cosθ, 0, 0)位置回正到这里整个交互还没有结束,当前在鼠标离开时,画面会停滞住,这样鼠标下次进入画面时也会闪动,所以需在离开时自动回正到初始位置上才行,我们先注册相关事件://?鼠标已经离开了视窗或者切出浏览器,执行回正动画body.addEventListener("mouseleave",?leave)window.onblur?=?leave在?leave?函数里将初始的?matrix?逐一取出并应用到图像上的话,画面会瞬间闪动,这并不优雅,所以我们需要让它们平滑恢复到初始位置,通常我们可能会这么做:先设置一个样式比如?transition: all .3s;?这表示变换效果将会缓动并在?300ms?后完成,但是这个样式不能在一开始就写上,不然前面的画面移动效果也会受到影响,所以得在执行回正时才设置,其它情况下则移除。这种方式虽然没什么问题,但需要额外利用 CSS 才能实现,能不能只用 JS 来做呢,我们先分析下?transition?中两个主要的参数:1.?持续时间2.?动画函数其实只要搞懂这两个参数,我们就可以用 JS 来实现 CSS 中的平滑缓动效果。动画进度先看持续时间,这个参数表明动画在经过一个明确的时间后结束,虽然持续时间是个变量,但无论动画持续多久,都是一个从 0% 变化到 100% 的过程,所以我们要把时间转化为能被确定的进度。在?requestAnimationFrame?每一帧执行时回调函数会接收一个参数?timestamp,我们可以以此来计算出进度:let?startTime;const?duration?=?300;?//?动画持续时间(毫秒)function?leave()?{??startTime?=?0;?//?离开时初始值归零??requestAnimationFrame(homing);?//?开始回弹动画}function?homing(timestamp)?{??!startTime?&(?startTime?=?timestamp)??const?elapsed?=?timestamp?-?startTime?//?计算经过时间??const?progress?=?Math.min(elapsed?/?duration,?1)??progress??(1?-?amt)?*?start?+?amt?*?end;该函数接收一个起始值?start?与目标值?end,插值系数:amt是在?0?到?1?之间的值,表示过渡的进度比例。我们在回正动画处理中,通过每一帧的这三个入参,返回对应的计算结果应用到矩阵变换中:function?animate(progress)?{??............??if?(isHoming)?{?//?回正时处理??????m.e?=?lerp(moveX?*?item.a?+?item.transform[4],?item.transform[4],?progress)??????move?=?0??????s?=?lerp(item.f???item.f?*?moveX?+?1?:?1,?1,?progress)??????g?=?lerp(item.g???item.g?*?moveX?:?0,?0,?progress)????}??.........??if?(item.deg)?{?//?有旋转角度??????const?deg?=?isHoming???lerp(item.deg?*?moveX,?0,?progress)?:?item.deg?*?moveX??...........}进度控制了动画过程,线性函数描述了动画曲线,缓动效果就这样实现了,至此,整个交互效果就和开头演示时一样了。加餐本来到这里就该结束了,但正好在文章写完那天,我登录B站时发现首页头图更新了。。那敢情好啊,我就把新出的效果也复刻一下吧!不过上面的代码是一行也不用改动的,只需要换一套数据就行了。打开B站,把以下代码粘贴在控制台(可能需要滑动一下头图),回车。const?layersEl?=?document.getElementsByClassName('animated-banner')[0].getElementsByClassName('layer');let?layers?=?[];const?pattern?=?/translate\(([-.\d]+px),?([-.\d]+px)\)/;for?(let?i?=?0;?i??+x.replace('px',?''))]????layers.push({width,?height,?url:src,?transform,?a:?0.01})}JSON.stringify(layers)把打印的数据拷贝出来,图片链接自行保存后替换,接下来就是对着B站的效果微调变换参数啦,这次的更新整体比之前的还简单些,不一会就调校完毕了,鳄鱼那部分实现逻辑较有出入,但无伤大雅,看看效果:完整代码码上掘金查看在线代码及体验效果:https://code.juejin.cn/pen/7267433230263910460核心代码只有几十行,你可以通过改变数据中的各项值来调整画面元素的交互变化程度及效果。最后让我们来回顾下,虽然整体效果看上去似乎也不算难,但本文知识点还是蛮多的,首先是如何利用鼠标事件计算以及执行动画;知道了什么是矩阵变换以及如何使用它实现平移旋转缩放等操作;利用三角函数推导了矩阵旋转的原理;使用线性差值函数实现了缓动回弹动画等。
|
|