Как добавить на веб-страницу плавную прокрутку с анимацией изображений

Как добавить на веб-страницу плавную прокрутку с анимацией картинок

Посмотреть демонстрацию Скачать исходный программный код

В данной статье вы узнаете, как добавить в HTML-разметку плавную прокрутку с дополнительными эффектами анимации для картинок.

Для чего добавлять подобную ​​плавную прокрутку на веб-страницу? При скроллинге часто возникают проблемы с выведением контента. При прокрутке картинок могут возникать резкие скачки. Чтобы избежать его появления, можно без проблем анимировать сам содержимое, передвигая его вверх или вниз, вместо использования «нативной» прокрутки.

Плавная прокрутка

Пример плавной прокрутки с эффектом перекоса показывает, как работает этот метод. Для контейнера, в котором содержится контент, установлено position: fixed и overflow: hidden. Благодаря данному дочерний элемент контейнера может перемещаться .

Раздел body веб-страницы получает высоту содержимого, чтобы выводилась полоса скроллинга. При прокрутке страницы зафиксированный контейнер останется на месте, когда его внутреннее содержимое передвигается в результате анимации. Это простой метод реализации плавной прокрутки.

В нашем примере мы будем использовать следующий HTML-код.

<body class="loading"><main><div data-scroll><!--... ---></div></main></body>

Основной элемент будет «закрепленным» контейнером, в то время как div data-scroll будет перемещаться.

Внутренняя анимация картинки

Для внутренней анимации понадобится само изображение и родительский контейнер, для которого установлено свойство overflow: hidden. Мы будем перемещать фоновое изображение, размещенное в div, вверх или вниз во время прокрутки. При этом изображение в блоке div должно быть больше, чем его родитель:

<div class="item"><div class="item__img-wrap"><div class="item__img"></div></div><!--... ---></div>

Установим стили для данных элементов. Мы будем использовать отступ вместо свойства высоты, чтобы сохранить правильное соотношение сторон для внутреннего блока div с изображением. Для этого мы используем медиа-функцию aspect ratio.

Зададим переменную image для фонового картинки в классе item__img-wrap, чтобы не писать слишком много кода.

.item__img-wrap {--aspect-ratio: 1/1.5;overflow: hidden;width: 500px;max-width: 100%;padding-bottom: calc(100% /(var(--aspect-ratio))); will-change: transform;}.item:first-child.item__img-wrap {--aspect-ratio: 8/10;--image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/1.jpg);}.item:nth-child(2).item__img-wrap {width: 1000px;--aspect-ratio: 120/76;--image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/2.jpg);}...

При прокрутке мы будем перемещать div с фоновым изображением. Так что он должен иметь большую высоту, чем его родитель. Для этого определим переменную overflow, которую будем использовать при расчете высоты и верхней точки в нескольких классах. Это может устанавливать различное значение свойства overflow для каждого картинки, которое изменяет визуальный эффект.

.item__img {--overflow: 40px;height: calc(100% +(2 * var(--overflow)));top: calc( -1 * var(--overflow));width: 100%;position: absolute;background-image: var(--image);background-size: cover;background-position: 50% 0%;will-change: transform;}.item__img--t1 {--overflow: 60px;}.item__img--t2 {--overflow: 80px;}.item__img--t3 {--overflow: 120px;}

Сейчас займемся JavaScript. Начнем с вспомогательных способов и переменных.

const MathUtils = { // сопоставляем координаты x из диапазона от [a, b] до [c, d] map:(x, a, b, c, d) =>(x - a) *(d - c) /(b - a) + c, // линейная интерполяция lerp:(a, b, n) =>(1 - n) * a + n * b};const body = document.body;

Для последующих вычислений необходимо приобрести высоту окна.

let winsize;const calcWinsize =() => winsize = {width: window.innerWidth, height: window.innerHeight};calcWinsize();

А также пересчитать это значение при изменении размера.

window.addEventListener('resize', calcWinsize);

Кроме этого нам необходимо отслеживать то, на какое число пикселей перемещается страница во время скроллинга.

let docScroll;const getPageYScroll =() => docScroll = window.pageYOffset || document.documentElement.scrollTop;window.addEventListener('scroll', getPageYScroll);

Сейчас создадим класс для возможности плавной прокрутки.

class SmoothScroll { constructor() { this.DOM = {main: document.querySelector('main')}; this.DOM.scrollable = this.DOM.main.querySelector('div[data-scroll]');     this.items = [];        [...this.DOM.main.querySelectorAll('.content >.item')].forEach(item => this.items.push(new Item(item)));  ...    }}new SmoothScroll();

У нас есть ссылка на контейнер, который должен стать «липким», и прокручиваемый элемент. Он будет перемещаться для имитации прокрутки.

Сейчас необходимо обновить значение translateY при прокрутке. А также и иные свойства, такие как scale или rotation. Создадим объект, который хранит эту конфигурацию. Но пока просто настроим translateY.

constructor() {    ...    this.renderedStyles = {        translationY: {            previous: 0,             current: 0,             ease: 0.1,            setValue: () => docScroll        }    };}

Мы будем использовать интерполяцию для достижения эффекта плавной прокрутки. Значения previous и current являются значениями для интерполяции. Текущее значение translationY будет значением между данными точками с определенным приращением.

ease — это сумма для интерполяции. Приведенная ниже формула вычисляет текущее значение перемещения:

previous = MathUtils.lerp(previous, current, ease)

Функцию setValue устанавливает значение, которое будет текущей позицией прокрутки. Выполним перечисленные выше операции при загрузке страницы, чтобы установить правильное значение translationY.

constructor() {    ...    this.update();}update() {    for (const key in this.renderedStyles ) {        this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();    }       this.layout();}layout() {    this.DOM.scrollable.style.transform = `translate3d(0,${-1*this.renderedStyles.translationY.previous}px,0)`;}

Мы устанавливаем оба значения интерполяции одинаковыми. В данном случае это координаты прокрутки. Благодаря данному анимация происходит при прокрутке страницы. После этого мы вызываем возможность layout, которая применит преобразование к элементу. Обратите внимание, что значение будет отрицательным, поскольку элемент перемещается вверх.

Для изменения разметки необходимо:

  • Установить в фиксированное положение главный элемент, а свойству overflow задать значение hidden, чтобы содержимое прилипало к экрану и не прокручивалось.
  • Установить высоту элемента body , чтобы полоса прокрутки оставалась на странице. Она будет такой же, как высота прокручиваемого элемента.
constructor() {    ...    this.setSize();    this.style();}setSize() {    body.style.height = this.DOM.scrollable.scrollHeight + 'px';}style() {    this.DOM.main.style.position = 'fixed';    this.DOM.main.style.width = this.DOM.main.style.height = '100%';    this.DOM.main.style.top = this.DOM.main.style.left = 0;    this.DOM.main.style.overflow = 'hidden';}

Нам также необходимо сбросить увеличение высоты body при изменении размера.

constructor() {    ...    this.initEvents();}initEvents() {    window.addEventListener('resize', () => this.setSize());}

После этого запускаем в цикле возможность, которая обновляет значения при прокрутке.

constructor() {    ...    requestAnimationFrame(() => this.render());}render() {    for (const key in this.renderedStyles ) {        this.renderedStyles[key].current = this.renderedStyles[key].setValue();        this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);    }    this.layout();        // для каждого элемента for (const item of this.items) {        // если элемент находится в пределах окна просмотра, вызываем возможность рендеринга if ( item.isVisible ) {            item.render();        }    }        // цикл…    requestAnimationFrame(() => this.render());}

Вызов возможности рендеринга запускается для каждого элемента, который находится внутри области просмотра. Это обновит положение элемента картинки.

Необходимо предварительно загрузить картинки, чтобы они выводились, и мы могли рассчитать правильное значение высоты. Для этого используем imagesLoaded:

const preloadImages = () => {    return new Promise((resolve, reject) => {        imagesLoaded(document.querySelectorAll('.item__img'), {background: true}, resolve);    });};

После загрузки картинок мы удаляем класс loading, получаем позицию прокрутки и инициализируем экземпляр SmoothScroll.

preloadImages().then(() => {    document.body.classList.remove('loading');    // Получаем позицию прокрутки getPageYScroll();    // Инициализируем SmoothScroll new SmoothScroll(document.querySelector('main'));});

Сейчас создадим класс Item для представления каждого элемента страницы (картинок).

class Item {    constructor(el) {        this.DOM = {el: el};        this.DOM.image = this.DOM.el.querySelector('.item__img');                this.renderedStyles = {            innerTranslationY: {                previous: 0,                 current: 0,                 ease: 0.1,                maxValue: parseInt(getComputedStyle(this.DOM.image).getPropertyValue('--overflow'), 10),                setValue: () => {                    const maxValue = this.renderedStyles.innerTranslationY.maxValue;                    const minValue = -1 * maxValue;                    return Math.max(Math.min(MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, minValue, maxValue), maxValue), minValue)                }            }        };    }    ...}

Сначала создаем объект renderedStyles, содержащий свойства, которые необходимо обновить. В этом случае мы перемещаем изображение (this.DOM.image) по оси Y. Далее определяем максимальное значение для перемещения (maxValue). Это значение мы предварительно установили в переменной CSS overflow. Также мы предполагаем, что минимальное значение для перемещения равно -1*maxVal.

Функцию setValue работает следующим образом:

  • Когда верхняя позиция элемента относительно области просмотра равна высоте окна, для перемещения устанавливается минимальное значение.
  • Когда верхняя позиция элемента относительно области просмотра больше или равно его высоте (элемент только что вышел из области просмотра), для перемещения устанавливается максимальное значение.

После этого необходимо установить начальные значения при загрузке. Мы также вычисляем высоту и верхнюю точку элемента, которые нам понадобятся для применения возможности, описанной ранее.

constructor(el) {    ...        this.update();}update() {    this.getSize();    for (const key in this.renderedStyles ) {        this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();    }    this.layout();}layout() {    this.DOM.image.style.transform = `translate3d(0,${this.renderedStyles.innerTranslationY.previous}px,0)`;}getSize() {    const rect = this.DOM.el.getBoundingClientRect();    this.props = {        height: rect.height,        top: docScroll + rect.top     }}

Когда размер окна изменяется:

initEvents() {    window.addEventListener('resize', () => this.resize());}resize() {    this.update();}

После этого определим возможность рендеринга, вызываемую внутри возможности SmoothScroll (requestAnimationFrame):

render() {    for (const key in this.renderedStyles ) {        this.renderedStyles[key].current = this.renderedStyles[key].setValue();        this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);    }    this.layout();}

Этот программный код выполняется только для элементов, которые находятся внутри области просмотра. Для этого используем API IntersectionObserver.

constructor(el) {    ...    this.observer = new IntersectionObserver((entries) => {        entries.forEach(entry => this.isVisible = entry.intersectionRatio > 0);    });    this.observer.observe(this.DOM.el);    ...}

Это все! Надеемся, что это руководство помогло вам реализовать плавную прокрутку картинок на собственном веб-сайте.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *