敞开成长之旅!这是我参与「日新计划 12 月更文挑战」的第2天,点击查看活动详情

前语

作为数码爱好者+前端开发,每次苹果发布新品我总会第一时间打开苹果官网,除了了解新品信息之外,也很等待苹果又搞出了什么样的web特效。看多了也总结出了规律,就想着自己着手试试,顺便给自己的个人网站的特效升升级。

苹果官网动效要点剖析

先从一个普通用户的视点来谈谈感受。

这儿只针对动画不谈规划,否则篇幅可就多了去了。 相对于平时见到的网站来说,我觉得苹果官网给我的感受便是很不相同,包括对比其他厂商的产品介绍页,到底是为什么会“很不相同”?在我看来除了产品自身美观、展现物料质量高之外,最重要的便是给人的“交互感”很强,仿佛我作为一个用户,在操控产品介绍的进展,节奏,其间又夹杂着许多美观的特效,很舒服。

剖析下这种动画“交互感“,我觉得是以下几个点带来的:

  • 可暂停:动画全程并不是自顾自的在循环履行,而是伴跟着我的翻滚中止,动画也会中止,乃至会有速度变化,我能够很自如的看到动画的某一瞬间。

  • 可倒退:跟着翻滚回退,动画也会同时会退,这种感觉十分美好,仿佛我能够操控时空倒流,回忆我错失的部分,特别是当年的airpods pro的结构动画,美好而震撼人心。

  • 悬停感:当用户快速翻滚页面的时分,其实很容易什么也看不清的,获取信息也会觉得疲惫,而动画始终悬停在屏幕中心,履行完结再自动移出视线,也会让人留意力会集、更好的传达产品信息。

放一点翻翻滚画的示例(gif帧率较低):

苹果官网动效探究-翻滚驱动视频及分区动画履行
苹果官网动效探究-翻滚驱动视频及分区动画履行

再从技术视点剖析一下

先说说悬停的完结。

  • 查看元素后发现很多运用了sticky布局(黏性布局)来完结动画的悬停,sticky布局能够完结某个元素在翻滚到距离顶部(当然也能够左右)某个数值的时分不再跟从页面翻滚——也便是悬停,直到它触摸到他的父级元素的底部,再跟从父级一起正常翻滚。详细界说能够网上查询,不再赘述。
  • 别的动画也不能一直悬停的,需要在翻滚到某个进打开端,某个进展完毕,要防止所有动画都在跟着翻滚同时履行,如果把每一个动画地点的区域划分开的话,最完美的状况便是每一个区块动画的开端和完毕和上一个区块动画相互衔接。所以这儿我简略界说一下这种逻辑称为“分区动画履行”。

再剖析下动画进退、暂停的完结。

经过对大部分动画的剖析,我发现苹果的动画进展操控主要经过三种方法完结:

  • 视频(操控视频进展)
  • 图片类/canvas/svg(经过很多操控切换完结动效进展分割)
  • webGL(了解程度不够,本文不做解析)

剖析完毕,编码完结

依据上面剖析,本文挑选完结以下要点:

1.分区动画

sticky布局+分区履行函数规划,完结分区动画的悬停、触发、衔接功用。

2.翻滚驱动视频操控

我了解到的有两种计划:

  1. 直接经过翻滚份额映射到视频进展,设置视频的currentTime来操控,这种计划会有严峻的卡顿问题。
  2. 惯例计划-将视频帧进行缓存,经过操控生成的帧图画切换来操控进展。

终究完结我在这儿挑选了计划1,想办法去解决卡顿的问题。

不过出于好奇计划2我也测验完结了一下,关于其间的完结与优劣,可参阅我的另一篇文章:《js完结对视频按帧缓存》

代码完结-分区动画

页面布局

首要需要考虑页面的布局,能够将每一个分区界说为一个section,这儿定了四个分区,如下:

<section id="head" class="titleBox">
</section>
<section id="intro" class="titleBox">
</section>
<section id="tech" class="titleBox">
</section>
<section id="company" class="titleBox">
</section>

每个section内置一个div,sticky布局用于完结悬停的作用

<section id="head" class="titleBox">
    <divhljs-attribute">width: 80%;height: 200px;background-color: #8d6969;position: sticky;top:0;">
    xxxxxx
    </div>
</section>

监听每个分区的开始和完毕

经过翻滚条的总体距离,结合每个分区的高度,能够核算出当时翻滚到了哪个分区,再依据分发到分区的详细动画,并带上这个分区的动画履行到了什么进展的参数。

初始化核算section的一些数据:

data.sections是section的节点id的list,不再赘述获取。

// 核算每个section的动画触发时机,例如head在0.12-0.32之间触发,存入animateMomentInfo
function countAnimateMomentInfo() {
    for (const node of data.sections) {
        data.animateMomentInfo[node.id] = blockAnimateStart(node)
    }
}
/**
 * 依据section的高度,核算它在页面的方位,得出一个section的动画在什么翻滚份额触发
 * @param node 节点
 * @returns {{end: number, begin: number}} 开端翻滚份额,完毕翻滚份额
 */
function blockAnimateStart(node) {
    let begin = countScrollRatio(node.offsetTop);// 节点头部距离页面顶部距离,占页面份额,例如xx节点头部在页面20%高度方位
    let end = countScrollRatio(node.offsetTop + node.clientHeight)// 节点底部距离页面顶部距离
    return {begin, end}
}

将翻滚距离发送给核算函数:

let top
window.onscroll = (e) => {
    top = document.documentElement.scrollTop;
    activateAnimate(countScrollRatio(top))
}

核算当时方位距离顶部高度占整个页面的百分比,即当时翻滚进展/份额:

/**
 * 核算当时方位距离顶部高度占整个页面的百分比,即当时翻滚进展/份额
 * @param scrollTop 当时方位
 * @returns {number} 翻滚进展0.0000-1.0000
 */
function countScrollRatio(scrollTop) {
    return Number((100 * scrollTop / (data.scrollHeight - data.clientHeight)).toFixed(4))
}

核算出当时翻滚进展属于哪个id对应section,并且带上此section的履行进展rate

/**
 *
 * @param rate 当时翻滚进展
 */
function activateAnimate(rate) {
    for (let key in data.animateMomentInfo) {
        let {begin, end} = data.animateMomentInfo[key]
        if (rate > begin && rate < end) {
            executeAnimate(key, ((rate - begin) / (end - begin)).toFixed(3))
        }
    }
}

依据上一步核算出id和进展rate,分发到详细的某个section的动画履行函数:

function executeAnimate(id, rate) {
    switch (id) {
        case "head":
            headAnimate(rate)
            break
        case "intro":
            introAnimate(rate)
            break
        case "tech":
            techAnimate(rate)
            break
        case "company":
            companyAnimate(rate)
            break
        default:
            log("no action")
    }
}

也便是说,到了这一步,现已能够完结每个section的动画开始和完毕的识别,子节点的悬停作用,以及对应履行进展进展的获取,为了更直观,粗糙展现一下作用:

苹果官网动效探究-翻滚驱动视频及分区动画履行

留意这儿动画进展的是能够暂停和倒退的,例如你的动画是字体从10px到50px的作用,此时字体随意跟着翻滚的上下改变大小了。

简略使用下做个小动画:

苹果官网动效探究-翻滚驱动视频及分区动画履行

代码完结-视频进展操控

直接依据动画履行份额设置视频进展

一开端想的是直接依据上一步获取到的分区动画履行份额,再结合视频的长度,能够核算出当时视频应该播映的方位,再不断设置为当时视频的currentTime就好了 例如这样:

videoNode = document.getElementById("head_video") // 节点
videoLength = Number(videoNode.duration) // 视频长度
videoNode.currentTime = rate*videoLength; //rate是动画进展

可是浏览器自带的scroll监听事情是不滑润的,自带节流,并且用户可能会极快的速度上下来回翻滚,所以获取到的翻滚进展并不是连续均匀的,依据用户的翻滚速度,获取到的一组scrollTop的值可能是:10px,11px,20px,90px,50px,110px,60px,200px这样的非线性数值,也就会导致核算出的份额也是严峻跳动的,终究的问题便是视频会十分卡顿,不可承受,由于设置的视频进展会呈现断层。

此时依照惯例思路,应该要走帧缓存的计划了,把视频转为图片来处理,可是我这儿仍是想着,已然翻滚的速率不是滑润的,那我能不能自己写一个转化函数,来抹平这种不滑润?

速率滑润转化函数

(这一部分比较抽象,渐渐看)

先简化一下问题,来完结两个函数,一个是测试函数,模仿翻滚视频进展的操作,依照指定距离设置无规律的进展数值。

试着着手完结这个模仿翻滚函数: 随机一个无规律的数组(模仿scrollTop值),例如:arr = [5, 7, 10, 2, 9, 12], 距离arr[i]秒后将aim改为arr[i+1],不断循环直到数组每个元素都被设置一次

留意:stopFlag是判别翻滚开端和中止的标志,详细完结不在这儿打开

let aim = 0//当时方针进展
// 模仿按距离n秒改变一个变量的值为n,例如3秒后把aim改为3,5秒后把aim改为5
loopCallMock() {
   let timeArr = [5, 7, 10, 2, 9, 12]
   let timer2 = (arr) => {
       if (!arr.length) {
           log("test over")
           return
       }
       stopFlag = false
       let currStep = arr.shift();
       aim = currStep;
       log(`aim change:${currStep}`)
       setTimeout(() => {
           log(`next loop time:${arr[0]}`)
           timer2(arr)
       }, currStep * 1000)
   }
   timer2(timeArr)
}

另一个便是滑润函数,不管方针值aim是多少,它都应该以相同的速度(其实便是后边的帧率)来履行某个操作,直到达到传入方针值。

// 匀速履行函数
UniformFun() {
    if (stopFlag) {
        clearInterval(timer)
        return
    }
    timer = setInterval(() => {
        if (stopFlag) {
            clearInterval(timer)
        }
        if (currProgress === aim) {
            log("at end aim")
            return
        }
        currProgress < aim ? currProgress++ : currProgress--;
        log(`1 second log this,curr:${curr},aim:${aim}`)
    }, 1000)
}

将转化函数使用到实践场景

上一步的模仿测试函数,也便是headAnimation(rate),其间rate便是改变的aim值;

经过之前分区动画部分的完结,现已能够经过进展份额rate,结合视频长度video.duration核算出当时视频的播映方位。 那么实践headAnimation(rate)的内容就应该是:

let aim = 3;
let timer;
let end;
let currProgress = 0; // 当时视频进展
const frameRate = 30;// 动画帧率,每秒渲染帧数量
const frameSpeedSecond = Number((1 / frameRate).toFixed(4));// 秒,单帧时长,即单次动画履行周期
const frameSpeedMs = frameSpeedSecond * 1000; //frameSpeedSecond 毫秒
const videoNode = document.getElementById("head-video")
let videoLength = 0 // 视频时长
function head2Animate(rate) {
    // 进展动画开端,本次视频方针进展,秒
    aim = (rate * videoLength).toFixed(4)
    /*  每次触发的rate规模不滑润,需要把rate处理滑润再设置动画进展,例如视频进展data.videoNode.currentTime
      只要滑动没有中止,此计时器永远在以frameSpeedMs帧(frameSpeedMs毫秒履行一次,每次进退长度33毫秒)的速率履行视频进展修改
      每次触发headAnimate只是在改变它的履行结尾,即aim
      */
    if (!timer) {
        timer = setInterval(() => {
            // 这个计时器自身要具有铲除自己的才能
            if (stopFlag) {
                clearInterval(timer)
                return;
            }
            // 依据方针进展与当时进展关系判别行进仍是后退
            currProgress < aim ? currProgress += frameSpeedSecond : currProgress -= frameSpeedSecond;
            videoNode.currentTime = currProgress; // 设置视频进展
        }, frameSpeedMs)
    }
}

到这一步,现已完结不管下一刻的视频被翻滚到什么进展,都能够保证视频能以指定的帧率跑到方针播映方位,不会卡顿了。

苹果官网动效探究-翻滚驱动视频及分区动画履行

结语:

本文只挑选了中心的功用点去完结,实践完结仍是有很多细节没有展现的,比方两个区块动画的联动、动画开端的时间规模规划、自适应的兼容、部分动画的进展需要加速或者减速等等。

由于是探究性质的项目,重点在于完结进程的思路和考虑,所以代码自身或许不够完美,欢迎指教和讨论。