原生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; } }); });
关键改动说明
- 跟踪单个图片的状态:不再用单个
activeCursor,而是给每张图片创建独立的currentX/currentY状态,存储在activeImagesState里。 - 反向更新图片位置:从最后一张图片往前更新,这样后面的图片能准确跟随前一张的实时位置,形成自然拖尾。
- 渐变的缓动速度:每张图片的缓动速度比前一张略慢,用
trailLag控制滞后程度,让拖尾效果更明显。 - 优化鼠标切换逻辑:切换网格元素时,重置上一个元素的所有图片位置,避免状态混乱。
你可以调整baseSpeed和trailLag的值来控制拖尾的流畅度和长度,也可以给图片加一点点缩放或者旋转的变化,让效果更生动~
备注:内容来源于stack exchange,提问作者Mathieu Préaud




