用 GSAP + ScrollTrigger 打造沉浸式视频滚动动画

页面打开时先播放一段全屏视频,随着用户下滑,文字逐渐浮现,紧接着大标题和图片内容缓缓登场,视差滚动效果可以提升用户体验和视觉冲击力的。

这个效果主要通过以下步骤实现:

  1. 初始设置:使用GSAP设置元素的初始状态(位置、透明度等)

  2. 创建时间轴:使用GSAP Timeline定义动画序列和时序

  3. 滚动触发:使用ScrollTrigger将动画与页面滚动绑定

  4. 动画序列

    • 顶部文字向下移动进入视图

    • 底部文字向上移动并渐显

    • 缩放区域逐渐显示并缩小到正常大小

    • 标题向上移动创造视觉冲击

核心在于 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>