WEB STUDIES

もっともっと、もっと先へ。株式会社アットワークMore and more,
further and further.

Tips

2026年2月27日

スクロールに連動して動画(連番ファイル)を再生する方法(GSAP)

Appleのサイトなどでよく見かけるのギミックですが、GSAPで再現してみました。
GSAP(GreenSock Animation Platform)は、高性能かつ制御性の高いJavaScriptアニメーションライブラリで、DOM / SVG / Canvas / WebGL など幅広い描画対象を扱えます。動画を連番の.jpgに落とし込んでスクロール量に比例してシーケンスファイルを順に描画しています
CSS Transitionsなどウェブ標準にも選択肢があるなかで、GSAPのメリットはどのようなところにあるのでしょうか?

  • CSS Transitions/AnimationsやWeb Animations APIより、GSAPのほうが制御の自由度が大きい。
  • 連続したモーションの管理に、GSAPが役立つ。
  • HTMLのDOMだけではなく、WebGL/Canvasの実装でもGSAPが利用できる

CSS Transitionsだけで実現できるような演出ではGSAPを使う必要はありません。また、BootstrapやMaterial DesignなどのCSSフレームワーク・デザインシステムにモーションが組み込まれているケースでも、GSAPは不要です。

「特別なウェブサイトを目指したい」「演出にこだわりたい」「オリジナルの動きを作りたい」といったケースでGSAPが役立ちます。
デモ

https://atwork.co.jp/gsap

HTML
<section>
    <h1>Scroll Down</h1>
  </section>
  <section class="sequence">
    <canvas id="sequence-canvas"></canvas>
  </section>
  <section>
    <h1>Next Section</h1>
  </section>
CSS

body {
margin: 0;
background: #fff;
color: #333;
font-family: sans-serif;
}

section {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.sequence {
  position: relative;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
}

canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
GSAP
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
Javascript
gsap.registerPlugin(ScrollTrigger);

    const frameCount = 100;
    const images = [];
    const canvas = document.getElementById("sequence-canvas");
    const context = canvas.getContext("2d");

    // 画像ファイルのパスを指定(例: seq_0001.jpg ~ seq_0100.jpg)
    const currentFrame = index => `/images/seq_${String(index + 1).padStart(3, '0')}.jpg`;

    // Canvasサイズを画面にフィット
    function resizeCanvas() {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      render();
    }

    window.addEventListener("resize", resizeCanvas);

    // 画像をプリロード
    for (let i = 0; i < frameCount; i++) {
      const img = new Image();
      img.src = currentFrame(i);
      images.push(img);
    }

    const imageSeq = { frame: 0 };

    gsap.to(imageSeq, {
      frame: frameCount - 1,
      snap: "frame",
      ease: "none",
      scrollTrigger: {
        trigger: ".sequence",
        start: "top top",
        end: "+=3000", // 再生に必要なスクロール量
        scrub: true,
        pin: true,
      },
      onUpdate: render
    });

    images[0].onload = render;

    function render() {
      const img = images[imageSeq.frame];
      if (!img) return;

      const scale = Math.max(canvas.width / img.width, canvas.height / img.height);
      const x = (canvas.width / 2) - (img.width / 2) * scale;
      const y = (canvas.height / 2) - (img.height / 2) * scale;
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.drawImage(img, x, y, img.width * scale, img.height * scale);
    }

    resizeCanvas();

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です


一覧に戻る