Эффект движения объекта наверх с остановкой скролла

Рейтинг: 0Ответов: 2Опубликовано: 05.05.2023

Прошу оказать небольшую помощь. Задача стоит в реализации решения с услугами, как на сайте https://www.cobramusicagency.com/ (проскролить до раздела "услуги агентства").

Есть div внутри которого расположены еще 6 элементов. Располагаться они должны друг под другом с небольшим смещением вниз. Когда пользователь долистывает до этого блока он должен зафиксироваться и объекты начиная с первого при скроле, должны смещаться вверх. Дошли до последнего - можем листать далее страницу.

Копал в сторону css stiky, но на данный момент зашел в тупик, как реализовать конкретно эту историю.

Сайт-пример сделана на Тильде.

Что я ищу? Может есть какое-то готовое решение? Или библиотека? Или кто-то может поделиться кодом?

В общем, надеюсь на Вашу помощь. Спасибо!!

<div class="history">
   <div class="history-item" id="historyId1"><h1 class="history-item-title">Content 1</h1></div>
   <div class="history-item" id="historyId2"><h1 class="history-item-title">Content 2</h1></div>
   <div class="history-item" id="historyId3"><h1 class="history-item-title">Content 3</h1></div>
   <div class="history-item" id="historyId4"><h1 class="history-item-title">Content 4</h1></div>
   <div class="history-item" id="historyId5"><h1 class="history-item-title">Content 5</h1></div>
   <div class="history-item" id="historyId6"><h1 class="history-item-title">Content 6</h1></div>
</div>

Ответы

▲ 6Принят

HTML/CSS

Создаем длинный контейнер

<div class="sticky-container"></div>
.sticky-container {
  height: 800vh;
}

Добавляем в него секцию с position:sticky

она будет привязана к верху экрана пока контейнер не прокручен

<div class="sticky-container">
  <div class="section sticky"></div>
</div>
.section {
  height: 100vh;
}

.sticky {
  position: sticky;
  top: 0;
  overflow: hidden;
}

В эту секцию добавляем слайды

<div class="sticky-container">
  <div class="section sticky">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
  </div>
</div>
.item {
  position: absolute;
  width: 100%;
  height: 100%;  
}

JS

Получаем ссылки на необходимые элементы

const scrollContainer = document.querySelector('.scroll-container')
const stickyContainer = document.querySelector('.sticky-container')
const stickySection = document.querySelector('.sticky')
const items = document.querySelectorAll('.item')

Меняем z-index слайдов

это можно сделать и в CSS

items.forEach((item, i) => {
  item.style.zIndex = items.length - i
})

Добавляем слушателя на событие scroll

scrollContainer.addEventListener('scroll', () => {
...
})

Получаем объекты которые содержат позицию и размер элементов

scrollContainer.addEventListener('scroll', () => {
  const scb = stickyContainer.getBoundingClientRect()
  const ssb = stickySection.getBoundingClientRect()
})

Делаем общие расчеты

scrollContainer.addEventListener('scroll', () => {
  const scb = stickyContainer.getBoundingClientRect()
  const ssb = stickySection.getBoundingClientRect()

  // Длина пути
  const distance = scb.height - ssb.height
  // Сколько пройдено на текущий момент
  const passed = ssb.top - scb.top
  // Длина одного отрезка(слайда)
  const segment = distance / (items.length - 1)
})

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

scrollContainer.addEventListener('scroll', () => {
  const scb = stickyContainer.getBoundingClientRect()
  const ssb = stickySection.getBoundingClientRect()

  // Длина пути
  const distance = scb.height - ssb.height
  // Сколько пройдено всего пути на текущий момент
  const passed = ssb.top - scb.top
  // Длина одного отрезка(слайда)
  const segment = distance / (items.length - 1)

  items.forEach((item, i) => {
    // Сколько пройдено этого отрезка на текущий момент
    const ty = clamp(passed - segment * i, 0, segment)
    // Перегоняем полученное число выше в диапазон 0 - 1
    const progress = ty / segment
    // Передвигаем слайд вверх
    item.style.transform = `translateY(${100 * progress * -1}%)`
  })
})

const scrollContainer = document.querySelector('.scroll-container')
const stickyContainer = document.querySelector('.sticky-container')
const stickySection = document.querySelector('.sticky')
const items = document.querySelectorAll('.item')

items.forEach((item, i) => {
  item.style.zIndex = items.length - i
})

scrollContainer.addEventListener('scroll', () => {
  const scb = stickyContainer.getBoundingClientRect()
  const ssb = stickySection.getBoundingClientRect()
  
  const distance = scb.height - ssb.height
  const passed = ssb.top - scb.top
  const segment = distance / (items.length - 1)
  
  items.forEach((item, i) => {
    const ty = clamp(passed - segment * i, 0, segment)
    const progress = ty / segment
    item.style.transform = `translateY(${100 * progress * -1}%)`
  })
})

function clamp(num, min, max) {
  return Math.min(Math.max(num, min), max)
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

body {
  font-family: sans-serif;
  font-size: 30vmin;
}

.scroll-container {
  width: 100%;
  height: 100%;
  overflow: hidden auto;
}

.section {
  height: 100vh;

  display: flex;
  align-items: center;
  justify-content: center;
}

.sticky-container {
  position: relative;
  height: 800vh;
}

.sticky {
  position: sticky;
  top: 0;
  overflow: hidden;
}

.item {
  position: absolute;
  width: 100%;
  height: 100%;  

  display: flex;
  align-items: center;
  justify-content: center;
}

.item:nth-child(1) {
  background-color: lightblue;
}

.item:nth-child(2) {
  background-color: lightpink;
}

.item:nth-child(3) {
  background-color: tomato;
}

.item:nth-child(4) {
  background-color: lightgreen;
}

.item:nth-child(5) {
  background-color: coral;
}
<div class="scroll-container">
  <div class="section">SCROLL</div>
  <div class="sticky-container">
    <div class="section sticky">
      <div class="item">1</div>
      <div class="item">2</div>
      <div class="item">3</div>
      <div class="item">4</div>
      <div class="item">5</div>
    </div>
  </div>
  <div class="section">END</div>
</div>

const scrollContainer = document.querySelector('.scroll-container')
const stickyContainer = document.querySelector('.sticky-container')
const stickySection = document.querySelector('.sticky')
const slides = document.querySelectorAll('.slide')

stickySection.style.setProperty('--slides', slides.length)

slides.forEach((slide, i) => {
  slide.style.setProperty('--index', slides.length - 1 - i)
})

addEventListener('resize', resize)
resize()

scrollContainer.addEventListener('scroll', () => {
  const scb = stickyContainer.getBoundingClientRect()
  const ssb = stickySection.getBoundingClientRect()
  
  const d = scb.height - ssb.height
  const p = ssb.top - scb.top
  const s = d / (slides.length - 1)
 
  slides.forEach((slide, i) => {
    const t = clamp(p - s * i, 0, s)
    const progress = t / s
    slide.style.transform = `translateY(${100 * progress * -1}%)`
  })
})

function clamp(num, min, max) {
  return Math.min(Math.max(num, min), max)
}

function resize() {
  document.documentElement.style.setProperty('--lvh', innerHeight + 'px')
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

body {
  font-family: sans-serif;
  font-size: 15vmin;
  color: white;
  background-color: black;
  text-align: center;
}

.scroll-container {
  width: 100%;
  height: 100%;
  overflow: hidden auto;
}

.section {
  height: var(--lvh, 100vh);

  display: flex;
  align-items: center;
  justify-content: center;
}

.sticky-container {
  position: relative;
  height: calc(var(--lvh, 100vh) * 6);
}

.sticky {
  position: sticky;
  top: 0;
  overflow: hidden;
}

.slide {
  position: absolute;
  top: 0;
  left: 0;
  
  width: 100%;
  height: 100%;
  
  z-index: var(--index);
  
  display: flex;
  justify-content: center;
  align-items: flex-end;
}

.slide-content {
  --offset: calc(var(--lvh, 100vh) * 0.3);
  --height: calc(var(--lvh, 100vh) - var(--offset));
  
  width: 95%;
  height: var(--height);
  
  display: flex;
  align-items: center;
  justify-content: center;
  
  border-radius: 4vmin;
  background-color: hsl(262, 74%, calc(30% + var(--index) * 5%));
  transform: translateY(calc(var(--index) * (var(--offset) / (var(--slides) - 1)) * -1));
}

.slide:last-child .slide-content {
  background-color: #b7f507;
}
<div class="scroll-container">
  <div class="section">Что мы делаем<br>с халапеньо?</div>
  <div class="sticky-container">
    <div class="section sticky">
      <div class="slide">
        <div class="slide-content">🤜</div> 
      </div>
      <div class="slide">
        <div class="slide-content">🤛</div> 
      </div>
      <div class="slide">
        <div class="slide-content">👊</div> 
      </div>
      <div class="slide">
        <div class="slide-content">✊</div> 
      </div>
      <div class="slide">
        <div class="slide-content">🔪</div> 
      </div>
      <div class="slide">
        <div class="slide-content">💣</div> 
      </div>
    </div>
  </div>
  <div class="section">🚮</div>
</div>

▲ 0

Как вариант, чтобы не сильно мучаться, можно воспользоваться библиотекой https://greensock.com и их плагином scrollTrigger.

В противном случае, как выше указали отслеживать событие scroll и изменять позицию элемента в соответствии с pageY