Jump to content
Search Community

Loop forward/backward carousel

Nzhiti test
Moderator Tag

Go to solution Solved by Nzhiti,

Recommended Posts

Hello GSAP community,

 

First of all I'd like to thank the developper and the community for the amazing work around GSAP, i've discover it few month ago and I can't imagine work without it now !

 

Let's get back to business !

 

I'm developping a carousel and i'm facing multiples issues :

 - I can't control the width of the magnet container, that means depending on the number of magnets, some magnets might be not fully visible and when you use the modifier to get back to position 0 it may override a magnet

- In my case the function prev works but not the next one (I've inverse the rotation of the carousel in purpose)

 

Any clues how to fix that ?

 

Have a good day 

See the Pen QWdxzvj by nicolas-zhiti (@nicolas-zhiti) on CodePen

Link to comment
Share on other sites

Welcome to the forums, @Nzhiti!

 

Since this is a somewhat common effect (a group of elements that seamlessly loop on the horizontal axis), I whipped together a helper function that makes it significantly easier. Here's a fork with it in place: 

See the Pen wvgOmBO?editors=0110 by GreenSock (@GreenSock) on CodePen

 

Here's the helper function isolated: 

/*
This helper function makes a group of elements animate along the x-axis in a seamless, responsive loop.

Features:
 - Uses xPercent so that even if the widths change (like if the window gets resized), it should still work in most cases.
 - When each item animates to the left or right enough, it will loop back to the other side
 - Optionally pass in a config object with values like "speed" (default: 1, which travels at roughly 100 pixels per second), paused (boolean), and repeat.
 - The returned timeline will have the following methods added to it:
   - next() - animates to the next element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
   - previous() - animates to the previous element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
   - toIndex() - pass in a zero-based index value of the element that it should animate to, and optionally pass in a vars object to control duration, easing, etc.
   - current() - returns the current index (if an animation is in-progress, it reflects the final index)
   - times - an Array of the times on the timeline where each element hits the "starting" spot. There's also a label added accordingly, so "label1" is when the 2nd element reaches the start.
 */
function horizontalLoop(items, config) {
	items = gsap.utils.toArray(items);
	config = config || {};
	let tl = gsap.timeline({repeat: config.repeat, paused: config.paused, defaults: {ease: "none"}}),
		length = items.length,
		startX = items[0].offsetLeft,
		times = [],
		widths = [],
		xPercents = [],
		curIndex = 0,
		pixelsPerSecond = (config.speed || 1) * 100,
		snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural
		totalWidth, curX, distanceToStart, distanceToLoop, item, i;
	// convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster.
	gsap.set(items, {xPercent: (i, el) => {
		let w = widths[i] = parseFloat(gsap.getProperty(el, "width", "px"));
		xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / w * 100 + gsap.getProperty(el, "xPercent"));
		return xPercents[i];
	}});
	gsap.set(items, {x: 0});
	totalWidth = items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX")
	for (i = 0; i < length; i++) {
		item = items[i];
		curX = xPercents[i] / 100 * widths[i];
		distanceToStart = item.offsetLeft + curX - startX;
		distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX");
		tl.to(item, {xPercent: snap((curX - distanceToLoop) / widths[i] * 100), duration: distanceToLoop / pixelsPerSecond}, 0)
		  .fromTo(item, {xPercent: snap((curX - distanceToLoop + totalWidth) / widths[i] * 100)}, {xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false}, distanceToLoop / pixelsPerSecond)
		  .add("label" + i, distanceToStart / pixelsPerSecond);
		times[i] = distanceToStart / pixelsPerSecond;
	}
	function toIndex(index, vars) {
		vars = vars || {};
		let newIndex = gsap.utils.wrap(0, length, index),
			time = times[newIndex];
		if (time > tl.time() !== index > curIndex) { // if we're wrapping the timeline's playhead, make the proper adjustments
			vars.modifiers = {time: gsap.utils.wrap(0, tl.duration())};
			time += tl.duration() * (index > curIndex ? 1 : -1);
		}
		curIndex = newIndex;
		vars.overwrite = true;
		return tl.tweenTo(time, vars);
	}
	tl.next = vars => toIndex(curIndex+1, vars);
	tl.previous = vars => toIndex(curIndex-1, vars);
	tl.current = () => curIndex;
	tl.toIndex = (index, vars) => toIndex(index, vars);
	tl.times = times;
	return tl;
}

Usage

const loop = horizontalLoop(".magnet", {paused: true});

next.addEventListener("click", () => loop.next({duration: 1, ease: "power1"}));
prev.addEventListener("click", () => loop.previous({duration: 1, ease: "power1"}));

Kinda fun, huh? 

 

And since I made all the movement based on xPercent, it's pretty much responsive, meaning you can alter the width and it should still work. here's a responsive version that makes the magnets always 20% of the width of that container (fitting nicely): 

See the Pen ZELPxWW?editors=0010 by GreenSock (@GreenSock) on CodePen

 

Does that help? 

 

  • Like 4
Link to comment
Share on other sites

It helps more than you think ! 😁

 

Thank you for that, is it up to date with gsap best practices ?

 

Still, if someone can explain to me what I did wrong would be highly appreciated.

I'm still gonna train this script you learn better when you practice :)

 

Thank you @Jack

Link to comment
Share on other sites

For one, you're using relative values like "-=200" and "+=200". That's just going to add/subtract to whatever the x value is at that moment in time.

 

For example, if x is 135 when you click prev, then it's going to animate x to 335, and not 400 like you're expecting.

 

Another thing is that this calculation doesn't work like you might expect when x is negative.

parseFloat(x) % containerWidth

 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder

 

Quote

To obtain a modulo in JavaScript, in place of a % n, use ((a % n ) + n ) % n

 

Or you can use the wrap util.

https://greensock.com/docs/v3/GSAP/UtilityMethods/wrap()

 

 

  • Like 1
Link to comment
Share on other sites

Using the toIndex() lead me to an unwanted behavior.

 

In this code pen if you click on the 5th magnet then on the 9th and finaly on the first you go back to first magnet on the left instead of the one on the right.

 

I don't know how to insert code pen again 😃

Link to comment
Share on other sites

5 hours ago, OSUblake said:

For one, you're using relative values like "-=200" and "+=200". That's just going to add/subtract to whatever the x value is at that moment in time.

 

For example, if x is 135 when you click prev, then it's going to animate x to 335, and not 400 like you're expecting.

 

Another thing is that this calculation doesn't work like you might expect when x is negative.


parseFloat(x) % containerWidth

 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder

 

 

Or you can use the wrap util.

https://greensock.com/docs/v3/GSAP/UtilityMethods/wrap()

 

 

Wrap util is definitely something I should work around to achieve this on my own.

Thank you

Link to comment
Share on other sites

  • 3 weeks later...
On 4/27/2021 at 5:43 AM, Nzhiti said:

Using the toIndex() lead me to an unwanted behavior.

 

In this code pen if you click on the 5th magnet then on the 9th and finaly on the first you go back to first magnet on the left instead of the one on the right.

I tweaked the helper function so that toIndex() will always go in the shortest direction: 

/*
This helper function makes a group of elements animate along the x-axis in a seamless, responsive loop.

Features:
 - Uses xPercent so that even if the widths change (like if the window gets resized), it should still work in most cases.
 - When each item animates to the left or right enough, it will loop back to the other side
 - Optionally pass in a config object with values like "speed" (default: 1, which travels at roughly 100 pixels per second), paused (boolean), and repeat.
 - The returned timeline will have the following methods added to it:
   - next() - animates to the next element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
   - previous() - animates to the previous element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
   - toIndex() - pass in a zero-based index value of the element that it should animate to, and optionally pass in a vars object to control duration, easing, etc. Always goes in the shortest direction
   - current() - returns the current index (if an animation is in-progress, it reflects the final index)
   - times - an Array of the times on the timeline where each element hits the "starting" spot. There's also a label added accordingly, so "label1" is when the 2nd element reaches the start.
 */
function horizontalLoop(items, config) {
	items = gsap.utils.toArray(items);
	config = config || {};
	let tl = gsap.timeline({repeat: config.repeat, paused: config.paused, defaults: {ease: "none"}}),
		length = items.length,
		startX = items[0].offsetLeft,
		times = [],
		widths = [],
		xPercents = [],
		curIndex = 0,
		pixelsPerSecond = (config.speed || 1) * 100,
		snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural
		totalWidth, curX, distanceToStart, distanceToLoop, item, i;
	gsap.set(items, { // convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster.
		xPercent: (i, el) => {
			let w = widths[i] = parseFloat(gsap.getProperty(el, "width", "px"));
			xPercents[i] = snap(parseFloat(gsap.getProperty(el, "x", "px")) / w * 100 + gsap.getProperty(el, "xPercent"));
			return xPercents[i];
		}
	});
	gsap.set(items, {x: 0});
	totalWidth = items[length-1].offsetLeft + xPercents[length-1] / 100 * widths[length-1] - startX + items[length-1].offsetWidth * gsap.getProperty(items[length-1], "scaleX")
	for (i = 0; i < length; i++) {
		item = items[i];
		curX = xPercents[i] / 100 * widths[i];
		distanceToStart = item.offsetLeft + curX - startX;
		distanceToLoop = distanceToStart + widths[i] * gsap.getProperty(item, "scaleX");
		tl.to(item, {xPercent: snap((curX - distanceToLoop) / widths[i] * 100), duration: distanceToLoop / pixelsPerSecond}, 0)
		  .fromTo(item, {xPercent: snap((curX - distanceToLoop + totalWidth) / widths[i] * 100)}, {xPercent: xPercents[i], duration: (curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond, immediateRender: false}, distanceToLoop / pixelsPerSecond)
		  .add("label" + i, distanceToStart / pixelsPerSecond);
		times[i] = distanceToStart / pixelsPerSecond;
	}
	function toIndex(index, vars) {
		vars = vars || {};
		(Math.abs(index - curIndex) > length / 2) && (index += index > curIndex ? -length : length); // always go in the shortest direction
		let newIndex = gsap.utils.wrap(0, length, index),
			time = times[newIndex];
		if (time > tl.time() !== index > curIndex) { // if we're wrapping the timeline's playhead, make the proper adjustments
			vars.modifiers = {time: gsap.utils.wrap(0, tl.duration())};
			time += tl.duration() * (index > curIndex ? 1 : -1);
		}
		curIndex = newIndex;
		vars.overwrite = true;
		return tl.tweenTo(time, vars);
	}
	tl.next = vars => toIndex(curIndex+1, vars);
	tl.previous = vars => toIndex(curIndex-1, vars);
	tl.current = () => curIndex;
	tl.toIndex = (index, vars) => toIndex(index, vars);
	tl.times = times;
	return tl;
}

Here's a fork with a slightly different color scheme :) 

See the Pen ZELPxWW?editors=0010 by GreenSock (@GreenSock) on CodePen

 

Better? 

  • Like 2
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...