takuma-hmng8
1/24/2023 - 1:44 PM

FixedView

  • passive:falseについて。
    • passive:trueを設定するとevent内のpreventDefaultが無効化される。これはスクロールイベントにactionを仕込んだ時、その完了を待ってデフォルトのスクロールを発生させてたことによるスクロールのずれを解消するために追加されたoption
    • つまり、scrollイベントを停止させるためには、passive:falseにして、event処理の冒頭でpreventDefaultを呼び出してあげるのが良い
import { gsap } from "gsap";
import { ScrollToPlugin } from "gsap/ScrollToPlugin";
import { inertiaScrollInit } from "../common/inertiaScroll";
gsap.registerPlugin(ScrollToPlugin);

/*===============================================
画面固定
===============================================*/

export class FixedView {
   constructor(
      scenesTarget,
      { buttonsTarget, isInfinitScroll, isMobile, isStartTop }
   ) {
      /*===============================================
		const
		===============================================*/
      this.SCENES = scenesTarget;
      this.BUTTONS = buttonsTarget;
      this.isInfinitScroll = isInfinitScroll;
      this.isMobile = isMobile;
      this.isStartTop = isStartTop ?? false; //シーン0からページがスタートする場合
      /*===============================================
		animation hook
		===============================================*/
      this.wheelingAnimation = false;
      this.leaveAnimation = false;
      this.enterAnimation = false;
      this.finishAnimation = false;
      this.restartAnimation = false;
      /*===============================================
		event handler
		===============================================*/
      this.EVENTARRY = ["touchmove", "wheel"];
      this.TOUCHEVENTARRY = ["touchend", "touchstart"];
      this.resizeEvent = false;
      this.scrollEvent = false;
      this.wheelEvent = false;
      this.touchEvent = false;
   }
   mount() {
      /*===============================================
		シーンの高さを設定する
		===============================================*/
      let SCENESHEIGHT;
      let THRESHOLD;
      const setSceneHeight = () => {
         SCENESHEIGHT = window.innerHeight;
         THRESHOLD = SCENESHEIGHT * 1;
         this.SCENES.forEach((element) => {
            element.setAttribute("style", `height:${SCENESHEIGHT}px;`);
         });
      };
      setSceneHeight();
      /*===============================================
		utils
		===============================================*/
      /********************
		ターゲットのTOPからのposを取得する
		********************/
      const PREVENTMARGIN = 10;
      const getTargetPos = (
         target,
         { position, isEnd = false, isFirst = false }
      ) => {
         const rect = target.getBoundingClientRect();
         const scrollTop =
            window.pageYOffset || document.documentElement.scrollTop;
         let pos = rect.top + scrollTop;
         if (position === "top" && isFirst === true) {
            pos = pos - THRESHOLD / 2 - PREVENTMARGIN;
            return pos;
         } else if (position === "top") {
            return pos;
         } else if (position === "bottom" && isEnd === true) {
            pos = pos + SCENESHEIGHT - THRESHOLD / 2 + PREVENTMARGIN;
            return pos;
         } else if (position === "bottom") {
            pos += SCENESHEIGHT;
            return pos;
         }
      };
      /********************
		シーンの切り替え
		********************/
      const sceneSwitch = async (
         target,
         {
            isInviewAnim = false,
            isEnd = false,
            isFirst = false,
            isWheelTrigger = false,
            isButtonTrigger = false,
         }
      ) => {
         const INVIEWDURATION = 0.5;
         let scrollToPos = 0;

         //トリガーがホイールの場合はscrollイベントのsceneSwitchを発火させない
         if (isWheelTrigger) {
            wheelState.isWheelTrigger = true;
         }
         if (isButtonTrigger) {
            buttonState.isButtonTrigger = true;
         }
         if (isEnd) {
            //ラスト/ファーストへの固定解除の場合に値を変更する
            //ラストシーンから順向きに固定終了の場合
            scrollToPos = getTargetPos(target, {
               position: "bottom",
               isEnd: true,
            });
         } else if (isFirst) {
            //ファーストシーンから順向きに固定終了の場合
            scrollToPos = getTargetPos(target, {
               position: "top",
               isFirst: true,
            });
         } else {
            scrollToPos = getTargetPos(target, { position: "top" });
         }

         //invewアニメーションの場合はホイールイベントを、duration分、preventさせる
         return new Promise((resolve) => {
            if (isInviewAnim) {
               wheelState.isInviewPrevent = true;
               gsap.to(window, {
                  duration: INVIEWDURATION,
                  ease: "power3.out",
                  scrollTo: scrollToPos,
                  onComplete: () => {
                     wheelState.isInviewPrevent = false;
                     resolve();
                  },
               });
            } else {
               window.scrollTo({ top: scrollToPos });
               resolve();
            }
         });
      };

      /*===============================================
		シーンの切り替えアニメーション
		===============================================*/
      /********************
		ホイールに応じた慣性アニメーションさせる
		********************/
      const wheelingAnimation = (target, scrollVol) => {
         if (this.wheelingAnimation) {
            //イベントフックでイベントが登録された場合
            this.wheelingAnimation({
               target: target,
               scrollVol: scrollVol,
               wheelState: wheelState,
            });
         } else {
         }
      };
      /********************
		ページトランジションアニメーション
		********************/
      const sceneTransitionAnimation = async ({
         currentTarget,
         nextTarget,
         isOut = false,
         isForward = false,
      }) => {
         return new Promise((resolve) => {
            if (isOut) {
               /********************
					leaveアニメーション
					********************/
               wheelState.isInviewPrevent = true;
               if (this.leaveAnimation) {
                  (async () => {
                     await this.leaveAnimation({
                        currentTarget: currentTarget,
                        nextTarget: nextTarget,
                        isForward: isForward,
                     });
                     resolve();
                  })();
               }
            } else {
               /********************
					enterアニメーション
					********************/
               if (this.enterAnimation) {
                  (async () => {
                     await this.enterAnimation({
                        currentTarget: currentTarget,
                        nextTarget: nextTarget,
                        isForward: isForward,
                     });
                     wheelState.isInviewPrevent = false;
                     resolve();
                  })();
               }
            }
         });
      };

      /*===============================================
		シーンの切り替え呼び出し用関数
		===============================================*/

      const sceneTransitonCallBack = async ({
         currentTarget,
         nextTarget,
         isForward = false,
         isWheel = false,
         isButton = false,
      }) => {
         await sceneTransitionAnimation({
            currentTarget: currentTarget,
            nextTarget: nextTarget,
            isOut: true,
            isForward: isForward,
         });
         await sceneSwitch(nextTarget, {
            isWheelTrigger: isWheel,
            isButtonTrigger: isButton,
         });
         await sceneTransitionAnimation({
            currentTarget: "",
            nextTarget: nextTarget,
            isForward: isForward,
         });
      };

      /*===============================================
		ボタンイベント
		===============================================*/

      const buttonState = {
         isButtonScrolling: false,
         isButtonTrigger: false,
      };

      async function handleButtonEvent() {
         buttonState.isButtonScrolling = true;

         if (!wheelState.isWheelActive) {
            //ホイールがアクティブ状態じゃない※まあボタンがエリア外から呼ばれる場合はないと思うけども
            await sceneSwitch(this.scenes[this.index], {
               isInviewAnim: true,
               isButtonTrigger: true,
            });
            //出現させる
            sceneTransitionAnimation({
               nextTarget: this.scenes[this.index],
            });
         } else {
            let direction = false;
            if (wheelState.scenePhase < this.index) {
               direction = true;
            }
            //ホイールがアクティブ状態
            await sceneTransitonCallBack({
               currentTarget: this.scenes[wheelState.scenePhase],
               nextTarget: this.scenes[this.index],
               isForward: direction,
               isButton: true,
            });
         }
         buttonState.isButtonScrolling = false;
         handleScrollEvent();
      }

      if (this.BUTTONS) {
         this.BUTTONS.forEach((element, index) => {
            element.addEventListener("click", {
               index: index,
               scenes: this.SCENES,
               handleEvent: handleButtonEvent,
            });
         });
      }

      /*===============================================
		ホイールイベント(シーンの遷移イベント)
		===============================================*/
      //状態管理
      const wheelState = {
         isWheeling: false,
         isWheelTrigger: false,
         isWheelActive: false,
         scenePhase: "0",
         scrollVol: 0,
         touchMoveVol: 0,
         scrollDeltaY: 0,
         scrollVolTimeOutID: 0,
         isInviewPrevent: false,
         isPrevent: false,
         preventTime: 500,
         threshold: 300,
         ticking: false,
         transitionCanceDur: 200,
      };

      // シーントランジション後preventTime間はホイールイベントが発火しないようにする
      const wheelPreventSwitch = () => {
         wheelState.scrollVol = 0;
         wheelState.isPrevent = true;
         setTimeout(() => {
            wheelState.isPrevent = false;
         }, wheelState.preventTime);
      };

      //ホイールイベント
      const wheelControl = (e) => {
         if (
            wheelState.isInviewPrevent === false &&
            wheelState.isPrevent === false
         ) {
            /********************
				ホイール量を取得
				********************/
            if (e?.deltaY) {
               wheelState.scrollVol += e.deltaY;
               wheelState.scrollDeltaY = e.deltaY;
            } else if (Object.keys(e?.changedTouches).length > 0) {
               const sy = e.changedTouches[0].screenY;
               if (wheelState.touchMoveVol === 0) {
                  wheelState.touchMoveVol = sy;
                  return;
               }
               const vol = (wheelState.touchMoveVol - sy) * 5;
               wheelState.scrollVol += vol;
               wheelState.scrollDeltaY = vol;
               wheelState.touchMoveVol = sy;
            } else {
               return;
            }

            /********************
				一定時間を超えるとscrollvolを0に戻す(加算されないようにする)
				********************/
            clearTimeout(wheelState.scrollVolTimeOutID);
            wheelState.scrollVolTimeOutID = setTimeout(() => {
               wheelState.scrollVol = 0;
            }, wheelState.transitionCanceDur);
            /********************
				スクロールに量応じて慣性アニメーションを加える
				********************/
            wheelingAnimation(
               this.SCENES[wheelState.scenePhase],
               wheelState.scrollDeltaY
            );
            /********************
				閾値を超えたら次のシーンに移動する
				********************/
            if (wheelState.scrollVol > wheelState.threshold) {
               //順向きのシーントランジション
               wheelPreventSwitch();
               if (wheelState.scenePhase === this.SCENES.length - 1) {
                  if (!this.isInfinitScroll) {
                     (async () => {
                        //解除コールバック
                        await this.finishAnimation();
                        //ラストシーンの場合、threshold分pos移動
                        sceneSwitch(this.SCENES[wheelState.scenePhase], {
                           isInviewAnim: true,
                           isEnd: true,
                        });
                     })();
                  } else {
                     sceneTransitonCallBack({
                        currentTarget: this.SCENES[wheelState.scenePhase],
                        nextTarget: this.SCENES[0],
                        isForward: true,
                        isWheel: true,
                     });
                  }
               } else {
                  sceneTransitonCallBack({
                     currentTarget: this.SCENES[wheelState.scenePhase],
                     nextTarget: this.SCENES[wheelState.scenePhase + 1],
                     isForward: true,
                     isWheel: true,
                  });
               }
            } else if (wheelState.scrollVol < wheelState.threshold * -1) {
               //逆向きのシーントランジション
               wheelPreventSwitch();
               if (wheelState.scenePhase === 0) {
                  if (!this.isInfinitScroll) {
                     if (this.isStartTop) return false;
                     //ファーストシーンの場合、threshold分pos移動
                     sceneSwitch(this.SCENES[wheelState.scenePhase], {
                        isInviewAnim: true,
                        isFirst: true,
                     });
                  } else {
                     sceneTransitonCallBack({
                        currentTarget: this.SCENES[wheelState.scenePhase],
                        nextTarget: this.SCENES[this.SCENES.length - 1],
                        isWheel: true,
                     });
                  }
               } else {
                  sceneTransitonCallBack({
                     currentTarget: this.SCENES[wheelState.scenePhase],
                     nextTarget: this.SCENES[wheelState.scenePhase - 1],
                     isWheel: true,
                  });
               }
            }
         }
      };

      /********************
		ホイールイベントハンドラ
		********************/

      const handleWheelEvent = (e) => {
         // スクロールを無効化
         e.preventDefault();
         e.stopPropagation();
         e.stopImmediatePropagation();
         //scrollイベント中とボタンからのスクロール中は呼び出さない
         if (
            scrollState.isScrolling === true ||
            buttonState.isButtonScrolling === true
         ) {
            return;
         }
         wheelState.isWheeling = true;
         //rAF
         if (!wheelState.ticking) {
            window.requestAnimationFrame(() => {
               wheelControl(e);
               wheelState.ticking = false;
            });
            wheelState.ticking = true;
         }
         wheelState.isWheeling = false;
      };

      const handleTouchEvent = () => {
         wheelState.touchMoveVol = 0;
         wheelState.scrollVol = 0;
      };

      this.wheelEvent = handleWheelEvent;
      this.touchEvent = handleTouchEvent;

      //ホイールイベント登録
      const addWheelEvent = (isAdd) => {
         if (isAdd) {
            //慣性スクロールの無効化
            inertiaScrollInit(false);
            wheelState.isWheelActive = true;
            this.EVENTARRY.forEach((element) => {
               document.addEventListener(element, this.wheelEvent, {
                  passive: false,
               });
            });
            if (this.isMobile) {
               this.TOUCHEVENTARRY.forEach((element) => {
                  document.addEventListener(element, this.touchEvent);
               });
            }
         } else {
            wheelState.isWheelActive = false;
            this.EVENTARRY.forEach((element) => {
               document.removeEventListener(element, this.wheelEvent, {
                  passive: false,
               });
            });
            //慣性スクロールの有効化
            inertiaScrollInit(true);
            if (this.isMobile) {
               this.TOUCHEVENTARRY.forEach((element) => {
                  document.removeEventListener(element, this.touchEvent);
               });
            }
         }
      };

      /*===============================================
		スクロールイベント(状態の管理)
		===============================================*/

      //状態管理
      const scrollState = {
         isScrolling: false,
         viewTop: "",
         viewBottom: "",
         timeOutID: 0,
         //位置修正を発火させるまでのinterval
         correctPosInterval: 200,
         ticking: false,
         //スクロールバーで操作された時の処理
         scrollBarPrevent: false,
         scrollBarPreventTimeOutID: 0,
      };

      //交差してるかどうかの判定
      const isInterSecting = (interSectionVal) => {
         if (
            interSectionVal >= THRESHOLD * -1 &&
            interSectionVal <= THRESHOLD
         ) {
            return true;
         } else {
            return false;
         }
      };

      //スクロールイベントが終わった時に位置がズレてる場合に正しい位置に修正する
      const correctPos = () => {
         if (!wheelState.isWheelActive) {
            return;
         }
         clearTimeout(scrollState.timeOutID);
         scrollState.timeOutID = setTimeout(() => {
            if (wheelState.isInviewPrevent) {
               return;
            }
            this.SCENES.forEach((element, index) => {
               if (element.dataset.view === "1") {
                  //現在のスクロール位置
                  const scrollYPos =
                     window.pageYOffset || document.documentElement.scrollTop;
                  //index番目の位置
                  const correctY = getTargetPos(element, { position: "top" });
                  //比較して合ってなかったら発火
                  if (scrollYPos !== correctY) {
                     console.log("fire");
                     sceneSwitch(this.SCENES[index], { isInviewAnim: true });
                     //アニメーションさせた要素を元の位置に戻しておく
                     sceneTransitionAnimation({
                        nextTarget: this.SCENES[index],
                     });
                  }
                  return;
               }
            });
         }, scrollState.correctPosInterval);
      };

      //wheelとbuttonがトリガーになってる場合はスクロールでのtransitionを発火させない
      const switchTriggerState = (event) => {
         if (!wheelState.isWheelTrigger && !buttonState.isButtonTrigger) {
            event();
         }
         wheelState.isWheelTrigger = false;
         buttonState.isButtonTrigger = false;
      };

      //位置を判定して状態のdatasetと、sceneのスイッチを行う。スクロールバーの操作の対策もかねる
      const scrollControl = () => {
         if (scrollState.scrollBarPrevent) return;
         console.log("scroll");
         scrollState.viewTop =
            window.pageYOffset || document.documentElement.scrollTop;
         scrollState.viewBottom = scrollState.viewTop + SCENESHEIGHT;
         //シーンの状態設定
         this.SCENES.forEach((element, index) => {
            //交差の判定
            const intersectTop =
               scrollState.viewBottom -
               getTargetPos(element, { position: "top" });
            const intersectBottom =
               getTargetPos(element, { position: "bottom" }) -
               scrollState.viewTop;
            element.dataset.intersectTop = intersectTop;
            element.dataset.intersectBottom = intersectBottom;
            //交差してたらtrueを返す
            const interSectionVal =
               element.dataset.intersectTop - element.dataset.intersectBottom;

            //状態に応じてイベント発火
            if (isInterSecting(interSectionVal)) {
               if (element.dataset.view === "1") {
                  //if isViewed, return false
                  return;
               }
               //ホイールのシーンフェーズにindexを設定
               wheelState.scenePhase = index;

               //スクロールイベントを登録
               if (wheelState.isWheelActive === false) {
                  addWheelEvent(true);
               }

               if (
                  (interSectionVal <= 0 && index === 0) ||
                  (interSectionVal >= 0 && index === this.SCENES.length - 1)
               ) {
                  /********************
						固定スタート
						********************/
                  if (!this.isInfinitScroll) {
                     switchTriggerState(() => {
                        sceneSwitch(this.SCENES[index], { isInviewAnim: true });
                     });
                  }
                  /********************
						下からリスタート
						********************/
                  if (index === this.SCENES.length - 1) {
                     this.restartAnimation();
                  }
               } else if (interSectionVal <= 0) {
                  /********************
						順向きでinview
						********************/
                  switchTriggerState(() => {
                     sceneTransitonCallBack({
                        currentTarget: this.SCENES[index - 1],
                        nextTarget: this.SCENES[index],
                        isForward: true,
                     });
                  });
               } else {
                  /********************
						逆向きでinview
						********************/
                  switchTriggerState(() => {
                     sceneTransitonCallBack({
                        currentTarget: this.SCENES[index - 1],
                        nextTarget: this.SCENES[index],
                     });
                  });
               }
               element.dataset.view = "1";
            } else if (element.dataset.view === "1") {
               if (
                  (index === 0 && interSectionVal <= 0) ||
                  (index === this.SCENES.length - 1 && interSectionVal >= 0)
               ) {
                  /********************
						固定解除
						********************/
                  if (wheelState.isWheelActive === true) {
                     //アニメーションさせた要素を元の位置に戻しておく
                     sceneTransitionAnimation({
                        nextTarget: this.SCENES[index],
                     });
                     //ホイールイベント解除
                     addWheelEvent(false);
                  }
               }
               element.dataset.view = "0";
            } else {
               element.dataset.view = "0";
            }
         });
      };

      /********************
		スクロールイベントハンドラ
		********************/
      const handleScrollEvent = () => {
         //wheelイベント中とボタンからのスクロール中は呼び出さない
         if (
            wheelState.isWheeling === true ||
            buttonState.isButtonScrolling === true
         ) {
            return;
         }
         //スクロール中
         scrollState.isScrolling = true;

         //rAF
         if (!scrollState.ticking) {
            window.requestAnimationFrame(() => {
               scrollControl();
               scrollState.scrollBarPrevent = true;
               scrollState.ticking = false;
            });
            scrollState.ticking = true;
         }
         //scrollbarでの操作時、連続でスクロールイベントが呼ばれないようにする
         clearTimeout(scrollState.scrollBarPreventTimeOutID);
         scrollState.scrollBarPreventTimeOutID = setTimeout(() => {
            scrollState.scrollBarPrevent = false;
         }, 100);

         //位置修正
         correctPos();
         //スクロール中の解除
         scrollState.isScrolling = false;
      };

      this.scrollEvent = handleScrollEvent;
      this.scrollEvent();
      document.addEventListener("scroll", this.scrollEvent);

      /*===============================================
		resize対応
		===============================================*/
      //リサイズを監視
      let resizeTimeOutID = 0;
      const handleResizeEvent = () => {
         //リサイズ動作を終えてから1秒後に発火
         clearTimeout(resizeTimeOutID);
         resizeTimeOutID = setTimeout(() => {
            setSceneHeight();
            handleScrollEvent();
         }, 200);
      };
      this.resizeEvent = handleResizeEvent;
      window.addEventListener("resize", this.resizeEvent);
   }
   unmount() {
      /*===============================================
		イベント解除
		===============================================*/
      //リサイズ
      window.removeEventListener("resize", this.resizeEvent);
      //スクロール
      document.removeEventListener("scroll", this.scrollEvent);
      //ホイールとタッチ
      this.EVENTARRY.forEach((element) => {
         document.removeEventListener(element, this.wheelEvent, {
            passive: false,
         });
      });
      if (this.isMobile) {
         this.TOUCHEVENTARRY.forEach((element) => {
            document.removeEventListener(element, this.touchEvent);
         });
      }
      /*===============================================
		datasetの解除
		===============================================*/
      this.SCENES.forEach((element) => {
         delete element.dataset.view;
         delete element.dataset.intersectTop;
         delete element.dataset.intersectBottom;
      });
      /*===============================================
		慣性スクロールの初期化
		===============================================*/
      inertiaScrollInit(true);
   }
   on(hook, event) {
      switch (hook) {
         case "wheel":
            this.wheelingAnimation = event;
            break;
         case "leave":
            this.leaveAnimation = event;
            break;
         case "enter":
            this.enterAnimation = event;
            break;
         case "finish":
            this.finishAnimation = event;
            break;
         case "restart":
            this.restartAnimation = event;
            break;
         default:
            break;
      }
   }
}
doctype html
html(lang="ja")
	include ../includes/common/_head.pug
body
	div.ly_wrapper
		main.ly_main
			section.hogehoge.__green
			section.hogehoge.__red
			section#js_fixedView.bl_fixedView
				.fixedView_scene.js_scene
					.fixedView_inner
						p.js_animTarget 1
						p.js_animTarget 1
						p.js_animTarget 1
						p.js_animTarget 1
				.fixedView_scene.js_scene
					.fixedView_inner
						p.js_animTarget 2
						p.js_animTarget 2
						p.js_animTarget 2
						p.js_animTarget 2
				.fixedView_scene.js_scene
					.fixedView_inner
						p.js_animTarget 3
						p.js_animTarget 3
						p.js_animTarget 3
						p.js_animTarget 3
				.fixedView_scene.js_scene
					.fixedView_inner
						p.js_animTarget 4
						p.js_animTarget 4
						p.js_animTarget 4
						p.js_animTarget 4
				.bl_fixedView_button
					button.js_button 1
					button.js_button 2
					button.js_button 3
					button.js_button 4
			section.hogehoge.__red
			section.hogehoge.__green
	include ../includes/common/_src.pug
import { FixedView } from "./modules/byTakuma/class/FixedView";
import { gsap } from "gsap";
//画面固定
const VIEWTARGET = document.getElementById("js_fixedView");
const SCENES = [...VIEWTARGET.getElementsByClassName("js_scene")];
const BUTTONS = [...VIEWTARGET.getElementsByClassName("js_button")];

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

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);
});

/*===============================================
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);
   });
});