滚动控制视频播放是如何实现的?GSAP ScrollTrigger + seek 实践 vivo官网案例

在 vivo、Apple、Tesla 等官网中,经常能看到这样一种效果:

www.vivo.com.cn/vivo/iqoo15...

页面向下滚动,视频一点点向前播放

页面向上回滚,视频又一点点倒退

视频没播放完,页面无法继续向下滚动

这种效果看起来像「视频跟着滚动播放」,但本质并不是播放,而是 seek

本文将会复刻这个效果,完整拆解 滚动控制视频播放的实现方式 ,以及 GSAP 在其中到底发挥了什么作用


一、这类效果本质上是在做什么?

先说结论:

滚动控制视频 ≠ 播放视频
滚动控制视频 = 用滚动不断 seek 视频

在 HTML5 中:video.currentTime = 3.2;

这一次赋值操作,就叫 seek ------ 也就是把视频播放头强制跳转到某个时间点。

而滚动驱动视频时,代码会不断执行:

scroll → currentTime → currentTime → currentTime

也就是说:滚动控制视频是一个"高频 seek"的极端使用场景


二、一次 seek,浏览器内部发生了什么?

当你设置:

js 复制代码
video.currentTime = 4.5;

浏览器并不是"立刻显示第 4.5 秒那一帧",而是要经历:

  1. 找到离 4.5 秒最近的关键帧(I-frame)
  2. 从关键帧开始解码
  3. 解码中间的 P / B 帧
  4. 渲染当前画面

这也是为什么:

  • 关键帧越少,seek 越慢
  • 滚动越频繁,视频越容易抖

三、GSAP 的代码到底有没有用到 seek?

答案是:有,而且用得非常多。

来看一段最常见的 GSAP + ScrollTrigger 写法:

js 复制代码
gsap.registerPlugin(ScrollTrigger);

const video = document.querySelector("video");
video.pause();

video.addEventListener("loadedmetadata", () => {
  gsap.to(video, {
    currentTime: video.duration,
    ease: "none",
    scrollTrigger: {
      trigger: ".scroll-video",
      start: "top top",
      end: "bottom bottom",
      scrub: true,
      pin: true
    }
  });
});

这段代码并没有绕开 seek,它做的事情是:

arduino 复制代码
ScrollTrigger 监听滚动
↓
计算滚动进度(0 ~ 1)
↓
GSAP 根据进度更新 currentTime
↓
video.currentTime 被不断赋值(seek)

所以可以明确地说:

GSAP 并没有避免 seek,而是系统性地管理了 seek


四、GSAP 真正解决的是什么问题?

如果不用 GSAP,很多人会写成这样:

ini 复制代码
window.addEventListener("scroll", () => {
  video.currentTime = calcByScroll();
});

这种写法的问题在于:

  • scroll 触发频率不可控
  • 同一帧内可能多次 seek
  • 惯性滚动、回弹难处理
  • iOS / Mac 触控板体验差

GSAP ScrollTrigger 的核心价值

1️⃣ 把滚动抽象成"稳定的进度"

GSAP 把滚动统一抽象成:

progress = 0 → 1

开发者不再关心 scrollTop、offset、临界点误差。


2️⃣ 用 requestAnimationFrame 控制更新频率

GSAP 内部保证:

一帧最多更新一次 currentTime

避免了"seek 风暴"。


3️⃣ scrub 实现正反向严格同步

vbnet 复制代码
scrub: true

意味着:

  • 向下滚 → 视频前进
  • 向上滚 → 视频回退
  • 停止滚动 → 视频停在当前帧

这是纯原生 scroll 非常难稳定实现的。


4️⃣ pin 实现滚动锁定

pin: true

视频播放完之前,当前 section 会被固定:

自然实现「视频没播完,页面不能继续滚」


五、为什么视频编码仍然是关键?

GSAP 只能决定 什么时候 seek

seek 快不快、准不准,取决于视频本身。

官网级方案通常会在制作阶段:

diff 复制代码
-g 1

让视频 每一帧都是关键帧,以保证:

  • 高频 seek 不回退
  • 滚动和画面严格同步

这也是为什么滚动视频效果往往需要配合 ffmpeg 做素材处理。


六、一个总结模型

可以用一句话总结整套方案:

滚动控制视频不是在播放视频,而是在用滚动驱动 seek。
GSAP 的作用不是消灭 seek,而是让 seek 变得可控、稳定,并与滚动严格同步。


七、结语

  • ✅ 滚动视频效果一定会用到 seek
  • ✅ GSAP 解决的是"节奏"和"同步"问题
  • ❌ GSAP 无法替代视频编码优化
  • 🔥 官网级体验 = GSAP + 合理滚动距离 + 正确的视频编码

最后代码贴在这里,复刻了iQOO官网页面中的两个视频滚动,css就不贴了。

js 复制代码
//子组件
'use client';

import { useEffect, useRef, useId } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

if (typeof window !== 'undefined') {
  gsap.registerPlugin(ScrollTrigger);
}

interface ScrollVideoProps {
  videoSrc: string;
  duration: number;
  className?: string;
}

export default function ScrollVideo({ videoSrc, duration, className = '' }: ScrollVideoProps) {
  const sectionRef = useRef<HTMLElement>(null);
  const videoRef = useRef<HTMLVideoElement>(null);
  const triggerId = useId();

  // 8px per frame, 60fps
  const scrollHeight = duration * 60 * 8;

  useEffect(() => {
    const video = videoRef.current;
    const section = sectionRef.current;
    if (!video || !section) return;

    video.pause();
    video.currentTime = 0;

    let st: ScrollTrigger | null = null;

    const handleLoadedMetadata = () => {
      st?.kill();

      st = ScrollTrigger.create({
        trigger: section,
        start: 'top top',
        end: 'bottom bottom',
        scrub: 0.2,
        id: triggerId,
        onUpdate: (self) => {
          if (video.readyState >= 1) {
            video.currentTime = self.progress * video.duration;
          }
        },
      });
    };

    if (video.readyState >= 1) {
      handleLoadedMetadata();
    } else {
      video.addEventListener('loadedmetadata', handleLoadedMetadata);
    }

    return () => {
      video.removeEventListener('loadedmetadata', handleLoadedMetadata);
      st?.kill();
    };
  }, [triggerId, duration]);

  return (
    <section
      ref={sectionRef}
      className={`scroll-video ${className}`}
      style={{ height: `${scrollHeight}px` }}
    >
      <div className="sticky top-0 h-screen w-full overflow-hidden">
        <video
          ref={videoRef}
          muted
          playsInline
          preload="auto"
          className="w-full h-full object-cover pointer-events-none"
        >
          <source src={videoSrc} type="video/webm" />
        </video>
      </div>
    </section>
  );
}


//父组件
import ScrollVideo from '@/components/ScrollVideo';

export default function Home() {
  return (
    <main className="bg-black">
      {/* Hero Section */}
      <section className="h-screen flex items-center justify-center">
        <div className="text-center text-white">
          <h1 className="text-5xl md:text-7xl font-bold mb-6">
            企业官网
          </h1>
          <p className="text-xl md:text-2xl text-gray-300 mb-8">
            向下滚动体验视频效果
          </p>
          <div className="animate-bounce">
            <svg 
              className="w-8 h-8 mx-auto text-white" 
              fill="none" 
              stroke="currentColor" 
              viewBox="0 0 24 24"
            >
              <path 
                strokeLinecap="round" 
                strokeLinejoin="round" 
                strokeWidth={2} 
                d="M19 14l-7 7m0 0l-7-7m7 7V3" 
              />
            </svg>
          </div>
        </div>
      </section>

      {/* Scroll Video 1 - 8秒 */}
      <ScrollVideo 
        videoSrc="/video.webm"
        duration={8}
        className="bg-black"
      />

      {/* Scroll Video 2 - 4秒 */}
      <ScrollVideo 
        videoSrc="/performance.webm"
        duration={4}
        className="bg-black"
      />

      {/* Content Section */}
      <section className="min-h-screen flex items-center justify-center bg-gradient-to-b from-black to-gray-900">
        <div className="text-center text-white max-w-4xl px-6">
          <h2 className="text-4xl md:text-5xl font-bold mb-8">
            创新科技
          </h2>
          <p className="text-lg md:text-xl text-gray-300 leading-relaxed">
            我们致力于为用户提供最前沿的技术体验,
            通过不断创新,打造卓越的产品与服务。
          </p>
        </div>
      </section>

      {/* Footer */}
      <footer className="py-12 bg-gray-900 text-center text-gray-400">
        <p>© 2025 企业官网. All rights reserved.</p>
      </footer>
    </main>
  );
}
相关推荐
一念之间lq2 小时前
Elpis 第四阶段· Vue3 完成动态组件建设
前端·vue.js
用户636836608552 小时前
前端使用nuxt.js的seo优化
前端
OldBirds2 小时前
烧脑时刻:Dart 中异步生成器与流
前端·后端
湛海不过深蓝2 小时前
【echarts】折线图颜色分段设置不同颜色
前端·javascript·echarts
昨晚我输给了一辆AE862 小时前
关于 react-hook-form 的 isValid 在有些场景下的值总是 false 问题
前端·react.js
xinyu_Jina2 小时前
Calculator Game:WebAssembly在计算密集型组合优化中的性能优势
前端·ui·性能优化
JustHappy2 小时前
「2025年终个人总结」🤬🤬回答我!你个菜鸟程序员这一年发生了啥?
前端
啃火龙果的兔子2 小时前
可以指定端口启动本地前端的npm包
前端·npm·node.js
new code Boy2 小时前
前端base-64 编码解码
前端·javascript·html