用JS实现钉钉官网的一部分动画效果

您所在的位置:网站首页 钉钉写代码 用JS实现钉钉官网的一部分动画效果

用JS实现钉钉官网的一部分动画效果

2023-11-10 12:08| 来源: 网络整理| 查看: 265

前言

最近在抖音刷到渡一前端袁老师的一个视频,视频中袁老师用原生的 js 实现了钉钉官网的一个动画效果,我把这个视频看了很多遍,觉得实现这个效果特别能锻炼人的思维,在不依赖框架、第三方库的情况下,自己使用原生的 js 去实现一遍,对自己的提升会非常大,因此我也试着去实现该动画效果。

ezgif.com-video-to-gif.gif

钉钉官网的动画效果如上,当然我这个只是实现了其中的一部分,其实其他的部分用一种思维都是可以实现的,由于时间关系,我只实现了下面的这部分:

image.png

固定动画区域

我们仔细观察页面的滚动时可以发现,当列表的动画在播放的时候,我一直在滑动滚动条,但是列表区域是固定的,并不会往下滑,而当动画播放结束之后,列表区域才可以被带走,也就是要固定一块区域用于播放动画,我们先来看下页面的布局结构:

image.png

HTML结构:

企业数字化一个钉钉就解决

playground (蓝色区域)的高度要尽可能大些,要留有足够的时间给列表区域播放动画;同时 animation-container (红色区域)要设置为粘性定位,当它的上边沿碰到其父元素 playground 时开始固定住,并播放动画,当它的下边沿碰到其父元素 playground 时的下边沿时动画播放结束,固定取消,随着滚动条的滚动,它会被父元素带走。样式代码如下:

.main-conteiner { width: 100%; height: 100%; } .logo-title { display: flex; flex-direction: column; align-items: center; width: 100%; height: 800px; margin-top: 150px; .logo { width: 200px; height: 200px; img { display: block; width: 100%; } } .title { width: 800px; height: 100px; margin-top: 50px; img { display: block; width: 100%; background: #040506; } } } .playground { width: 100%; height: 3000px; .animation-container { position: sticky; top: 0; display: flex; justify-content: center; align-items: center; width: 100%; background: #040506; height: 710px; .list { display: flex; flex-wrap: wrap; width: 1200px; height: 460px; background: #15171b; border-radius: 20px; padding: 100px; box-sizing: border-box; } .list-item { width: 80px; height: 80px; margin-right: 73.333px; img { display: block; width: 80px; height: 80px; border-radius: 20px; } } .mr-0 { margin-right: 0; } .mt-100 { margin-top: 100px; } } } .last-area { display: flex; justify-content: center; align-items: center; width: 100%; height: 800px; background: #0a0610; img { display: block; width: 200px; margin-right: 20px; } span { display: inline-block; line-height: 60px; font-size: 23px; font-weight: 700; color: #fff; } } 实现动画

动画区域以及布局结构、样式处理完之后,接下来该思考如何去实现动画

首先,我们要理解动画的本质,我个人的理解是 坐标系,横坐标是时间,纵坐标是某个值,也就是随着时间的变化,某个样式的值(透明度、偏移量...)在发生均匀的改变,可以看下下面的这张图,当然我这里的横坐标是 scroll (滚动的距离)

image.png

这里可以写一个函数来计算对应的值

// 动画曲线 - 根据传入的横坐标计算对应的纵坐标(value) function createAnimation( scrollstart: number, scrollEnd: number, valueStart: number, valueEnd: number ) { return function (scroll: number) { if (scroll = scrollEnd) { return valueEnd; } return ( valueStart + ((valueEnd - valueStart) * (scroll - scrollstart)) / (scrollEnd - scrollstart) ); }; }

由于列表中的每个元素的动画程度都不一样,因此需要准备一个 map 结构做映射:其中 key 为 dom 元素,value 为一个对象,里面是一些需要变化的样式,而且每个样式值都是一个函数,根据当前滚动条的位置设置不同的值:

image.png

监听页面的滚动事件,更新列表中每个 dom 元素的样式

// 映射 - dom => {} const animationMap = new Map(); // 更新dom的样式,遍历map给每个dom元素设置样式(透明度、偏移量、放缩....) function updatestyles() { const scroll = window.scrollY; for (let [dom, value] of animationMap) { for (const cssProp in value) { dom.style[cssProp] = value[cssProp](scroll); } } } // 初始时调用一次 updatestyles(); // 监听滚动条事件 window.addEventListener("scroll", updatestyles);

关键是要搞定 animationMap,另外写个函数来进行处理,在页面挂载的时候调用一次

onMounted(() => { updateMap(); });

scrollStart、scrollEnd 分别表示滚动距离为多少时开始动画、结束动画,这两个值都需要进行一些计算,涉及到的距离比较多,大家可以自行画图来进行分析

// 更新map function updateMap() { // 每次调用时先将之前的清除掉,在窗口大小等发生改变时重新处理 animationMap.clear(); const playGroundRect = playground.value.getBoundingClientRect(); const scrollStart = playGroundRect.top + window.scrollY; const scrollEnd = playGroundRect.bottom + window.scrollY - window.innerHeight; for (const item of listRef.value.children) { animationMap.set(item, getDomAnimation(scrollStart, scrollEnd, item)); } }

getDomAnimation 方法也就是实现上面所说的 map 结构中的 value,其返回两个 函数,用于计算对于的样式属性值,整个过程中透明度(opacity)由 0 -> 1,放缩比例(scale)由 0.5 -> 1,这里比较难处理的是偏移量(translate),每个元素都会往中间靠(水平、垂直方向上);由于每个元素这些属性值的变化不完全一样,因此需要给每个 dom 元素加个自定义属性 data-order,同时使动画具有对称性。

const orderMap = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 2, 5: 1, 6: 0, 7: 0, 8: 1, 9: 2, 10: 3, 11: 2, 12: 1, 13: 0, }; function getDomAnimation(scrollStart: number, scrollEnd: number, dom: any) { scrollStart = scrollStart + dom.dataset.order * 600; const opacityAimation = createAnimation(scrollStart, scrollEnd, 0, 1); const opacity = function (scroll: number) { return opacityAimation(scroll); }; const xAnimation = createAnimation( scrollStart, scrollEnd, animationRef.value.clientWidth / 2 - dom.offsetLeft - dom.clientWidth / 2, 0 ); const yAnimation = createAnimation( scrollStart, scrollEnd, animationRef.value.clientHeight / 2 - dom.offsetTop - dom.clientHeight / 2, 0 ); const scaleAnimation = createAnimation(scrollStart, scrollEnd, 0.5, 1); const transform = function (scroll: number) { return `translate(${xAnimation(scroll)}px, ${yAnimation( scroll )}px) scale(${scaleAnimation(scroll)})`; }; return { opacity, transform, }; } 实现效果

代码的话基本就完成了,下面我们看下最终的效果:

ezgif.com-video-to-gif (1).gif 完整代码

DingTalk.vue

企业数字化一个钉钉就解决 import { ref, onMounted } from "vue"; const listRef = ref(); const playground = ref(); const animationRef = ref(); onMounted(() => { updateMap(); }); const list = [ { name: "即时沟通", url: "https://img.alicdn.com/imgextra/i3/O1CN01TVWH501KYCEaUxPgv_!!6000000001175-0-tps-480-480.jpg", }, { name: "组织", url: "https://img.alicdn.com/imgextra/i1/O1CN01vqBxIP1L5d98G9xJD_!!6000000001248-0-tps-480-480.jpg", }, { name: "智能人事", url: "https://img.alicdn.com/imgextra/i3/O1CN01AKJU3T1tSi2NAFHFP_!!6000000005901-0-tps-480-480.jpg", }, { name: "组织大脑", url: "https://img.alicdn.com/imgextra/i3/O1CN01AKJU3T1tSi2NAFHFP_!!6000000005901-0-tps-480-480.jpg", }, { name: "OA审批", url: " https://img.alicdn.com/imgextra/i1/O1CN01sphiAp1Fj7bOKEppz_!!6000000000522-2-tps-480-480.png", }, { name: "邮箱", url: "https://img.alicdn.com/imgextra/i2/O1CN015T9BdZ28Ns5n1A82W_!!6000000007921-0-tps-480-480.jpg", }, { name: "Teambition", url: " https://img.alicdn.com/imgextra/i3/O1CN01bc6FKX1OCuHkDC2J7_!!6000000001670-0-tps-480-480.jpg", }, { name: "文档", url: "https://img.alicdn.com/imgextra/i1/O1CN01Y936lS1whluhvos2E_!!6000000006340-0-tps-480-480.jpg", }, { name: "音视频会议", url: "https://img.alicdn.com/imgextra/i3/O1CN017Wk2Cp1lp8VaSd22U_!!6000000004867-0-tps-480-480.jpg", }, { name: "开放平台", url: "https://img.alicdn.com/imgextra/i4/O1CN016JIoXg1lMHYbmErdR_!!6000000004804-0-tps-240-240.jpg", }, { name: "宜搭", url: "https://img.alicdn.com/imgextra/i3/O1CN011GyvMe1of0bAveV4u_!!6000000005251-0-tps-480-480.jpg", }, { name: "钉闪会", url: "https://img.alicdn.com/imgextra/i2/O1CN01zFSNcP26wYrM09A4S_!!6000000007726-0-tps-480-480.jpg", }, { name: "连接器", url: "https://img.alicdn.com/imgextra/i2/O1CN0142F9wc23s261dItxf_!!6000000007310-0-tps-480-480.jpg", }, { name: "酷应用", url: "https://img.alicdn.com/imgextra/i4/O1CN012wi8vZ1xt3HbO0ttd_!!6000000006500-0-tps-480-480.jpg", }, ]; const orderMap = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 2, 5: 1, 6: 0, 7: 0, 8: 1, 9: 2, 10: 3, 11: 2, 12: 1, 13: 0, }; // 动画曲线 - 根据传入的横坐标计算对应的纵坐标(value) function createAnimation( scrollstart: number, scrollEnd: number, valueStart: number, valueEnd: number ) { return function (scroll: number) { if (scroll = scrollEnd) { return valueEnd; } return ( valueStart + ((valueEnd - valueStart) * (scroll - scrollstart)) / (scrollEnd - scrollstart) ); }; } // 映射 - dom => {} const animationMap = new Map(); function getDomAnimation(scrollStart: number, scrollEnd: number, dom: any) { scrollStart = scrollStart + dom.dataset.order * 600; const opacityAimation = createAnimation(scrollStart, scrollEnd, 0, 1); const opacity = function (scroll: number) { return opacityAimation(scroll); }; const xAnimation = createAnimation( scrollStart, scrollEnd, animationRef.value.clientWidth / 2 - dom.offsetLeft - dom.clientWidth / 2, 0 ); const yAnimation = createAnimation( scrollStart, scrollEnd, animationRef.value.clientHeight / 2 - dom.offsetTop - dom.clientHeight / 2, 0 ); const scaleAnimation = createAnimation(scrollStart, scrollEnd, 0.5, 1); const transform = function (scroll: number) { return `translate(${xAnimation(scroll)}px, ${yAnimation( scroll )}px) scale(${scaleAnimation(scroll)})`; }; return { opacity, transform, }; } // 更新map function updateMap() { // 每次调用时先将之前的清除掉,窗口大小等发生改变时重新处理 animationMap.clear(); const playGroundRect = playground.value.getBoundingClientRect(); const scrollStart = playGroundRect.top + window.scrollY; const scrollEnd = playGroundRect.bottom + window.scrollY - window.innerHeight; for (const item of listRef.value.children) { animationMap.set(item, getDomAnimation(scrollStart, scrollEnd, item)); } } // 更新dom的样式,遍历map给每个dom元素设置样式(透明度、偏移量、放缩....) function updatestyles() { const scroll = window.scrollY; for (let [dom, value] of animationMap) { for (const cssProp in value) { dom.style[cssProp] = value[cssProp](scroll); } } } // 初始时调用一次 updatestyles(); // 监听滚动条事件 window.addEventListener("scroll", updatestyles); .main-conteiner { width: 100%; height: 100%; } .logo-title { display: flex; flex-direction: column; align-items: center; width: 100%; height: 800px; margin-top: 150px; .logo { width: 200px; height: 200px; img { display: block; width: 100%; } } .title { width: 800px; height: 100px; margin-top: 50px; img { display: block; width: 100%; background: #040506; } } } .playground { width: 100%; height: 3000px; .animation-container { position: sticky; top: 0; display: flex; justify-content: center; align-items: center; width: 100%; background: #040506; height: 710px; .list { display: flex; flex-wrap: wrap; width: 1200px; height: 460px; background: #15171b; border-radius: 20px; padding: 100px; box-sizing: border-box; } .list-item { width: 80px; height: 80px; margin-right: 73.333px; img { display: block; width: 80px; height: 80px; border-radius: 20px; } } .mr-0 { margin-right: 0; } .mt-100 { margin-top: 100px; } } } .last-area { display: flex; justify-content: center; align-items: center; width: 100%; height: 800px; background: #0a0610; img { display: block; width: 200px; margin-right: 20px; } span { display: inline-block; line-height: 60px; font-size: 23px; font-weight: 700; color: #fff; } }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3