外链论坛

 找回密码
 立即注册
搜索
查看: 21|回复: 1

苹果营销页的交互动画,见证CSS的魔力

[复制链接]

2790

主题

316

回帖

9191万

积分

论坛元老

Rank: 8Rank: 8

积分
91919870
发表于 2024-9-2 12:08:57 | 显示全部楼层 |阅读模式

前言

前两天在浏览 苹果 16寸 营销页面 的时候,发掘了几个比较有意思的交互,心里想着自己虽然是一个穷逼,然则知识是无界限的呀,于是便科研了一波。

文案重点讲交互效果,因此文中会有非常多 gif 图,大众最好连上无线再看,示例代码链接我放在了文案底部,有需要自取。

两个效果

翻盖效果

一个是屏幕慢慢打开的效果,在屏幕打开的过程中,「电脑照片 是在屏幕中固定不动的,直到打开完毕关闭完毕的时候再让 「电脑照片 随着滚动条滚动。

缩放照片

起始时是一张全屏的照片,在滚动过程中慢慢变成另一张照片,接着这张照片以屏幕正中间为基准点慢慢缩小,在缩小的过程中,这张图是定在屏幕中央的,缩小到必定值的时候,照片随着滚动条滚动。

前置知识

再动手写代码之前,咱们需要认识几个在接下来代码中要用到的知识点。

粘性定位 sticky

能够简单的认为是 「相对定位 relative」「固定定位 fixed」 的混合,元素在跨越指定范围前为相对定位,之后为固定定位。

sticky 元素固定的相对偏移是相针对离它近期拥有滚动框的祖先元素,倘若祖先元素都不能够滚动,那样是相针对 viewport 来计算元素的偏移量。

一个例子

如下代码,html 结构如下:

<body> <h1>我是 sticky 的第1个 demo</h1> <nav> <h3>导航A</h3> <h3>导航B</h3> <h3>导航C</h3> </nav> <article><p>...</p> <p>...</p> // ... </article></body>

样式如下:

nav { display: table; width: 100%; position: sticky; top: 0;}

在代码中 nav 元素会按照 body 进行粘性定位,在 viewport 视口滚动到元素 top 距离少于 0px 之前,元素为相对定位,便是说他会随着文档滚动。之后,元素将固定在与顶部距离 0px 的位置。

原理

sticky 原理大众能够看一下张鑫旭老师的 深入理解position sticky粘性定位的计算规则,能够先简单看一下老师讲解 sticky 时用的这个图:

照片引自 张鑫旭 深入理解position sticky

其中 <nav> 是 sticky 元素,蓝色框区域是 sticky 的爸爸元素,用于承载 sticky 元素,红色区域是 <nav> 相对的能够滚动的元素。

全部蓝色区域在红色区域中的时候,sticky 元素是粘性效果的(如图一);

当慢慢的向上滑的时候,蓝色的盒子超过了红色的滚动元素,那样 sticky 元素就会在蓝色的框中向下滑,实现粘性效果(如图2、三);

当蓝色的盒子划出红色的盒子的时候,由于 sticky 元素在蓝色的框子中,因此就直接被一波带走了,粘性效果(如图三)。

其实这般咱们能够很清楚的晓得为何 sticky 元素的高度为何不能等于它爸爸的高度了,由于倘若相等的话,粘性定位元素已然完全了实现粘性效果的空间,就相当于失效了。

以上原理参考了张鑫旭老师的 深入理解position sticky粘性定位的计算规则,文案中有讲解 「流盒」「粘性约束矩形」 的概念解释,以及详细的代码结构和 css 实现,大众能够查看原文。

常用例子

在业务中咱们可能会遇到这般一种场景:即一个列表,列表中的数据需要按照时间表示况且时间需要在滚动的时候固定在最顶部,这个时候咱们能够运用 sticky 来处理这个问题:

详细 html 结构如下:

<body> <h1>时间固定demo</h1><div className={styles.wrapper}> <section> <h4>5月20日</h4> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> </section> <section> <h4>5月19日</h4> <ul> <li>1</li> <li>2</li> <li>3</li></ul> </section> // ...</body>

样式如下:

body { margin: 0px; padding: 100px; height: 2000px;}h4 { margin: 2em 0 0; bac公斤round-color: #333; color: #fff; padding: 10px;top: 0; z-index: 1; position: sticky;}

代码如上,其中每一起是一个 <section>,而后给 <h4> 设置 sticky 定位,这般能够实现以上效果了。

重视

当然运用 sticky 的时候,咱们需要重视几个点:

父级元素不可有任何 overflow:visible 以外的 overflow 设置,否则粘滞效果。倘若你设置的 sticky 效果,能够瞧瞧父级元素们有设置 overflow:hidden,去掉就能够了。

必须指定 top、bottom、left、right 4 个值之一,否则只会处在相对定位。

父元素的高度不可小于 sticky 元素的高度(参考上面原理解释)

sticky 元素仅在其父元素内生效(参考上面原理解释)

还有一个不得不提的便是兼容性,咱们能够在 Can I use 官网瞧瞧 sticky 的兼容性,一片红:

在 IE 下完全是废了,倘若你的项目需要思虑 IE 的话,你就需要运用 fixed 来兼容了。

滚动视差 bac公斤round-attachment

什么是滚动视差,来看一下下面这个例子就明白了:

「视差滚动」(Parallax Scrolling)指的是让多层背景以区别的速度移动,形成立体的运动效果,带来非常出色的视觉体验。

上图中的效果,咱们只需要一行 css 就能够实现了,不需要写繁杂的 js 代码,直接设置 bac公斤round-attachment: fixed 就完成为了

html 结构<body> <section className={`${styles.gImg} ${styles.gImg1}`}>IMG1</section><section className={`${styles.gImg} ${styles.gImg2}`}>IMG2</section> <section className={`${styles.gImg} ${styles.gImg3}`}>IMG3</section></body>样式代码section { height: 100vh;}.gImg {bac公斤round-attachment: fixed; bac公斤round-size: cover; bac公斤round-position: center center; width: 100%;}.gImg1 { bac公斤round-image: url(@/assets/mac1.jpg);}.gImg2 {bac公斤round-image: url(@/assets/mac2.jpg);}.gImg3 { bac公斤round-image: url(@/assets/mac4.jpg);}

经过滚动视差这个 css 咱们基本上能够实现第二个动画了。

关于滚动视差的讲解,大众能够参考这篇文案 滚动视差?CSS 不在话下,写的很仔细

Canvas 画图

其实第二个动画咱们能够运用 canvas 画图来实现,咱们能够一起画布中画出两张照片按照滚动的距离,去表示两张照片在画布中的比例。

能够经过 canvas 供给的 drawImage 办法来进行画图,这个办法供给了多种方式在 Canvas 上绘制图像。

例如咱们需要实现的画出如下图:

其实咱们就需要截取第1照片的上半部分,下一张照片的下半部分,而后进行拼接就 ojbk 了,瞧瞧参数解释图:

这儿咱们需要传入 7 个参数,来实现咱们需要的效果:

ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

详细参数的意思笔者再也不这儿细讲了,大众能够参考一下 drawImage() MDN 文档。

思路大致是首要绘制第1照片做为底图,而后咱们经过绘制第二张照片,覆盖掉部分第1照片这般能够实现前面说到的效果。假设咱们照片的原始宽高为 2048*1024,画布的体积为 544*341,在滚动的时候的偏移距离为 offsetTop,这般咱们能够写出如下代码:

function drawImage() { context.drawImage(img1, 0, 0, 2048, 1024, 0, 0, 544, 341);context.drawImage(img2, 0, 滚动偏移距离 * 1024 / 341, 2048, 1024, 0, 滚动偏移距离, 544, 341);}

之前笔者运用过 ctx.drawImage(image, dx, dy, dWidth, dHeight),能够参考笔者写的 运用 React Hooks 实现仿石墨的照片预览插件,这次用到了 7 个参数,大众能够参考这篇文案照片画到canvas 上的几种办法,写的很仔细

transform 中的 matrix

CSS3 中运用 transform 能够对元素进行变换。其中包括:位移、旋转、偏移、缩放。transform 能够运用 translate/rotate/skew/scale 的方式来掌控元素变换,能够运用 matrix 的方式来掌控元素变换。

举个例子:

// 代码一transform: matrix(1.5, 0, 0, 1.5, 0, 190.5);// 代码二transform: scale(1,5, 1.5) translate(0, 190.5)

上面两行代码的意思是同样的,之后咱们做到第二个动画的时候会用到这个属性。

倘若大众想要深入认识这个属性,能够参考:大学没学过数学要理解 CSS3 transform 中的 matrix

开撸

初始化项目

工欲善其事,必先利其器。笔者运用 react Hooks 来完成这两个动画效果,并运用 umi 快速初始化一个项目,详细的初始化过程能够参考笔者写的 dva理论到实践——帮你扫清dva的知识盲点,里面仔细介绍了怎样运用脚手架快速搭建一个项目。

搭建完成之后,笔者将之前讲到的例子都放在这儿,方便大众点进去查看。

翻盖效果

翻盖效果其实很简单,你们绝对想不到,苹果营销页是怎么做的?

「它用了 120 张照片按照滚动距离来画出对应的在这个滚动位置上该展示的照片,对,你听错。我之前以为应该是 css3 掌控盖的方向从而实现翻盖效果的,是我想多了,哈哈哈。

思路

那样咱们实现就很简单了,咱们只需要做以下几点:

首要要定义一个常量,规定从盖着到完全打开需要 「滚动多少距离」来完成,咱们这儿定义为 400px。咱们需要晓得什么时候起始进行翻盖合盖操作,这个能够照片在屏幕正中间的时候,让其起始动画。// 起始动画的 scrollTop// $(#imgWrapper) 放照片的容器,html 结构下面有startOpen = $(#imgWrapper).offset().top - (window.innerHeight / 2 - $(#imgWrapper).height() / 2);当翻盖合盖的时候,咱们需要将电脑固定在视口中,等到完全翻开合上的时候,再让起随滚动条滚动,这儿咱们能够运用 position: sticky。html 结构<body> // ...<div className={styles.stickyContainer}> <div className={styles.stickyWrapper}> <div id="imgWrapper" className={styles.imgWrapper}> <imgsrc={require(`@/assets/${asset}.jpg`)} alt="照片1" /> </div> </div> </div> // ...</body>

其中动态引入照片咱们能够经过 require(照片路径) 来完成,如上面的代码,咱们只需要计算出对应滚动距离所需要展示的照片名字就可

样式代码.stickyContainer {height: 150vh;}.stickyWrapper { height: 100vh; position: sticky; top: 100px;}.imgWrapper { width: 100vh; height: 521px; margin: 0 auto;}.imgWrapper img { width: 100%;}

接着便是在滚动的过程中计算出当先需要表示照片是那一张,咱们上面说到:120 张照片,在 400px 的滚动距离中完成动画。

首要咱们再加载完成后能够得出,咱们能够得出起始动画的距离文档顶部的滚动值 startOpen,因此呢咱们能够得出如下代码:

useEffect(() => { // 绑定事件window.addEventListener(scroll, scrollEvent, false); // 起始动画的滚动距离 // startOpenstartOpen = $(#imgWrapper).offset().top - (window.innerHeight / 2 - $(#imgWrapper).height() / 2); return ()=>{ window.removeEventListener(scroll, scrollEvent, false); }}, []);// 滚动事件const scrollEvent = () => { // 实时的 scrollTopconst scrollTop = $(html).scrollTop(); let newAsset = if (scrollTop > startOpen && scrollTop < startOpen + 400) {let offset = Math.floor((scrollTop - startOpen) / 400 * 120); if (offset < 1) { offset = 1; } else if (offset > 120) { offset = 120; } if (offset < 10) { newAsset = `large_000${offset}`;} else if (offset < 100) { newAsset = `large_00${offset}`; } else { newAsset = `large_0${offset}`; } } // 边界值判断 // .... // 设置照片 url setAsset(newAsset);};预览效果

这个翻盖动画很简单,120张照片换着来,实时渲染对应的照片,其实什么技术含量,大众能够尝试一下用其他的办法实现一波。

缩放照片

缩放照片到屏幕这个动画咱们能够用两个方式实现,一个是 「滚动视差」 实现,一个是 canvas 在滚动过程中实时渲染照片

起始之前咱们来看一下放大的之前图,如下:

它由两张照片构成,屏幕中表示照片,他与 电脑外壳 的上间距是 18px,当放大了之后,照片电脑外壳照片 的上边距应该是 18 * 放大比率。

电脑外壳照片,如下:

接下来咱们起始介绍两种实现方式。

Canvas 实现

Canvas 实现是将屏幕中表示的这张照片由 Canvas 来画。

思路

其实这个动画有两部分构成,一个是 照片覆盖」,一个是 照片缩小」

照片覆盖

运用 Canvas 来处理运用 Canvas 实现咱们需要运用 drawImage 办法将两张照片画到一张画布上。只需要经过滚动的距离,对应计算出详细某个时候画布应该画多少比例的第1张图,画多少比例的第二张图,就能够处理了。只需要晓得什么时候起始照片覆盖。

照片缩小

咱们运用 transform: matrix 来实现,其中照片缩小是基于屏幕正中央的点进行缩放的。

咱们按照滚动的距离相应的计算出相应放大比率和 translate 的值,如下图,实时改变 transform: matrix 的参数值就行了。

这儿咱们需要计算出几个临界点的值,例如最大/小的放大比率,最大/小偏移值,起始缩小的点等。

「在进行动画的时候,canvas 包裹容器应该是 sticky 定位在视口中的,直到动画结束,canvas 包裹容器才会随着滚动条滚动。」有些重要的值

这儿咱们需要晓得几个值:

定义的常量// canvas 表示照片宽度const CANVAS_WIDTH = 544;// canvas 表示照片高度const CANVAS_HEIGHT = 341;// 动画连续的距离const ZOOM_SCROLL_RANGE = 400;// canvas 表示照片 实质宽度const IMG_NATURAL_WIDTH = 2048;// canvas 表示照片 实质高度const IMG_NATURAL_HEIGHT = 1024;放大比率(curScale),用于 matrix 的 scale 值

最小的放大比率为 1,即是自己

最大的放大比率是屏幕的高度除以屏幕表示照片的比率,这儿笔者将 canvas 画出来的照片宽高定位 544 * 341。

const CANVAS_WIDTH = 544;const CANVAS_HEIGHT = 341;const scaleRadio = window.innerHeight / CANVAS_HEIGHT;

因此放大比率的区间应该是 1 ~ scaleRadio 之间。

偏移距离(translate),用于 matrix 的 偏移值

最大的偏移距离,应该是当 curScale 为 1 的时候,包裹元素距离视口顶部的距离,咱们的缩放始终都是基于屏幕正中央这个点来进行放大/缩小的,因此能够很简单的得出:

// 最大的 translatelet StartScale = 0;StartScale = window.innerHeight / 2 - $(#img-wrapper).height() / 2;

最小的偏移距离,应该是在 curScale 为 scaleRadio 时,包裹元素距离视口顶部的距离,这个时候,咱们就需要用到之前说到的视屏照片到电脑外壳的 top = 18px 这个值了,由于照片进行了放大,因此最小的偏移距离应该为:

miniTranslate = - 18 * scaleRadio

因此偏移距离的区间应该是 miniTranslate ~ StartScale 之间。

起始缩放操作的初始点(NewStartScale)

其实很简单咱们需要在第二章照片完全覆盖掉第1张的照片的时候就进行起始缩放,这个值可以经过「Canvas 包裹元素距离顶部文档的top值」 加上 「一屏的高度」 就能计算出。

let NewStartScale = 0;NewStartScale = $(#section-sticky-hero).offset().top + window.innerHeight;核心代码

核心代码便是滚动时候的计算:

const scrollEvent = () => { // 当前的 scrollTop const scrollTop = $(html).scrollTop(); // 放大比率 默认为最大 let curScale = scaleRadio; // 偏移距离 默认为最小let translate = -scaleRadio * 18; // StartScale:最大的偏移距离 // NewStartScale:起始缩放操作的初始 // 就 return if (!NewStartScale || !StartScale) return; // 计算 当前的 curScale// (scaleRadio - 1) / ZOOM_SCROLL_RANGE):每 1px 放大多少 // scrollTop + scaleRadio * 18 - NewStartScale:当前滚动了多少curScale = scaleRadio - ((scaleRadio - 1) / ZOOM_SCROLL_RANGE) * (scrollTop + scaleRadio * 18 - NewStartScale); // 边界值处理 if (curScale > scaleRadio) { curScale = scaleRadio; } else if (curScale < 1) {curScale = 1; } // 计算 当前的 translate // 从 scaleRadio * 18 起始 // all = scaleRadio * 18 + StartScale // 滑动过程中持续相加translate = -scaleRadio * 18 + ((scrollTop + scaleRadio * 18 - NewStartScale) / ZOOM_SCROLL_RANGE * (scaleRadio * 18 + StartScale)); // 边界值处理 if (translate > StartScale) { translate = StartScale;} else if (translate < -scaleRadio * 18) { translate = - scaleRadio * 18; } // 运用 canvas 画图 if (image1 && image2) { // 在照片覆盖周期 // curScale 还是最大的比率if (curScale === scaleRadio) { drawImage({ img1: image1, img2: image2, secTop: CANVAS_HEIGHT * (scrollTop + 18 * scaleRadio - NewStartScale) / window.innerHeight, }); } else {// 倘若不是最大的比率,说明照片已然覆盖完了 // 直接表示第二章 drawImage({ img1: image1, img2: image2, secTop: 0, }); } } // 设置样式 $(#img-wrapper).css({transform: `matrix(${curScale}, 0, 0, ${curScale}, 0, ${translate})`, });};

html 结构如下:

<body> // ... 其他内容 <div id="section-sticky-hero" className={styles.stickyContainer}><div className={styles.componentContainer}> <div className={styles.imgWrapper} id="img-wrapper"> <canvas ref={canvasRef} id="canvas" className={styles.canvas}></canvas></div> </div> </div> // ... 其他内容</body>

篇幅有限,笔者只列举了滚动事件的代码和 html 结构,其他的代码,例如 drawImage 这个办法大众有兴趣的话,能够参考源码。

预览效果图滚动视差实现

前面咱们讲了滚动视差的原理,有了这个 bac公斤round-attachment: fixed 属性,第二个动画基本上已然实现一半了。

实现思路

和上面的 canvas 画图相比的话,其实便是照片覆盖的这一步不同样,其他基本上都是类似的,包含边界值的计算。

照片覆盖

这儿咱们需要将两张照片都设置为背景照片同期咱们需要给第二张照片套上 电脑外壳照片

第1照片充满屏幕的时候,就给两张照片同期加上 bac公斤round-attachment: fixed 属性,不可起始的时候就加上这个属性,否则就会变成下面这个效果:

照片缩小

这儿咱们运用  transform: matrix 来做这个放大缩小,咱们运用 bac公斤round-position 和 bac公斤round-size 来进行照片「缩小/放大和偏移」

其他的都与 Canvas 实现的原理大同小异。核心代码

滚动规律代码如下:

const CANVAS_WIDTH = 544;const CANVAS_HEIGHT = 341;const WRAPPER_WIDTH = 694;const WRAPPER_HEIGHT = 408;const ZOOM_SCROLL_RANGE = 400;// scalaRadio// 照片放大的最大的倍数const scaleRadio = window.innerHeight / CANVAS_HEIGHT;const scrollEvent = () => {const scrollTop = $(html).scrollTop(); let curScale = scaleRadio; let translate = -scaleRadio * 18; if (!imgFixFixed || !StartScale) return;// 第1照片的 距离文档的顶部的距离为 imgFixFixed // 第1照片的高度为 100vh,即一屏的高度 // 因此第二章照片的 scrollTop 为 imgFixFixed + window.innerHeightif (scrollTop > imgFixFixed && scrollTop < imgFixFixed + window.innerHeight) { // 设置 fixed 属性 setFixImg(true); } else { setFixImg(false); } // 假设咱们缩放的距离是 400 // 那样咱们能够计算出 每 1px 缩放的比例 // 接着一这个比例乘以滚动的距离curScale = scaleRadio - ((scaleRadio - 1) / ZOOM_SCROLL_RANGE) * (scrollTop - imgFixFixed - window.innerHeight); // curScale 边界值处理 // ... // 从 scaleRadio * 18 起始// all = scaleRadio * 18 + StartScale // 滑动过程中持续相加translate = -scaleRadio * 18 + ((scrollTop - imgFixFixed - window.innerHeight) / ZOOM_SCROLL_RANGE * (scaleRadio * 18 + StartScale)); // translate 边界值处理 // ... // 设置照片的 css 样式 // 进行照片基于中心点的缩放 $(#g-img2).css({ "width": curScale * CANVAS_WIDTH,"height": curScale * CANVAS_HEIGHT, "margin-top": `${translate + 18 * curScale}px`, }); $(#img-wrapper).css({ "width": scaleRadio * WRAPPER_WIDTH,"height": scaleRadio * WRAPPER_HEIGHT, "bac公斤round-size": `${curScale * WRAPPER_WIDTH}px ${curScale * WRAPPER_HEIGHT}px`, "bac公斤round-position": `center ${translate}px`, });};

html 结构如下:

<body> // ... 其他内容 <section id="g-img" className={`${styles.gImg} ${styles.gImg1} ${fixImg ? styles.fixed : }`}>IMG1</section><div className={styles.stickyContainer}> <div className={styles.componentContainer}> <div className={styles.imgWrapper} id="img-wrapper"><section id="g-img2" className={`${styles.gImg} ${styles.gImg2} ${fixImg ? styles.fixed : }`}>IMG2</section> </div> </div> </div> // ... 其他内容</body>预览效果图

总结

今天讲了两个苹果营销页面的动画,文案没什么难点,重点是对几个基本知识点的运用。「粘性定位」「滚动视差」「Canvas 画图」「matrix 属性的运用 等等,期盼大众有所帮忙

「实不相瞒,想要个赞!」

亲,点这涨工资 

回复

使用道具 举报

2930

主题

2万

回帖

9956万

积分

论坛元老

Rank: 8Rank: 8

积分
99569372
发表于 2024-10-2 18:40:41 | 显示全部楼层
我们有着相似的经历,你的感受我深有体会。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

站点统计|Archiver|手机版|小黑屋|外链论坛 ( 非经营性网站 )|网站地图

GMT+8, 2024-11-5 22:40 , Processed in 0.075897 second(s), 19 queries .

Powered by Discuz! X3.4

Copyright © 2001-2023, Tencent Cloud.