1

I'm trying to create multiple image trails for a grid. Each trail follow the cursor, I found an example on Codepen to illustrate the effect I'm looking for. The example is made with GSAP, but for this project I would prefer not using any libraries.

I've success to make the block of images following the cursor but I don't find the way to reproduce the effect in Javascript.

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(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; transform: translate(-50%, -50%); 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; }
<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>

1
  • "I've success to make the block of images following the cursor but I don't find the way to reproduce the effect in Javascript." Can you please rephrase the question...I do not grok. 😕 Commented Feb 24 at 12:20

2 Answers 2

2

To create a staggered effect of images following the pointer, each image should interpolate its current position towards the position of the next image. Obviously, since the first image does not have any following one, that element position interpolates towards the pointer's XY:

newX = lerp(currentX, targetX, factor); 

(where targetX is either the pointer's X or the following Element's X).

To retrieve the current position and assign the new x, y I've used the CSS Properties (CSS Vars) with .getPropertyValue() and .setProperty() respectively.

const lerp = (curr, target, factor) => curr + (target - curr) * factor; const factor = 0.15; // Higher value = faster const pointer = { x: 0, y: 0, update({pageX, pageY, currentTarget}) { this.x = pageX - currentTarget.offsetLeft; this.y = pageY - currentTarget.offsetTop; } }; let raf; let elsImages; const animate = (isLerp) => { let {x, y} = pointer; // Init with the Pointer's XY coordinates elsImages.forEach((el) => { const xNew = lerp(+(el.style.getPropertyValue("--x") ?? 0), x, isLerp ? factor : 1); const yNew = lerp(+(el.style.getPropertyValue("--y") ?? 0), y, isLerp ? factor : 1); el.style.setProperty("--x", xNew); el.style.setProperty("--y", yNew); x = xNew; // then, store image's coordinates y = yNew; }); raf = requestAnimationFrame(animate); }; const start = (evt) => { pointer.update(evt); elsImages = evt.currentTarget.querySelectorAll(".js-image"); animate(false); evt.currentTarget.classList.add("is-visible"); }; const move = (evt) => { pointer.update(evt); }; const stop = (evt) => { elsImages = undefined; cancelAnimationFrame(raf); evt.currentTarget.classList.remove("is-visible"); }; document.querySelectorAll(".js-post").forEach((elBox) => { elBox.addEventListener("pointerenter", start); elBox.addEventListener("pointermove", move); elBox.addEventListener("pointerleave", stop); });
body{ font-family: "helvetica", arial, sans-serif; } .grid{ display: grid; width: 100%; grid-template-columns: repeat(2, 1fr); touch-action: none; gap: 1rem; } .grid__item{ position: relative; display: flex; justify-content: center; align-content: center; overflow: hidden; background-color: #333; aspect-ratio: 1; &::before { content: attr(data-number); color: #888; font-size: 5rem; margin: auto; } } .grid__item-image{ position: absolute; pointer-events: none; top: 0; left: 0; width: 50%; height: 50%; object-fit: cover; opacity: 0; transform: translate(-50%, -50%); translate: calc(var(--x) * 1px) calc(var(--y) * 1px); transition: opacity .3s; .is-visible & { opacity: 1; } }
<div class="grid"> <div class="grid__item js-post" data-number="1"> <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 class="grid__item js-post" data-number="2"> <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 class="grid__item js-post" data-number="3"> <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 class="grid__item js-post" data-number="4"> <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>

PS, I've modified your HTML (removed unnecessary elements) and simplified as well the CSS. And it also works on touch devices.

Sign up to request clarification or add additional context in comments.

14 Comments

Thanks a lot for your help @Roko C. Buljan. It works great on Firefox and Chrome, but for any reason the images don't show up in Safari (v15).
I mean the browser Safari in desktop version.
@MathieuPréaud I don't have such browser so I cannot test and debug, I'm afraid.
It's weird, I can't figure it out why Safari is having this issue. It's looks like evt.currentTarget.classList.add("is-visible");doesn't add the class to the images. Could it be an issue in relation with browser compatibility?
@MathieuPréaud evt.currentTarget.classList.add("is-visible"); is pretty basic JS - so it's not that line
|
1

Another much simpler solution would be using JS's Animation API.
Also, use CSS :hover > & { opacity: 1; } to handle the appearing images.

const start = ({currentTarget}) => currentTarget._multiplier = 0; const move = ({currentTarget, pageX: x, pageY: y}) => { currentTarget.querySelectorAll(".js-image").forEach((elBall, i) => { elBall.animate({ translate: `${x - currentTarget.offsetLeft}px ${y - currentTarget.offsetTop}px`, }, { duration: (i + 1) * 500 * currentTarget._multiplier, delay: i * 100 * currentTarget._multiplier, fill: "forwards" }); }); currentTarget._multiplier = 1; }; document.querySelectorAll(".js-post").forEach((elBox) => { elBox.addEventListener("pointerenter", start); elBox.addEventListener("pointermove", move); });
body { font: 1rem/1.4 helvetica, arial, sans-serif; } .grid { display: grid; width: 100%; grid-template-columns: repeat(2, 1fr); touch-action: none; gap: 1rem; } .grid__item { position: relative; display: flex; justify-content: center; align-content: center; overflow: hidden; background-color: #333; aspect-ratio: 1; &::before { content: attr(data-number); color: #888; font-size: 5rem; margin: auto; } } .grid__item-image { position: absolute; pointer-events: none; top: 0; left: 0; width: 50%; height: 50%; object-fit: cover; opacity: 0; transform: translate(-50%, -50%); transition: opacity .3s; :hover > & { opacity: 1; } }
<div class="grid"> <div class="grid__item js-post" data-number="1"> <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 class="grid__item js-post" data-number="2"> <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 class="grid__item js-post" data-number="3"> <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 class="grid__item js-post" data-number="4"> <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>

2 Comments

Thanks again @RokoCBuljan, I like this approach too. I'm not still very familiar with the JS's API animation so it's a good start for me.
@MathieuPréaud you're welcome. Here's a good intro paper: developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/…

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.