GSAP ScrollTriggerを使う(HTML/JS)

前書き

GSAPはGreenSock社が開発しているJavaScriptライブラリです。
アニメーションを設定するのにとても便利です。
その使い方を掲載しています。

目次

  1. 準備
  2. Tween
  3. Timeline
  4. ScrollTrigger
  5. 一度だけ
  6. 繰り返し
  7. スクロール量に応じて動かす
  8. ピン留め
  9. クラスを付ける
  10. activeになった時に何かする
  11. ScrollTriggerの活用例
  12. スクロールする度に何かする
  13. 複数箇所を同じ様に動かす
  14. 複数箇所を同じ様に動かす__batch
  15. 横にスクロールさせる
  16. 順番にスクロールさせる
  17. 異なる速度でスクロールさせる(パララックス)
  18. スクロールしたらパッと切り替える
  19. スクロールしたらパッと切り替える__observe
  20. 奥にスクロールしている様に見せる
  21. 参考

準備

こちらのページからダウンロード出来ます。

GSAP Installation

当ページでは、3つのファイルを読み込んでいます。


import { gsap } from './gsap_3.10.4/esm/gsap-core.js';
import { CSSPlugin } from './gsap_3.10.4/esm/CSSPlugin.js';
import { ScrollTrigger } from './gsap_3.10.4/esm/ScrollTrigger.js';

gsap.registerPlugin(ScrollTrigger);

Tween

GSAPの基本の動作です。アニメーションの設定が出来ます。

Tween

動作後の値を指定__to()

再開

gsap.to(".a01", { rotation: 225, x: 100, duration: 1 });

動作前の値を指定__from()

再開

gsap.from(".a02", { rotation: 225, x: 100, duration: 1 });

動作前後の値を指定__fromTo()

再開

gsap.fromTo(
  ".a03",
  { rotation: 225, x: 0 },
  { rotation: 0, x: 100, duration: 1 },
);

動作無し__set()


gsap.set(".a04", { rotation: 225, x: 100 });

連続して同じ様に動かす__stagger

再開

<div class="b01"></div>
<div class="b01"></div>
<div class="b01"></div>
<div class="b01"></div>
<div class="b01"></div>

gsap.from( ".b01", {
  scale: 0.1,
  rotation: 180,
  duration: 0.5,
  stagger: 0.25 //0.25秒おきに
});

ヨーヨーの様に動かす__yoyo


gsap.from( ".b02", {
  x: 100,
  rotation: 225,
  duration: 1,
  repeat: -1, //ずっと繰り返す
  yoyo: true,
});

Timeline

アニメーションを繋げることが出来ます。動作のタイミングを柔軟に調整出来ます。

Timeline

アニメーションを繋げる

再開

const tl = gsap.timeline();
const left = document.querySelector(".tl01_left");
const right = document.querySelector(".tl01_right");

tl.from(left, { scale: 0.1, duration: 0.5 })
  .from(right, { scale: 0.1, duration: 0.5 })
  .to(left, { rotation: 180, duration: 0.5 }, "+=0.5") //0.5秒後
  .to(right, { rotation: 180, duration: 0.5 }, "<" ); //上のアニメーションと同時

ScrollTrigger

スクロールイベントを制御する為のプラグインです。
markers属性をtrueにすれば、画面右側に目印が表示するので、発火位置の調整がし易いです。

ScrollTrigger

一度だけ


gsap.to(".st01", {
  x: 100,
  rotation: 225,
  duration: 1,
  scrollTrigger: {
    trigger: '.st01',
    start: 'top 75%', //triggerの上端が、画面の高さの75%の位置に来たら
  }
});

繰り返し__toggleActions

この箇所でマーカーを利用しています。


gsap.to(".st02", {
  x: 100,
  rotation: 225,
  duration: 1,
  scrollTrigger: {
    trigger: '.st02',
    start: 'top 75%', //triggerの上端が、画面の高さの75%の位置に来たら
    end: 'bottom 25%', //triggerの下端が、画面の高さの25%の位置に来たら
    toggleActions: "play reverse play reverse", //繰り返し onEnter, onLeave, onEnterBack, onLeaveBack,
    markers: true, // マーカー表示
  }
});

スクロール量に応じて動かす__scrub


gsap.to(".st03", {
  x: 100,
  rotation: 225,
  scrollTrigger: {
    trigger: '.st03',
    start: 'top 75%',
    end: 'bottom 25%',
    scrub: true, //スクロール量に応じて動かす
  }
});

ピン留め__pin


gsap.to(".st04", {
  x: 100,
  rotation: 225,
  scrollTrigger: {
    trigger: '.st04',
    start: 'top 60%',
    end: "+=200", //start + 200px
    scrub: true, //スクロール量に応じて動かす
    pin: true, //ピン留め
  }
});

クラスを付ける__toggleClass


.active{
  background: #ff0000;
}

ScrollTrigger.create({
  trigger: ".st05",
  start: 'top 75%',
  end: 'bottom 25%',
  toggleClass: 'active',
});

activeになった時に何かする__onToggle


.active{
  background: #ff0000;
}

ScrollTrigger.create({
  trigger: ".st06",
  start: 'top 75%',
  end: 'bottom 25%',
  onToggle: (self) => {
    if(self.isActive){
      self.trigger.classList.add("active");
    }else{
      self.trigger.classList.remove("active");
    }
  },
});

ScrollTriggerの活用例

下記ページにあるサンプルを参考にしています。

ScrollTrigger Demos

スクロールする度に何かする__onUpdate


const tl = gsap.timeline();

ScrollTrigger.create({
  trigger: ".st07",
  start: 'top 75%',
  end: 'bottom 25%',
  onUpdate: (self) => {
    if(!tl.isActive()){ //タイムラインがアクティブでない時
      const skewDeg = self.direction * 20 + "deg"; // direction(スクロール方向)は、1 か -1
      tl.to(self.trigger, {skewY : skewDeg, duration: 0.5})
        .to(self.trigger, {skewY : "0deg" , duration: 0.5});
    }
  },
});

複数箇所を同じ様に動かす


document.querySelectorAll(".fdin_up").forEach((item) => {
  gsap.from(
    item,
    {
      y: 10,
      opacity: 0,
      duration: 1,
      scrollTrigger: {
        trigger: item,
        start: 'top 75%',
        toggleActions: "play none none reverse", //繰り返し onEnter, onLeave, onEnterBack, onLeaveBack,
      },
    },
  );
});

複数箇所を同じ様に動かす__batch


<div class="wrap60">
  <div class="item60"></div>
  <!-- 中略 -->
</div>

.wrap60{
  display: flex; flex-wrap: wrap; gap: 50px 4%;
}
.item60
  width: calc( ( 100 - 8) / 3 * 1% ); height: 2em; background: #346c9c;
  @media screen and (min-width: 768px)
    width: calc( ( 100 - 12) / 4 * 1% );
  }
}

gsap.set(".item60", { y: 10, opacity: 0, });

ScrollTrigger.batch(".item60", {
  batchMax: 4,
  onEnter: batch => gsap.to( batch, { y: 0, opacity: 1, stagger: 0.15, }),
  onLeaveBack: batch => gsap.to( batch, { y: 10, opacity: 0, stagger: 0.15, }),
  start: 'top 75%',
  end: 'bottom 25%',
});

横にスクロールさせる

1
2
3

<div class="area10">
  <div class="wrap10">
    <div class="pn10 pn11"><span>1</span></div>
    <div class="pn10 pn12"><span>2</span></div>
    <div class="pn10 pn13"><span>3</span></div>
  </div>
</div>

.area10{
  overflow: hidden;
}
.wrap10{
  display: flex;
}
.pn10{
  height: 100vh;
}
.pn11{ background: #346c9c; }
.pn12{ background: #b1c3cb; }
.pn13{ background: #595757; }

const area = document.querySelector(".area10");
const wrap = document.querySelector(".wrap10");
const panels = document.querySelectorAll(".pn10");
const num = panels.length;

gsap.set(wrap, { width: num * 100 + "%" });
gsap.set(panels, { width: 100 / num + "%" });

gsap.to(panels, {
  xPercent: -100 * ( num - 1 ),
  ease: "none",
  scrollTrigger: {
    trigger: area,
    start: "top top",
    end: "+=100%",
    scrub: 0.5, //スクロール量に応じて動かす
    pin: true, //ピン留め
    snap: { //キリの良い位置へ移動させる
      snapTo: 1 / ( num - 1 ),
      duration: 0.5,
    },
  }
});

順番にスクロールさせる

1
2
3

<div class="area20">
  <div class="wrap20">
    <div class="pn20 pn21"><span>1</span></div>
    <div class="pn20 pn22"><span>2</span></div>
    <div class="pn20 pn23"><span>3</span></div>
  </div>
</div>

.wrap20{
  position: relative; height: 50vh;
}
.pn20{
  position: absolute; top:0; width: 40%; height: 100%; margin: auto;
}
.pn21{ background: #346c9c; left: 0; }
.pn22{ background: #b1c3cb; left: 0; right: 0; }
.pn23{ background: #595757; right: 0; }

const area = document.querySelector(".area20");
const areaH = area.offsetHeight;
const panels = document.querySelectorAll(".pn20");
const num = panels.length;

panels.forEach((panel, i) => {
  gsap.set(panel, {
    zIndex : num - i,
  });
});

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: area,
    start: "top 25%",
    end: "+=100%",
    scrub: true,
    pin: true,
  }
});

tl.to(".pn21", { y: -areaH * 1.5, duration: 1 })
  .to(".pn22", { y: -areaH * 1.5, duration: 1 })

異なる速度でスクロールさせる(パララックス)

1
2
3

<div class="area30">
  <div class="wrap30">
    <div class="pn30 pn31"><span>1</span></div>
    <div class="pn30 pn32"><span>2</span></div>
    <div class="pn30 pn33"><span>3</span></div>
  </div>
</div>

.wrap30{
  position: relative; padding: 30em 0 0;
}
.pn30{
  position: absolute; top: 0; bottom: 0; width: 30%; height: 4em; margin: auto;
}
.pn31{ background: #346c9c; left: 2%; }
.pn32{ background: #b1c3cb; left: 0; right: 0; }
.pn33{ background: #595757; right: 2%;  }

const area = document.querySelector(".area30");

gsap.fromTo(".pn32",
  {
    yPercent: 200,
  },
  {
    yPercent: -200,
    ease: "none",
    scrollTrigger: {
      trigger: area,
      start: "top bottom",
      end: "bottom top",
      scrub: true, //動作を遅らせない
    },
  }
);

gsap.fromTo(".pn33",
  {
    yPercent: 400,
  },
  {
    yPercent: -400,
    ease: "none",
    scrollTrigger: {
      trigger: area,
      start: "top bottom",
      end: "bottom top",
      scrub: 1, //動作を遅らせる
    },
  }
);

スクロールしたらパッと切り替える

1
2
3
4
5
6

<div class="area40">
  <div class="wrap40">
    <div class="pn40 pn41"><span>1</span></div>
    <div class="pn40 pn42"><span>2</span></div>
    <div class="pn40 pn43"><span>3</span></div>
    <div class="pn40 pn44"><span>4</span></div>
    <div class="pn40 pn45"><span>5</span></div>
    <div class="pn40 pn46"><span>6</span></div>
  </div>
</div>

.wrap40{
  position: relative; height: 50vh;
}
.pn40{
  display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
.active40{
  display: flex;
}
.pn41,.pn44{ background: #346c9c; }
.pn42,.pn45{ background: #b1c3cb; }
.pn43,.pn46{ background: #595757; }

const area = document.querySelector(".area40");
const panels = document.querySelectorAll(".pn40");
const length = panels.length;

const itemSwitch = (progress) => {
  const snapVal = gsap.utils.snap(1, progress * length - 0.5); //0 , 1 , 2...length
  const clampVal = gsap.utils.clamp(0, length - 1 , snapVal); //0 ~ length -1 の間に収める
  panels.forEach((item, index) => {
    if( index == clampVal){
      item.classList.add("active40");
    }else{
      item.classList.remove("active40");
    }
  });
}
itemSwitch(0);

ScrollTrigger.create({
  trigger: area,
  start: "top 25%",
  end: "+=100%",
  scrub: true,
  pin: true,
  onUpdate: (self) => itemSwitch(self.progress),
  onLeaveBack: () => itemSwitch(0),
  onLeave: () => itemSwitch(1),
});

スクロールしたらパッと切り替える__observe

1
2
3
4
5
6

<div class="area80">
  <div class="wrap80">
    <div class="pn80 pn81"><span>1</span></div>
    <div class="pn80 pn82"><span>2</span></div>
    <div class="pn80 pn83"><span>3</span></div>
    <div class="pn80 pn84"><span>4</span></div>
    <div class="pn80 pn85"><span>5</span></div>
    <div class="pn80 pn86"><span>6</span></div>
  </div>
</div>

.wrap80{
  position: relative; height: 50vh;
}
.pn80{
  display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
.active80{
  display: flex;
}
.pn81,.pn84{ background: #346c9c; }
.pn82,.pn85{ background: #b1c3cb; }
.pn83,.pn86{ background: #595757; }

const area = document.querySelector(".area80");
const panels = document.querySelectorAll(".pn80");
const length = panels.length;
let isObserve = false;
let current = 0;

const itemSwitch = (num) => {
  if(num >= 0 && num < length){
    panels.forEach((item, index) => {
      if( index == num){
        item.classList.add("active80");
      }else{
        item.classList.remove("active80");
      }
      current = num;
    });
  }
}
itemSwitch(current);

ScrollTrigger.create({
  trigger: area,
  start: "top 25%",
  end: "+=700px",
  pin: true,
  onUpdate: (self) => self.isActive ? isObserve = true : isObserve = false,
  onLeaveBack: () => itemSwitch(0),
  onLeave: () => itemSwitch(length - 1),
});

ScrollTrigger.observe({
  type: "wheel,touch,scroll,pointer",
  tolerance: 100,
  wheelSpeed: -1,
  scrollSpeed: -1,
  onDown: () => isObserve && itemSwitch(current - 1),
  onUp: () => isObserve && itemSwitch(current + 1),
});

奥にスクロールしている様に見せる

1
2
3

<div class="area50">
  <div class="wrap50">
    <div class="waku50"></div>
    <div class="pn50 pn51"><span>1</span></div>
    <div class="pn50 pn52"><span>2</span></div>
    <div class="pn50 pn53"><span>3</span></div>
  </div>
</div>

.area50{
  overflow: hidden;
}
.wrap50{
  position: relative; height: 100vh;
}
.waku50{
  height: 100%; border: 10px solid #333; box-shadow: inset 0 0 5px 5px #aaa;
}
.pn50{
  position: absolute;
}
.pn51{ background: #346c9c; }
.pn52{ background: #b1c3cb; }
.pn53{ background: #595757; }

const area = document.querySelector(".area50");
const panels = document.querySelectorAll(".pn50");
const num = panels.length;

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: area,
    start: "top top",
    end: "+=150%",
    scrub: true,
    pin: true,
  }
});

panels.forEach((panel, i) => {
  gsap.set(panel, {
    zIndex : num - i,
  });
});

gsap.set(".waku50", { scale: 0.9, });
gsap.set(".pn51", { scale: 0, width: "75%", height: "50%", left: "12.5%", top: "25%", });
gsap.set(".pn52", { scale: 0, width: "75%", height: "50%", left: "12.5%", top: "25%", });
gsap.set(".pn53", { scale: 0, width: "100%", height: "75%", left: 0, top: "12.5%", });

tl.to(".waku50", { scale: 1.2, duration: 0.5})
  .to(".pn51", { scale: 1, left: "-37.5%", top: "5%", duration: 1 },"-=0.5")
  .to(".pn51", { opacity: 0, duration: 0.2 }, "-=0.2")
  .to(".pn52", { scale: 1, left: "62.5%", top: "55%", duration: 1 }, "-=0.5")
  .to(".pn52", { opacity: 0, duration: 0.2 }, "-=0.2")
  .to(".pn53", { scale: 1, duration: 1 }, "-=0.5")

参考

DOCS

ScrollTrigger Demos

関連記事

文字に簡単な動きをつける(HTML/CSS/JS)

Intersection Observer APIを使う(JavaScript)

固定ヘッダの高さ分、スクロール位置をずらす(HTML/CSS)

先頭に戻る
ページの先頭に戻る