Пример создания галереи с изображениями, красивыми заголовками, параллаксом и миниатюрной навигацией

JSON

Данные в формате JSON

[
  {
    "title": "DUBLDOM",
    "link": "https://test.ru",
    "image": "./assets/images/1.JPG"
  },
  {
    "title": "JUNIPER TREE",
    "link": "https://test.ru",
    "image": "./assets/images/2.JPG"
  },
  {
    "title": "Chill out",
    "link": "https://test.ru",
    "image": "./assets/images/3.JPG"
  },
  {
    "title": "Kandalaksha",
    "link": "https://test.ru",
    "image": "./assets/images/4.JPG"
  }
]

PHP

Базовая разметка

$json = file_get_contents(__DIR__ . '/assets/projects.json');
$projects = json_decode( $json, true );

foreach ( $projects as $project ) {
  ?>
    <div 
      class="projects__item" 
      data-gallery-item
      data-image="<?= $project['image'] ?>"
    >
      <div class="projects__item-bg">
        <img src="<?= $project['image'] ?>" alt="">
      </div>

      <a 
        href="<?= $project['link'] ?>"
        target="_blank"
        rel="noopener noreferrer"
        title="<?= $project['title'] ?>"
        class="projects__item-link"
      >
        <?= $project['title'] ?>
      </a>
    </div>
  <?php
}

JavaScript

Вся логика в одном классе

class Gallery {
  /**
   * @type {HTMLElement}
   */
  container;

  items = [];

  /**
   * @type {DOMRect}
   */
  rect;

  /**
   * @type {HTMLElement}
   */
  navigation;

  interpolatedProgress = 0;

  isInViewPort = false;
  isInViewPortFull = false;

  /**
   * 
   * @param {HTMLElement} container 
   * @returns 
   */
  constructor(container) {
    if (!container) {
      return
    }

    this.container = container;
    this.fillItems();
    this.buildNavigation();
    this.loop();
  }

  update() {
    this.handleNavigation();
    this.handleItemParallax();
    this.handleTitles();
  }

  handleNavigation() {
    if (this.isInViewPortFull) {
      this.navigation.classList.add('active');
    } else {
      this.navigation.classList.remove('active');
    }

    this.interpolatedProgress = -this.interpolation(this.rect.top, 0, this.rect.height 
      - window.innerHeight, 0, 100 * (this.items.length - 1));

    if (this.interpolatedProgress < 0) {
      this.interpolatedProgress = 0;
    }

    if (this.interpolatedProgress > this.rect.height - window.innerHeight) {
      this.interpolatedProgress = this.rect.height - window.innerHeight;
    }

    const frame = this.navigation.querySelector('.projects__nav-frame');

    frame.style.transform = `translate3d(${this.interpolatedProgress}%, 0, 0)`;
  }

  handleItemParallax() {
    this.items.forEach(item => {
      if (!item.isInViewPort) {
        return;
      }

      item.progress = this.interpolation(item.rect.top, 0, item.rect.height, 0, -50);
      const image = item.el.querySelector('img');

      image.style.transform = `translate3d(0, ${item.progress}%, 0)`;
    });
  }

  handleTitles() {
    this.items.forEach(item => {
      if (!item.prepared) {
        this.prepareTitle(item);
        return
      }
      if (!item.isInViewPort) {
        return;
      }

      const text = item.el.querySelector('.projects__item-link');
      text.style.transform = `translate3d(0, ${item.progress * 5}px, 0)`;

      if (Math.abs(item.progress) >= 20) {
        this.hideTitle(text);
      } else {
        this.revealTitle(text);
      }
    });
  }

  getRect() {
    this.rect = this.container.getBoundingClientRect();
  }

  checkViewPort() {
    this.isInViewPort = this.rect.top < window.innerHeight
      && this.rect.top + this.rect.height > 0;

    this.isInViewPortFull = this.rect.top <= 1
      && this.rect.top + this.rect.height >= window.innerHeight;

    if (!this.isInViewPort) {
      return;
    }

    this.items.forEach(item => {
      item.rect = item.el.getBoundingClientRect();
      item.isInViewPort = item.rect.top <= window.innerHeight
        && item.rect.top + item.rect.height >= 0;
    });
  }

  loop() {
    this.getRect();
    this.checkViewPort();

    if (this.isInViewPort) {
      this.update();
    }
    requestAnimationFrame(this.loop.bind(this));
  }

  fillItems() {
    const items = this.container.querySelectorAll('[data-gallery-item]');

    items.forEach((el, ind) => {
      this.items.push({
        index: ind,
        el: el,
        isInViewPort: false,
        rect: null,
        progress: 0,
        title: el.querySelector('.projects__item-link')?.textContent,
      });
    });
  }

  buildNavigation() {
    const navEl = document.createElement('div');
    navEl.classList.add('projects__nav');

    navEl.innerHTML = `
      <div class="projects__nav-frame"></div>
    `;

    this.items.forEach(item => {
      const el = document.createElement('div');
      el.classList.add('projects__nav-item');
      el.dataset.index = item.index;
      el.title = item.title;
      el.innerHTML = `<img src="${item.el.dataset.image}" alt="">`;
      navEl.append(el);

      el.addEventListener('click', (e) => {
        e.preventDefault();
        this.onNavItemClick(item);
      });
    });

    this.navigation = navEl;
    this.container.append(navEl);
  }

  onNavItemClick(item) {
    item.el.scrollIntoView({
      behavior: 'smooth',
      block: 'start',
    });
  }

  prepareTitle(item) {
    const text = item.el.querySelector('.projects__item-link');

    if (!text) {
      return;
    }

    item.prepared = true;

    let spanCount = 0;

    const words = text.textContent.trim().split(' ');

    text.innerHTML = '';

    words.forEach(word => {
      const wordEl = document.createElement('span');

      const letters = word.split('');

      letters.forEach(letter => {
        const span = document.createElement('span');
        span.innerHTML = letter;
        span.dataset.index = spanCount;
        span.setAttribute('style', `--coeff:${spanCount}`);
        wordEl.append(span);
        spanCount += 1;
      });

      const span = document.createElement('span');
      span.innerHTML = ' ';
      wordEl.append(span);

      text.append(wordEl);
    });

  }

  hideTitle(el) {
    el.classList.remove('reveal');
  }

  revealTitle(el) {
    el.classList.add('reveal');
  }

  interpolation(value, min, max, newMin, newMax) {
    let newValue = ((value - min) / (max - min)) * (newMax - newMin) + newMin;
    return newValue;
  }
};

const initGallery = () => {
  Array.from(document.querySelectorAll('[data-gallery]')).forEach(el => {
    new Gallery(el);
  });
};

initGallery();

CSS

Какие-то стили, для красоты

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  font-family: "Montserrat", Arial, sans-serif;
  font-size: 1.1vw;
  line-height: 1.5;
}

p {
  margin: 2rem 0;
}

.container {
  padding: 1rem 3rem;
}

.projects__item {
  display: flex;
  justify-content: center;
  align-items: center;
}

.projects__item-bg {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

.projects__item-bg img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: brightness(50%);
}

.projects__item-link {
  position: relative;
  z-index: 1;
  text-align: center;
  text-decoration: none;
  font-size: 4rem;
  color: white;
  font-weight: 900;
  text-transform: uppercase;
}

.projects__nav {
  position: fixed;
  left: 50%;
  bottom: 1rem;
  width: max-content;
  z-index: 2;
  display: flex;
  justify-content: center;
  transition: 0.25s ease-in-out;
  visibility: hidden;
  pointer-events: none;
  opacity: 0;
  transform: translateX(-50%);
}

.projects__nav::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -1rem;
  width: 100vw;
  height: calc(100% + 4rem);
  transform: translate(-50%, 0);
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(0deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
  mix-blend-mode: overlay;
}

.projects__nav.active {
  visibility: visible;
  pointer-events: auto;
  opacity: 1;
}

.projects__nav-frame {
  width: 7rem;
  height: 4rem;
  border: 2px solid #61cced;
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  background-color: rgba(0, 0, 0, 0.3);
}

.projects__nav-item {
  width: 7rem;
  height: 4rem;
  cursor: pointer;
}

.projects__nav-item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.projects__item-link span[data-index] {
  opacity: 0;
  display: inline-block;
  will-change: transform;
  transform: translate3d(0.5rem, 0.5rem, 0);
  transition: 0.5s ease-in-out;
  transition-delay: calc(0.03s * var(--coeff));
}

.projects__item-link.reveal span[data-index] {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

[data-gallery] {
  position: relative;
}

[data-gallery-item] {
  position: relative;
  height: 100vh;
  overflow: hidden;
}