GSAP備忘録(その2)

GSAP備忘録(その2)

目次
  1. ModifiersPluginとUtilityメソッド
  2. ScrollTrigger

ModifiersPluginとUtilityメソッド

スライダーを作ってみる
ModifiersPlugin(コアのプラグイン)で逆方向の動きをつけたり、Utilityメソッドのwrap()を使うとカウンターのロジックが簡単になります

                         

おおまかな流れ

  1. cssでスライダーのアイテムのラッパーはvisibility: hidden;とopacity:0;にする
  2. gsap.set(‘ラッパー’, { autoAlpha: 1 }); gsap.set(‘スライダーのアイテム’, { x: ‘-100%’ })をセットする
  3. スライダーがINする時のタイムラインfromTo:(x: ‘-100%’,autoAlpha: 0, からx:0 ,autoAlpha: 1)
  4. スライダーがOUTする時のタイムラインto: (x:300, autoAlpha: 0)へ
  5. outからinへのトランジションのタイムライン(add(out).add(in))
    add()メソッドを使用すると、タイムラインの任意の場所にトゥイーン、タイムライン、ラベル、コールバックを追加できます。
  6. 初めにINのタイムライン 、そのあとはクリックイベントでトランジションのタイムラインが発火する

INのタイムラインで背景色も変化させる
onStart :アニメーションの開始時に呼び出す関数
onStartParams :onStartの関数の引数を配列で設定します

//INのタイムラインを返す関数
function timeLineIn(index){
//スライダーに0から連番のclass名を付けておく
const el = document.querySelector(`div.item${index}`);
const tlIn = gsap.timeline();
        tlIn.fromTo(el, {
        x: '-100%',
        autoAlpha: 0,
        }, {
        duration: 0.7,
        x: 0,
        autoAlpha: 1,
        onStart: updateClass,
        onStartParams: [`no-${index}`],
        })
 return tlIn;
}
//アニメーション開始時に背景色をつけるラッパー要素にクラスを追加する
function updateClass(parame) {
    document.querySelector('.wrapper').className = `wrapper ${parame}`;
}

utils.wrap()は数値を指定された範囲に配置し、最大値を超えると最初に折り返され、最小値よりも小さい場合は最後に折り返されるようにします

  let currentIndex = 0;
  const totalItems = document.querySelectorAll('.item').length;
  const wrapper = gsap.utils.wrap(0, totalItems);

//カウントの値が最後なら0に
 const nextIndex = wrapper(currentIndex + 1);
//カウントの値が0なら最終の値に
 const prevIndex = wrapper(currentIndex - 1);

ModifiersPluginはGSAPが適用する値をインターセプトし、それを最初のパラメーターとしてGSAPが適用する新しい値を返します
ModifiersPluginを利用してprevボタンを押した時は、INとOUTの逆方向のアニメーションを作成します
またutils.unitize()でフィードするときに単位を取り除き、結果に単位を追加します

//INのタイムラインを返す関数
function timeLineIn(direction, index) {
//引数が'prev'ならtrue
    const prev = direction === 'prev'; 
    const el = document.querySelector(`div.item${index}`);
    const button = el.querySelector('.to-section');
    const tlIn = gsap.timeline({
//画像とSECTIONへのボタン共通なのでdefaults
        defaults: {
            // 逆方向
            modifiers: {
                x: gsap.utils.unitize(function(x){
                    return prev ? Math.abs(x) : x;
            })
            }
         }
         });
        tlIn.fromTo(el, {
        x: '-100%',
        autoAlpha: 0,
        }, {
        duration: 0.7,
        x: 0,
        autoAlpha: 1,
        onStart: updateClass,
        onStartParams: [`no-${index}`],
        })
        .from(button, { duration: 0.2, x:-20, autoAlpha: 0 })
        return tlIn;
}
//OUTのタイムラインを返す関数
function timeLineOut(direction, index) {
  const prev = direction === 'prev'; 
  const el = document.querySelector(`div.item${index}`);
  const tlOut = gsap.timeline();
    tlOut.to(el, {
        duration: 0.7,
        x: 300,
        autoAlpha: 0,
        // 逆方向
        modifiers: {
            x: gsap.utils.unitize(function (x) {
                return prev ? -x : x;
         })
        }
    })
    return tlOut;
}

アニメーション中かの判別

function isTweening() {
    return gsap.isTweening('.item');
}

コードを見る

<div class="wrapper">
   <div class="items">
        <button class="btn prev"><span class="screen-reader-text">Prev</span></button>
        <button class="btn next"><span class="screen-reader-text">Next</span></button>
      <div class="item item0">
           <div class="item-image">
               <img src="https://koro-koro.com/wp-content/uploads/2022/05/1.png" alt="">
            </div>
          <p class="to-section"><a href="#" class="button" target="_blank" rel="noopener">SECTION 1</a></p>
      </div>
      <div class="item item1">
         <div class="item-image">
             <img src="https://koro-koro.com/wp-content/uploads/2022/05/2.png" alt="">
         </div>
          <p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 2</a></p>
      </div>
      <div class="item item2">
         <div class="item-image">
             <img src="https://koro-koro.com/wp-content/uploads/2022/05/3.png" alt="">
         </div>
         <p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 3</a></p>
      </div>
    <div class="item item3">
         <div class="item-image">
             <img src="https://koro-koro.com/wp-content/uploads/2022/05/5.png" alt="">
         </div>
         <p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 4</a></p>
      </div>
   <div class="item item4">
        <div class="item-image">
             <img src="https://koro-koro.com/wp-content/uploads/2022/05/4.png" alt="">
        </div>
        <p class="to-section"><a class="button" href="#" target="_blank" rel="noopener">SECTION 5</a></p>
     </div>
   </div>
</div>
.wrapper{ overflow-x: hidden;}
.wrapper.no-0 {background-color: #9a63b4;}
.wrapper.no-1 {background-color: #cf7836;}
.wrapper.no-2 {background-color: #2e82b9;}
.wrapper.no-3 {background-color: #2e6a19;}
.wrapper.no-4 {background-color: #e4b640;}
.items {
    width: 90%;
    margin: 0 auto;
    height: calc(25vh + 250px);
    position: relative;
    visibility: hidden;
    opacity:0; 
}
.item {
    text-align: center;
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
}
.item-image,
.item-image img{border-radius: 50%;}
.item-image {
    position: relative;
    width: 250px;
    height: 250px;
    margin: 5vh auto 0 auto;
}
.item-image img {
    position: relative;
    display: block;
}
.button-container {
    position: relative;
}
a.button {
    text-decoration: none;
    width: 150px;
    display: inline-block;
    margin: 0 auto;
    color: rgba(255,255,255,0.7);
    padding: 8px 0;
    border: 1px rgba(255,255,255,1) solid;
    border-radius: 2px;
}
button.btn {
    display: block;
    width: 70px;
    height: 70px;
    background-color: transparent;
    border: none;
    color: rgba(255,255,255,0.7);
    border-radius: 50%;
    transition: all 0.3s linear;
    overflow: hidden;
    position: absolute;
    top: 25vh;
    z-index: 1;
}
button.btn.next {
    left: auto;
    right: 0;
}
button.btn:hover,
button.btn:focus,
button.btn:active {
    outline: none;
    background-color: rgba(0,0,0,0.4);
    border-radius: 50%;
    cursor: pointer;
}
button.btn:before {
    content: "\2039";
    display: block;
    position: absolute;
    top: -13px;
    font-size: 70px;
    font-weight: 300;
}
button.btn.prev:before {
    content: "\2039";
    left: 23px;
}
button.btn.next:before {
    content: "\203a";
    left: 28px;
}
.dots {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    bottom: 2vh;
    left: 0;
    right: 0;
    top: auto;
}
.dot {
    overflow: hidden;
    background-color: rgba(255,255,255,0.2);
    width: 20px;
    height: 20px;
    text-indent: -9999em;  
    border: none;
    border-radius: 50%;
    transition: all 0.3s linear;
    margin: 0 8px;
    cursor: pointer;
}
.dot:focus,
.dot:active,
.dot:hover {
    outline: none;
    background-color: rgba(255,255,255,0.6);
}
.dot.active {
    background-color: rgba(0,0,0,0.4);
}
//INのタイムラインを返す関数
function timeLineIn(direction, index) {
    const prev = direction === 'prev'; 
    const el = document.querySelector(`div.item${index}`);
    const button = el.querySelector('.to-section');
    const tlIn = gsap.timeline({
        defaults: {
            modifiers: {
                x: gsap.utils.unitize(function(x){
                    return prev ? Math.abs(x) : x;
            })
            }
         }
         });
        tlIn.fromTo(el, {
        x: '-100%',
        autoAlpha: 0,
        }, {
        duration: 0.7,
        x: 0,
        autoAlpha: 1,
        onStart: updateClass,
        onStartParams: [`no-${index}`],
        })
        .from(button, { duration: 0.2, x:-300, autoAlpha: 0 })
        return tlIn;
}

//INアニメーション開始時のコールバック
function updateClass(parame) {
    document.querySelector('.wrapper').className = `wrapper ${parame}`;
}

//OUTのタイムラインを返す関数
function timeLineOut(direction, index) {
  const prev = direction === 'prev'; 
  const el = document.querySelector(`div.item${index}`);
  const tlOut = gsap.timeline();
    tlOut.to(el, {
        duration: 0.7,
        x: 300,
        autoAlpha: 0,
        modifiers: {
            x: gsap.utils.unitize(function (x) {
                return prev ? -x : x;
         })
        }
    })
    return tlOut;
}

//トランジションのタイムラインを返す関数
function timeLineTransition(direction, toIndex) {
    const tlTransition = gsap.timeline();
    tlTransition
        .add(timeLineOut(direction, currentIndex))
        .add(timeLineIn(direction, toIndex));
  // アニメーションの開始時に呼び出す関数
        onStart: updateCurrentIndex(toIndex)
    return tlTransition;
}
//トランジションアニメーション開始時のコールバック
function updateCurrentIndex(toIndex) {
    currentIndex = toIndex;
  //ドットにactiveクラスをつける
    document.querySelectorAll('.dot').forEach(function (el, index) {
        el.setAttribute('class', 'dot')
        if (index === currentIndex) {
            el.classList.add('active')
        }
    })
}
//アニメーション中かを判別
function isTweening() {
    return gsap.isTweening('.item');
}

//ドットを作成する関数
function createDot() {
    const newDiv = document.createElement('div');
    newDiv.setAttribute('class', 'dots');
    for (let index = 0; index < totalItems; index++){
        const element = document.createElement('button');
        const text = document.createTextNode(index);
        element.appendChild(text);
        element.setAttribute('class', 'dot');
        if (currentIndex === index) {
            element.classList.add('active')
        }
     //クリックされた時
        element.addEventListener('click', function () {
            if (!isTweening()) {
                const direction = index > currentIndex ? 'next' : 'prev'
                timeLineTransition(direction, index);
            }
        })
        newDiv.appendChild(element);
    }
    document.querySelector('.items').appendChild(newDiv);
}
//set
 gsap.set('.items', { autoAlpha: 1 });
 gsap.set('.item', { x: '-100%' })

 let currentIndex = 0;
 const totalItems = document.querySelectorAll('.item').length;
 const wrapper = gsap.utils.wrap(0, totalItems);

//最初のスライダーがINする
timeLineIn('next', currentIndex)
//ドット
createDot()

//次へのボタンが押された時
document.querySelector('button.next').addEventListener('click', function (e) {
    e.preventDefault();
    const nextIndex = wrapper(currentIndex + 1);
    !isTweening() && timeLineTransition('next', nextIndex);
 })
//前へのボタンが押された時
document.querySelector('button.prev').addEventListener('click', function(e){
    e.preventDefault();
    const prevIndex = wrapper(currentIndex - 1);
    !isTweening() &&  timeLineTransition('prev', prevIndex);
})

ScrollTrigger

100pxスクロールした時に、bodyにis-fixedクラスをつける場合は、これだけでOK

gsap.registerPlugin(ScrollTrigger);
ScrollTrigger.create({
        start: 100,
//endがないと最後までスクロールした時にクラスが外れる
        end: 'bottom bottom-=100',
        toggleClass: {
            targets: 'body',
            className: 'is-fixed'
        }
    })

UtilityメソッドのtoArrayは、ほとんどすべての配列のようなオブジェクトを配列に変換します
とても便利です

セクションごとにアニメーションをつけます
最もシンプルな例が
ビューポートのボトムにトリガー要素のトップが現れた時にクラスをつける
ビューポートのトップにトリガー要素のボトムが現れた時にクラスが外れる
*要するにトリガー要素が、ビューポートに入っている間はクラスがついている状態です

gsap.registerPlugin(ScrollTrigger);
//toArrayを使うことで、配列として扱えます
const allSections = gsap.utils.toArray('section');
    allSections.forEach((section, index) => {
    gsap.set(section, {
            scrollTrigger: {
         trigger: section,
              toggleClass: 'active',
         start: "top bottom-=250px",
              end: "bottom 250px",             
//idはマーカーに追加表示されます(ScrollTrigger.getById()で取得もできる)
              id: `sec-${index+1}`,
              markers: true
           }
       })
    })
1
2
3
4
5
start
ScrollTriggerの開始位置(スクローラーの上部/左側が基準です)
*スクローラーのデフォルトはビューポートです
文字列
“top center”:トリガーの上部がスクローラーの中央に当たったとき
“top bottom-=100px”:トリガーの上部がスクローラーの下部から100px上に当たったとき
数値
200: 200ピクセルスクロールした時
endTrigger
ScrollTriggerの終了位置の計算に使用される要素(または要素のセレクター)
*トリガー要素が異なる場合のみ設定します
end
ScrollTriggerの終了位置
文字列
“bottom center”:endTriggerの下部がスクローラーの中央に当たったとき
“center 100px”:endTriggerの中心がスクローラーの上部から100px下に当たったとき

startとendでスクローラー(ビューポート)を調節できます
*top center bottomの使い方が感覚的でないような気がするw(ややこしい〜😂 )

  scrollTrigger: {
         trigger: section,
              toggleClass: 'active',
//ビューポートのボトムから250px上
         start: "top bottom-=250px",
//ビューポートの上部から250px下
              end: "bottom 250px",             
           }   
1
2
3
4
5

<img>にwidthやheight属性がない場合などで、高さが反映されない時があります
その時は、ScrollTrigger.refresh()で、ラッパー要素を生成し直してくれます

mousemoveイベントで遊んでみる
gsap.utils.toArrayは、document.querySelectorAllの代わりに使っています
*親の要素のCSSには、perspective: 1000pxを設定して奥行きを出します

document.querySelector('.images').addEventListener('mousemove', moveImges)
function moveImges(e) {
//offsetX,Y イベントが発生した要素上のX座標とY座標
    const { offsetX, offsetY, target } = e
    const { clientWidth, clientHeight } = target

    const xPos = (offsetX / clientWidth) - 0.5;
    const yPos = (offsetY / clientHeight) - 0.5;

    const images = gsap.utils.toArray('.image')
    images.forEach((image, index) => {
        gsap.to(image, {
            duration: 1.2,
            x: xPos * 20 *(index + 0.5),
            y: yPos * 100 * (index * 1.5 + 0.5),
            rotationY: xPos * 100,
            rotationX: yPos * 10,
            ease:'Power3.out'
        });
    })
}

PC(マウスイベント)