苹果官网Ipad mini滚动动画实现原理探究

Posted 前端开发博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了苹果官网Ipad mini滚动动画实现原理探究相关的知识,希望对你有一定的参考价值。

关注公众号 前端开发博客,领27本电子书

回复加群,自助秒进前端群

背景

最近因工作需要,要开发两个比较炫酷的动画效果,早期我个人在这方面积累太少,导致实际开发过程走了不少弯路,本文特此总结一下,希望各位同行能吸取经(jia)验(ban)教训少走弯路早日下班。

废话不多说,我们先来看几个苹果官网比较炫酷的几个动画效果,你在此也可以思考一下,该如何实现这些动画效果。

走过的弯路

当我拿到设计同学的设计稿,告知我要实现的第一个动画就类似这种时,我第一反应是想到用一个视频来进行实现,通过控制视频 currentTime 属性来进行视频进度的控制,再监听scroll事件,当他触发时我就将 currentTime 的值进行加减操作,结果这是非常傻的一个操作。因为当用户在触摸板上进行快速向上或向下滚动时,scroll 事件的响应并不是连续触发,而是节流之后的最后几次响应,如下图所示: 对此,一时我竟不知所措,只好一番Google,找到了原来监听 scroll 事件只是第一步,第二步是需要基于scrollTopscrollHeightclientHeight 三者进行计算,求出一个滚动系数,基于该计算结果再进行相关动效计算。

const  scrollTop, scrollHeight, clientHeight  = document.documentElement;
const scrolled = scrollTop / (scrollHeight - clientHeight);

套用上面的公式,我满心欢喜的拿着计算出来的 scrolled 值去设置 currentTime,以为能踩点下班时,却发现动画效果是实现了,但是当我快速的滚动时竟然会出现掉帧卡顿跳跃的情况,对于一个追求极致用户体验的我来说,这种情况我是没法忍受的。

奈何自己不够聪明,只有承认自己的无知,被迫向苹果官网妥协。在工位上认真研究苹果官网上的效果是如何实现的,通过认真调试研究后发现,苹果官网竟然是使用几十张甚至百多张图片逐一去加载、再拼接的方式实现的类似效果 络请求,最终还是没采用该方案去实现

那几日我一直在想有没有一种可能,既能让请求变少,又能不掉帧,还能实现丝滑的效果,如果能看起来有那么点牛逼哄哄的感觉就更好了。通过Google搜索换了各种关键词均找不到相关资料,那几日真的到了怀疑自己能力的地步,每日百思不得其解、朝思夜想,次日夜里在梦中都在思考这个怎么才能实现,我一直不信邪,也不相信苹果官网所有的类似动效,都使用这种方式去实现的,我翻遍苹果官网每一个产品页面,终于在 ipad-mini[2] 的页面找到了想要的答案。

由于线上代码均被压缩又没有源码,只好在苹果官网压缩后的代码文件中大海捞针,找到他的关键代码实现,通过断点调试一行一行的跟进,最终不负有心人,自己成功复刻了苹果的这个实现效果。

Ipad mini的实现方式分解

1. 搭建基本的sticky结构 前端开发博客

要实现Ipad mini的这个滚动动效,首先需要搭建一个基于position: sticky定位的页面基本结构,在结构中.sticky节点的高度为100vh,并设置overflow: hidden,这里我们需要让sticky节点一直固定在屏幕顶部,不需要让它进行滚动。.content节点中每一个 section 节点都是一块内容区域,他们的高度由自身需要占用多少滚动距离自行设定,我实现的例子中将content节点下的每一个section子节点的高度都和.timeline-wrapper下的.timeline三个节点高度进行了绑定,因为在用户滚动的时候,用户肉眼看到的是.sticky节点下的内容位移变化,但滚动的响应区域是.timeline-wrapper节点,这样即可实现.timeline-wrapper滚动多少距离,.content节点就设置多少偏移量,从而达到交互与肉眼看到的视觉内容进行匹配。

2. 缓存视频帧 前端开发博客

有了上述基本协同的结构后,就可以开始在页面加载的时候,去遍历我们要请求的视频资源列表,拿到列表中每一个视频资源地址,调用createVideo工具函数获取video节点对象。

function createVideo(url) 
  const video = document.createElement("video");

  video.src = url;
  video.muted = true;
  video.playbackRate = 1;
  video.currentTime = 0;
  video.setAttribute("muted", "");
  video.setAttribute("playsinline", "");
  video.setAttribute("type", "video/webm");
  video.setAttribute("preload", "none");
  video.classList.add("video");
  video.style.display = "none";
  window.document.body.appendChild(video);

  return video;

获取到视频资源信息后,我们就可以开始进行视频资源帧的缓存操作,在进行帧缓存之前,我们需要大概计算一个视频资源我们需要具体缓存多少帧,例如我例子中拟定的一个视频大概缓存230帧,默认每一个缓存帧的位置都为false状态。

在正式进行缓存之前,我们需要让视频开始进行播放,并立即去执行视频资源缓冲操作,同时监听canplaythrough事件,该事件触发时表示当前已经加载足够的数据来播放视频,直到其结束都不会再进行缓冲内容了。这样我们在该事件触发的时候就可以放心的去轮训该视频资源,我这里设置的是每30ms就去执行一次视频缓存帧的创建操作,再基于当前视频资源创建视频帧,将创建了的视频帧存储到framsStore数组中以供后续使用。

function cacheFrame(videoMetaData) 
  return new Promise((resolve, reject) => 
    const  url, frameCount  = videoMetaData;
    const video = createVideo(url);
    const framsStore = new Array(frameCount).fill(false);
    let videoWidth = 0;
    let videoHeight = 0;
    let setIn = 0;
    let framsNumber = 0;

    video.play();
    video.addEventListener("loadedmetadata", (res) => 
      videoWidth = video.videoWidth;
      videoHeight = video.videoHeight;
    );

    video.addEventListener("ended", () => 
      resolve(framsStore);
    );

    video.addEventListener("waiting", (res) => 
      clearInterval(setIn);
    );

    video.addEventListener("error", () => 
      reject([]);
    );

    video.addEventListener("canplaythrough", (res) => 
      clearInterval(setIn);

      setIn = setInterval(() => 
        if (framsNumber >= frameCount) clearInterval(setIn);

        framsStore[framsNumber] = createFrame(video, videoWidth, videoHeight);
        framsNumber++;
      , fps);
    );
  );

在进行视频帧缓存时,我们需要将每一次轮训时的视频资源绘制到承载对象上。如果浏览器支持 OffscreenCanvas[3] 则优先使用该API,否则则降级为通过Canvas画布的方式进行承载。

function createFrame(video, videoWidth, videoHeight) 
  const canvas = window.OffscreenCanvas
    ? new OffscreenCanvas(videoWidth, videoHeight)
    : document.createElement("canvas");
  const context = canvas.getContext("2d");

  canvas.width = videoWidth;
  canvas.height = videoHeight;
  context.drawImage(video, 0, 0, videoWidth, videoHeight);

  return canvas;

3. 基于滚动系数渲染缓存的视频帧

当缓存完毕视频帧后,则可以监听scroll事件,在滚动触发时基于计算出的系数与缓存的帧总数相乘,则能求出当前滚动的距离与应该绘制的视频帧。

window.addEventListener("scroll", () => 
  const scrolled =
    document.documentElement.scrollTop /
    (document.documentElement.scrollHeight -
      document.documentElement.clientHeight);
      
  frames = frames.filter((item) => item !== false);

  const frameIndex = parseInt(frames.length * scrolled) + 1;

  if (frames[frameIndex] !== undefined) 
      renderFrame(ctx, frames[frameIndex]);
  

  document.querySelector(".content").style.transform = `matrix(1, 0, 0, 1, 0, -$document.documentElement.scrollTop)`;
);

function renderFrame(ctx, frame) 
  ctx.clearRect(0, 0, 1600, 1176);
  ctx.drawImage(frame, 0, 0);

最终完整版代码 -> DEMO代码[4]

在线预览地址 -> 地址[5]

ps:由于github服务器访问比较慢,需要加载视频资源,可能会花一点点时间

总结

本文通过对苹果官网Ipad mini滚动动画的实现原理进行了探究,手动实现了该动画的核心原理部分,还有很多细枝末梢的部分未进行一一实现,苹果官网在实现的过程中还考虑到了更多性能和兼容性方面的问题,这些细节的地方都值得细细推敲和学习,感兴趣的可以去苹果官网Ipad Mini产品页详细调试查看。

对比苹果官网已有的2种滚动动画的实现可以发现,当滚动动效需要在第一屏就进行显示的场景下时,更推荐使用多图的方式进行实现,如果你的网页第一屏没有动画,当用户滚动到第一屏以外的区域才进行动画展示时,则通过视频的方式实现会更好。

最后,若本文有不足之处还望大佬们评论区指出。🤟

关于本文
作者:轻松学编程
https://juejin.cn/post/7061976278932389918

参考资料

[1]

https://codepen.io/stevenlei/pen/wvKYwgZ

[2]

https://www.apple.com/ipad-mini/

[3]

https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas

[4]

https://github.com/Heqingsong/Animate/tree/master/Ipad%20Mini

[5]

https://heqingsong.github.io/Animate/Ipad%20Mini/index.html

推荐链接

  1. Web 视频播放前前后后那些事

  2. 前端程序员简历模板整理和下载

  3. 10个CSS3动画工具,值得你收藏!

关注公众号 前端开发博客,回复以下关键词

查看更多优质内容!

加群 | 进阶 | 电子书 | 资料 | 面试 

简历 | 简历模板 | 思维图 | 知识点 | Vue脑图

分享好文和朋友一起看~

创作不易,加个点赞、在看 支持一下哦!

以上是关于苹果官网Ipad mini滚动动画实现原理探究的主要内容,如果未能解决你的问题,请参考以下文章

最超值iPad突然上架苹果官网:巨便宜!

苹果mini4怎么重置系统

特价:iPad mini2 32G 仅售1499

周末特价:原装二手iPad mini1 仅售899

再见!iPad mini

苹果IPAD,手表,笔记本系列pro,air2,mini4,Air,Mini2特价!苹果笔记本全系列特价!