在上一篇文章中,我们对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
呈现卡片式布局等等。
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的语法:
| 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属性我们能够实现很多意想不到的动画效果。
我们发现在.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>
|
虽然上面的方式实现了可替换元素的固定宽高比,但是不适用于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%。
很多小伙伴肯定会好奇,为什么加了padding就能实现这样的效果;我们从mdn上来找答案,看下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如何使用呢?我们就不需要像上面的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模块展示。
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样式就可以实现从底部滑动上来,实现渐显的效果。
| .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", }, }); }); };
|
这里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
样式,让其实现描边的效果:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| ellipse { animation: magic 1.5s linear; animation-fill-mode: both; animation-delay: 3s; } @keyframes magic { 0% { stroke-dasharray: 0 220% 0; } 100% { stroke-dasharray: 0% 0% 220%; } }
|
我们的图案就像下面一样动起来了:
很多小伙伴对stroke-dasharray这个样式可能不是很了解,我们先看下mdn上的用法:
它是由数值或者百分比组成的一个数列,数列中的数值,第一个表示点的大小,第二值表示两个点之间的空隙大小;一般的写法如: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, }, }) .to(".magic-path", { strokeDasharray: "0% 0% 220%", }) .to(".magic-circle", { duration: 0.5, opacity: 1, }) .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"); });
|
这样注册后我们每次都需要手动调用gsap.effects,或者我们还设置extendTimeline: true
,在任意时间线之后都可以调用该效果。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| gsap.registerEffect({ name: "rainbow", 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() .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, }, ); 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如同卡片横向排列,随着滚动条而移动,首先也来欣赏一下页面的滚动效果:
页面结构看似很复杂,其实主要就三层结构:
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,来达到毛玻璃的遮罩效果。
| .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 = wrapper.getBoundingClientRect().left * 2 + cardWidth * cardsNumber + 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荣耀官网】即可获取。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里