苹果官网Ipad mini滚动动画实现原理探究
Posted 前端开发博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了苹果官网Ipad mini滚动动画实现原理探究相关的知识,希望对你有一定的参考价值。
关注公众号 前端开发博客,领27本电子书
回复加群,自助秒进前端群
背景
最近因工作需要,要开发两个比较炫酷的动画效果,早期我个人在这方面积累太少,导致实际开发过程走了不少弯路,本文特此总结一下,希望各位同行能吸取经(jia)验(ban)教训少走弯路早日下班。
废话不多说,我们先来看几个苹果官网比较炫酷的几个动画效果,你在此也可以思考一下,该如何实现这些动画效果。
走过的弯路
当我拿到设计同学的设计稿,告知我要实现的第一个动画就类似这种时,我第一反应是想到用一个视频来进行实现,通过控制视频 currentTime
属性来进行视频进度的控制,再监听scroll
事件,当他触发时我就将 currentTime
的值进行加减操作,结果这是非常傻的一个操作。因为当用户在触摸板上进行快速向上或向下滚动时,scroll
事件的响应并不是连续触发,而是节流之后的最后几次响应,如下图所示: 对此,一时我竟不知所措,只好一番Google,找到了原来监听 scroll
事件只是第一步,第二步是需要基于scrollTop
、scrollHeight
、clientHeight
三者进行计算,求出一个滚动系数,基于该计算结果再进行相关动效计算。
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
推荐链接
关注公众号 前端开发博客,回复以下关键词
查看更多优质内容!
加群 | 进阶 | 电子书 | 资料 | 面试
简历 | 简历模板 | 思维图 | 知识点 | Vue脑图
分享好文和朋友一起看~
创作不易,加个点赞、在看 支持一下哦!
以上是关于苹果官网Ipad mini滚动动画实现原理探究的主要内容,如果未能解决你的问题,请参考以下文章