JSFiddle - React, Tailwind, and code Playground

by HDL52

HTML

<script src="https://unpkg.co/gsap@3/dist/gsap.min.js"></script>
<script src="https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js"></script>
<section>
  <h1>Vertical image loop with scroll acceleration with gsap</h1>

  <h2 class="credit"><a href="https://thisisadvantage.com" target="_blank">Made by Advantage</a></h2>
</section>

<div class="gallery">

  <div class="col">
    <div class="image">
      <img src="https://raw.githubusercontent.com/Hongda-OSU/PicGo-2.3.1/master/img1u6qw3ad64bz8nsedrd2wddgrax5wew.jpg">
    </div>
    <div class="image">
      <img src="https://raw.githubusercontent.com/Hongda-OSU/PicGo-2.3.1/master/imgl7nga0eot9mfzdog0r7hja2avzx8jzf.jpg">
    </div>
    <div class="image">
      <img src="https://raw.githubusercontent.com/Hongda-OSU/PicGo-2.3.1/master/imgodr8vukcwco0h34p0ngc84462vs454j.jpg">
    </div>
  </div>
  
 <div class="col">
    <div class="image">
      <img src="https://raw.githubusercontent.com/Hongda-OSU/PicGo-2.3.1/master/imgj1udos782ybmzugwd1ltkozc9zxj3gz.jpg">
    </div>
    <div class="image">
      <img src="https://raw.githubusercontent.com/Hongda-OSU/PicGo-2.3.1/master/img4gb8ou3p2t5vv347i6lopc13wtm82qw.jpg">
    </div>
    <div class="image">
      <img src="https://raw.githubusercontent.com/Hongda-OSU/PicGo-2.3.1/master/img578ut571o3a1a8kj2su9jra6dz7p5ey.jpg">
    </div>
  </div>
  
  <div class="col">
    <div class="image">
      <img src="https://cdn.jsdelivr.net/gh/Hongda-OSU/PicGo/image/8shhfm8bsq648izivnrc32jq9e0d62e.jpg">
    </div>
    <div class="image">
      <img src="https://cdn.jsdelivr.net/gh/Hongda-OSU/PicGo/image/oojeu86ioebk0vnvnicn0q59u030jfv.jpg">
    </div>
    <div class="image">
      <img src="https://cdn.jsdelivr.net/gh/Hongda-OSU/PicGo/image/2xu5x71d85ctcsi9vsh3oa5vn6fskhq.jpg">
    </div>
  </div><a
</div>

CSS

body {
  font-family: Termina, sans-serif;
  color: #111;
  background: #eee;
  overflow-x: hidden;
}

section {
  padding: 10vw;
  height: 100%;
  min-height: 500vh;
  overflow: visible;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow-y: visible;
}

.credit {
  text-align: center;
  z-index: 999;
  font-size: 0.8rem;
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  writing-mode: vertical-rl;
  text-orientation: mixed;

  a {
    color: #fff;
  }
}

* {
  box-sizing: border-box;
}

h1 {
  font-weight: 800;
  margin: 2rem auto;
  font-size: clamp(3vw, 2rem, 4rem);
  text-align: center;
  z-index: 999;
  max-width: 800px;
  mix-blend-mode: difference;
  pointer-events: none;
  color: white;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: grid;
  place-items: center;
}

.gallery {
  z-index: 1;
  display: flex;
  flex-direction: row;
  justify-content: center;
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  overflow: visible;

  @media (max-width: 768px) {
    width: 160%;
  }
}

.col {
  display: flex;
  flex: 1;
  flex-direction: column;
  width: 100%;
  align-self: flex-start;
  justify-self: flex-start;
}

.col:nth-child(2) {
  align-self: flex-end;
  justify-self: flex-end;
}

.image {
  width: 100%;
  height: clamp(500px, 50vw, 700px);
  filter: saturate(0);
  object-fit: cover;
  padding: 1rem;
}

.image:hover {
  z-index: 99999999999 !important;
  filter: saturate(1);
}

img {
  transition: 0.3s ease-out;
  overflow: hidden;
  box-shadow:
    0 2.8px 2.2px rgba(0, 0, 0, 0.034),
    0 6.7px 5.3px rgba(0, 0, 0, 0.048),
    0 12.5px 10px rgba(0, 0, 0, 0.06),
    0 22.3px 17.9px rgba(0, 0, 0, 0.072),
    0 41.8px 33.4px rgba(0, 0, 0, 0.086),
    0 100px 80px rgba(0, 0, 0, 0.12);
  width: 100%;
  height: 100%;
  object-fit: cover;
}

JavaScript

console.clear()

gsap.registerPlugin(ScrollTrigger)

const additionalY = {
  val: 0,
}
let additionalYAnim
let offset = 0
const cols = gsap.utils.toArray(".col")

cols.forEach((col, i) => {
  const images = col.childNodes

  // DUPLICATE IMAGES FOR LOOP
  images.forEach((image) => {
    var clone = image.cloneNode(true)
    col.appendChild(clone)
  })

  // SET ANIMATION
  images.forEach((item) => {
    let columnHeight = item.parentElement.clientHeight
    let direction = i % 2 !== 0 ? "+=" : "-=" // Change direction for odd columns

    gsap.to(item, {
      y: direction + Number(columnHeight / 2),
      duration: 20,
      repeat: -1,
      ease: "none",
      modifiers: {
        y: gsap.utils.unitize((y) => {
          if (direction == "+=") {
            offset += additionalY.val
            y = (parseFloat(y) - offset) % (columnHeight * 0.5)
          } else {
            offset += additionalY.val
            y = (parseFloat(y) + offset) % -Number(columnHeight * 0.5)
          }

          return y
        }),
      },
    })
  })
})

const imagesScrollerTrigger = ScrollTrigger.create({
  trigger: "section",
  start: "top 50%",
  end: "bottom 50%",
  onUpdate: function (self) {
    const velocity = self.getVelocity()
    if (velocity > 0) {
      if (additionalYAnim) additionalYAnim.kill()
      additionalY.val = -velocity / 2000
      additionalYAnim = gsap.to(additionalY, {
        val: 0,
      })
    }
    if (velocity < 0) {
      if (additionalYAnim) additionalYAnim.kill()
      additionalY.val = -velocity / 3000
      additionalYAnim = gsap.to(additionalY, {
        val: 0,
      })
    }
  },
})