Параллакс для объектов на странице: движение от мыши

А началось всё с того, что нужно было добавить параллакс эффект для элементов баннера в проекте. Я не очень люблю подключать всякие библиотеки, и подумал, что справлюсь с движением написанием такого небольшого скрипта. Тем более, что уже неоднократно такое писал и… всё работало.

Мой код — минимализм

Разметка меняться не будет. Поэтому HTML напишу тут один раз. Как впрочем и CSS.

HTML
<div class="page">
	<div class="object object_1" data-animate data-kof="100">
		<h1>Object 1</h1>
	</div>
	<div class="object object_2" data-animate data-kof="10">
		<h1>Object 2</h1>
	</div>
</div>
CSS
.page {
  position: relative;
  overflow: clip;
}

.object {
  position: absolute;
  border: 1px solid #000;
  border-radius: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #000;
  color: #fff;
  top: 50%;
  left: 50%;
  will-change: translate;
}
.object_1 {
  transform: translate(-150%, -100%);
  height: 200px;
  width: 200px;
}
.object_2 {
  transform: translate(50%, 0%);
  height: 300px;
  width: 300px;
}
JavaScript
let animatedBlocks = document.querySelectorAll('[data-animate]');
window.addEventListener('mousemove', (e) => {
    animatedBlocks.forEach(block => {
        let rect = block.getBoundingClientRect();
        let kof = block.dataset.kof;
        block.style.translate = `${Math.floor((e.clientX - rect.left) / (-3 * kof))}px ${Math.floor((e.clientY - rect.top) / (-1 * kof))}px`; // минус перед 1 влияет на сторону движения
    });
});

И в целом всё сработало. Но как-то стали проявляться фризы и дрожания. И тут я понял, что браузеру, наверное тяжеловато перерисовывать всё это дело.

ИИ поможет оптимизировать код

И действительно помог. После тестирования, всё заработало как надо. И конечно же я попросил подробно расписать новый код. Ведь им еще часто придется пользоваться.

Сразу глянуть в CodePen

Напишем класс для Параллакс эффекта

Если не брать в расчет комментарии в коде, то строчек там совсем немного. Но я специально попросил ИИ добавить разъяснения, чтоб в последствии не бегать по ресурсам, если вдруг захочу опять углубиться в тему.

JavaScript
/**
 * ParallaxEffect - класс для создания эффекта параллакса на основе движения мыши
 * 
 * Использование: new ParallaxEffect();
 * 
 * HTML разметка требует:
 * - Элементы с атрибутом data-animate
 * - Атрибут data-kof для контроля интенсивности параллакса (чем выше - тем медленнее движение)
 * 
 * Пример:
 * <div class="object" data-animate data-kof="100">Content</div>
 */
class ParallaxEffect {
    constructor() {
        // ========== ИНИЦИАЛИЗАЦИЯ СВОЙСТВ ==========
        
        /**
         * Получаем все элементы с атрибутом data-animate
         * querySelectorAll возвращает NodeList, который позже преобразуем в массив
         */
        this.animatedBlocks = document.querySelectorAll('[data-animate]');
        
        /**
         * blockData - массив объектов с кешированными данными каждого элемента
         * Каждый объект содержит:
         * - element: ссылка на DOM-элемент
         * - kof: коэффициент параллакса из data-kof атрибута
         * - rect: кешированные размеры и позиция элемента (getBoundingClientRect)
         */
        this.blockData = [];
        
        /**
         * Текущие координаты мыши
         * Храним их отдельно для избежания повторных вызовов e.clientX/e.clientY
         */
        this.mouseX = 0;
        this.mouseY = 0;
        
        /**
         * Флаг для предотвращения множественных requestAnimationFrame вызовов
         * Если уже запланировано обновление, не планируем новое
         * Это важно, так как mousemove может срабатывать 60-100+ раз в секунду
         */
        this.ticking = false;
        
        // ========== ИНИЦИАЛИЗАЦИЯ ДАННЫХ ==========
        
        /**
         * Инициализируем данные всех блоков при загрузке
         * Кешируем информацию, чтобы не пересчитывать каждый кадр
         */
        this.initBlocksData();
        
        // ========== ПРИВЯЗКА ОБРАБОТЧИКОВ СОБЫТИЙ ==========
        
        /**
         * mousemove - основное событие для отслеживания движения мыши
         * false указывает использовать фазу "capturing" вместо "bubbling"
         * для немного лучшей производительности
         */
        window.addEventListener('mousemove', (e) => this.onMouseMove(e), false);
        
        /**
         * resize - пересчитываем позиции элементов при изменении размера окна
         * Это необходимо, так как getBoundingClientRect меняется при ресайзе
         */
        window.addEventListener('resize', () => this.initBlocksData());
    }
    
    /**
     * initBlocksData() - инициализирует и кеширует данные всех отслеживаемых элементов
     * Вызывается при загрузке и при resize окна
     */
    initBlocksData() {
        /**
         * Преобразуем NodeList в массив и создаем объект данных для каждого элемента
         * Array.from() необходим, так как forEach на NodeList не поддерживается везде
         */
        this.blockData = Array.from(this.animatedBlocks).map(block => ({
            element: block,                              // Сам DOM-элемент
            kof: parseFloat(block.dataset.kof) || 1,    // Коэффициент из атрибута (или 1 по умолчанию)
            rect: null                                   // Будет заполнено в updateBlocksRect()
        }));
        
        /**
         * Обновляем кешированные размеры и позиции всех элементов
         */
        this.updateBlocksRect();
    }
    
    /**
     * updateBlocksRect() - кеширует текущие размеры и позицию каждого элемента
     * Вызывается при инициализации и resize
     * getBoundingClientRect() - "дорогая" операция (вызывает reflow), поэтому кешируем
     */
    updateBlocksRect() {
        this.blockData.forEach(data => {
            /**
             * getBoundingClientRect возвращает объект с:
             * - left, top - позиция относительно окна просмотра
             * - width, height - размеры элемента
             * 
             * Это статические значения, которые не меняются до ресайза/скролла
             */
            data.rect = data.element.getBoundingClientRect();
        });
    }
    
    /**
     * onMouseMove(e) - обработчик события mousemove
     * 
     * @param {MouseEvent} e - объект события mousemove с координатами мыши
     * 
     * Функция:
     * 1. Сохраняет координаты мыши
     * 2. Использует requestAnimationFrame для синхронизации обновлений
     * 3. Избегает множественных расчетов благодаря флагу ticking
     */
    onMouseMove(e) {
        // Сохраняем текущие координаты мыши
        this.mouseX = e.clientX;
        this.mouseY = e.clientY;
        
        /**
         * Проверяем флаг ticking
         * Если false - это означает, что обновление еще не было запланировано на этот кадр
         * Если true - обновление уже ждет следующего requestAnimationFrame
         * 
         * Это предотвращает множественные calculateParallax вызовы,
         * потому что mousemove срабатывает намного чаще, чем экран обновляется (60 FPS)
         */
        if (!this.ticking) {
            this.ticking = true;
            
            /**
             * requestAnimationFrame синхронизирует наше обновление с refresh rate экрана
             * Обычно это 60 раз в секунду, но может быть 120+ на современных мониторах
             * Браузер будет вызывать нашу функцию перед следующей перерисовкой
             */
            requestAnimationFrame(() => this.updateParallax());
        }
    }
    
    /**
     * updateParallax() - рассчитывает и применяет трансформации параллакса
     * 
     * Логика:
     * 1. Для каждого элемента рассчитываем относительное положение мыши к центру элемента
     * 2. Делим смещение на коэффициент параллакса (kof) для получения амплитуды движения
     * 3. Применяем CSS трансформацию translate
     * 
     * Вызывается один раз за кадр благодаря requestAnimationFrame и флагу ticking
     */
    updateParallax() {
        // Проходим по каждому отслеживаемому элементу
        this.blockData.forEach(data => {
            const rect = data.rect;        // Кешированные размеры и позиция
            const kof = data.kof;          // Коэффициент параллакса из data-kof
            
            /**
             * Рассчитываем центр элемента на экране
             * rect.left - расстояние левого края элемента от левого края окна
             * rect.width - ширина элемента
             * rect.left + rect.width / 2 - x координата центра элемента
             * 
             * Аналогично для Y координаты
             */
            const centerX = rect.left + rect.width / 2;
            const centerY = rect.top + rect.height / 2;
            
            /**
             * Рассчитываем смещение мыши от центра элемента
             * this.mouseX - centerX = насколько пиксели мыши вправо/влево от центра
             * 
             * Деление на (-3 * kof) означает:
             * - Минус (-) инвертирует движение (когда мышь вправо, элемент влево)
             * - 3 - множитель для контроля амплитуды горизонтального движения
             * - kof - чем больше коэффициент, тем меньше смещение (медленнее движение)
             * 
             * Пример: мышь на 300px вправо от центра, kof=100
             * offsetX = 300 / (-3 * 100) = -1px
             * Элемент сместится на 1px влево при движении мыши вправо
             */
            const offsetX = (this.mouseX - centerX) / (-3 * kof);
            const offsetY = (this.mouseY - centerY) / (-1 * kof);
            
            /**
             * Применяем CSS трансформацию translate
             * toFixed(1) - округляем до 1 знака после запятой для плавного движения
             * без резких скачков целых чисел
             * 
             * Используем свойство translate (современный CSS) вместо transform
             * для лучшей производительности - не требует перерасчета всех других трансформаций
             * 
             * ПРИМЕЧАНИЕ: translate автоматически перевалось в transform браузером,
             * но это ново и имеет лучшую производительность
             */
            data.element.style.translate = `${offsetX.toFixed(1)}px ${offsetY.toFixed(1)}px`;
        });
        
        /**
         * Сбрасываем флаг ticking в false
         * Это позволяет следующему mousemove событию запланировать новое обновление
         */
        this.ticking = false;
    }
}

/**
 * ========== ИНИЦИАЛИЗАЦИЯ ==========
 * 
 * DOMContentLoaded гарантирует, что DOM полностью загружен перед созданием класса
 * Это важно, потому что нам нужно найти все элементы с [data-animate]
 */
document.addEventListener('DOMContentLoaded', () => {
    new ParallaxEffect();
});

Подробное объяснение работы

Архитектура класса

Класс разделен на три основные фазы работы:

Фаза 1: Инициализация (constructor)
При создании объекта класс получает все элементы с атрибутом data-animate и кеширует их данные. Это критически важно, потому что каждый вызов getBoundingClientRect() требует браузеру пересчета всей геометрии страницы (reflow).

Фаза 2: Отслеживание движения (onMouseMove)
При движении мыши обновляются координаты в переменных mouseX и mouseY. Вместо прямого пересчета параллакса используется requestAnimationFrame для синхронизации с частотой обновления экрана.

Фаза 3: Применение эффекта (updateParallax)
Один раз за кадр (обычно 60 раз в секунду) рассчитываются новые позиции элементов и применяются CSS трансформации.

Ключевая оптимизация: флаг ticking

Markdown
mousemove срабатывает 60-100+ раз/сек

├─ Событие 1: ticking=false → запускаем updateParallax()
├─ Событие 2: ticking=true → пропускаем (уже запланировано)
├─ Событие 3: ticking=true → пропускаем
├─ Событие 4: ticking=true → пропускаем

Один кадр экрана (16.67ms)

updateParallax() выполнится и установит ticking=false

Следующие события снова смогут запустить updateParallax()

Без этого механизма браузер выполнял бы расчеты параллакса 60+ раз между кадрами экрана, что приводит к потерям производительности.

Коэффициент параллакса (kof)

Коэффициент управляет интенсивностью параллакса:

kofДвижениеПрименение
10Быстрое (чувствительное)Фоновые элементы, которые должны реагировать активно
100Медленное (инертное)Передние элементы или основной контент
200+Очень медленноеЭлементы, которые должны двигаться минимально

Формулы в коде:

  • offsetX = (mouseX — centerX) / (-3 * kof) — горизонтальное смещение (делим на -3 для интенсивности)
  • offsetY = (mouseY — centerY) / (-1 * kof) — вертикальное смещение (делим на -1 для интенсивности)

Минус перед множителем инвертирует направление движения (параллакс «отталкивается» от мыши).

CSS для оптимизации

CSS
.object {
    /* will-change указывает браузеру готовиться к изменениям этого свойства */
    will-change: translate;
    
    /* Отключаем трехмерные трансформации на элементе, чтобы не вызывать лишних перерисовок */
    backface-visibility: hidden;
    
    /* transform3d включает GPU ускорение */
    transform: translate3d(0, 0, 0);
}

Внедрение в проект

1. Подготовка HTML

Все элементы, которые должны участвовать в параллаксе, должны иметь атрибут data-animate:

HTML
<div class="page">
    <!-- Элемент с высоким kof = медленное движение -->
    <div class="object object_1" data-animate data-kof="100">
        <h1>Object 1</h1>
    </div>
    
    <!-- Элемент с низким kof = быстрое движение -->
    <div class="object object_2" data-animate data-kof="10">
        <h1>Object 2</h1>
    </div>
</div>

2. CSS настройки

CSS
.page {
    position: relative;
    overflow: hidden; /* или clip - для обрезания выходящего за границы контента */
    width: 100%;
    min-height: 100vh;
}

.object {
    position: absolute;
    will-change: translate;
    backface-visibility: hidden;
    /* Остальные стили... */
}

3. Подключение JavaScript

HTML
<script src="path/to/parallax.js"></script>
<!-- Класс автоматически инициализируется при DOMContentLoaded -->

Примеры использования

Пример 1: Множество слоев с разной интенсивностью

HTML
<div class="parallax-container">
    <div class="layer" data-animate data-kof="200">Дальний слой</div>
    <div class="layer" data-animate data-kof="100">Средний слой</div>
    <div class="layer" data-animate data-kof="20">Близкий слой</div>
</div>

Пример 2: Комбинация с другими анимациями

JavaScript
// Расширение класса для дополнительных эффектов
class AdvancedParallaxEffect extends ParallaxEffect {
    updateParallax() {
        super.updateParallax();
        
        // Добавляем ротацию или масштабирование в зависимости от положения мыши
        this.blockData.forEach(data => {
            const rect = data.rect;
            const distance = Math.sqrt(
                Math.pow(this.mouseX - (rect.left + rect.width / 2), 2) +
                Math.pow(this.mouseY - (rect.top + rect.height / 2), 2)
            );
            
            const scale = 1 + (distance / 1000) * 0.1;
            data.element.style.transform = `scale(${scale})`;
        });
    }
}

Пример 3: Управление эффектом (включение/выключение)

JavaScript
class ControllableParallaxEffect extends ParallaxEffect {
    constructor() {
        super();
        this.enabled = true;
    }
    
    toggle() {
        this.enabled = !this.enabled;
    }
    
    updateParallax() {
        if (this.enabled) {
            super.updateParallax();
        }
    }
}

// Использование
const parallax = new ControllableParallaxEffect();
// parallax.toggle(); - отключить/включить

Производительность и отладка

Проверка производительности

Откройте Developer Tools и перейдите на вкладку Performance:

  1. Запустите запись (Ctrl+Shift+E)
  2. Двигайте мышь над элементами
  3. Остановите запись
  4. Проверьте, чтобы функция updateParallax() вызывалась не более 60 раз в секунду

Отладка

Добавьте временное логирование:

JavaScript
updateParallax() {
    console.log('updateParallax вызвана', performance.now());
    
    this.blockData.forEach(data => {
        // ... остальной код
    });
}

Возможные проблемы

Дрожание все еще присутствует: Проверьте, что используется will-change: translate в CSS и что значения округлены до 1 знака после запятой.

Прерывистое движение: Убедитесь, что нет других скриптов, которые вызывают тяжелые операции в то же время. Используйте Performance API для отладки.

Элементы не двигаются: Проверьте консоль на ошибки, убедитесь, что элементы имеют position: absolute или position: relative, и что атрибут data-animate присутствует в HTML.