GSAP实战仿荣耀官网的页面滚动效果

  在上一篇文章中,我们对GSAP的用法有了一个简单的了解,本文我们就结合GSAP的用法教程,仿照荣耀官网MagicOS的页面,实现一个酷炫的网页效果。

整体样式布局

  我们先来欣赏一下页面的效果,每一幕如同电影开场一样缓缓的呈现效果,大家可以点击这个链接来欣赏效果:

整体效果

  在整体布局上,我们发现,它是通过多个section来划分每一屏的;这里的一屏,可以理解为一个动画效果的划分,每一屏的高度大致等于100vh。

  大多数的section再嵌套一层.section-wrapper来包裹内部的元素,同时使用margin: 0 auto;来让wrapper左右居中:

1
2
3
4
5
6
7
8
9
10
11
<div class="main magic-os">
<!-- 第一屏 -->
<section class="section-hero section-dark">
<div class="section-wrapper"></div>
</section>
<!-- 第三屏 -->
<section class="section-magic">
<div class="section-wrapper"></div>
</section>
<!-- 省略其他屏... -->
</div>

  样式上,将很多屏公共的、通用样式抽离出来,放到.magic-os中,比如给section添加黑色的背景.section-dark.section-headline是主标题,.section-intro是介绍性的文字,.section-link是跳转链接等等;不同屏有相同的布局和呈现效果,样式上也可以通用,比如.section-start呈现svg画图和.section-card-view呈现卡片式布局等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.magic-os {
background-color: #fff;
section {
position: relative;
z-index: 1;
background-color: #fff;
}
.section-dark {
color: #fff;
background-color: #000;
}
}
// 第一屏
.section-hero {
}
// 第三屏
.section-magic {
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  而每一屏特有的样式则在下面独立出来。

首屏适配

  首屏是整个网站的门面,体现出整个网站的特色与风格;我们看到首屏的设计还是比较简洁明了的,一个logo、主标题和slogan;随着屏幕宽度不断的缩放,文字的宽度和图片的大小也在随之缓慢的等比缩放,适配了各尺寸的屏幕。

  缓慢的效果主要是通过transition属性来实现的,常见的用法是:transition: 1s表示过渡效果需要1秒来完成;这里我们发现后面还带有一个时间值:transition: 1s 0.5s;我们回顾一下transition的语法:

1
transition: property duration timing-function delay;

  不难猜出来1s表示完成时间duration,0.5s表示延迟时间delay;因此上面的就相当于下面的省略写法:

1
transition: all 1s ease 0.5s

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  不知道大家有没有遇到多个属性需要使用transition的情形,笔者一般会偷懒,使用all让它们的完成时间差不多;但是如果几个属性的完成时间差距较大,就需要使用逗号将多个属性复合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
width: 100px;
height: 100px;
border: 3px solid black;
margin: 30px;
cursor: pointer;
transition: width 0.5s, background-color 1s 0.5s, transform 2s;
&:hover {
width: 200px;
background-color: red;
transform: translateY(100px);
}
}

  通过transition属性我们能够实现很多意想不到的动画效果。

复合transition属性

  我们发现在.section-wrapper外层还有一个比较特殊的类名,就是.aspect-ratio,这就涉及到了如何通过CSS来实现固定宽高比。

CSS实现固定宽高比

  首先,可替换元素(replaced element)实现固定宽高比就比较简单了,和其他元素不同,它们本身有像素宽度和高度的概念;这里说到了一个概念:可替换元素,其实就是浏览器根据元素的标签和属性,来决定元素的具体显示内容;可替换元素的内容不受当前文档的样式的影响。

CSS可以影响可替换元素的位置,但不会影响到可替换元素自身的内容

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  比如iframe也是可替换元素,可能有自己的样式表,CSS不能影响其内部的样式;常见的可替换元素有iframe、video、img、embed;与之相对应的就是不可替换元素了,它们内容可以受CSS渲染控制;我们常见的div、p、span等大多数都是不可替换元素。

  我们就来看下img固定宽高比,只需要设置width或者height为一个具体值,另一个属性设置为auto即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="wrap">
<img src="./images/1.jpg" class="img" />
</div>
</template>
<style lang="scss">
.wrap {
position: relative;
width: 50vw;
margin: 0 auto;
.img {
width: 100%;
height: auto;
}
}
</style>

img实现固定宽高比

  虽然上面的方式实现了可替换元素的固定宽高比,但是不适用于div、span等不可替换元素,因为它们本身是没有尺寸的,默认的高度都是0。

  对于不可替换元素,我们能想到一种方式是通过js来实现,页面加载时获取宽度,根据宽高比rate计算出高度然后赋值style属性即可;别忘了,还需要监听resize,这样的方式也能实现。

  另一种就是我们下面介绍的纯CSS的实现方式了,我们使用padding来撑大div的高度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="wrap">
<div class="cont"></div>
</div>
</template>
<style lang="scss">
.wrap {
position: relative;
width: 50vw;
margin: 0 auto;
.cont {
background-color: black;
height: 0;
padding: 0;
padding-bottom: 75%;
}
}
</style>

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们看到div元素的宽高比也是固定的了,大致相当于4/3,也就是75%。

div实现固定宽高比

  很多小伙伴肯定会好奇,为什么加了padding就能实现这样的效果;我们从mdn上来找答案,看下mdn对于padding属性的解释,当取百分比值的时候,是相当于包含块的宽度来计算的:

mdn对于padding解释

  通过这种方式,div的高度实际上是被padding给撑开的;我们可以将上面的样式抽离成一个通用的样式.aspect-ratio;在需要用到固定宽高比的地方直接使用类名即可,给wrap元素设置一个padding-bottom样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div class="wrap aspect-ratio">
<div class="cont"></div>
</div>
</template>
<style lang="scss">
// 抽离出来的公共样式
.aspect-ratio {
position: relative;
&::before {
display: block;
content: "";
}
& > :first-child {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
// 使用时给父级before加padding控制宽高比
.wrap {
position: relative;
width: 50vw;
margin: 0 auto;
&::before {
padding-bottom: 75%;
}
}
.cont {
background-color: red;
}
</style>

  这样wrap盒子就被before元素撑开了,如果我们想要在里面放入内容,还需要将div内部元素使用绝对定位充满整个内容;这种方式虽然能够实现,但是只能高度随着宽度改变而改变,缺点是并不能反过来,宽度随着高度改变。

  W3C提出一个保持纵横比的规范属性:aspect-ratio,我们看到目前大部分主流的浏览器也已经支持了,支持率已经有90%;但是IE还是全版本不支持,如果你不需要考虑支持IE,可以考虑使用该属性。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

aspect-ratio浏览器支持程度

  那么aspect-ratio如何使用呢?我们就不需要像上面的padding那样来套娃了,只需要在CSS添加一行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="box"></div>
</template>
<style lang="scss">
.box {
position: relative;
width: 50vw;
margin: 0 auto;
// 直接添加宽高比
aspect-ratio: 4 / 3;
background-color: red;
}
</style>

  第二屏也是使用.aspect-ratio来实现视频元素宽高的固定比例,这里就不再赘述了。

滚动渐显

  我们往下继续看,第三屏是滚动渐显的效果,这里就用到了GSAP的滚动触发,我们先欣赏一下页面的效果:

滚动触发效果

  这一屏的页面布局也比较简单,一个section-headline标题,section-content内容包裹四个section-item模块展示。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<section class="section-magic">
<div class="section-wrapper">
<h2 class="section-headline fade-copy fade-trigger">4大技术加持 共筑新体验</h2>
<div class="section-content fade-copy fade-trigger">
<div class="section-item">
<img class="section-icon" src="./images/icon-magic-ring.svg" alt="" />
<h3 class="section-headline-reduced">MagicRing 信任环</h3>
<p class="section-intro">跨系统可信互联</p>
</div>
<div class="section-item">
<img class="section-icon" src="./images/icon-magic-ring.svg" alt="" />
<h3 class="section-headline-reduced">Magic Live 智慧引擎</h3>
<p class="section-intro">平台级AI能力</p>
</div>
<!-- 省略其他... -->
</div>
</div>
</section>

  我们发现,这里section-headline标题section-content内容都加了两个特殊的样式fade-copy和fade-trigger,fade-copy的样式比较简单,初始化通过opacity: 0进行隐藏,同时使用transform让它在原始位置Y轴偏下方;触发时,再加上active样式就可以实现从底部滑动上来,实现渐显的效果。

1
2
3
4
5
6
7
8
9
.fade-copy {
transition: opacity 0.5s, transform 0.5s;
transform: translateY(50px);
opacity: 0;
&.active {
transform: translateY(0px);
opacity: 1;
}
}

  CSS的样式实现了,那么最最最关键的问题来了,如何在滚动时触发给fade-copy元素添加active类名呢?这里我们就用到了ScrollTrigger滚动触发了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const triggerFn = () => {
const triggerList = document.querySelectorAll(".fade-trigger");
triggerList.forEach((item) => {
const hook = item.getAttribute("data-hook") || "70%";
gsap.timeline({
scrollTrigger: {
trigger: item,
start: "top " + hook,
toggleClass: "active",
// markers: true,
},
});
});
};

  这里hook参数用来设置滚动触发起始的位置,默认是在距离屏幕顶部70%的高度;我们在写代码的时候,很多时候不知道元素滚动到什么时候会触发,因此可以给ScrollTrigger添加markers: true添加页面上的标记,来调试滚动条触发的位置;还不了解ScrollTrigger用法的小伙伴可以点击这里

  我们通过forEach循环来遍历页面上所有的.fade-trigger元素,每个元素都绑定了滚动触发的事件;因此在下面的很多地方,我们发现都是使用该类名来实现的效果。

svg动画

  svg绘制的动画效果,图形可以进行无限缩放,也不会失真,相较于图片也更加的灵活;说到失真就不得不提荣耀Magic5的超动态臻彩显示技术,让HDR照片和视频栩栩如生,结合荣耀鹰眼精彩抓拍,让图像永不失真。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  本文不对svg的具体使用教程进行深入的探讨,我们简单看下gsap是如何结合svg实现强大的动画效果。

  在第四屏、十一屏、十五屏和十九屏都有类似的svg动画效果,我们以第四屏为例,首先欣赏一下页面的效果:

滚动触发效果

  页面上通过前面三个ellipse元素绘制椭圆形描边,设置transform让每个旋转一定的角度,形成对称的图案;最后一个ellipse是中心的圆形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<g fill="none" stroke-dasharray="0 220% 0">
<ellipse
class="magic-path"
cx="74.8447318"
cy="68.4"
rx="31.5406825"
ry="68.2132305"
></ellipse>
<ellipse
class="magic-path"
cx="74.8447318"
cy="68.4"
rx="31.4542843"
ry="68.4"
></ellipse>
<ellipse
class="magic-path"
cx="74.8447318"
cy="68.4"
rx="31.5406825"
ry="68.2132305"
></ellipse>
<ellipse
class="magic-circle"
fill="#D7A85B"
cx="74.8447318"
cy="68.4"
rx="10.4847614"
ry="10.5230769"
></ellipse>
</g>

  图形绘制后,现在需要的就是如何让他们动起来,这里借助stroke-dasharray样式,让其实现描边的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ellipse {
animation: magic 1.5s linear;
animation-fill-mode: both;
animation-delay: 3s;
}
@keyframes magic {
0% {
stroke-dasharray: 0 220% 0;
}
100% {
// 如果写成220% 0% 0%就是顺时针
stroke-dasharray: 0% 0% 220%;
}
}

  我们的图案就像下面一样动起来了:

svg

  很多小伙伴对stroke-dasharray这个样式可能不是很了解,我们先看下mdn上的用法:

stroke-dasharray样式

  它是由数值或者百分比组成的一个数列,数列中的数值,第一个表示点的大小,第二值表示两个点之间的空隙大小;一般的写法如:stroke-dasharray:10, 2表示点10px,点空隙2px;上面样式中刚开始0 220% 0其实相当于0 220%,表示空隙占满全部的空间,也就是不显示了。

使用stroke-dashoffset也能实现类似的效果。

  现在图案有了也动起来了,我们就不用CSS的动画了;我们需要结合GSAP来让它和滚动条实现互动了,还记得我们之前说过,GSAP也能控制svg的属性,让svg动起来么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
gsap
.timeline({
scrollTrigger: {
trigger: ".magic-svg",
start: "top 60%",
end: "bottom 100%",
scrub: 0.5,
},
})
// 让ellipse实现描边
.to(".magic-path", {
strokeDasharray: "0% 0% 220%",
})
// 让中心圆圈渐显
.to(".magic-circle", {
duration: 0.5,
opacity: 1,
})
// 让ellipse从细到粗渐变
.from(
".magic-path",
{
duration: 0.5,
stroke: "#d7a85b",
strokeWidth: 2,
},
"<",
);

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们发现,很多动画结束后,都有相同的效果,主标题headline渐隐展示、副标题subhead和链接link都从下方滚动展示出来;这里就需要介绍一个新的函数:gsap.registerEffect,可以让我们在全局注册想要的效果,直接调用,不用每次都重复造轮子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gsap.registerPlugin(SplitText);

// 注册
gsap.registerEffect({
name: "rainbow",
effect: (target, config) => {
let split = new SplitText(target, { type: "chars,words,lines" });
return gsap.from(split.chars, { opacity: 0, y: -100, stagger: 0.05 });
},
});

// 初始化调用
onMounted(() => {
gsap.effects.rainbow(".h1");
gsap.effects.rainbow(".h2");
});

registerEffect注册效果

  这样注册后我们每次都需要手动调用gsap.effects,或者我们还设置extendTimeline: true,在任意时间线之后都可以调用该效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gsap.registerEffect({
name: "rainbow",
// extendTimeline设置为true,可以直接在任何GSAP时间线上调用效果
// 让结果立即插入到定义的位置(默认是在最后的位置)
extendTimeline: true,
// ...其他代码
});

// 调用
onMounted(() => {
gsap.timeline()
.rainbow(".h1")
.rainbow(".h2");
});

  这样rainbow效果就会在时间线上顺序调用;我们回到荣耀的页面注册函数上来,发现在全局注册了一个tech4的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
gsap.registerEffect({
name: "tech4",
extendTimeline: true,
effect: function (targets) {
let tl = gsap
.timeline()
// 整个svg从放大效果回到正常
.from(targets[0], {
duration: 0.5,
scale: 5,
yPercent: 80,
})
// 标题逐渐显示
.to(targets[1], {
duration: 0.5,
opacity: 1,
})
// 副标题从下向上滚动
.fromTo(
targets[2],
{
y: 60,
},
{
y: 0,
opacity: 1,
},
);
// link链接从下向上滚动
if (targets[3]) {
tl.fromTo(
targets[3],
{
y: 60,
},
{
y: 0,
autoAlpha: 1,
},
);
}
return tl;
},
});

  因此在动画效果结束后,都会调用这个tech4效果来对标题、副标题等元素进行处理。

1
2
3
4
gsap
.timeline()
// 其他的效果
.tech4([svg, headline, subhead, link, wrapper], "<");

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

卡片式布局

  我们前面介绍过卡片式布局的通用样式是.section-card-view,这种布局将两个或多个div如同卡片横向排列,随着滚动条而移动,首先也来欣赏一下页面的滚动效果:

卡片式布局效果

  页面结构看似很复杂,其实主要就三层结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<section class="section-connect-4 section-card-view">
<div class="sticky-wrapper">
<div class="sticky-content">
<div class="section-wrapper">
<div class="section-card"> <!-- 卡片内容--> </div>
<div class="section-card"> <!-- 卡片内容--> </div>
</div>
</div>
</div>
</section>
</template>
<style lang="scss">
.section-card-view {
.sticky-wrapper {
height: 108.333333vw;
}
.sticky-content {
position: sticky;
width: 100%;
height: auto;
top: 65px;
overflow: hidden;
}
.section-wrapper {
position: relative;
display: flex;
width: 70.833333vw;
margin: 0 auto;
}
.section-card {
position: relative;
flex-shrink: 0;
width: 100%;
}
.section-card + .section-card {
margin-left: 3.125vw;
}
}
</style>

  我们仔细来看下它的层级结构,首先是.sticky-wrapper设置高度108vw,用来撑开高度;中间的元素.sticky-content设置position:sticky,就是我们用来实现粘性定位的主要元素了,这样页面在滚动时就能保证内容始终距离顶部悬浮一定高度;.section-wrapper设置display:flex,是内部flex布局的容器。

  我们发现第二个元素刚开始会有缩小并且毛玻璃的效果,弱化内容的展示,随着滚动逐渐清晰;初始化时可以通过css设置blur,来达到毛玻璃的遮罩效果。

1
2
3
4
5
.section-card + .section-card .section-card-content {
transform: scale(0.8);
transform-origin: left;
filter: blur(10px);
}

  页面有了,那如果让卡片滚动起来呢?又到了我们的GSAP开始大显身手的时候了;实现的逻辑其实也非常简单,粘性定位元素.sticky-content在滚动时保持悬浮位置不变,让其内部的flex布局元素.section-wrapper向右移动,这样就让我们有种错觉,滚动条向下时将卡片推着移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const cardViewFn = () => {
const sections = document.querySelectorAll(".section-card-view");

sections.forEach((section) => {
const wrapper = section.querySelector(".section-wrapper");
const stickyWrapper = section.querySelector(".sticky-wrapper");

gsap.to(wrapper, {
scrollTrigger: {
trigger: stickyWrapper,
start: "top 65",
end: "bottom 100%",
scrub: 0,
},
ease: "none",
x: -swiperOffset,
});
})
}

  我们查找页面上所有的.section-card-view,遍历元素将其绑定ScrollTrigger事件;scrub属性将滚动条和.sticky-wrapper元素的x轴位移绑定;其实现的效果如下:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

滚动位移

  我们看到上面的代码中有一个swiperOffset变量,猜测就是wrapper的位移距离,那它是如何来计算的呢?我们将整个flex布局元素.section-wrapper内部的所有卡片想象成一个整体的div,它向左移动的距离就是整体的宽度减去页面的宽度,因此我们主要的工作就是计算它的宽度。

位移距离

  计算方式直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const screenWidth = document.documentElement.clientWidth;
const cardWidth = cards[0].clientWidth;
const cardMargin = Number(window.getComputedStyle(cards[1]).getPropertyValue("margin-left").slice(0, -2));
const cardsNumber = cards.length;

const swiperOffset =
// 距离页面左侧的宽度 * 2
wrapper.getBoundingClientRect().left * 2
// 每个卡片宽度 * 卡片数量
+ cardWidth * cardsNumber
// 卡片的左侧距离 * (卡片数量 - 1)
+ cardMargin * (cardsNumber - 1)
// 屏幕的宽度
- screenWidth;

gsap.to(wrapper, {
// 省略其他代码
x: -swiperOffset,
});

  那么现在wrapper也滚动起来了,我们就需要将第二个及其以后的卡片内容在滚动时逐渐放大清晰,去掉模糊效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cardScroll = cardWidth + cardMargin;
const stickyTop = 65;

cards.forEach(function (card, index) {
if (index > 0) {
const startTrigger = stickyTop - cardScroll * (index - 1);

gsap.to(card.querySelector(".section-card-content"), {
scrollTrigger: {
trigger: card,
start: "top " + startTrigger,
end: "+=" + cardScroll / 3,
scrub: 0,
},
ease: "none",
filter: "blur(0px)",
scale: 1,
});
}
});

  最终实现的效果如下:

实现效果

  滚动效果丝滑的如同荣耀Magic5的悬浮流线四曲面屏一样,无界视域,自在掌控。

总结

  通过本文,我们结合实际的案例,对GSAP的使用方式有了更进一步的了解;但是由于篇幅和精力的限制,本文主要分析了滚动渐显、svg动画和卡片式布局的几个效果,实际页面中有非常丰富的效果,本文只是窥探了其中的极少一部分的效果,大家如果感兴趣可以自行在荣耀官网查看。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

本文所有源码敬请关注公众号【前端壹读】,后台回复关键词【GASP荣耀官网】即可获取。


本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。