2

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

 1 year ago
source link: https://xieyufei.com/2023/04/14/GSAP-Practise.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

整体样式布局

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

整体效果

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

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

<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呈现卡片式布局等等。

.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的语法:

transition: property duration timing-function delay;

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

transition: all 1s ease 0.5s

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

.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属性

复合transition属性

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

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

CSS实现固定宽高比

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

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

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

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

<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实现固定宽高比

img实现固定宽高比

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

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

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

<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%。

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

mdn对于padding解释

mdn对于padding解释

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

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

<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浏览器支持程度

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

<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模块展示。

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

<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样式就可以实现从底部滑动上来,实现渐显的效果。

.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滚动触发了:

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是中心的圆形。

<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样式,让其实现描边的效果:

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样式

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

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

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

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,可以让我们在全局注册想要的效果,直接调用,不用每次都重复造轮子。

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注册效果

registerEffect注册效果

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

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

gsap.registerEffect({
name: "rainbow",
// extendTimeline设置为true,可以直接在任何GSAP时间线上调用效果
// 让结果立即插入到定义的位置(默认是在最后的位置)
extendTimeline: true,
// ...其他代码
});

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

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

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效果来对标题、副标题等元素进行处理。

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

卡片式布局

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

卡片式布局效果

卡片式布局效果

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

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

<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,来达到毛玻璃的遮罩效果。

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

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

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,它向左移动的距离就是整体的宽度减去页面的宽度,因此我们主要的工作就是计算它的宽度。

位移距离

  计算方式直接上代码:

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也滚动起来了,我们就需要将第二个及其以后的卡片内容在滚动时逐渐放大清晰,去掉模糊效果。

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动画和卡片式布局的几个效果,实际页面中有非常丰富的效果,本文只是窥探了其中的极少一部分的效果,大家如果感兴趣可以自行在荣耀官网查看。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK