Веб

Плавная смена фона при скролле в JavaScript без резких переходов

Как сделать плавную анимацию смены статичного фонового изображения при скролле: используйте слои с opacity, CSS transitions, Vanilla JS с requestAnimationFrame или GSAP ScrollTrigger. Примеры кода для fade-эффекта без лагов.

Как сделать плавную смену статичного фонового изображения при скролле в JavaScript без резких переходов?

Текущий код меняет фон резко при достижении определенных позиций скролла:

js
// Смена фона при скролле
const back = document.getElementById('back');
const pathImg_1 = "/img/back/back.avif";
const pathImg_2 = "/img/back/back_2.jpg";
const pathImg_3 = "/img/back/back.avif";

window.addEventListener('scroll', ()=>{
 const windowHight = window.innerHeight;
 const hight_1box = document.getElementById('main').offsetHeight;
 const header = document.getElementById('header').offsetHeight;
 const mainBox = document.getElementById('main');
 const hight_2box = document.getElementById('scills_box').offsetHeight;
 const hight_3box = document.getElementById('about').offsetHeight;
 let scrollPosition = window.scrollY;

 const firstShift = hight_1box - header - (header*0.2);
 const secondShift = hight_1box+hight_2box+hight_3box + header;

 if(firstShift<=scrollPosition){
 back.style.backgroundImage = `url(${pathImg_2})`;
 mainBox.style.backgroundColor = 'black';
 }
 if(secondShift<=scrollPosition){
 back.style.backgroundImage = `url(${pathImg_3})`;
 }
 if(secondShift>=scrollPosition){
 back.style.backgroundImage = `url(${pathImg_2})`;
 }
 if(firstShift>=scrollPosition){
 back.style.backgroundImage = `url(${pathImg_1})`;
 mainBox.style.backgroundColor = 'rgba(0, 0, 0, 0.26)';
 }

})

Логика: блоки пролистываются поверх статичного фона. После черного блока фон должен плавно смениться на новый, видимый через прозрачный блок. Рекомендуйте библиотеки (например, GSAP, AOS) или варианты кода с CSS transitions/opacity для smooth background change on scroll.

Для плавной смены статичного фонового изображения при скролле замените резкую установку backgroundImage на два наложенных слоя с плавным изменением opacity через CSS transitions. В вашем коде просто добавьте второй и третий div с position: fixed, transition: opacity 0.6s ease и меняйте их прозрачность по прогрессу скролла — это создаст fade-effect без скачков. Лучший вариант — библиотека GSAP с ScrollTrigger, где анимация привязана к позиции прокрутки: gsap.to('#back2', {opacity: 1, scrollTrigger: {trigger: '#main', scrub: true}}).


Содержание


Проблемы вашего кода и почему фон меняется резко

Ваш скрипт работает по принципу if-условий: как только scrollPosition пересекает порог вроде firstShift, фон мгновенно переключается на pathImg_2. Это создает резкий скачок, потому что backgroundImage не анимируется по умолчанию — браузер просто подставляет новое изображение. Плюс, логика с if(secondShift>=scrollPosition) конфликтует: она может перезаписывать предыдущие изменения хаотично.

А что если трафик на сайт вырастет, и на мобильных скролл будет дерганым? Такие простые слушатели window.scroll вызываются 60+ раз в секунду, жрут CPU и не дают плавности. Блоки вроде #main с прозрачным backgroundColor: rgba(0,0,0,0.26) пролистываются поверх фиксированного #back, но без перехода это выглядит как обрезка фото ножницами.

Решение простое: не меняйте изображение напрямую. Используйте наложение слоев — нижний фон статичный, верхний fade-in’ится. Это стандарт для анимации при скролле, где opacity анимируется от 0 до 1 за 0.5-0.6s.


Базовый подход: два слоя с CSS opacity

Начните с HTML: добавьте несколько фиксированных фонов.

html
<div id="back-wrapper" style="position: fixed; top:0; left:0; width:100%; height:100%; z-index: -1; overflow: hidden;">
 <div id="back1" style="position: absolute; top:0; left:0; width:100%; height:100%; background-image: url('/img/back/back.avif'); background-size: cover; opacity: 1; transition: opacity 0.6s ease;"></div>
 <div id="back2" style="position: absolute; top:0; left:0; width:100%; height:100%; background-image: url('/img/back/back_2.jpg'); background-size: cover; opacity: 0; transition: opacity 0.6s ease;"></div>
 <div id="back3" style="position: absolute; top:0; left:0; width:100%; height:100%; background-image: url('/img/back/back.avif'); background-size: cover; opacity: 0; transition: opacity 0.6s ease;"></div>
</div>

CSS transition на opacity сделает fade плавным — браузер сам интерполирует. В JS просто:

js
const back1 = document.getElementById('back1');
const back2 = document.getElementById('back2');
const back3 = document.getElementById('back3');
// ... ваши расчеты firstShift, secondShift

window.addEventListener('scroll', () => {
 let scrollPosition = window.scrollY;
 if (scrollPosition >= firstShift) {
 back1.style.opacity = 0;
 back2.style.opacity = 1;
 }
 if (scrollPosition >= secondShift) {
 back2.style.opacity = 0;
 back3.style.opacity = 1;
 }
});

Почему это работает лучше? Opacity — анимируемое свойство, в отличие от backgroundImage. Но на длинных страницах scroll-событие все равно тормозит. Переходим к оптимизации.


Vanilla JS решение с requestAnimationFrame

Чтобы избежать лагов, добавьте throttle через requestAnimationFrame — он синхронизирует с 60fps браузера. Плюс, вычисляйте прогресс плавно: не бинарный if, а progress = Math.min(scrollY / диапазон, 1) для градиентного fade.

Вот полный рефакторинг вашего кода:

js
let ticking = false;
const backs = {
 1: document.getElementById('back1'),
 2: document.getElementById('back2'),
 3: document.getElementById('back3')
};

function updateBackground() {
 const scrollY = window.scrollY;
 const windowHeight = window.innerHeight;
 // ... ваши hight_1box и т.д.
 const firstShift = hight_1box - header - (header * 0.2);
 const secondShift = hight_1box + hight_2box + hight_3box + header;
 
 // Плавный прогресс для первого перехода
 const progress1 = Math.max(0, Math.min(1, (scrollY - firstShift + windowHeight * 0.2) / (windowHeight * 0.5)));
 backs[1].style.opacity = 1 - progress1;
 backs[2].style.opacity = progress1;
 
 // Второй переход
 const progress2 = Math.max(0, Math.min(1, (scrollY - secondShift + windowHeight * 0.2) / (windowHeight * 0.5)));
 backs[2].style.opacity = progress2 * (1 - progress2); // Кросс-фейд
 backs[3].style.opacity = progress2;

 ticking = false;
}

window.addEventListener('scroll', () => {
 if (!ticking) {
 requestAnimationFrame(updateBackground);
 ticking = true;
 }
});

Этот код из ru.stackoverflow дает настоящий плавный скролл без библиотек. Прозрачные блоки вроде #main теперь идеально показывают новый фон сквозь fade.


GSAP ScrollTrigger для идеальной анимации при скролле

Хотите профессиональный уровень? GSAP — король анимации смены фона. Подключите <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script> и ScrollTrigger.

Установка: gsap.registerPlugin(ScrollTrigger);

Код для вашей страницы:

js
gsap.registerPlugin(ScrollTrigger);

gsap.timeline({
 scrollTrigger: {
 trigger: "#main",
 start: "top bottom",
 end: "#scills_box bottom",
 scrub: 1, // Привязка к скорости скролла
 ease: "power2.inOut"
 }
})
.to("#back1", {opacity: 0, duration: 1})
.to("#back2", {opacity: 1, duration: 1}, 0);

gsap.timeline({
 scrollTrigger: {
 trigger: "#about",
 start: "top bottom",
 end: "bottom bottom",
 scrub: 1
 }
})
.to("#back2", {opacity: 0, duration: 1})
.to("#back3", {opacity: 1, duration: 1}, 0);

Scrub делает переход пропорциональным скроллу — скроллишь медленно, анимация растягивается. Для трех изображений используйте одну timeline с позициями: .to("#back2", {}, 0.5).to("#back3", {}, 1). Примеры в GSAP форуме показывают, как это бьет vanilla на headless.


IntersectionObserver и альтернативы вроде AOS

Если GSAP overkill, попробуйте IntersectionObserver — нативный API для появления элементов при скролле. Добавьте data-bg к блокам:

js
const observer = new IntersectionObserver((entries) => {
 entries.forEach(entry => {
 if (entry.isIntersecting) {
 const nextBg = entry.target.dataset.bg;
 const current = document.querySelector('.back-current');
 const next = document.querySelector('.back-next');
 next.style.backgroundImage = `url(${nextBg})`;
 next.style.opacity = 1;
 current.style.opacity = 0;
 current.classList.remove('back-current');
 next.classList.add('back-current');
 }
 });
}, { threshold: 0.5 });

observer.observe(document.getElementById('main'));
observer.observe(document.getElementById('scills_box'));

AOS (Animate On Scroll) проще: подключите CDN, добавьте data-aos="fade-up" к блокам, но для фона комбинируйте с opacity. jQuery-вариант из gravitsapa подойдет для legacy: $(window).scroll(function(){ if(scroll >= 500) $('.back-next').animate({opacity:1},400); });.


Оптимизация и производительность

Не забывайте: preload изображений (<link rel="preload" as="image" href="/img/back/back_2.jpg">), чтобы fade не ждал загрузки. Тестируйте на мобильных — fixed backgrounds иногда глючат в iOS. Для parallax добавьте transform: translateY в GSAP.

Если страницы длинные, комбинируйте с will-change: opacity в CSS. И да, throttle спасет от 1000+ вызовов scroll в секунду на тач-устройствах.


Источники

  1. qna.habr.com/q/309680
  2. ru.stackoverflow.com/questions/717611
  3. gravitsapa.info/…jquery
  4. gsap.com/…scroll
  5. webgolovolomki.com/…skrolle
  6. ru.stackoverflow.com/…prokrutke
  7. dejurka.ru/…pure-css
  8. gsap.com/…on-scroll

Заключение

Плавная смена фона при скролле достигается opacity-слоями с transitions или GSAP ScrollTrigger — забудьте про резкие if. Начните с vanilla JS для простоты, перейдите на GSAP для wow-эффекта. Тестируйте на реальных устройствах, и ваша страница полетит. Готовый код сэкономит часы — копируйте и адаптируйте под свои пороги.

Авторы
Проверено модерацией
Модерация
Плавная смена фона при скролле в JavaScript без резких переходов