前两天在浏览 苹果 16寸 营销页面 的时候,发现了几个比较有意思的交互,内心想着本身虽然是一个穷逼,可是知识是无界限的呀,因而便研究了一波。javascript
文章主要讲交互效果,因此文中会有不少
gif
图,你们最好连上无线再看,示例代码连接我放在了文章底部,有须要自取。css
一个是屏幕慢慢打开的效果,在屏幕打开的过程当中,电脑图片 是在屏幕中固定不动的,直到打开完毕或者关闭完毕的时候再让 电脑图片 随着滚动条滚动。html
开始时是一张全屏的图片,在滚动过程当中慢慢变成另外一张图片,接着这张图片以屏幕正中间为基准点慢慢缩小,在缩小的过程当中,这张图是定在屏幕中央的,缩小到必定值的时候,图片随着滚动条滚动。java
再动手写代码以前,咱们须要了解几个在接下来代码中要用到的知识点。react
sticky
能够简单的认为是 相对定位 relative
和 固定定位 fixed
的混合,元素在跨越指定范围前为相对定位,以后为固定定位。css3
sticky
元素固定的相对偏移是相对于离它最近的具备滚动框的祖先元素,若是祖先元素都不能够滚动,那么是相对于 viewport
来计算元素的偏移量。git
以下代码,html
结构以下:github
<body>
<h1>我是 sticky 的第一个 demo</h1>
<nav>
<h3>导航A</h3>
<h3>导航B</h3>
<h3>导航C</h3>
</nav>
<article>
<p>...</p>
<p>...</p>
// ...
</article>
</body>
复制代码
样式以下:canvas
nav {
display: table;
width: 100%;
position: sticky;
top: 0;
}
复制代码
在代码中 nav
元素会根据 body
进行粘性定位,在 viewport
视口滚动到元素 top
距离小于 0px
以前,元素为相对定位,也就是说他会随着文档滚动。以后,元素将固定在与顶部距离 0px
的位置。c#
sticky
原理你们能够看一下张鑫旭老师的 深刻理解position sticky粘性定位的计算规则,能够先简单看一下老师讲解 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;
background-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
来兼容了。
background-attachment
什么是滚动视差,来看一下下面这个例子就明白了:
视差滚动(Parallax Scrolling)是指让多层背景以不一样的速度移动,造成立体的运动效果,带来很是出色的视觉体验。
上图中的效果,咱们只须要一行 css
就能够实现了,不须要写复杂的 js
代码,直接设置 background-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 {
background-attachment: fixed;
background-size: cover;
background-position: center center;
width: 100%;
}
.gImg1 {
background-image: url(@/assets/mac1.jpg);
}
.gImg2 {
background-image: url(@/assets/mac2.jpg);
}
.gImg3 {
background-image: url(@/assets/mac4.jpg);
}
复制代码
经过滚动视差这个 css
咱们基本上能够实现第二个动画了。
关于滚动视差的讲解,你们能够参考这篇文章 滚动视差?CSS 不在话下,写的很详细。
Canvas
画图其实第二个动画咱们也可使用 canvas
画图来实现,咱们能够在一块画布中画出两张图片,根据滚动的距离,去显示两张图片在画布中的比例。
能够经过 canvas
提供的 drawImage
方法来进行画图,这个方法提供了多种方式在 Canvas
上绘制图像。
好比咱们须要实现的画出以下图:
其实咱们就须要截取第一章图片的上半部分,下一张图片的下半部分,而后进行拼接就 ojbk
了,看看参数解释图:
这里咱们须要传入 7 个参数,来实现咱们须要的效果:
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
复制代码
具体参数的意思笔者也就再也不这里细讲了,你们能够参考一下 drawImage() MDN 文档。
思路大体是首先绘制第一张图片,做为底图,而后咱们经过绘制第二张图片,覆盖掉部分第一张图片,这样就能够实现前面提到的效果。假设咱们图片的原始宽高为 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}>
<img src={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);
// 开始动画的滚动距离
// startOpen
startOpen = $('#imgWrapper').offset().top - (window.innerHeight / 2 - $('#imgWrapper').height() / 2);
return ()=>{
window.removeEventListener('scroll', scrollEvent, false);
}
}, []);
// 滚动事件
const scrollEvent = () => {
// 实时的 scrollTop
const 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
方法将两张图片画到一张画布上。只须要经过滚动的距离,对应计算出具体某个时候画布应该画多少比例的第一张图,画多少比例的第二张图,就能够解决了。只须要知道何时开始图片覆盖。
咱们使用 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 的时候,包裹元素距离视口顶部的距离,咱们的缩放一直都是基于屏幕正中央这个点来进行放大/缩小的,因此能够很简单的得出:
// 最大的 translate
let StartScale = 0;
StartScale = window.innerHeight / 2 - $('#img-wrapper').height() / 2;
复制代码
最小的偏移距离,应该是在 curScale
为 scaleRadio
时,包裹元素距离视口顶部的距离,这个时候,咱们就须要用到以前提到的视屏图片到电脑外壳的 top = 18px
这个值了,由于图片进行了放大,因此最小的偏移距离应该为:
miniTranslate = - 18 * scaleRadio
复制代码
因此偏移距离的区间应该是
miniTranslate ~ StartScale
之间。
NewStartScale
)其实很简单咱们须要在第二章图片彻底覆盖掉第一张的图片的时候就进行开始缩放,这个值能够经过 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
这个方法,你们有兴趣的话,能够参考源码。
前面咱们也讲了滚动视差的原理,有了这个 background-attachment: fixed
属性,第二个动画基本上已经实现一半了。
和上面的 canvas
画图相比的话,其实就是图片覆盖的这一步不同,其余基本上都是相似的,包括边界值的计算。
这里咱们须要将两张图片都设置为背景图片,同时咱们须要给第二张图片套上 电脑外壳图片。
当第一张图片充满屏幕的时候,就给两张图片同时加上 background-attachment: fixed
属性,不能一开始的时候就加上这个属性,否则就会变成下面这个效果:
这里咱们不使用 transform: matrix
来作这个放大缩小,咱们使用 background-position
和 background-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;
// 第一张图片的 距离文档的顶部的距离为 imgFixFixed
// 第一章图片的高度为 100vh,即一屏的高度
// 因此第二章图片的 scrollTop 为 imgFixFixed + window.innerHeight
if (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,
"background-size": `${curScale * WRAPPER_WIDTH}px ${curScale * WRAPPER_HEIGHT}px`,
"background-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 属性的使用 等等,但愿对你们有所帮助。
实不相瞒,想要个赞!