页面打开时先播放一段全屏视频,随着用户下滑,文字逐渐浮现,紧接着大标题和图片内容缓缓登场,视差滚动效果可以提升用户体验和视觉冲击力的。
这个效果主要通过以下步骤实现:
-
初始设置:使用GSAP设置元素的初始状态(位置、透明度等)
-
创建时间轴:使用GSAP Timeline定义动画序列和时序
-
滚动触发:使用ScrollTrigger将动画与页面滚动绑定
-
动画序列:
-
顶部文字向下移动进入视图
-
底部文字向上移动并渐显
-
缩放区域逐渐显示并缩小到正常大小
-
标题向上移动创造视觉冲击
-
核心在于 ScrollTrigger.create() 和 时间轴 (gsap.timeline) 的配合。
- gsap.timeline:负责定义动画本身(比如视频透明度变化、文字上移、大标题缩放、图片进场等)。它就像一条"动画脚本",规定了顺序和节奏。如果没有时间轴,动画就是单点触发,缺少连贯性。
- ScrollTrigger.create():负责把这个时间轴和用户滚动绑定在一起。它控制了动画什么时候开始 (start)、什么时候结束 (end)、是否跟随滚动进度 (scrub),以及固定住元素 (pin)。它就像"驾驶员",根据滚动条的进度来驱动时间轴。

<style>
.section-Index2 {
overflow: hidden;
}
#shopify-section-{{section.id}} .section_Index2All {
background: {{ section.settings.background }};
margin-top: {{section.settings.Desktop_top}}px;
margin-bottom: {{section.settings.Desktop_bottom}}px;
padding-top: {{section.settings.Desktop_paddingTop}}px;
padding-bottom: {{section.settings.Desktop_paddingBottom}}px;
}
@media(max-width:999px) {
#shopify-section-{{section.id}} .section_Index2All {
margin-top: {{section.settings.Mobile_top}}px;
margin-bottom: {{section.settings.Mobile_bottom}}px;
padding-top: {{section.settings.Mobile_paddingTop}}px;
padding-bottom: {{section.settings.Mobile_paddingBottom}}px;
}
}
.videoContainer {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
#nebula-scroll-section {
position: relative;
height: 100vh;
}
.background-video,
.background-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
}
/* 第一屏:视频和上下文字 */
.videoContainer_top {
width: 100%;
height: 100%;
opacity: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: relative;
z-index: 1;
}
.top-text {
width: 100%;
text-align: center;
transform: translateY(-96px);
color: white;
z-index: 2;
}
.bottom-text {
width: 100%;
text-align: center;
transform: translateY(206px);
color: white;
z-index: 2;
opacity: 0;
position: absolute;
}
/* 第二屏:白底大字 */
.videoContainer_middle {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
z-index: 3;
mix-blend-mode: color-dodge;
}
.videoContainer_middle .zoom-title {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0px;
font-size: 200px;
line-height: 234px;
font-weight: bold;
}
.zoom-inner-top{
position: absolute;
top: 20%;
z-index: 4;
left: 50%;
transform: translate(-50%, 0%);
max-height: 380px;
overflow: hidden;
}
/* .zoom-inner-top .background-image{
max-height: 380px;
} */
.zoom-inner-bottom{
z-index: 4;
color: var(--my-text-01);
position: absolute;
bottom:30px;
width: 100%;
}
.zoom-inner-bottom-inner{
display: flex;
flex-direction: column;
max-width: 1000px;
width: 100%;
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
text-align: center;
gap: 12px;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.zoom-inner-bottom .zoom-inner-bottom-title{
font-size: 36px;
line-height: 1.3;
font-weight: 600;
}
.zoom-inner-bottom .zoom-inner-bottom-text{
/* margin-bottom: 12px; */
}
.zoom-inner-bottom .zoom-inner-bottom-svg{
margin: 24px 0;
}
@media(max-width: 1200px) {
.videoContainer_middle .zoom-title {
font-size: 120px;
}
}
@media screen and (max-width: 768px) {
.videoContainer_middle .zoom-title{
line-height:75px ;
font-size: 72px;
text-align: center;
}
.zoom-inner-bottom .zoom-inner-bottom-title{
font-size: 24px;
line-height: 32px;
font-weight: 600;
gap: 8px;
letter-spacing: 0.3px;
}
.zoom-inner-bottom .zoom-inner-bottom-svg{
margin: 16px 0;
}
.zoom-inner-bottom{
bottom: 24px;
}
.zoom-inner-top{
height: 300px;
}
}
</style>
<div class="section_Index2All">
<div class="videoContainer" id="nebula-scroll-section">
<!-- 第一屏:视频和文字 -->
<div class="videoContainer_top">
<div class="top-text animate-text my_h_26 crociris_title">{{ section.settings.title }}</div>
<div class="video-wrapper">
{% if section.settings.video != blank %}
<native-video class="pc background-video">
{{- section.settings.video | video_tag: autoplay: true, playsinline: true, muted: true, loop: true -}}
</native-video>
{% elsif section.settings.img != blank %}
<img
src="{{ section.settings.img | image_url: width:'1920x' }}"
alt="{{ section.settings.img.alt }}"
class="pc background-image"
>
{% endif %}
{% if section.settings.m_video != blank %}
<native-video class="mobile background-video">
{{- section.settings.m_video | video_tag: autoplay: true, playsinline: true, muted: true, loop: true -}}
</native-video>
{% elsif section.settings.img_mobile != blank %}
<img
src="{{ section.settings.img_mobile | image_url: width:'750x' }}"
alt="{{ section.settings.img_mobile.alt }}"
class="mobile background-image"
>
{% endif %}
<div class="video-overlay"></div>
</div>
<div class="bottom-text animate-text my_h_14 font-weight-400">{{ section.settings.content }}</div>
</div>
<div class="zoom-inner-top">
{% if section.settings.img_1 != blank %}
<img
src="{{ section.settings.img_1 | image_url: width:'1920x' }}"
alt="{{ section.settings.img_1.alt }}"
class="pc background-image"
>
{% endif %}
{% if section.settings.img_mobile_1 != blank %}
<img
src="{{ section.settings.img_mobile_1 | image_url: width:'750x' }}"
alt="{{ section.settings.img_mobile_1.alt }}"
loading="lazy"
class="mobile background-image"
>
{% endif %}
</div>
<!-- 第二屏:白底大字 -->
<div class="videoContainer_middle">
<h2 class="zoom-title page-width">{{ section.settings.title }}</h2>
</div>
<div class="zoom-inner-bottom page-width">
<div class="zoom-inner-bottom-inner">
{% if section.settings.title-text != blank %}
<div class="zoom-inner-bottom-title">{{ section.settings.title-text }}</div>
{% endif %}
{% if section.settings.text != blank %}
<div class="zoom-inner-bottom-text text-text">{{ section.settings.text }}</div>
{% endif %}
{% if section.settings.svg != blank %}
<div class="zoom-inner-bottom-svg">{{ section.settings.svg }}</div>
{% endif %}
{% if section.settings.btn_link != blank and section.settings.btn_text != blank %}
<a href="{{ section.settings.btn_link }}" class="zoom-inner-bottom-btn btn_2">{{ section.settings.btn_text }}</a>
{% endif %}
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
gsap.registerPlugin(ScrollTrigger);
// 初始状态
gsap.set('.videoContainer_top', { opacity: 1 });
gsap.set('.video-wrapper', { width: 1920, height: 911 });
gsap.set('.top-text', { y: -96, maxHeight: 0, overflow: 'hidden' });
gsap.set('.bottom-text', { y: 206, maxHeight: 0, overflow: 'hidden' });
// gsap.set('.videoContainer_middle', { opacity: 0, scale: 3.5 }); // 第二屏初始很大并隐藏
gsap.set('.videoContainer_middle', { opacity: 0, scale: 7 });
// 主时间轴
let masterTl = gsap.timeline({ defaults: { ease: 'power2.inOut' } });
// 第一屏保持显示
masterTl.to('.videoContainer_top', { opacity: 1, ease: 'none' });
// 第二屏出现
masterTl.to('.videoContainer_middle', {
opacity: 1,
scale: 1,
duration: 1.2,
ease: 'power2.out'
});
// 在上一个动画进行到 70% 时开始文字动画
masterTl.to('.zoom-title', {
y: '-30%',
duration: 1,
ease: 'power2.inOut'
}, '-=0.7'); // 1.2 * 0.7 = 0.84,减掉0.36意味着动画在前一个动画进行70%左右时开始
// 图片从上往下进入
masterTl.fromTo('.zoom-inner-top',
{ yPercent: -100, xPercent: -50, opacity: 0 },
{ yPercent: 0, xPercent: -50, opacity: 1, duration: 1, ease: 'power2.out' },
'<'
);
// 底部文字从下往上进入
masterTl.fromTo('.zoom-inner-bottom',
{ y: '100%', opacity: 0 },
{ y: '0%', opacity: 1, duration: 1, ease: 'power2.out' },
'<' // 和上一个动画同时执行
);
// 第一屏上下文字展开
masterTl.to(
'.top-text',
{
y: 0,
maxHeight: 1000,
onStart: () => {
gsap.set('.top-text', { overflow: 'visible' });
},
},
'<'
);
masterTl.to(
'.bottom-text',
{
y: 0,
maxHeight: 1000,
onStart: () => {
gsap.set('.bottom-text', { overflow: 'visible' });
},
},
'<'
);
// 计算 ScrollTrigger 结束位置
function calculateEndOffset() {
const totalDuration = masterTl.totalDuration();
return totalDuration * 1000;
}
let scrollTrigger;
function initScrollTrigger() {
if (scrollTrigger) {
scrollTrigger.kill();
masterTl.progress(0).pause();
}
scrollTrigger = ScrollTrigger.create({
trigger: '#nebula-scroll-section',
start: 'top top',
end: `+=${calculateEndOffset()}`,
scrub: true,
pin: true,
animation: masterTl,
invalidateOnRefresh: true,
});
}
initScrollTrigger();
// resize 防抖
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
initScrollTrigger();
ScrollTrigger.refresh();
}, 200);
});
// 动态内容变化监听
const nebulaSection = document.getElementById('nebula-scroll-section');
if (nebulaSection) {
new ResizeObserver(() => {
initScrollTrigger();
ScrollTrigger.refresh();
}).observe(nebulaSection);
}
});
</script>