Параллакс для объектов на странице: движение от мыши
А началось всё с того, что нужно было добавить параллакс эффект для элементов баннера в проекте. Я не очень люблю подключать всякие библиотеки, и подумал, что справлюсь с движением написанием такого небольшого скрипта. Тем более, что уже неоднократно такое писал и… всё работало.
Мой код — минимализм
Разметка меняться не будет. Поэтому HTML напишу тут один раз. Как впрочем и CSS.
<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>.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;
}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Напишем класс для Параллакс эффекта
Если не брать в расчет комментарии в коде, то строчек там совсем немного. Но я специально попросил ИИ добавить разъяснения, чтоб в последствии не бегать по ресурсам, если вдруг захочу опять углубиться в тему.
/**
* 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
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 для оптимизации
.object {
/* will-change указывает браузеру готовиться к изменениям этого свойства */
will-change: translate;
/* Отключаем трехмерные трансформации на элементе, чтобы не вызывать лишних перерисовок */
backface-visibility: hidden;
/* transform3d включает GPU ускорение */
transform: translate3d(0, 0, 0);
}Внедрение в проект
1. Подготовка HTML
Все элементы, которые должны участвовать в параллаксе, должны иметь атрибут data-animate:
<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 настройки
.page {
position: relative;
overflow: hidden; /* или clip - для обрезания выходящего за границы контента */
width: 100%;
min-height: 100vh;
}
.object {
position: absolute;
will-change: translate;
backface-visibility: hidden;
/* Остальные стили... */
}3. Подключение JavaScript
<script src="path/to/parallax.js"></script>
<!-- Класс автоматически инициализируется при DOMContentLoaded -->Примеры использования
Пример 1: Множество слоев с разной интенсивностью
<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: Комбинация с другими анимациями
// Расширение класса для дополнительных эффектов
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: Управление эффектом (включение/выключение)
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:
- Запустите запись (Ctrl+Shift+E)
- Двигайте мышь над элементами
- Остановите запись
- Проверьте, чтобы функция
updateParallax()вызывалась не более 60 раз в секунду
Отладка
Добавьте временное логирование:
updateParallax() {
console.log('updateParallax вызвана', performance.now());
this.blockData.forEach(data => {
// ... остальной код
});
}Возможные проблемы
Дрожание все еще присутствует: Проверьте, что используется will-change: translate в CSS и что значения округлены до 1 знака после запятой.
Прерывистое движение: Убедитесь, что нет других скриптов, которые вызывают тяжелые операции в то же время. Используйте Performance API для отладки.
Элементы не двигаются: Проверьте консоль на ошибки, убедитесь, что элементы имеют position: absolute или position: relative, и что атрибут data-animate присутствует в HTML.