javascriptaddeventlistenermousemovemouse-cursor

Multiple cursors follow mouse in Javascript


I have a grid with multiple items. Each item has its own custom cursor with the div .grid__item-cursor.

What I'm trying to achieve is when hovering an item, its cursor follows the mouse with a smooth speed.

The issue is when hovering an item, all the cursors from the document move at the same time. And there is also too much distance between the custom cursor and the mouse.

const cursors = document.querySelectorAll(".js-cursor");

let aimX = 0;
let aimY = 0;

cursors.forEach((cursor) => {
  let currentX = 0;
  let currentY = 0;

  let speed = 0.2;

  const animate = function () {
    currentX += (aimX - currentX) * speed;
    currentY += (aimY - currentY) * speed;

    cursor.style.left = currentX + "px";
    cursor.style.top = currentY + "px";

    requestAnimationFrame(animate);
  };

  animate();
});

const posts = document.querySelectorAll(".js-post");
posts.forEach((post) => {
  const cursor = post.querySelector(".js-cursor");

  post.addEventListener("mousemove", function (event) {
    aimX = event.pageX;
    aimY = event.pageY;
  });

  post.addEventListener("mouseenter", function () {
    cursor.classList.add("is-visible");
  });

  post.addEventListener("mouseleave", function () {
    cursor.classList.remove("is-visible");
  });
});
body {
  font-family: "helvetica", arial, sans-serif;
}

.grid {
  display: grid;
  width: 100%;
  grid-template-columns: repeat(4, 1fr);
  grid-column-gap: 1rem;
  grid-row-gap: 1rem;
}

.grid__item {
  display: flex;
  justify-content: center;
  align-content: center;
  position: relative;
  padding: 25%;
  overflow: hidden;
  background-color: #333;
}

.grid__item-number {
  color: #888;
  font-size: 5rem;
}

.grid__item-cursor {
  position: absolute;
  top: 0;
  left: 0;
  padding: 0.25em;
  background-color: red;
}

.grid__item-cursor.is-visible {
  background-color: yellow;
}
<div class="grid">
  <div class="grid__item js-post">
    <div class="grid__item-number">1</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
  <div class="grid__item js-post">
    <div class="grid__item-number">2</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
  <div class="grid__item js-post">
    <div class="grid__item-number">3</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
  <div class="grid__item js-post">
    <div class="grid__item-number">4</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
  <div class="grid__item js-post">
    <div class="grid__item-number">5</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
  <div class="grid__item js-post">
    <div class="grid__item-number">6</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
  <div class="grid__item js-post">
    <div class="grid__item-number">7</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
  <div class="grid__item js-post">
    <div class="grid__item-number">8</div>
    <div class="grid__item-cursor js-cursor">Read more</div>
  </div>
</div>


Solution

  • Here is a version that only follows the mouse in the grid element. When the mouse leaves the element, the position of the read READ ME is reset to 0,0 of the grid element. I wanted to delegate but the event bubbling made that non-trivial

    const posts = document.querySelectorAll('.js-post');
    
    let activePost = null;
    let activeCursor = null;
    let currentX = 0, currentY = 0;
    let aimX = 0, aimY = 0;
    const speed = 0.2;
    
    const animate = () => {
      if (activeCursor) {
        currentX += (aimX - currentX) * speed;
        currentY += (aimY - currentY) * speed;
        activeCursor.style.left = currentX + 'px';
        activeCursor.style.top = currentY + 'px';
      }
      requestAnimationFrame(animate);
    };
    
    animate();
    
    posts.forEach(post => {
      post.addEventListener('mouseenter', (e) => {
        // Hide the previous grid element's cursor immediately, if any.
        if (activePost && activePost !== post && activeCursor) {
          activeCursor.classList.remove('is-visible');
          // Reset the previous cursor to 0,0 relative to its container.
          activeCursor.style.left = '0px';
          activeCursor.style.top = '0px';
        }
        activePost = post;
        activeCursor = post.querySelector('.js-cursor');
    
        // Get grid item's bounding rectangle for local coordinate conversion.
        const rect = post.getBoundingClientRect();
        currentX = e.clientX - rect.left;
        currentY = e.clientY - rect.top;
        aimX = currentX;
        aimY = currentY;
        
        // Position the cursor immediately at the mouse's location.
        activeCursor.style.left = currentX + 'px';
        activeCursor.style.top = currentY + 'px';
        activeCursor.classList.add('is-visible');
      });
    
      post.addEventListener('mousemove', (e) => {
        if (activePost === post && activeCursor) {
          const rect = post.getBoundingClientRect();
          aimX = e.clientX - rect.left;
          aimY = e.clientY - rect.top;
        }
      });
    
      post.addEventListener('mouseleave', () => {
        if (activePost === post && activeCursor) {
          activeCursor.classList.remove('is-visible');
          // Reset the coordinates to the top-left (0,0) of the grid element.
          activeCursor.style.left = '0px';
          activeCursor.style.top = '0px';
          // Also reset the internal coordinates so the next activation starts from 0,0.
          currentX = 0;
          currentY = 0;
          aimX = 0;
          aimY = 0;
          activePost = null;
          activeCursor = null;
        }
      });
    });
    body {
      font-family: "helvetica", arial, sans-serif;
    }
    
    .grid {
      display: grid;
      width: 100%;
      grid-template-columns: repeat(4, 1fr);
      grid-column-gap: 1rem;
      grid-row-gap: 1rem;
    }
    
    .grid__item {
      display: flex;
      justify-content: center;
      align-content: center;
      position: relative;
      padding: 25%;
      background-color: #333;
    }
    
    .grid__item-number {
      color: #888;
      font-size: 5rem;
    }
    
    .grid__item-cursor {
      position: absolute;
      top: 0;
      left: 0;
      padding: 0.25em;
      background-color: red;
    }
    
    .grid__item-cursor.is-visible {
      background-color: yellow;
    }
    <div class="grid">
      <div class="grid__item js-post">
        <div class="grid__item-number">1</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
      <div class="grid__item js-post">
        <div class="grid__item-number">2</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
      <div class="grid__item js-post">
        <div class="grid__item-number">3</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
      <div class="grid__item js-post">
        <div class="grid__item-number">4</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
      <div class="grid__item js-post">
        <div class="grid__item-number">5</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
      <div class="grid__item js-post">
        <div class="grid__item-number">6</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
      <div class="grid__item js-post">
        <div class="grid__item-number">7</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
      <div class="grid__item js-post">
        <div class="grid__item-number">8</div>
        <div class="grid__item-cursor js-cursor">Read more</div>
      </div>
    </div>