You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

原生JavaScript实现网格元素的多图片鼠标跟随轨迹效果

原生JavaScript实现网格元素的多图片鼠标跟随轨迹效果

嘿,我懂你想要的效果啦——每个网格卡片上,鼠标移动时会有一串图片跟着形成拖尾,而且不用任何库对吧?你现在的代码已经搞定了单个元素的缓动跟随,但还没让多张图片形成依次滞后的轨迹,我来帮你把这个效果补全~

核心思路

要实现多图片拖尾,关键是给每一张图片都独立维护位置状态,让它们的目标位置依次滞后于前一个元素的当前位置,这样就能形成自然的跟随轨迹了。我们不用改太多现有代码,只需要调整跟踪逻辑,把单个cursor的处理改成一组图片的批量处理就行。

完整实现代码

HTML

<div class="grid">
  
  <div class="grid__item js-post">
    <div class="grid__item-number">1</div>
    <div class="grid__item-cursor js-cursor">
       <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/13/18/09/canyon-7589820_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/02/22/33/autumn-7566201_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2023/04/05/09/44/landscape-7901065_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2020/09/04/16/18/mountains-5544365_1280.jpg">
    </div>
  </div>
  
  <div class="grid__item js-post">
    <div class="grid__item-number">2</div>
    <div class="grid__item-cursor js-cursor">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/13/18/09/canyon-7589820_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/02/22/33/autumn-7566201_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2023/04/05/09/44/landscape-7901065_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2020/09/04/16/18/mountains-5544365_1280.jpg">
    </div>
  </div>
  
  <div class="grid__item js-post">
    <div class="grid__item-number">3</div>
    <div class="grid__item-cursor js-cursor">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/13/18/09/canyon-7589820_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/02/22/33/autumn-7566201_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2023/04/05/09/44/landscape-7901065_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2020/09/04/16/18/mountains-5544365_1280.jpg">
    </div>
  </div>
  
  <div class="grid__item js-post">
    <div class="grid__item-number">4</div>
    <div class="grid__item-cursor js-cursor">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/13/18/09/canyon-7589820_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2022/11/02/22/33/autumn-7566201_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2023/04/05/09/44/landscape-7901065_1280.jpg">
      <img class="grid__item-image js-image" src="https://cdn.pixabay.com/photo/2020/09/04/16/18/mountains-5544365_1280.jpg">
    </div>
  </div>
  
</div>

CSS

body{
  font-family: 'helvetica', arial, sans-serif;
}

.grid{
  display: grid;
  width: 100%;
  grid-template-columns: repeat(2, 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;
  width: 150px;
  height: 200px;
  pointer-events: none;
  z-index: -1;
  opacity: 0;
  transition: opacity .3s ease .1s;
}

.grid__item-cursor.is-visible{
  z-index: 1;
  opacity: 1;
}

.grid__item-image{
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* 给图片加一点点透明度,拖尾更自然 */
  opacity: 0.8;
}

JavaScript

const posts = document.querySelectorAll('.js-post');

let activePost = null;
// 改成存储当前激活的图片组状态,而不是单个cursor
let activeImagesState = null;
const baseSpeed = 0.2;
// 拖尾的滞后系数,每张图片比前一个慢一点
const trailLag = 0.15;

const animate = () => {
  if (activeImagesState) {
    // 从最后一张图片往前更新,让每张图片跟随前一张的位置
    for (let i = activeImagesState.images.length - 1; i >= 0; i--) {
      const imgState = activeImagesState.states[i];
      // 第一张图片跟随鼠标目标位置,后面的跟随前一张的当前位置
      const targetX = i === 0 ? activeImagesState.aimX : activeImagesState.states[i-1].currentX;
      const targetY = i === 0 ? activeImagesState.aimY : activeImagesState.states[i-1].currentY;
      
      // 每张图片的缓动速度依次降低,形成拖尾
      const speed = baseSpeed - (i * trailLag);
      
      imgState.currentX += (targetX - imgState.currentX) * speed;
      imgState.currentY += (targetY - imgState.currentY) * speed;
      
      activeImagesState.images[i].style.left = imgState.currentX + 'px';
      activeImagesState.images[i].style.top = imgState.currentY + 'px';
    }
  }
  requestAnimationFrame(animate);
};

animate();

posts.forEach(post => {
  const images = post.querySelectorAll('.js-image');
  
  post.addEventListener('mouseenter', (e) => {
    // 隐藏之前的拖尾
    if (activePost && activePost !== post && activeImagesState) {
      activePost.querySelector('.js-cursor').classList.remove('is-visible');
      // 重置之前的图片位置
      activeImagesState.images.forEach((img, i) => {
        img.style.left = '0px';
        img.style.top = '0px';
        activeImagesState.states[i].currentX = 0;
        activeImagesState.states[i].currentY = 0;
      });
    }
    
    activePost = post;
    const rect = post.getBoundingClientRect();
    const initialX = e.clientX - rect.left;
    const initialY = e.clientY - rect.top;
    
    // 初始化每张图片的位置状态
    const imageStates = Array.from(images).map(() => ({
      currentX: initialX,
      currentY: initialY
    }));
    
    activeImagesState = {
      images: images,
      states: imageStates,
      aimX: initialX,
      aimY: initialY
    };
    
    // 立即定位所有图片到鼠标位置
    images.forEach((img, i) => {
      img.style.left = initialX + 'px';
      img.style.top = initialY + 'px';
    });
    
    post.querySelector('.js-cursor').classList.add('is-visible');
  });

  post.addEventListener('mousemove', (e) => {
    if (activePost === post && activeImagesState) {
      const rect = post.getBoundingClientRect();
      activeImagesState.aimX = e.clientX - rect.left;
      activeImagesState.aimY = e.clientY - rect.top;
    }
  });

  post.addEventListener('mouseleave', () => {
    if (activePost === post && activeImagesState) {
      post.querySelector('.js-cursor').classList.remove('is-visible');
      // 重置所有图片位置和状态
      activeImagesState.images.forEach((img, i) => {
        img.style.left = '0px';
        img.style.top = '0px';
        activeImagesState.states[i].currentX = 0;
        activeImagesState.states[i].currentY = 0;
      });
      activeImagesState = null;
      activePost = null;
    }
  });
});

关键改动说明

  1. 跟踪单个图片的状态:不再用单个activeCursor,而是给每张图片创建独立的currentX/currentY状态,存储在activeImagesState里。
  2. 反向更新图片位置:从最后一张图片往前更新,这样后面的图片能准确跟随前一张的实时位置,形成自然拖尾。
  3. 渐变的缓动速度:每张图片的缓动速度比前一张略慢,用trailLag控制滞后程度,让拖尾效果更明显。
  4. 优化鼠标切换逻辑:切换网格元素时,重置上一个元素的所有图片位置,避免状态混乱。

你可以调整baseSpeedtrailLag的值来控制拖尾的流畅度和长度,也可以给图片加一点点缩放或者旋转的变化,让效果更生动~

备注:内容来源于stack exchange,提问作者Mathieu Préaud

火山引擎 最新活动