滚动控制视频播放是如何实现的?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>
  );
}
相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax