Infinite scrolling?!

Hi @doordewar,

Thank you for posting this thread because it was a great challenge! :wink:

Here is my take on this:

The first thing was to get 100 images from an API somewhere and here is a useful one: https://picsum.photos/

Using this bash script to do download them automatically:

# download.sh

set -B

for i in {1..100}; do
  let id=i*2
  wget -O $i.png https://picsum.photos/id/$id/200
done
$ bash download.sh

Then this is a schema of the different variables and the layout:

The idea is to display the images linearly and have an infinite scroll. The first step was to have a working scroll effect with the mouse which I did with simple velocity/acceleration stuff.

Next was to display the current scroll position with a scroll bar, for this the map() function is quite useful.

Last step was to load/unload images as soon as you scroll too far. This was the most tricky part but I figured out the way to do this with a LinkedList to easily add or remove images.

So the images are loaded and removed as chunks (of 5 for example) as soon as you go too far so you only have a fixed amount of loaded images at every time.

infinite_scroll

This is the final code:

// Joseph HENRY
// https://discourse.processing.org/t/infinite-scrolling/34028

import java.util.LinkedList;

// Images are stored in a LinkedList for FIFO insert and removal
LinkedList<PImage> images;

int totalImages = 100;

// The amount of images loaded in memory
int imagesBufferSize = 10;

// Individual image height
int imageHeight = 200;

// The total height of images
int totalHeight = totalImages * imageHeight;

// Image offset of scroll
int imagesOffset = 0;

// Chunk size of images loaded/unloaded when scrolling
int imagesChunkLoadingSize = 5;

// The scroll position in pixels
int scrollPosition = 0;

// Scroll velocity added to the position
float scrollVelocity = 0;

// Scroll acceleration added to the velocity
float scrollAcceleration = 0;

int scrollBarWidth = 10;

/**
 * Taken from: https://easings.net/#easeOutCubic
 */
float easeOutCubic(float x) {
  return 1 - pow(1 - x, 3);
}

/**
 * Loads an image at the specified index
 */
PImage loadImageAtIndex(int i) {
  return loadImage(str(i) + ".png");
}

void setup() {
  size(200, 500);

  images = new LinkedList<PImage>();
  
  // First load a chunk of images
  for (int i = 1; i <= imagesBufferSize; i++) {
    images.add(loadImageAtIndex(i));
  }
}

void displayScrollBar() {
  // Scrollbar backdrop
  noStroke();
  fill(0, 200);
  int scrollBarX = width - scrollBarWidth;
  rect(scrollBarX, 0, scrollBarWidth, height);

  // Loaded area bar
  float loadedAreaY = map(imagesOffset * imageHeight, 0, totalHeight, 0, height);
  float loadedAreaHeight = map(images.size(), 0, totalImages, 0, height);
  fill(#9664A0);
  rect(scrollBarX, loadedAreaY, scrollBarWidth, loadedAreaHeight, 10);

  // Scroll handle bar
  int visibleSize = height;
  float scrollHandleHeight = (float) visibleSize / totalHeight * height;
  float scrollHandlePosition = map(scrollPosition, 0, totalHeight - height, 0, height - scrollHandleHeight);
  fill(#7982DE);
  rect(scrollBarX, scrollHandlePosition, scrollBarWidth, scrollHandleHeight, 10);
}

void handleImageLoading() {
  // Compute the limits of the loaded images
  int firstImageYPosition = imagesOffset * imageHeight;
  int lastImageYPosition = firstImageYPosition + images.size() * imageHeight;
  
  // Determine if we are close to the limits
  boolean closeToFirstImage = scrollPosition < firstImageYPosition + imageHeight / 2.0;
  boolean closeToLastImage = scrollPosition > lastImageYPosition - height - imageHeight / 2.0;

  if (closeToFirstImage || closeToLastImage) {
    int offsetAdd = 0;

    for (int i = 0; i < imagesChunkLoadingSize; i++) {
      int nextImageIndex = closeToFirstImage ? imagesOffset - i : images.size() + imagesOffset + 1 + i;
      
      // If the image can be loaded (we are not at the boundaries)
      if (nextImageIndex >= 1 && nextImageIndex <= totalImages) {
        PImage img = loadImageAtIndex(nextImageIndex);
        
        // Append at the end or the beginning depending on the direction
        if (closeToFirstImage) {
          images.addFirst(img);
          images.removeLast();
        } else {
          images.addLast(img);
          images.removeFirst();
        }

        offsetAdd += (closeToFirstImage ? -1 : 1);
      }
    }
    
    // Update the image offset
    imagesOffset += offsetAdd;
  }
}

void updateScrollPhysics() {
  // Introduce drag in acceleration and velocity
  scrollAcceleration *= 0.9;
  scrollVelocity += scrollAcceleration;
  
  scrollVelocity *= 0.95;
  scrollPosition += scrollVelocity;
  
  // Limit the scroll min and max value
  scrollPosition = constrain(scrollPosition, 0, totalHeight - height);
}

void displayImages() {
  for (int i = 0; i < images.size(); i++) {
    PImage img = images.get(i);
    image(img, 0, (i + imagesOffset) * imageHeight - scrollPosition);
  }
}

void draw() {
  background(255);

  displayImages();
  displayScrollBar();

  handleImageLoading();

  fill(255, 0, 0);
  textSize(15);
  text("Loaded " + images.size() + " images", 10, 30);

  updateScrollPhysics();
}

void mouseDragged() {
  // When dragged use the cursor movements
  scrollPosition -= mouseY - pmouseY;
}

void mouseReleased() {
  // Compute the cursor y diff
  int diffY = -(mouseY - pmouseY);
  
  // Cancel velocity when going in the other direction
  if (!(scrollAcceleration < 0 && diffY < 0) || !(scrollAcceleration > 0 && diffY > 0)) {
    scrollVelocity = 0;
  }
  
  // Add movement to the scroll
  scrollVelocity = diffY / 10.0;
  scrollAcceleration = diffY / 5.0;
}

Have fun! :yum:

1 Like