GSAP の ScrollTrigger.batch
を使えば、リストなどに簡単にアニメーションを設定することができる。
https://gsap.com/docs/v3/Plugins/ScrollTrigger/static.batch()/
batch でアニメーションを設定したときに、アニメーションが実行された要素に class を付けたいとか、要素に何か処理をしたい要望があったのでそのメモ
- TypeScript:
^5.5.4
- GSAP:
^3.12.5
ScrollTrigger.batch を使ったアニメーションの基本
cf. static-batch | GSAP | Docs & Learning
import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(ScrollTrigger); // アニメーションさせる要素の表示を初期化 gsap.set(".listItem", { autoAlpha: 0, y: 50 }); ScrollTrigger.batch('.listItem', { interval: 0.1, // バッチ処理が行われるまでの時間 (秒) start: "top-=200 center", // ターゲットの top - 200px に画面の中央が来たら開始 end: "bottom+=50 center" // ターゲットの bottom + 50px に画面の中央が来たら終了 once: true, // 一度だけ実行する markers: true, // debug 用に start, end のガイドを表示させる onEnter: (elements) => { // onEnter 内でアニメーションの設定をする gsap.to(elements, { stagger: 0.5, // 同じバッチ内でアニメーションが実行されたときの時差 autoAlpha: 1, y: 0, }); }, });
対象要素に class 名を付けたいだけなら attr
が便利
例えばアニメーションが始まった際に isLoaded
クラスを付け、data-status="loaded"
にするなら下記のような感じ
ScrollTrigger.batch('.listItem', { interval: 0.1, start: "top-=200 center", end: "bottom+=50 center" once: true, markers: true, onEnter: (elements) => { gsap.to(elements, { stagger: 0.5, autoAlpha: 1, y: 0, // attr は完全に置換するので元のクラス名を残す必要があるなら書いておく attr: { class: ".listItem .isLoaded", "data-status": "loaded" } }); }, });
⚠️ attr
の注意点
- attribute をまるっと置換してしまうので、必要なものは記述する必要があり HTML と密結合になってしまう
attr
は trigger になっている要素しか対象にしないので、子要素や親要素になにかしたい場合は対応できない (CSSで対応できる範囲でしか操作できない)- 文字列を返す関数を設置できたが、ドキュメントには書かれてないので動作は担保されてなさそう
ScrollTrigger.batch('.listItem', { // 略 onEnter: (elements) => { gsap.to(elements, { stagger: 0.5, autoAlpha: 1, y: 0, attr: { class: (_index, item) => { const cx = item.classList.value; return cx + ' isLoaded'; }, }, }); }, });
↑ これも動作したが、ドキュメントにこの方法は載ってないっぽいので動作保証はなさそう
🙆 1度だけ対象要素以外も操作するなら onEnter
内の gsap.to
内にイベントを設定するのが良さそう
後述するが、onToggle
は start, end を跨ぐ度に実行され、onLeave
はスクロール速度では発火しないケースがあった。
onEnter
さえすればアニメーションは発火するので、その中で操作する処理を書いてしまうのが確実かなという印象
ScrollTrigger.batch('.listItem', { // 略 onEnter: (elements) => { gsap.to(elements, { stagger: 0.5, autoAlpha: 1, y: 0, // attr は置換で扱いづらいので使わない // attr: { class: ".listItem .isLoaded", "data-status": "loaded" } }, onStart: (index, item) => { // アニメーションが開始された時点で実行される item?.classList.add('isLoaded'); item?.dataset.status = 'loaded'; const parent = item?.parentElement; parent?.classList.add('isChildLoaded'); }, onLeave: (index, item) => { // gsap.to の設定的に end が無いので onStart 直後に発火するので使い分ける意味はほぼ無い }, onComplete: (arg: undefined) => { // バッチで実行されたアニメーションが全て完了したら呼び出されるが、引数が `undefined` なので使いづらい }, }); }, });
onComplete
onComplete
はバッチになっているアニメーションが全て完了したら呼び出されるので便利だが、引数が無いので扱いづらい
Arrow function ではなく通常の関数にすることで this
から対象を引っ張ってくることもできるが _target
というアンダースコア付きのプロパティなので、アンチパターンではないかと思う。(操作できるにはできる)
ScrollTrigger.batch('.listItem', { // 略 onEnter: (elements) => { gsap.to(elements, { stagger: 0.5, autoAlpha: 1, y: 0, // attr は置換で扱いづらいので使わない // attr: { class: ".listItem .isLoaded", "data-status": "loaded" } }, onStart: (index, item) => { // アニメーションが開始された時点で実行される item?.classList.add('isLoaded'); item?.dataset.status = 'loaded'; const parent = item?.parentElement; parent?.classList.add('isChildLoaded'); }, onComplete: function() { const self = this; const elemens = self?._targets; if ( !Array.isArray(elemens) ) { return; } elemens.forEach((el) => { if (!el || el instanceof HTMLElement === false) { return; } el.classList.add('isComplete'); el.dataset.status = 'complete'; }); } }); }, });
🤔 要素を操作したい場合は onToggle
が使える
アニメーションが始まった際に親要素に isLoaded
クラスを付け、子要素のdata-status
属性をを loaded
にするなら下記のような感じ
ただし、onToggle
は start, end を跨いだタイミングで実行されるので 1回だけ実行させたいようなケースでは工夫が必要
ScrollTrigger.batch('.listItem', { // 略 onEnter: (elements) => { // onEnter 内で各 .listItem のアニメーションを設定 gsap.to(elements, { stagger: 0.5, autoAlpha: 1, y: 0, }); }, onToggle: (elements) = > { // 今回のバッチで実行される要素の配列が引数として渡される elements.forEach((el) = > { const parent = el.parentElement; const child = el.firstElementChild; parent?.classList.add("isLoaded"); child?.dataset.status = "loaded"; }); }, });
⚠️ onToggle
の注意点
onToggle
は start, end をまたぐ度に実行されるので、1度だけ操作したい・終了時に操作したい場合は扱いに注意が必要once
のときスクロール速度が早いとonToggle
は実行されないケースがあったので確実性は微妙
🤔 対象からスクロールが出た際に何か実行するなら onLeave
公式ドキュメントに例が載っている onLeave
はバッチの対象から出る時に実行されるので、アニメーションが完了した後に要素に対して何か処理を行う際に使える
ScrollTrigger.batch('.listItem', { // 略 onEnter: (elements) => { // onEnter 内で各 .listItem のアニメーションを設定 gsap.to(elements, { stagger: 0.5, autoAlpha: 1, y: 0, }); }, onLeave: (elements) = > { // 対象外になったバッチに含まれる要素の配列が引数として渡される elements.forEach((el) = > { el.dataset.status = "leaved"; const parent = el.parentElement; parent?.classList.add("isLeaved"); }); }, });
⚠️ onLeave
の注意点
- スクロール速度によっては結構な確率で発火しない、感覚的に end の
onToggle
の方がまだ発火される可能性が高い気がする onEnter
からアニメーションの時間分 setTimeout させるほうが確実性はありそう
🙅 onStart
, onComplete
は ScrollTrigger.batch
にはない
Timeline .vars に onStart
, onComplete
の項目もあったが、ScrollTrigger.batch
では onStart
, onComplete
は発火しないっぽい (※ ScrollTrigger .vars のドキュメントが薄く vars の中身は同じだと思っていたが)
対象要素が複数なので、何をもって start なのか complete なのか判断が難しいので使えないのは分からなくもない
ScrollTrigger.batch('.listItem', { // 略 onEnter: (elements) => { // onEnter 内で各 .listItem のアニメーションを設定 gsap.to(elements, { stagger: 0.5, autoAlpha: 1, y: 0, }); }, // onStart, onComplete は発火ししなかった onStart: (evt: any) => { console.log('onStart', evt); }, onComplete: (evt: any) => { console.log('onComplete', evt); }, });
ScrollTrigger.batch でアニメーション実行時に要素に変更を加えたいときのまとめ
onEnter
内で実行するのが無難once: true
のとき、onToggle
,onLeave
はスクロール速度によっては発火されない場合があるonEnter
,onToggle
とonLeave
,onToggle
の順番は担保されないattr
,onToggle
の発火順も担保されてない
onToggle
は start 時と end 時に実行される- end になる前に start を逆方向にスクロールして跨いでも再度実行される
- class 名を追加したいだけなら
attr
が便利だが、class 名をまるっと置換してしまうので注意が必要attr
は対象の要素しか操作できないので、親や子要素など対象要素を起点に他の要素を操作したい場合は別のアプローチが必要attr
の value に文字列を返す関数を設定できたがドキュメントに載ってないので動作が担保されるかは怪しい
- アニメーション完了時に何かをしたいのであれば
onEnter
内でアニメーションの時間分setTimeout
させるのが無難そう (微妙だけど) - onLoad 時に、既にアニメーションが終了しているスクロール位置の場合は、スクロール量とコンテンツの位置から GSAP を動作させず完了とさせる処理を挟むほうが良さそう
Sample
↓ スクロールを上下させると once: true
でも end に到達する前に start を跨ぐ度に onToggle
が実行されるのが確認できる
See the Pen GSAP batch animation callback by KIKIKI (@kikiki_kiki) on CodePen.
batch でアニメーションが開始された要素と、その要素を起点に DOM 操作したかっただけだったんだけど、結構沼だった。
そして、このあと ScrollTrigger.batch
使わなくてもできるじゃん… となってしまった訳ですが、それはまた別のお話…
おわり。゚(゚ ◜ᴗ◝゚)゚。
[参考]