1

I would like to build an image gallery like this where pictures keep looping automatically but at the same time, it's possible to interact through the gallery using the mouse wheel as well. I would also achieve an effect like that where images, instead of scrolling out of the screen shrink on the side.

So I was trying to understand where to start and I couldn't find any library for the task so I tried from scratch using vanilla js, this is my attempt.

var testArray = document.querySelectorAll(".image");
var totImg = testArray.length;

var contIterazioni = 0;
var test = testArray[contIterazioni];
var bounding = test.getBoundingClientRect();



 function LoopFunction() {
     setInterval(galleryScroll, 20);
 }
 function galleryScroll() {
     test = testArray[contIterazioni];
     bounding = test.getBoundingClientRect();
     if (bounding.left == 0) {
      //console.log("immagine bordo sx");
      if (test.width > 0) {
        test.width = test.width - .25;
      } else {
        contIterazioni = contIterazioni + 1;
        if (contIterazioni >= totImg){
          contIterazioni = 0;
        }
      } 
    }
 }

LoopFunction();
body{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.container {
  width: 100vw;
  height: 100vh;
  display: flex;
  overflow: hidden;
}
<div class="container">
  <img src="https://media-cdn.tripadvisor.com/media/photo-s/0e/eb/ad/3d/crazy-cat-cafe.jpg" alt="" class="image">
  <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Chairman_Meow_Bao.jpg/1200px-Chairman_Meow_Bao.jpg" alt="" class="image">
  <img src="https://static.scientificamerican.com/sciam/cache/file/92E141F8-36E4-4331-BB2EE42AC8674DD3_source.jpg" alt="" class="image">
  <img src="https://cdn.britannica.com/91/181391-050-1DA18304/cat-toes-paw-number-paws-tiger-tabby.jpg" alt="" class="image">
  <img src="https://www.orlandocatcafe.com/wp-content/uploads/2020/07/14.png" alt="" class="image">
  
</div>

Unfortunately, I couldn't figure out how to loop the images. Can you help me understand how to do it? Do you think my approach can be a good one? or would you recommend other ways to get that effect?

Thanks!

baband
  • 123
  • 12

2 Answers2

4

Use for loop when you go through all images to set their width to their initial value. Also add extra three images from start of the carousel so there wouldn't be empty space when it comes to an end.

    var testArray = document.querySelectorAll(".image");
    var totImg = testArray.length;

    var contIterazioni = 0;
    var test = testArray[contIterazioni];
    var bounding = test.getBoundingClientRect();

     var imgsWidths = []
     window.onload = () => {
       for(i = 0; i < testArray.length; i++) {
           imgsWidths[i] = testArray[i].width
       }
    }
     
   function galleryScroll() {
       test = testArray[contIterazioni];
       bounding = test.getBoundingClientRect();
       if (bounding.left == 0) {
        if (test.width > 0) {
          test.width = test.width - 2;
        } else {
          contIterazioni = contIterazioni + 1;
          if (contIterazioni >= totImg - 3){
            for(i = 0; i < testArray.length; i++) {
              testArray[i].width = imgsWidths[i]
            }
            contIterazioni = 0;
          }
        } 
      }
     window.requestAnimationFrame(galleryScroll)
     }

    window.requestAnimationFrame(galleryScroll);
body{
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    .container {
      width: 100vw;
      height: 100vh;
      display: flex;
      overflow: hidden;
    }
<div class="container">
      <img src="https://media-cdn.tripadvisor.com/media/photo-s/0e/eb/ad/3d/crazy-cat-cafe.jpg" alt="" class="image">
      <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Chairman_Meow_Bao.jpg/1200px-Chairman_Meow_Bao.jpg" alt="" class="image">
      <img src="https://static.scientificamerican.com/sciam/cache/file/92E141F8-36E4-4331-BB2EE42AC8674DD3_source.jpg" alt="" class="image">
      <img src="https://cdn.britannica.com/91/181391-050-1DA18304/cat-toes-paw-number-paws-tiger-tabby.jpg" alt="" class="image">
      <img src="https://www.orlandocatcafe.com/wp-content/uploads/2020/07/14.png" alt="" class="image">
      <img src="https://media-cdn.tripadvisor.com/media/photo-s/0e/eb/ad/3d/crazy-cat-cafe.jpg" alt="" class="image">
      <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Chairman_Meow_Bao.jpg/1200px-Chairman_Meow_Bao.jpg" alt="" class="image">
      <img src="https://static.scientificamerican.com/sciam/cache/file/92E141F8-36E4-4331-BB2EE42AC8674DD3_source.jpg" alt="" class="image">
    </div>
Omicron
  • 365
  • 4
  • 9
  • 1
    Maybe also inplement [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) instead of a `setInterval` for smoother animations – Reyno May 26 '21 at 13:27
  • I tried to save every image initial `width` in an array, so I could assign values from this array to images instead of fixed value, but I couldn't read widths of the images properly – Omicron May 26 '21 at 13:28
  • `requestAnimationFrame` implemented – Omicron May 26 '21 at 13:33
  • 1
    FYI: Your idea with the image sizes is easy to implement. You just need to get the image width insde the [window.load](https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event) event. Like `window.addEventListener('load', [function to get image sizes here then start gallery], false);` – Reyno May 26 '21 at 13:48
  • ah yes, I forgot that it works like this, thank You I'll implement this – Omicron May 26 '21 at 13:53
2

While keeping your same approach, this should work similarly as the example you provided. If you want the slideshow to keep it's width, all images should have the same width. Your error was not adding back the image that dissapeared to the container, at the end and with it's original width, or as in this case expanding again.

// From https://www.javascripttutorial.net/javascript-queue/

function Queue() {
  this.elements = [];
}

Queue.prototype.enqueue = function(e) {
  this.elements.push(e);
};

Queue.prototype.dequeue = function() {
  return this.elements.shift();
};

Queue.prototype.isEmpty = function() {
  return this.elements.length == 0;
};

Queue.prototype.peek = function() {
  return !this.isEmpty() ? this.elements[0] : undefined;
};

Queue.prototype.length = function() {
  return this.elements.length;
}

let images = new Queue();

document.querySelectorAll(".image").forEach(img => images.enqueue(img));


var totImg = images.length();

var first = images.dequeue();
var last;

var firstWidth = first.width;
var lastWidth = 0.0;


var bounding = first.getBoundingClientRect();

var originalWidth = first.width;
var lastOriginalWidth = 0.0;

var step = 1.0;
var lastStep = 0.0;


function LoopFunction() {
  setInterval(galleryScroll, 20);
}

function galleryScroll() {
  bounding = first.getBoundingClientRect();

  if (bounding.left == 0) {
    //console.log("immagine bordo sx");
    if (first.width > 0) {
      firstWidth -= step;
      if (firstWidth < 0) {
        firstWidth = 0;
      }
      first.style.width = firstWidth + 'px';
    } else {
      if (last && last.width != lastOriginalWidth) {
        last.width = lastOriginalWidth;
      }

      let container = document.querySelector('.container');
      container.removeChild(first);

      last = first;
      lastOriginalWidth = originalWidth;
      last.width = 0.0;
      lastWidth = 0.0;

      images.enqueue(last);
      container.appendChild(last);

      first = images.dequeue();
      originalWidth = first.width;
      firstWidth = originalWidth;
      lastStep = step * lastOriginalWidth / originalWidth;
    }
  }

  if (last && last.width <= lastOriginalWidth) {
    lastWidth += lastStep;
    last.style.width = lastWidth + 'px';
    if (last.width > lastOriginalWidth) {
      last.width = lastOriginalWidth;
    }
  }
}

LoopFunction();
body {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.container {
  width: 100vw;
  height: 100vh;
  display: flex;
  overflow: hidden;
}
<div class="container">
  <img src="https://media-cdn.tripadvisor.com/media/photo-s/0e/eb/ad/3d/crazy-cat-cafe.jpg" alt="" class="image">
  <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Chairman_Meow_Bao.jpg/1200px-Chairman_Meow_Bao.jpg" alt="" class="image">
  <img src="https://static.scientificamerican.com/sciam/cache/file/92E141F8-36E4-4331-BB2EE42AC8674DD3_source.jpg" alt="" class="image">
  <img src="https://cdn.britannica.com/91/181391-050-1DA18304/cat-toes-paw-number-paws-tiger-tabby.jpg" alt="" class="image">
  <img src="https://www.orlandocatcafe.com/wp-content/uploads/2020/07/14.png" alt="" class="image">

</div>

Update #1

As requested by OP, here is some further explenation of the changes I made:

  • I used a simple queue from javascripttutorial to have a data structure that reflects the objective, i.e. having images that move from right to left, and once they "exit" the scene, can get enqueued again.

  • Following this concept, we need pointers to both head and tail of the queue in order to update their animation, in particular first, firstWidth and originalWidth are used respectively to point to the head, keep track of its current width during the animation and the originalWidth to which it should return when re-entering the scene. Similarly, this happens for the last as well.

  • Once this is in place, our loop will be made of two parts, one that handles the head animation and moves the images in the queue when the head image reaches width == 0 and one that handles the tail re-entering the scene and expanding. To achieve continuity, we use a simple proportion after we have our new queue: step : originalWidth = lastStep : lastOriginalWidth, which we use to determine at each frame how much respectively the head and tail images compress and expand. as step is a fixed parameter, lastStep is easily computed through simple arithmetics: lastStep = step * lastOriginalWidth / originalWidth.

  • The code:

      let container = document.querySelector('.container');
      container.removeChild(first);

      last = first;
      lastOriginalWidth = originalWidth;
      last.width = 0.0;
      lastWidth = 0.0;

      images.enqueue(last);
      container.appendChild(last);

      first = images.dequeue();
      originalWidth = first.width;

is just a contextual switch over the changing the queue, where the pointers and respective data get update to reflect the changes in the data structure. So the first becomes last, the next image in the queue is the new compressing first, as such originalWidth shifts.

Let's go further

Now, to achieve your desired implementation, this still needs some work. First of all, the queue should only reflect the actual carousel, not all the images you have, so ideally you would have a first static data structure with all the images and respective metadata, in this case we mainly need their original width. then you would have a queue representing the images that are being animated.

Now, you can either reuse this code and change the width of the images to be all equal and fit the viewport you are using for your animation, which should work fine. The other option is to mirror the code for the head image to the tail, have a fixed step, so while the head compresses of step pixels, the tail expands of the same step pixels, and when the tail reaches maximum width, introduce a new image at the end of the queue which becomes the new tail.

To go in further detail on the last, the algorithm would look something like this:

  • initialize static list of image containers with their respective width (imgList);
  • initialize an empty queue (imgQueue);
  • initialize a pointer pointing to the last image introduced in the queue (listPtr);
  • get the width of the viewport your carousel should fill (vpWidth);
  • while you have not filled such width, keep adding images to the queue and moving the listPtr left in the imgList;
  • have looping function similar to the one above but where the if over last replicates similar steps to the first ones;

This should be everything, I will work on the code in a while, now I need some sleep ;).

Update #2

Okay! Worked out something that actually works decently. If you have suggestions please let me know, in the meanwhile, here it is:

// From https://www.javascripttutorial.net/javascript-queue/
function Queue() {
  this.elements = [];
}
Queue.prototype.enqueue = function(e) {
  this.elements.push(e);
};
Queue.prototype.dequeue = function() {
  return this.elements.shift();
};
Queue.prototype.isEmpty = function() {
  return this.elements.length == 0;
};
Queue.prototype.first = function() {
  return !this.isEmpty() ? this.elements[0] : undefined;
};
Queue.prototype.length = function() {
  return this.elements.length;
};
// Added function to peek last element in queue
Queue.prototype.last = function() {
  return !this.isEmpty() ? this.elements[this.elements.length - 1] : undefined;
};
// Added function that pops the head, enqueues it and returns it
Queue.prototype.shift = function() {
  const head = this.dequeue();
  this.enqueue(head);
  return head;
}

// Returns a queue of HTMLElements based on the given class
function loadQueue(className) {
  const rQueue = new Queue();
  document.querySelectorAll('.' + className).forEach(image => rQueue.enqueue(image));
  return rQueue;
}

// Queue of images to be added to the animation queue
const imgQueue = loadQueue('image');
// Images being animated inside the div
const imgAnimQueue = new Queue();

// To limit calls to the carousel
const carousel = $('.container');

function init() {

  // Width of the viewport being used for the animation
  // TODO: this shoud update whenever the page is resized, or at least the div
  const vpWidth = carousel.width();

  // Keeps track of the width of the images added to the animation queue
  var currentFilledWidth = 0;

  // Now that we have loaded the static information, let's clear the div
  // that will be used as carousel, so that we can add the correct number
  // of images back in
  carousel.empty();

  // Filling the animation queue
  while (currentFilledWidth < vpWidth) {
    // In order not to change the static data, we clone the image HTMLElement
    const img = $(imgQueue.shift()).clone();
    // Enqueuing the new image in the animation queue
    imgAnimQueue.enqueue(img);
    // Adding this image into the carousel
    img.appendTo(carousel);
    currentFilledWidth = currentFilledWidth + img.width();

    // If we overflow the carousel width, we set the tail image to fill
    if (currentFilledWidth > vpWidth) {
      const overflow = currentFilledWidth - vpWidth;
      img.width(img.width() - overflow);
      currentFilledWidth -= overflow;
    }
  }

  const step = 1;
  var firstWidth = imgAnimQueue.first().width();
  var lastWidth = imgAnimQueue.last().width();

  // Now the loop can start
  window.requestAnimationFrame(animateCarousel);

  // Main function that animates the carousel
  function animateCarousel() {
    let first = imgAnimQueue.first();
    let last = imgAnimQueue.last();

    // If the image is still not disappeared, keep compressing it
    if (firstWidth > 0) {
      firstWidth -= step;
      if (firstWidth < 0) firstWidth = 0;
      first.width(firstWidth);
      // Otherwise, remove it from the carousel and update all the data structs
    } else {
      first.remove();
      imgAnimQueue.dequeue();

      if (imgAnimQueue.first() != last) {
        first = imgAnimQueue.first();
        firstWidth = first.width();
      } else {
        first = last;
        firstWidth = lastWidth;

        imgAnimQueue.enqueue($(imgQueue.shift()).clone());
        last = imgAnimQueue.last();
        lastWidth = last.width();
        last.appendTo(carousel);
      }
    }

    if (lastWidth <= last.attr("data-original-width")) {
      lastWidth += step;
      // The image has completely expanded, let's introduce the next one
      if (lastWidth >= last.attr("data-original-width")) {
        last.width(last.attr("data-original-width"));
        imgAnimQueue.enqueue($(imgQueue.shift()).clone());

        last = imgAnimQueue.last();
        last.width(0);
        lastWidth = last.width();
        last.appendTo(carousel);
        // Otherwise just keep expanding it
      } else {
        last.width(lastWidth);
      }
    }

    window.requestAnimationFrame(animateCarousel);
  }
}
body {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.container {
  width: 100vw;
  height: 100vh;
  display: flex;
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
  var loadedImages = 0;
  const count = 4;

  function loadImage(img) {
    if (loadedImages != count) {
      $(img).attr("data-original-width", $(img).width());
      loadedImages += 1;
      if (loadedImages == count) init();
    }
  }

</script>

<div class="container">
  <img onload="loadImage(this)" src="https://media-cdn.tripadvisor.com/media/photo-s/0e/eb/ad/3d/crazy-cat-cafe.jpg" alt="1" class="image">
  <img onload="loadImage(this)" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Chairman_Meow_Bao.jpg/1200px-Chairman_Meow_Bao.jpg" alt="2" class="image">
  <img onload="loadImage(this)" src="https://cdn.britannica.com/91/181391-050-1DA18304/cat-toes-paw-number-paws-tiger-tabby.jpg" alt="3" class="image">
  <img onload="loadImage(this)" src="https://www.orlandocatcafe.com/wp-content/uploads/2020/07/14.png" alt="4" class="image">
</div>
Sandro Massa
  • 98
  • 1
  • 10
  • Thank you very much for your example! I was trying to understand how to make imgs coming from the right start "expanding" only when they enter the viewport and not before. can you help me understand how to do it? Thanks! – baband May 26 '21 at 15:40
  • @baband Sure! I'll update my answer to have a more comprehensive explanation of the code. – Sandro Massa May 26 '21 at 19:49