loading...
Cover image for This simple math hack lets you create an image carousel without any if statements

This simple math hack lets you create an image carousel without any if statements

ranewallin profile image Rane Wallin ・3 min read

If you are a web developer or a web developer student, you have probably made at least one image carousel in your career. In fact, you've probably made a few. While there are plenty of image carousel libraries out there, sometimes you want (or need) to make it from scratch.

Most image carousels are made from arrays of image data. When some event triggers a change (a timeout, button click, etc) the current image data is replaced with the next element in the array. The tricky part for many comes when they reach the end of the array. Now what? If you've been writing complex if statements trying to check for this condition, I'm here to tell you there's a better way.

Observe the code below:

const imageData = [ 'image1.png', 'img2.png', 'img3.png' ];
let currentImage = 0;

const handleImageChange = () => {
  currentImage = (currentImage + 1) % imageData.length;
}

That's it. That's the whole thing. (Explanation below amazed Keanu.)

Keanu Reeves looking dumbfounded, or maybe just dumb

How it works

Let's assume we have an array of 10 elements. Modulo division (what happens when you use the % operator) returns the remainder of the division. If currentImage is 0, then (0 + 1) % 10 is the remainder of 1/10, which is 1. This is because we cannot actually divide 1 by 10, so the whole dang 1 is left over. The same is true of 2 - 9. None of these numbers can be divided by 10, so the number itself is the remainder. The magic happens when we get to 10.

Since our array is zero-index, there is no tenth element. This works in our favor! When you divide a number by itself, the remainder is 0, which means our currentImage will be set to 0. This means that as soon as we get past the end of our array, its going to go back to the beginning. Nifty, yeah?

In computer science, this is known as a circular array. The array itself is just a plain ole array, but we use this math trick to allow it to loop indefinitely.

But, wait! What if we want to go the other way? Don't worry, I got you!

We can do the same thing in reverse. The formula for this is (currentValue - 1 + totalElements) % totalElements. If we add this to the above example, it could look something like this.

const imageData = [ 'image1.png', 'img2.png', 'img3.png' ];
const currentImage = 0;

const handleImageChange = (direction) => {
  if (direction == 'forward')
    currentImage = (currentImage + 1) % imageData.length;
  else
    currentImage = (currentImage - 1 + imageData.length) % imageData.length;
}

I know, I know, I said there wouldn't be any if statements, and there aren't, at least not for actually moving forward and back through the elements. We just need to know which direction to go.

This isn't just great for image carousels. Any time you need to increment through an array one element at a time, this will eliminate any condition checking to see if you are at the end.

Cover image by Michael and Sherry Martin (flickr)

Posted on by:

ranewallin profile

Rane Wallin

@ranewallin

I'm a full stack web developer and java engineer. I am currently available for hire.

Discussion

markdown guide
 

Amazing!

You could remove the "if" splitting the handler into 2 functions or using a hashmap;

const imageData = [ 'image1.png', 'img2.png', 'img3.png' ];
let currentImage = 0;

const nextImage = () => currentImage = (currentImage + 1) % imageData.length;

const prevImage = () => currentImage = (currentImage - 1 + imageData.length) % imageData.length;
 

Yes, though if the we are still keeping the code DRY we would just be offsetting the if somewhere else. We wouldn't want two separate buttons that only different by which handler they call, so the button would need some logic in it to decide which handler is the right handler. That's the cool thing about programming, though. So many different ways to solve every problem.

 

Yeah, I wasn't saying it's wrong, and I know that the point of this post is to show the math, I just showed one of the possible solutions, but you confused me a little bit now...

What would be the difference in the pseudo-code below and why the second one wouldn't follow the DRY principle?

// some logics in the button...
carousel.handleImageChange('forward');
...
// some logics in the button...
carousel.handleImageChange('other direction');
...
// some logics in the button...
carousel.nextImage()
...
// some logics in the button...
carousel.prevImage()
...

Nothing wrong with it at all. My point was that you still have an if statement, it's just in a different place. The only way to eliminate it would be if there were two completely different button components, one for forward and one for back, that called different code, which would then be less dry. I wasn't saying there was anything wrong with what you posted :).

 

Once you figure out the modulus operator, it's a pretty cool operator! A ternary operator can get rid of that last if:

const imageData = [ 'image1.png', 'img2.png', 'img3.png' ];
let currentImage = 0;

const handleImageChange = (direction) => {
    currentImage = (direction == 'forward') ? 
      (currentImage + 1) % imageData.length : 
      (currentImage - 1 + imageData.length) % imageData.length;
}

(also changed currentImage to let (otherwise "Assignment to constant variable." error)

 

I wouldn't say a ternary is better than an if, especially for multi-line things like this.

 

Thanks. I fixed it. I do that all the time.

 
const imageData = [ 'image1.png', 'img2.png', 'img3.png' ];
let currentImage = 0;

const handleImageChange = (direction) => {
    currentImage = (currentImage + imageData.length + ((direction == 'forward') ? 1 : -1)) % imageData.length;
}
 

If you create a direction enum like so:

const direction = { FORWARD: 1, BACKWARD: -1 },

const handleImageChange = offset => {
  currentImage = (currentImage + imageData.length + offset) % imageData.length;
}

// call with enum value:
handleImageChange(direction.BACKWARD);

And you're golden. Well, except when you call handleImageChange(-7)

Solution: currentImage = (currentImage + imageData.length + (offset % imageData.length)) % imageData.length
😊

 

I don't think this will work as written. You only add the number of elements to the first part if you are going backwards. This looks like it's adding it either way. If I am at the last element (2) in this example then this would return in the next index being 3 instead of 0.

 

Wrong. Adding either way works. 3 modulo 3 is zero, as is 6 modulo 3, 9 modulo 3 etc.

 

What if instead of passing a string to the function, you gave it an integer number of images to move?

const imageData = [ 'image1.png', 'img2.png', 'img3.png', ... ];
let currentImage = 0;
____
const handleImageChange = (imageShift) => {
  currentImage = Math.max(
    0,
    Math.min(
      imageData.length - 1,
      (currentImage + imageShift) % imageData.length
    )
  );
}
____
const firstImage = () => handleImageChange(-imageData.length);
const prevImage = () => handleImageChange(-1);
const nextImage = () => handleImageChange(1);
const lastImage = () => handleImageChange(imageData.length);
 

Great tip!
I think you mistyped css in the tags and added cs instead

 

Thanks. I meant cs, like computer science. :)

 

Oh! I thought of C sharp, but that makes more sense 😅

 
 

This is really cool. It is always fun to see the power of simple math operations applied to the behavior of things like this and what it can drive code to do. Great post!

 

Pretty nifty. Previewing the next element after the modulo would be a big challenge to try without ifs.

 

You could just do the same thing. I.e. nextImg = (currentImage + 1) % imageData.length;