Одним из наиболее выразительных способов взаимодействия с интерфейсом становится привязка анимации к координатам курсора. В Lottie Web это достигается управлением текущим кадром анимации на основе положения мыши или указателя, что позволяет превращать статичную иллюстрацию в реактивный элемент интерфейса.
Любая Lottie-анимация в браузере имеет дискретную структуру кадров, доступную через объект анимации:
animation.currentFrame — текущий кадрanimation.totalFrames — общее количество кадровanimation.goToAndStop(frame, isFrame) — переход к
конкретному кадруОсновная идея синхронизации заключается в преобразовании координаты курсора в нормализованное значение диапазона 0 → 1, которое затем масштабируется в диапазон кадров анимации.
const animation = lottie.loadAnimation({
container: document.getElementById('lottie'),
renderer: 'svg',
loop: false,
autoplay: false,
path: 'animation.json'
});
Наиболее распространённый сценарий — управление анимацией по горизонтальному положению курсора.
const container = document.getElementById('lottie');
function onMove(event) {
const rect = container.getBoundingClientRect();
const x = event.clientX - rect.left;
const progress = x / rect.width;
const frame = Math.floor(progress * animation.totalFrames);
animation.goToAndStop(frame, true);
}
window.addEventListener('mousemove', onMove);
[0, 1] при помощи
clamp-логикиconst clamp = (value, min, max) => Math.min(Math.max(value, min), max);
Расширенная модель управления использует одновременно две оси координат. Это позволяет управлять не только прогрессом, но и дополнительными параметрами, например направлением или выбором сегмента анимации.
function onMove(event) {
const rect = container.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width;
const y = (event.clientY - rect.top) / rect.height;
const progress = clamp(x, 0, 1);
const frame = progress * animation.totalFrames;
animation.goToAndStop(frame, true);
}
Вертикальная координата часто используется для модификации поведения:
Прямое присвоение кадра курсору приводит к резким скачкам. Для сглаживания применяется интерполяция (lerp), обеспечивающая инерционное движение.
let targetProgress = 0;
let currentProgress = 0;
function onMove(event) {
const rect = container.getBoundingClientRect();
targetProgress = (event.clientX - rect.left) / rect.width;
targetProgress = clamp(targetProgress, 0, 1);
}
function animate() {
currentProgress += (targetProgress - currentProgress) * 0.1;
const frame = currentProgress * animation.totalFrames;
animation.goToAndStop(frame, true);
requestAnimationFrame(animate);
}
window.addEventListener('mousemove', onMove);
animate();
Использование коэффициента сглаживания:
Помимо позиции, может использоваться скорость перемещения указателя. Это создаёт эффект динамического ускорения анимации.
let lastX = 0;
let velocity = 0;
function onMove(event) {
const dx = event.clientX - lastX;
lastX = event.clientX;
velocity = dx;
}
Далее скорость преобразуется в коэффициент изменения кадра:
function animate() {
const frame = animation.currentFrame + velocity * 0.5;
animation.goToAndStop(frame, true);
requestAnimationFrame(animate);
}
Такой подход используется для создания:
Для поддержки сенсорных устройств применяется
pointermove, обеспечивающий унифицированную модель
ввода.
window.addEventListener('pointermove', (event) => {
const rect = container.getBoundingClientRect();
const progress = (event.clientX - rect.left) / rect.width;
const frame = clamp(progress, 0, 1) * animation.totalFrames;
animation.goToAndStop(frame, true);
});
Дополнительные преимущества:
pressure)Для сложных интерфейсов используется разбиение области на зоны, каждая из которых соответствует отдельному диапазону кадров.
function onMove(event) {
const rect = container.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width;
let startFrame, endFrame;
if (x < 0.33) {
startFrame = 0;
endFrame = animation.totalFrames * 0.33;
} else if (x < 0.66) {
startFrame = animation.totalFrames * 0.33;
endFrame = animation.totalFrames * 0.66;
} else {
startFrame = animation.totalFrames * 0.66;
endFrame = animation.totalFrames;
}
const localProgress = (x % 0.33) / 0.33;
const frame = startFrame + localProgress * (endFrame - startFrame);
animation.goToAndStop(frame, true);
}
Связывание обработки событий с рендер-циклом позволяет снизить нагрузку и устранить дёргания интерфейса.
let latestEvent = null;
window.addEventListener('mousemove', (event) => {
latestEvent = event;
});
function render() {
if (latestEvent) {
const rect = container.getBoundingClientRect();
const x = (latestEvent.clientX - rect.left) / rect.width;
const frame = clamp(x, 0, 1) * animation.totalFrames;
animation.goToAndStop(frame, true);
}
requestAnimationFrame(render);
}
render();
В некоторых сценариях курсор управляет не только кадром, но и направлением проигрывания.
let lastProgress = 0;
function onMove(event) {
const rect = container.getBoundingClientRect();
const progress = (event.clientX - rect.left) / rect.width;
animation.setDirection(progress > lastProgress ? 1 : -1);
animation.play();
lastProgress = progress;
}
Такой подход используется для:
Для регулирования реакции анимации вводится коэффициент чувствительности:
const sensitivity = 1.5;
function onMove(event) {
const rect = container.getBoundingClientRect();
let progress = (event.clientX - rect.left) / rect.width;
progress = Math.pow(progress, sensitivity);
const frame = progress * animation.totalFrames;
animation.goToAndStop(frame, true);
}
Повышение степени:
Иногда требуется активировать синхронизацию только при нахождении курсора внутри контейнера.
let active = false;
container.addEventListener('mouseenter', () => active = true);
container.addEventListener('mouseleave', () => active = false);
window.addEventListener('mousemove', (event) => {
if (!active) return;
const rect = container.getBoundingClientRect();
const progress = (event.clientX - rect.left) / rect.width;
animation.goToAndStop(progress * animation.totalFrames, true);
});
Сложные интерфейсы используют синхронизацию сразу нескольких анимаций с разными коэффициентами чувствительности.
const animA = lottie.loadAnimation({...});
const animB = lottie.loadAnimation({...});
function onMove(event) {
const rect = container.getBoundingClientRect();
const progress = (event.clientX - rect.left) / rect.width;
animA.goToAndStop(progress * animA.totalFrames, true);
animB.goToAndStop((1 - progress) * animB.totalFrames, true);
}
Так формируются: