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

news/2024/7/19 13:56:06 标签: java, js, html, css, javascript
htmledit_views">
html" title=js>js_content">

前言

探究的过程有意思。今日前端早读课文章由FITURE魔镜高级前端工程师@何青松投稿分享。

@何青松,曾任freeCodeCamp成都社区负责人,现任FITURE魔镜高级前端工程师,目前负责公司国际化业务Web开发,拥有丰富的2d图形开发经验,喜欢追求极致的用户体验。

正文从这开始~~

背景

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

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

走过的弯路

当我拿到设计同学的设计稿,告知我要实现的第一个动画就类似这种时,我第一反应是想到用一个视频来进行实现,通过控制视频 currentTime 属性来进行视频进度的控制,再监听scroll事件,当他触发时我就将 currentTime 的值进行加减操作,结果这是非常傻的一个操作。因为当用户在触摸板上进行快速向上或向下滚动时,scroll 事件的响应并不是连续触发,而是节流之后的最后几次响应,如下图所示:

51e381ba2dfa837e5913c21b4e1002ef.png

对此,一时我竟不知所措,只好一番Google,找到了原来监听 scroll 事件只是第一步,第二步是需要基于scrollTop、scrollHeight、clientHeight 三者进行计算,求出一个滚动系数,基于该计算结果再进行相关动效计算。

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

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

奈何自己不够聪明,只有承认自己的无知,被迫向苹果官网妥协。在工位上认真研究苹果官网上的效果是如何实现的,通过认真调试研究后发现,苹果官网竟然是使用几十张甚至百多张图片逐一去加载、再拼接的方式实现的类似效果,当看到请求面板中那一大堆的网络请求时,我的内心是排斥的,网络上那么多性能优化的文章都告诫我们,要尽可能的减少资源加载的请求,图片能合并就合并,代码能压缩就压缩。但苹果官网却玩的这么开放,找了一圈社区发现已经有博主实现了该效果,效果看起来还相当不错-> Apple iPhone SE - Rotation,但我个人实在是看不下去这一堆网络请求,最终还是没采用该方案去实现。

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

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

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节点就设置多少偏移量,从而达到交互与肉眼看到的视觉内容进行匹配。

41063ac95738ba448cdee78c9269b212.png

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 则优先使用该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:https://github.com/Heqingsong/Animate/tree/master/Ipad%20Mini

总结

本文通过对苹果官网Ipad mini滚动动画的实现原理进行了探究,手动实现了该动画的核心原理部分,还有很多细枝末梢的部分未进行一一实现,苹果官网在实现的过程中还考虑到了更多性能和兼容性方面的问题,这些细节的地方都值得细细推敲和学习,感兴趣的可以去苹果官网Ipad Mini产品页详细调试查看。
对比苹果官网已有的2种滚动动画的实现可以发现,当滚动动效需要在第一屏就进行显示的场景下时,更推荐使用多图的方式进行实现,如果你的网页第一屏没有动画,当用户滚动到第一屏以外的区域才进行动画展示时,则通过视频的方式实现会更好。

具体Demo预览,可通过文末阅读原文查看。

关于本文
作者:@何青松
原文:https://juejin.cn/post/7061976278932389918


http://www.niftyadmin.cn/n/1781013.html

相关文章

[ruby][vim] 用正则替换旧的 hash rocket 语法

2019独角兽企业重金招聘Python工程师标准>>> Change this way :param > valuefor this way param: valueopen .rb file in vim and do %s/:\([^,"]*\) >/\1:/gif you want use the interactive mode, add c in the end of command %s/:\([^,"]*\) &…

安装jar文件到本地仓库mvn install:install-file

2019独角兽企业重金招聘Python工程师标准>>> mvn install:install-file -DgroupId<groupId> -DartifactId<artifactId> -Dversion1.0.0 -Dpackagingjar -Dfile<myfile.jar> 完整命令&#xff1a; mvn install:install-file -Dfileyour-artifact-1…

如何被连续提拔脱颖而出?

原创不易&#xff0c;求分享、求一键三连前段时间总有小伙伴问我如何脱颖而出&#xff1f;一时间不知道如何说起&#xff0c;下来后细细思量&#xff0c;觉得可以分三个阶段&#xff1a;无脑担当&#xff0c;甚至担而不当&#xff1b;完善认知&#xff0c;有取舍&#xff0c;有…

英语单词前缀规则总结

2019独角兽企业重金招聘Python工程师标准>>> superword是一个Java实现的英文单词分析软件&#xff0c;主要研究英语单词音近形似转化规律、前缀后缀规律、词之间的相似性规律等等。 1、A- (not, without) (hit 87) 1、Adam2、Alex3、aback4、aboard5、abound6、abou…

httpd-2.4 基础配置图解及实现

前提httpd 2.4编译安装的目录是/usr/local/apache&#xff0c;配置文件目录是/etc/httpd249/配置文件目录结构/etc/httpd249/httpd.conf # 主配置文件 /etc/httpd249/extra/httpd-default.conf # 默认配置文件(包括keepalive和AccessFileName设置等) /etc/httpd249/extra/httpd…

《大明王朝》阴谋诡计,下三路招呼

原创不易&#xff0c;求分享、求一键三连前情回顾《大明王朝》雪崩前&#xff0c;精英们的狂欢《大明王朝》君以此兴&#xff0c;必以此亡《大明王朝》天地不仁&#xff0c;百官不争《大明王朝》书生误国&#xff0c;妄谈方略前面提到&#xff0c;浙江已经是一块是非之地&#…

HashMap 如何解决hash冲突

2019独角兽企业重金招聘Python工程师标准>>> 在Java编程语言中&#xff0c;最基本的结构就是两种&#xff0c;一种是数组&#xff0c;一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造&#xff0c;HashMap也一样。 当程序试图将多个 key-value 放入 …

重启《React 知命境》更新计划说明

大家好&#xff0c;我是这波能反杀。一个 ~ 懒人 ~。我从 2015 年就开始学习和使用 React&#xff0c;亲身经历了 React 发展的几乎所有变迁过程&#xff0c;具备丰富的 React 项目实战经验&#xff0c;小项目、大项目、巨型项目&#xff0c;PC、移动端、小程序、iOS、Android我…