Apple官网动画风格的实现

4,531 阅读2分钟

前言

本人非常喜欢Apple的官网动画,然后最近又正好在帮朋友做一个工厂的官网。借此机会做一个类似Apple风格的网站。

想实现的动画效果

想实现一个随着鼠标滚动画线以及元素依次出现的效果

精髓在于一条公式

Apple网站灵动的精髓在于有很多随鼠标滚动的动画。因此我们要处理的就是随着鼠标滚动的值与html元素之间的变化。

这条公式的作用是:随着鼠标滚动,页面从顶部到底部,scrolled会从0-1变化。

  let scrolled = html.scrollTop / (html.scrollHeight - html.clientHeight)

由于我是数学菜鸡,刚开始不太理解这条公式,后面想明白了。

分享一下我的思考过程

用到的API:

  1. html.scrollTop是html页面已经滚动的值
  2. html.scrollHeight是html页面的高度
  3. html.clientHeight是页面窗口的高度,也就是我们在浏览器看到的内容高度

当初的疑问

  1. html.scrollTop / (html.scrollHeight - html.clientHeight) :当滚动的像素等于页面高度的像素时,html.scrollTop / (html.scrollHeight - html.clientHeight)就等于1。
  2. 为什么要 (html.scrollHeight - html.clientHeight) ,页面滚动到底时,由于html.scrollHeight得到的是页面已经滚动的像素值,但这个值并不是页面到底的值,也就是说。此时html.scrollHeight与html.scrollHeight并不相等,相差一个视窗的高度像素值,所以需要减去视窗的高度。

在网页中的具体应用

由于并不是整个页面滚动,所以需要把公式由原来的滚动距离相较于整个页面高度改成相较于我们需要实现效果的区域

公式就变成了这样

  let parentElement = el.parentElement; //获取父元素
 let htmlScrollTop = document.documentElement.scrollTop + document.documentElement.clientHeight// 页面已滚动的高度
 let offsetTop = parentElement.offsetTop //元素顶部距离页面高度
   if (htmlScrollTop >= offsetTop && htmlScrollTop <= offsetHeight) {
  let scrolled = (htmlScrollTop - offsetTop) / (offsetHeight - offsetTop)
  }

改动的地方:

  1. 将滚动高度改为滚动高度减去展示动画所在元素的高度(htmlScrollTop - offsetTop)
  2. 将底部高度改为展示动画所在元素底部的高度
  3. 判断滚动的距离是否在元素高度和元素底部高度之间

这样就能实现网页滚动到展示动画所在元素时scrolled值增长,滚动到元素底部时,为1。然后就可以随着值的改变通过dom改变元素了!

效果实现片段

需要注意的是这里是vue3的代码片段。

画线:通过设置长度值慢慢把线画出来el.style.height = ${scrolled * 90}%

li元素的依次出现:

计算每个元素应该出现的区间,然后通过设置 CSS 自定义属性来显示元素。

                let total = 1 / liElements.length
                for (let index = 0; index < liElements.length; index++) {
                    let row = liElements[index]
                    let start = total * index
                    let end = total * (index + 1)
                    let progress = (scrolled - start) / (end - start)
                    if (progress >= 1) progress = 1
                    if (progress <= 0) progress = 0
                    row.style.setProperty('--progress', progress)
                }

全部代码

html部分:

<div class="line" v-scrolling-line></div>
     <li class="liLeft">
                <h2>2000年</h2>
                <h2>成立永发模具厂</h2>
                <p>以小作坊形式开始运作</p>
                <img src="src\assets\images\page2-1.jpg" alt="">
            </li>
            <li class="liRigth">
                <h2>2006年</h2>
                <h2>引进先进设备</h2>
                <p>通过多次技改项目的投入,工厂
                    不断引进设备、人才、软件。</p>
                <img src="src\assets\images\page2-2.jpg" alt="">
            </li>
            <li class="liLeft">
                <h2>2016年</h2>
                <h2>更名为天龙模具加工厂</h2>
                <p>现为集产品和模具开发、设计、
                    制造于一体的科技型企业。</p>
                <img src="src\assets\images\page2-3.jpg" alt="">
            </li>

js部分:

const vScrollingLine = {
    mounted: (el) => {
        window.addEventListener('scroll', () => {
​
            let parentElement = el.parentElement; //获取父元素
            let htmlScrollTop = document.documentElement.scrollTop + document.documentElement.clientHeight// 已滚动高度
            let offsetTop = parentElement.offsetTop //元素距离页面高度
            let offsetHeight = offsetTop + parentElement.offsetHeight  //元素底部距离页面高度,也就是顶部加上元素的高度。
​
            let liElements = document.querySelectorAll('.page2>li')
            if (htmlScrollTop >= offsetTop && htmlScrollTop <= offsetHeight) {
                let scrolled = (htmlScrollTop - offsetTop) / (offsetHeight - offsetTop)
                el.style.height = `${scrolled * 90}%`
​
                let total = 1 / liElements.length
                for (let index = 0; index < liElements.length; index++) {
                    let row = liElements[index]
                    let start = total * index
                    let end = total * (index + 1)
                    let progress = (scrolled - start) / (end - start)
                    if (progress >= 1) progress = 1
                    if (progress <= 0) progress = 0
                    row.style.setProperty('--progress', progress)
                }
            }
​
        })
    }
}

css部分:

   .line {
            position: absolute;
            left: 50%;
            top: 0%;
            transform: translateX(-50%);
​
            width: 13px;
            border-bottom-left-radius: 12px;
            border-bottom-right-radius: 12px;
            background-color: white;
        }
​
        li {
            list-style: none;
            --progress: 0;
            opacity: var(--progress);
            img {
                width: 30vw;
            }
​
            &.liLeft {
                transform: scale(calc(1.8 - (0.8 * var(--progress)))) translateX(-80%) translateY(calc(-60px * (1 - var(--progress))));
            }
​
            &.liRigth {
                transform: scale(calc(1.8 - (0.8 * var(--progress)))) translateX(80%) translateY(calc(-60px * (1 - var(--progress))));
            }
        }

附上自己在pad画的思考过程

9487e5a52d05e8d44b6c97b398239ef.jpg

通过节流算法优化性能和动画卡顿

什么是节流算法

节流的原理是,如果你频繁触发一个函数,那么在一段时间内只执行一次,不管这段时间内触发多少次。这种技术常用于滚动事件,比如滚动加载更多,滚动监听等场景。 一个经典的节流算法:

function throttle(func, wait) {
    let lastTime = 0;
    return function() {
        let now = Date.now();
        if (now - lastTime > wait) {
            func();
            lastTime = now;
        }
    }
}

具体到本案例中的使用:

这段代码实现了通过节流算法,哪怕在有效范围内触发了滚动监视器,其中的回调函数也并不会一直执行,会保持17毫秒的间隔,理论上这里通过数值越小动画越流畅,但是由于对DOM的操作会消耗大量的性能,实际上反而会造成卡顿,而人眼能感知的最高帧率大约是60帧/秒,也就是每帧大约16.67毫秒。JavaScript无法处理带小数点的毫秒,所以四舍五入设置为了17

function thorttle(func,waitTime){
let startTime = 0
return function(){
    let now = Date.now()
    if(now-startTime >= waitTime){
        func.apply(this,arguments)
        startTime = now
    }
}
}

let pageScroll = (el)=>{
return function(){
    let parentElement = el.parentElement; //获取父元素
            let htmlScrollTop = document.documentElement.scrollTop + document.documentElement.clientHeight// 已滚动高度
            let offsetTop = parentElement.offsetTop //元素距离页面高度
            let offsetHeight = offsetTop + parentElement.offsetHeight  //元素底部距离页面高度,也就是顶部加上元素的高度。

            let liElements = document.querySelectorAll('.page2>li')
            if (htmlScrollTop >= offsetTop && htmlScrollTop <= offsetHeight) {
                let scrolled = (htmlScrollTop - offsetTop) / (offsetHeight - offsetTop)
                el.style.height = `${scrolled * 90}%`
                let total = 1 / liElements.length
                for (let index = 0; index < liElements.length; index++) {
                    let row = liElements[index]
                    let start = total * index
                    let end = total * (index + 1)
                    let progress = (scrolled - start) / (end - start)
                    if (progress >= 1) progress = 1
                    if (progress <= 0) progress = 0
                    row.style.setProperty('--progress', progress)
                }
            }
}
}


const vScrollingLine = {
    mounted: (el) => {
        window.addEventListener('scroll',thorttle(pageScroll(el),17))
    }
}