Search code examples
javascriptjquerycsscursormouse

jQuery: mouse following element wont stick with cursor when scrolling


I have a small problem and I can't get it to fix.

I developed a element that is following the cursor and way it needs to function is that the border around the cursor needs to stick with the position of the cursor. But the problem I've got right now is that it won't stick when scrolling down.

You can check at the demo below what I mean.

The problem seems that it is not correctly checking the height of the page, thats why its not correctly positioning. Am I right?

const windowW = window.innerWidth;
const windowH = window.innerHeight;
const maxLength = Math.max(windowW, windowH);

const cursorWidth = 100;
const cursorR = cursorWidth >> 1;
const cursorDelay = 10;

const buttons = Array.from(document.querySelectorAll('.border-button'));

const cursor = {
    el: document.querySelector('.border-cursor'),
    x: windowW >> 1,
    y: windowH >> 1,
    scaleX: 1,
    scaleY: 1,
};

const target = {
    x: windowW >> 1,
    y: windowH >> 1,
    width: cursorWidth,
    followMouse: true,
};

const norm = (val, max, min) => (val - min) / (max - min);
const toDegrees = r => r * (180 / Math.PI);
const distanceBetween = (v1, v2) => Math.sqrt((v1.x - v2.x) * (v1.x - v2.x) + (v1.y - v2.y) * (v1.y +- v2.y));

const loop = () => {
    const destX = target.x - cursorR;
    const destY = target.y - cursorR;

    const newX = cursor.x + ((destX - cursor.x) / cursorDelay);
    const newY = cursor.y + ((destY - cursor.y) / cursorDelay);
    const angle = angleBetween(cursor.x, cursor.y, newX, newY);

    if (target.followMouse) {
        const distance = Math.abs(distanceBetween(target, cursor));
        const scale = norm(distance, maxLength, cursorR);
        cursor.scaleX = 1 + scale;
        cursor.scaleY = 1 - scale;
    } else {
        const targetScale = target.width / cursorWidth;

        cursor.scaleX += (targetScale - cursor.scaleX) / (cursorDelay / 2);
        cursor.scaleY = cursor.scaleX;
    }

    cursor.x = newX;
    cursor.y = newY;

    cursor.el.style.transform = `translate(${cursor.x}px, ${cursor.y}px) rotate(${toDegrees(angle)}deg) scale(${cursor.scaleX}, ${cursor.scaleY})`;

    requestAnimationFrame(loop);
};

const angleBetween = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1);

const onPointerMove = (e) => {
    if (!target.followMouse) {
        return;
    }

    const pointer = (e.touches && e.touches.length) ? e.touches[0] : e;
    const { clientX: x, clientY: y } = pointer;

    target.x = x;
    target.y = y;
};

const onPointerOver = (e) => {
    const btn = e.target;
    const rect = btn.getBoundingClientRect();

    target.followMouse = false;
    target.x = rect.left + (rect.width >> 1);
    target.y = rect.top + (rect.height >> 1);

    target.width = Math.max(rect.width, rect.height) + 50;
};

const onPointerOut = () => {
    target.followMouse = true;
    target.width = cursorWidth;
};

document.body.addEventListener('mousemove', onPointerMove);
document.body.addEventListener('touchmove', onPointerMove);

buttons.forEach((btn) => {
    btn.addEventListener('touchstart', onPointerOver);
    btn.addEventListener('mouseover', onPointerOver);

    btn.addEventListener('touchend', onPointerOut);
    btn.addEventListener('mouseout', onPointerOut);
});

loop();

const windowW = window.innerWidth;
const windowH = window.innerHeight;
const maxLength = Math.max(windowW, windowH);

const cursorWidth = 100;
const cursorR = cursorWidth >> 1;
const cursorDelay = 10;

const buttons = Array.from(document.querySelectorAll('.border-button'));

const cursor = {
	el: document.querySelector('.border-cursor'),
	x: windowW >> 1,
	y: windowH >> 1,
	scaleX: 1,
	scaleY: 1,
};

const target = {
	x: windowW >> 1,
	y: windowH >> 1,
	width: cursorWidth,
	followMouse: true,
};

const norm = (val, max, min) => (val - min) / (max - min);
const toDegrees = r => r * (180 / Math.PI);
const distanceBetween = (v1, v2) => Math.sqrt((v1.x - v2.x) * (v1.x - v2.x) + (v1.y - v2.y) * (v1.y +- v2.y));

const loop = () => {
	const destX = target.x - cursorR;
	const destY = target.y - cursorR;

	const newX = cursor.x + ((destX - cursor.x) / cursorDelay);
	const newY = cursor.y + ((destY - cursor.y) / cursorDelay);
	const angle = angleBetween(cursor.x, cursor.y, newX, newY);

	if (target.followMouse) {
		const distance = Math.abs(distanceBetween(target, cursor));
		const scale = norm(distance, maxLength, cursorR);
		cursor.scaleX = 1 + scale;
		cursor.scaleY = 1 - scale;
	} else {
		const targetScale = target.width / cursorWidth;

		cursor.scaleX += (targetScale - cursor.scaleX) / (cursorDelay / 2);
		cursor.scaleY = cursor.scaleX;
	}

	cursor.x = newX;
	cursor.y = newY;

	cursor.el.style.transform = `translate(${cursor.x}px, ${cursor.y}px) rotate(${toDegrees(angle)}deg) scale(${cursor.scaleX}, ${cursor.scaleY})`;

	requestAnimationFrame(loop);
};

const angleBetween = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1);

const onPointerMove = (e) => {
	if (!target.followMouse) {
		return;
	}

	const pointer = (e.touches && e.touches.length) ? e.touches[0] : e;
	const { clientX: x, clientY: y } = pointer;

	target.x = x;
	target.y = y;
};

const onPointerOver = (e) => {
	const btn = e.target;
	const rect = btn.getBoundingClientRect();

	target.followMouse = false;
	target.x = rect.left + (rect.width >> 1);
	target.y = rect.top + (rect.height >> 1);

	target.width = Math.max(rect.width, rect.height) + 50;
};

const onPointerOut = () => {
	target.followMouse = true;
	target.width = cursorWidth;
};

document.body.addEventListener('mousemove', onPointerMove);
document.body.addEventListener('touchmove', onPointerMove);

buttons.forEach((btn) => {
	btn.addEventListener('touchstart', onPointerOver);
	btn.addEventListener('mouseover', onPointerOver);

	btn.addEventListener('touchend', onPointerOut);
	btn.addEventListener('mouseout', onPointerOut);
});

loop();
html,
body {
  margin: 0;
  padding: 0;
}

.wrapper {
  width: 100vw;
  min-height: 1500px;
  display: flex;
  flex-direction: row;
  align-items: center;
}

.container {
  width: 100%;
  display: flex;
  padding: 0 1rem;
}

.cursor {
  position: absolute;
  z-index: 10;
  width: 100px;
  height: 100px;
  border: 2px solid #23bfa0;
  border-radius: 50%;

  pointer-events: none;
}

.button {
  padding: 1rem;

  background-color: #23bfa0;
  border: none;
  box-shadow: 0 0 7px 0px rgba(0, 0, 0, 0.2);

  color: white;
  font-size: 1.2rem;

  cursor: pointer;

  transition: box-shadow 0.1s ease-in, transform 0.1s ease-in;
  
  &--small {
    padding: 0.75rem;
    font-size: 0.75rem;
  }
  
  &:hover {
    transform: translate(0%, -2px);
    box-shadow: 0px 4px 9px 2px rgba(0, 0, 0, 0.2)
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<div class="cursor border-cursor"></div>

<div class="wrapper">
  <div class="container">
    <button class="button button--small border-button">small</button>
    <button class="button border-button">hover me</button>
    <button class="button border-button">hover me more</button>
  </div>
</div>
</body>


Solution

  • In onPointerMove, try replacing:

    const { clientX: x, clientY: y } = pointer;
    

    with:

    const { pageX: x, pageY: y } = pointer;
    

    Here's a good post explaining the differences between these values: https://stackoverflow.com/a/9335517/965834

    Also, change:

    target.x = rect.left + (rect.width >> 1);
    target.y = rect.top + (rect.height >> 1);
    

    into:

    target.x = window.scrollX + rect.left + (rect.width >> 1);
    target.y = window.scrollY + rect.top + (rect.height >> 1);
    

    This takes into account scrolling when calculating the position of your buttons.

    Demo:

    const windowW = window.innerWidth;
    const windowH = window.innerHeight;
    const maxLength = Math.max(windowW, windowH);
    
    const cursorWidth = 100;
    const cursorR = cursorWidth >> 1;
    const cursorDelay = 10;
    
    const buttons = Array.from(document.querySelectorAll('.border-button'));
    
    const cursor = {
    	el: document.querySelector('.border-cursor'),
    	x: windowW >> 1,
    	y: windowH >> 1,
    	scaleX: 1,
    	scaleY: 1,
    };
    
    const target = {
    	x: windowW >> 1,
    	y: windowH >> 1,
    	width: cursorWidth,
    	followMouse: true,
    };
    
    const norm = (val, max, min) => (val - min) / (max - min);
    const toDegrees = r => r * (180 / Math.PI);
    const distanceBetween = (v1, v2) => Math.sqrt((v1.x - v2.x) * (v1.x - v2.x) + (v1.y - v2.y) * (v1.y +- v2.y));
    
    const loop = () => {
    	const destX = target.x - cursorR;
    	const destY = target.y - cursorR;
    
    	const newX = cursor.x + ((destX - cursor.x) / cursorDelay);
    	const newY = cursor.y + ((destY - cursor.y) / cursorDelay);
    	const angle = angleBetween(cursor.x, cursor.y, newX, newY);
    
    	if (target.followMouse) {
    		const distance = Math.abs(distanceBetween(target, cursor));
    		const scale = norm(distance, maxLength, cursorR);
    		cursor.scaleX = 1 + scale;
    		cursor.scaleY = 1 - scale;
    	} else {
    		const targetScale = target.width / cursorWidth;
    
    		cursor.scaleX += (targetScale - cursor.scaleX) / (cursorDelay / 2);
    		cursor.scaleY = cursor.scaleX;
    	}
    
    	cursor.x = newX;
    	cursor.y = newY;
    
    	cursor.el.style.transform = `translate(${cursor.x}px, ${cursor.y}px) rotate(${toDegrees(angle)}deg) scale(${cursor.scaleX}, ${cursor.scaleY})`;
    
    	requestAnimationFrame(loop);
    };
    
    const angleBetween = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1);
    
    const onPointerMove = (e) => {
    	if (!target.followMouse) {
    		return;
    	}
    
    	const pointer = (e.touches && e.touches.length) ? e.touches[0] : e;
    	const { pageX: x, pageY: y } = pointer;
    
    	target.x = x;
    	target.y = y;
    };
    
    const onPointerOver = (e) => {
    	const btn = e.target;
    	const rect = btn.getBoundingClientRect();
    
    	target.followMouse = false;
    	target.x = window.scrollX + rect.left + (rect.width >> 1);
    	target.y = window.scrollY + rect.top + (rect.height >> 1);
    
    	target.width = Math.max(rect.width, rect.height) + 50;
    };
    
    const onPointerOut = () => {
    	target.followMouse = true;
    	target.width = cursorWidth;
    };
    
    document.body.addEventListener('mousemove', onPointerMove);
    document.body.addEventListener('touchmove', onPointerMove);
    
    buttons.forEach((btn) => {
    	btn.addEventListener('touchstart', onPointerOver);
    	btn.addEventListener('mouseover', onPointerOver);
    
    	btn.addEventListener('touchend', onPointerOut);
    	btn.addEventListener('mouseout', onPointerOut);
    });
    
    loop();
    html,
    body {
      margin: 0;
      padding: 0;
    }
    
    .wrapper {
      width: 100vw;
      min-height: 1500px;
      display: flex;
      flex-direction: row;
      align-items: center;
    }
    
    .container {
      width: 100%;
      display: flex;
      padding: 0 1rem;
    }
    
    .cursor {
      position: absolute;
      z-index: 10;
      width: 100px;
      height: 100px;
      border: 2px solid #23bfa0;
      border-radius: 50%;
    
      pointer-events: none;
    }
    
    .button {
      padding: 1rem;
    
      background-color: #23bfa0;
      border: none;
      box-shadow: 0 0 7px 0px rgba(0, 0, 0, 0.2);
    
      color: white;
      font-size: 1.2rem;
    
      cursor: pointer;
    
      transition: box-shadow 0.1s ease-in, transform 0.1s ease-in;
      
      &--small {
        padding: 0.75rem;
        font-size: 0.75rem;
      }
      
      &:hover {
        transform: translate(0%, -2px);
        box-shadow: 0px 4px 9px 2px rgba(0, 0, 0, 0.2)
      }
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <body>
    <div class="cursor border-cursor"></div>
    
    <div class="wrapper">
      <div class="container">
        <button class="button button--small border-button">small</button>
        <button class="button border-button">hover me</button>
        <button class="button border-button">hover me more</button>
      </div>
    </div>
    </body>