<div class="wrapper">
  <div class="js_stickBtn button"></div>
  <div class="js_stickBtn_low button __low"></div>
</div>
.wrapper{
  display:flex;
  align-items:center;
  justify-content:center;
  height:100vh;
}
.button{
  width:8rem;
  height:8rem;
  background-color:red;
  border-radius:50%;
  margin:0 2rem;
}
.button.__low{
  background-color:blue;
}
const durationVal = 0.6;

//タッチデバイス判定
const isTouch = () => {
   const touch_event = window.ontouchstart;
   const touch_points = navigator.maxTouchPoints;

   if (touch_event !== undefined && 0 < touch_points) {
      return true;
   } else {
      return false;
   }
};

//rAF制御
class LimitFrameRate {
   constructor(framesPerSecond) {
      this.interval = Math.floor(1000 / framesPerSecond);
      this.previousTime = performance.now();
   }
   isLimitFrames(timestamp) {
      const deltaTime = timestamp - this.previousTime;
      const isLimitOver = deltaTime <= this.interval;
      if (!isLimitOver) {
         this.previousTime = timestamp - (deltaTime % this.interval);
      }
      return isLimitOver;
   }
}

//くっつくボタンのclass
class StickBtn {
   constructor(target, easeMove, fps) {
      this.mouseX = 0;
      this.mouseY = 0;
      this.targetOffsetX = 0;
      this.targetOffsetY = 0;
      this.easeMove = easeMove;
      this.limitFrames = new LimitFrameRate(fps);

      this.target = target;
      this.id = null;

      this.stickAnimLoop = function (timestamp) {
         if (this.limitFrames.isLimitFrames(timestamp)) {
            this.start();
            return;
         }
         console.log('tick');
         let xTo = gsap.quickTo(this.target, 'x', {
               duration: durationVal,
               ease: 'power1.out',
            }),
            yTo = gsap.quickTo(this.target, 'y', {
               duration: durationVal,
               ease: 'power1.out',
            });
         xTo(this.mouseX * this.easeMove);
         yTo(this.mouseY * this.easeMove);
         this.start();
      };

      this.start = function () {
         this.id = requestAnimationFrame(this.stickAnimLoop.bind(this));
      };
      this.stop = function () {
         cancelAnimationFrame(this.id);
         gsap.to(this.target, {
            x: 0,
            y: 0,
            duration: durationVal,
            ease: 'back.out(4)',
            overwrite: true,
         });
      };
      this.mouseEnter = function (e) {
         e.stopPropagation();
         this.targetOffsetX = e.target.clientWidth / 2;
         this.targetOffsetY = e.target.clientHeight / 2;
         this.start();
      };
      this.mouseEnterInit = this.mouseEnter.bind(this);
      this.mouseMove = function (e) {
         e.stopPropagation();
         this.mouseX = e.offsetX - this.targetOffsetX;
         this.mouseY = e.offsetY - this.targetOffsetY;
      };
      this.mouseMoveInit = this.mouseMove.bind(this);
      this.mouseOut = function (e) {
         e.stopPropagation();
         this.mouseX = 0;
         this.mouseY = 0;
         this.stop();
      };
      this.mouseOutInit = this.mouseOut.bind(this);
   }

   //登録メソッド
   add() {
      if (!isTouch()) {
         this.target.addEventListener('mouseenter', this.mouseEnterInit);
         this.target.addEventListener('mousemove', this.mouseMoveInit);
         this.target.addEventListener('mouseleave', this.mouseOutInit);
      } else {
         return;
      }
   }
   //解除メソッド
   remove() {
      this.target.removeEventListener('mouseenter', this.mouseEnterInit);
      this.target.removeEventListener('mousemove', this.mouseMoveInit);
      this.target.removeEventListener('mouseleave', this.mouseOutInit);
   }
}



//初期化
//インスタンスを格納する配列
let stickBtnInstanceArr = [];
 
const stickBtnInit = (isAdd) => {
   if (isAdd) {
      const highTarget = [...document.getElementsByClassName('js_stickBtn')];
      const lowTarget = [...document.getElementsByClassName('js_stickBtn_low')];
      if (highTarget.length) {
         highTarget.forEach((element) => {
            let stickBtn = new StickBtn(element, 1, 60);
            stickBtnInstanceArr.push(stickBtn);
            stickBtn.add();
         });
      }
      if (lowTarget.length) {
         lowTarget.forEach((element) => {
            let stickBtn = new StickBtn(element, 0.08, 60);
            stickBtnInstanceArr.push(stickBtn);
            stickBtn.add();
         });
      }
   } else {
      if (stickBtnInstanceArr.length) {
         stickBtnInstanceArr.forEach((element) => {
            element.remove();
         });
         stickBtnInstanceArr = [];
      }
   }
};

stickBtnInit(true);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.co/gsap@3/dist/gsap.min.js