📑

慣性が効いてるみたいなワンスクロールつくってみた

2023/01/25に公開

慣性がきいてるみたいなワンスクロール

ワンスクロールみたいなスクロールを制限する挙動ってUXの観点から結構嫌われがちだけれど、サイトの目的とかデザインによっては、リッチな表現になったりするかなと思ってます。特にデザイン会社である弊社(https://twitter.com/funtech_inc) 案件だと結構使ったりします。

ホイールイベントメソッドを登録できるので、慣性的にしないこともできます〜

結論:できたのはこれ

https://snippets.cacher.io/snippet/50c32bc3c457c58e18ba

要件はこんな感じ

  • ページの途中から画面固定を始めることができる
  • 最初から画面固定にすることもできる
    • その際に無限スクロールにすることができる
  • トランジションアニメーションメソッドを追加可能にする
  • ホイール中のアニメーションメソッドを追加可能にする
  • ボタンで特定のシーンにジャンプすることができる
  • スクロールバーの操作や↓↑ボタンの操作があった場合にも状態が変わる
  • resizeに対応
  • スマホ対応
    • falseにもできる
  • マウスホイールなどで無茶苦茶スクロールして位置がズレた場合、それを検出して位置を修正させる
    • ので、スクロールバーを直接操作しやがった場合にも対応できる
  • scrollとwheel eventをrAFで間引く あんま意味ないかも
  • クラスにする

使い方

あんまり使い所はないかもですが、、、
もし使ったよ!って方いたら改善点とか教えていただけると助かります!
(ライブラリつくったことないので、時間みつけて作ってみようかな)

初期化

const fixedView = new FixedView(SCENES, {
   buttonsTarget: BUTTONS,
   isInfinitScroll: false,
   isMobile: true,
});
fixedView.mount();

第一引数にシーンのターゲット、第二引数にoptionが入ります。

ホイールイベントの登録

let wheelTimeOutID = 0;
/*===============================================
wheel
===============================================*/
fixedView.on("wheel", (obj) => {
   //デフォルトの挙動
   const moveY = obj.scrollVol * -0.1;
   //慣性アニメーション
   gsap.context(() => {
      gsap.to(".js_animTarget", {
         y: `+=${moveY}`,
         duration: 0.3,
         ease: "power3.out",
         stagger: {
            each: 0.01,
         },
      });
   }, obj.target);
   //一定時間が経過すると元に戻るアニメーション
   clearTimeout(wheelTimeOutID);
   wheelTimeOutID = setTimeout(() => {
      if (!obj.wheelState.isInviewPrevent) {
         //inviewが発火してる間は発火させない
         gsap.context(() => {
            gsap.to(".js_animTarget", {
               y: 0,
               duration: 0.4,
               ease: "back.out(3)",
               stagger: {
                  each: 0.01,
               },
            });
         }, obj.target);
      }
   }, obj.wheelState.transitionCanceDur);
});

wheelイベントが変えるobjにはwheelStateというオブジェクトが入ってます。isInviewPreventがtrue間はホイールイベントが発火しないようにできます。transitionCanceDurはスクロール量のthresholdを0にするまでの時間です。

leaveとenterアニメーションの登録

※leaveとenterはそれぞれresolveを待つようになってるので、promiseを返すようにする必要があります

/*===============================================
leaveアニメーション
===============================================*/
fixedView.on("leave", async (obj) => {
   let moveVol = 12;
   let moveY = 0;
   let setY = 0;
   const DURATIONVAL = 0.6;

   if (obj.isForward) {
      moveY = `${moveVol * -1}rem`;
      setY = `${moveVol}rem`;
   } else {
      moveY = `${moveVol}rem`;
      setY = `${moveVol * -1}rem`;
   }
   //次の要素を操作しておく
   gsap.context(() => {
      gsap.set(".js_animTarget", {
         y: setY,
         opacity: 0,
      });
   }, obj.nextTarget);
   //今表示されてるのをフェードアウトさせる
   return new Promise((resolve) => {
      gsap.context(() => {
         gsap.to(".js_animTarget", {
            y: moveY,
            opacity: 0,
            duration: DURATIONVAL,
            ease: "power3.out",
            stagger: {
               each: 0.01,
            },
            onComplete: () => {
               resolve();
            },
         });
      }, obj.currentTarget);
   });
});

/*===============================================
enterアニメーション
===============================================*/
fixedView.on("enter", async (obj) => {
   const DURATIONVAL = 0.6;
   return new Promise((resolve) => {
      gsap.context(() => {
         gsap.to(".js_animTarget", {
            y: "0",
            opacity: 1,
            duration: DURATIONVAL,
            ease: "power3.out",
            stagger: {
               each: 0.01,
            },
            onComplete: () => {
               resolve();
            },
         });
      }, obj.nextTarget);
   });
});

Discussion