W3docs

Анимации на JavaScript

Анимации на JavaScript: requestAnimationFrame, функции плавности, цепочки, Web Animations API, производительность и доступность.

Анимации на JavaScript — это динамичный способ улучшить пользовательский опыт на веб-страницах. От тонких эффектов до сложных движений, владение анимациями на JavaScript способно значительно повысить интерактивность и визуальную привлекательность ваших проектов. Это исчерпывающее руководство предлагает глубокое изучение анимаций на JavaScript: фундаментальные концепции, ключевые техники и практические примеры, которые помогут вам уверенно создавать захватывающие анимации.

Введение в анимации на JavaScript

Анимации на JavaScript управляют элементами Document Object Model (DOM), создавая визуальные эффекты. Они используются для перемещения, изменения размеров и трансформации элементов в ответ на взаимодействия пользователя или автоматически с течением времени. Понимая основные принципы и техники, вы сможете создавать анимации, которые одновременно производительны и визуально привлекательны.

Зачем использовать JavaScript для анимаций?

Анимации на JavaScript обеспечивают больший контроль и гибкость по сравнению с CSS-анимациями. Они позволяют создавать более сложные последовательности и взаимодействия, делая возможными высоко настраиваемые эффекты. Анимации на JavaScript могут динамически управляться и нередко необходимы, когда нужно реагировать на ввод пользователя или интегрироваться с другими возможностями JavaScript.

Как правило:

  • Используйте CSS (transition/@keyframes) для простых, декларативных изменений состояния, таких как наведение, появление и переключение. Браузер оптимизирует их в отдельном потоке компоузитора, поэтому они, как правило, наиболее производительны. См. CSS animations.
  • Используйте JavaScript, когда анимация зависит от данных, жестов пользователя, физики или логики, которую CSS не может выразить — например, следование за мышью, привязка к динамическим позициям или цепочки из множества шагов с условиями ветвления.

Для наименее трудоёмкого варианта на JavaScript встроенный Web Animations API (рассмотрен ниже) обеспечивает производительность на уровне CSS с императивным управлением, тогда как requestAnimationFrame даёт полный покадровый контроль, когда он необходим.

Базовые концепции анимаций на JavaScript

Прежде чем переходить к конкретным техникам, важно понять несколько базовых концепций, лежащих в основе анимаций на JavaScript:

  1. Кадры в секунду (FPS): частота, с которой отображаются кадры анимации. Более высокое значение FPS обеспечивает более плавные анимации.
  2. Функции плавности (Easing Functions): эти функции управляют ускорением и замедлением анимаций, добавляя естественность движению.
  3. Функции синхронизации (Timing Functions): определяют длительность и задержку анимаций.
  4. Ключевые кадры (Keyframes): задают начальное, конечное и промежуточные состояния анимации.

requestAnimationFrame vs. setInterval

До появления requestAnimationFrame анимации создавались с помощью setInterval и setTimeout, которые вызывали коллбэк каждые несколько миллисекунд. У такого подхода два недостатка: интервал не синхронизирован с частотой обновления экрана (что приводит к потере или задвоению кадров), а таймер продолжает срабатывать даже когда вкладка находится в фоне, расходуя процессор и заряд аккумулятора.

requestAnimationFrame(callback) решает обе проблемы. Браузер вызывает ваш callback непосредственно перед следующей перерисовкой — обычно около 60 раз в секунду, в соответствии с дисплеем — и автоматически приостанавливается, когда вкладка скрыта. Коллбэк получает аргумент timestamp с высоким разрешением, который используется для вычисления прогресса на основе реально прошедшего времени, а не фиксированной частоты кадров.

// setInterval: fixed timer, not synced to the screen, runs in background tabs
let pos = 0;
const id = setInterval(() => {
  pos += 2;
  if (pos >= 200) clearInterval(id);
}, 16);

// requestAnimationFrame: synced to repaint, pauses in background tabs
function frame(timestamp) {
  // use timestamp to drive the animation
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Используйте requestAnimationFrame для всего, что обновляет визуальное отображение на каждом кадре.

Простой пример анимации на JavaScript

Начнём с простого примера, где мы анимируем движение квадрата по экрану.

<!DOCTYPE html>
<html>
<head>
  <style>
    #square {
      width: 50px;
      height: 50px;
      background-color: red;
      position: absolute;
      left: 0;
      top: 50px;
    }
  </style>
</head>
<body>
  <div id="square"></div>
  <script>
    const square = document.getElementById('square');
    let pos = 0;

    function move() {
      if (pos < window.innerWidth - 50) {
        pos += 2;
        square.style.transform = 'translateX(' + pos + 'px)';
        requestAnimationFrame(move);
      }
    }

    move();
  </script>
</body>
</html>

Этот пример демонстрирует базовую анимацию, в которой красный квадрат движется слева направо по экрану. Анимация использует функцию requestAnimationFrame, которая обеспечивает плавное движение, синхронизируя анимацию с частотой обновления дисплея. Функция move постепенно обновляет позицию квадрата и продолжает вызывать себя, пока квадрат не достигнет края окна.

Информация

Хотя JS-анимации требуют частых обновлений стилей, предпочтительнее использовать transform и opacity, чтобы избежать принудительного пересчёта макета (layout reflow).

Продвинутые техники анимаций на JavaScript

Использование функций плавности

Функции плавности делают анимации более естественными, изменяя скорость анимации. Они обычно принимают нормализованное значение прогресса от 0 до 1. Вот пример использования функции плавности для анимации квадрата:

<!DOCTYPE html>
<html>
<head>
  <style>
    #square {
      width: 50px;
      height: 50px;
      background-color: blue;
      position: absolute;
      left: 0;
      top: 50px;
    }
  </style>
</head>
<body>
  <div id="square"></div>
  <script>
    const square = document.getElementById('square');
    let start = null;
    const duration = 2000; // Animation duration in milliseconds

    function easeOutQuad(t) {
      return t * (2 - t);
    }

    function animate(timestamp) {
      if (!start) start = timestamp;
      let elapsed = timestamp - start;
      let progress = Math.min(elapsed / duration, 1);
      let easedProgress = easeOutQuad(progress);

      square.style.transform = 'translateX(' + (window.innerWidth - 50) * easedProgress + 'px)';

      if (progress < 1) {
        requestAnimationFrame(animate);
      }
    }

    requestAnimationFrame(animate);
  </script>
</body>
</html>

Этот пример улучшает простую анимацию, добавляя функцию плавности easeOutQuad. Функции плавности делают анимации более естественными, регулируя скорость анимируемого элемента со временем. В данном случае синий квадрат движется слева направо, начиная быстро и замедляясь по мере приближения к концу. Длительность анимации установлена в 2000 миллисекунд, а позиция квадрата обновляется на основе значения плавного прогресса.

Цепочки анимаций

Цепочки анимаций позволяют выполнять несколько анимаций последовательно. Вот пример квадрата, который сначала движется вправо, а затем вниз:

<!DOCTYPE html>
<html>
<head>
  <style>
    #square {
      width: 50px;
      height: 50px;
      background-color: green;
      position: absolute;
      left: 0;
      top: 0;
    }
  </style>
</head>
<body>
  <div id="square"></div>
  <script>
    const square = document.getElementById('square');
    let xPos = 0;
    let yPos = 0;

    function moveRight() {
      if (xPos < window.innerWidth - 50) {
        xPos += 2;
        square.style.transform = 'translateX(' + xPos + 'px)';
        requestAnimationFrame(moveRight);
      } else {
        xPos = 0;
        yPos = 0; // Explicitly reset yPos before downward phase
        square.style.transform = 'translateX(0px)';
        requestAnimationFrame(moveDown);
      }
    }

    function moveDown() {
      if (yPos < window.innerHeight - 50) {
        yPos += 2;
        square.style.transform = 'translateY(' + yPos + 'px)';
        requestAnimationFrame(moveDown);
      } else {
        yPos = 0; // Reset for repeated cycles
        requestAnimationFrame(moveRight);
      }
    }

    moveRight();
  </script>
</body>
</html>

Этот пример демонстрирует цепочку анимаций: зелёный квадрат сначала движется вправо, а затем вниз. Функция moveRight перемещает квадрат вправо до тех пор, пока он не достигнет края окна. Как только квадрат достигает правого края, yPos явно сбрасывается в 0 перед вызовом moveDown. Такое последовательное выполнение в сочетании с правильным управлением состоянием при повторных циклах демонстрирует, как создавать более сложные последовательности анимаций путём объединения их в цепочки.

Анимация нескольких свойств

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

<!DOCTYPE html>
<html>
<head>
  <style>
    #square {
      width: 50px;
      height: 50px;
      background-color: purple;
      position: absolute;
      left: 0;
      top: 0;
    }
  </style>
</head>
<body>
  <div id="square"></div>
  <script>
    const square = document.getElementById('square');
    let pos = 0;

    function changeColor(progress) {
      let red = Math.floor(255 * progress);
      let green = Math.floor(255 * (1 - progress));
      return `rgb(${red}, ${green}, 0)`;
    }

    function animate() {
      if (pos < window.innerWidth - 50) {
        pos += 2;
        let progress = pos / (window.innerWidth - 50);
        square.style.transform = 'translateX(' + pos + 'px)';
        square.style.backgroundColor = changeColor(progress);
        requestAnimationFrame(animate);
      }
    }

    animate();
  </script>
</body>
</html>

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

Web Animations API

Ручное написание цикла requestAnimationFrame даёт полный контроль, но для большинства анимаций на основе временной шкалы в браузере уже есть встроенный движок: Web Animations API (WAAPI). Вызов element.animate(keyframes, options) запускает анимацию вне основного потока, когда это возможно (как и CSS), обеспечивая плавное воспроизведение без написания цикла кадров вручную. Также возвращается объект Animation, который можно приостановить, воспроизвести в обратном направлении, перемотать или дождаться его завершения.

<!DOCTYPE html>
<html>
<head>
  <style>
    #box {
      width: 50px;
      height: 50px;
      background-color: teal;
      position: absolute;
      left: 0;
      top: 50px;
    }
  </style>
</head>
<body>
  <div id="box"></div>
  <script>
    const box = document.getElementById('box');

    const animation = box.animate(
      [
        { transform: 'translateX(0)', opacity: 1 },
        { transform: 'translateX(300px)', opacity: 0.3 }
      ],
      {
        duration: 2000,
        easing: 'ease-out',
        fill: 'forwards'
      }
    );

    // The returned object gives you imperative control:
    animation.finished.then(() => console.log('done'));
    // animation.pause();  animation.reverse();  animation.currentTime = 1000;
  </script>
</body>
</html>

Первый аргумент — массив ключевых кадров (каждый является объектом CSS-свойств); второй задаёт duration, easing и поведение через такие параметры, как fill (сохранить конечное состояние), iterations, direction и delay. Поскольку easing принимает те же значения cubic-bezier() и ключевые слова, что и CSS, вам часто вообще не нужно самостоятельно писать математику для плавности.

Предпочитайте WAAPI, когда ваша анимация представляет собой фиксированную временную шкалу изменений свойств. Оставляйте ручной цикл requestAnimationFrame для случаев, когда каждый кадр зависит от живого ввода или вычисленного состояния — например, физика, перетаскивание или значения, которые заранее неизвестны.

Остановка и реверс анимаций

Цикл requestAnimationFrame продолжает работать до тех пор, пока вы не прекратите планировать новые кадры. Чтобы немедленно отменить его, сохраните идентификатор, возвращаемый requestAnimationFrame, и передайте его в cancelAnimationFrame:

let rafId;

function loop(timestamp) {
  // ...update visuals...
  rafId = requestAnimationFrame(loop);
}

rafId = requestAnimationFrame(loop);

// Later, e.g. on a button click or when the element is removed:
cancelAnimationFrame(rafId);

Всегда отменяйте запущенные анимации перед удалением их элементов из DOM, иначе цикл продолжит изменять отсоединённый узел, тратя ресурсы на каждом кадре. При использовании Web Animations API эквиваленты — просто animation.cancel(), animation.pause() и animation.reverse().

Доступность: учитывайте предпочтение уменьшенного движения

Большое или быстрое движение может вызывать тошноту, головокружение или мигрень у некоторых пользователей. Операционные системы предоставляют предпочтение «уменьшить движение», и его следует учитывать. В JavaScript считайте его с помощью matchMedia и либо отключите анимацию, либо сразу перейдите к конечному состоянию:

const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (prefersReducedMotion) {
  // Skip the animation: apply the final state instantly
  square.style.transform = 'translateX(300px)';
} else {
  requestAnimationFrame(animate);
}

Эта единственная проверка делает ваши интерфейсы более инклюзивными и удобными для пользователей, отказавшихся от анимации.

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

Чтобы обеспечить плавные и эффективные анимации, следуйте этим советам по оптимизации производительности:

  1. Используйте requestAnimationFrame: этот метод синхронизируется с частотой обновления дисплея, обеспечивая более плавные анимации.
  2. Минимизируйте манипуляции с DOM: группируйте обновления DOM для сокращения перерасчётов макета и перерисовок. Предпочитайте transform и opacity для изменений позиции, чтобы задействовать аппаратное ускорение GPU.
  3. Оптимизируйте CSS: используйте CSS-свойства с аппаратным ускорением, такие как transform и opacity.
  4. Ограничивайте частоту событий: сокращайте частоту событий, таких как resize или scroll, с помощью техник throttling или debouncing.
  5. Правильно останавливайте анимации: используйте cancelAnimationFrame для остановки анимаций, когда они больше не нужны. Сохраняйте идентификатор анимации и вызывайте cancelAnimationFrame(id) для предотвращения утечек памяти. Пример:
    let animationId = requestAnimationFrame(animate);
    // ... later ...
    cancelAnimationFrame(animationId);
  6. Избегайте layout thrashing: не считывайте свойства макета (например, offsetWidth, getBoundingClientRect) внутри цикла анимации, так как это заставляет браузер пересчитывать стили в середине кадра и может вызывать рывки.
Информация

Вы также можете создавать анимации только с помощью CSS (без JavaScript), и это обычно приводит к лучшей производительности. См. CSS animations.

Заключение

Мастерство в создании JavaScript-анимаций предполагает понимание базовых принципов, применение продвинутых техник и оптимизацию производительности. Следуя этому руководству, вы сможете создавать захватывающие и отзывчивые анимации, улучшающие пользовательский опыт. Экспериментируйте с различными функциями плавности, цепочками и анимациями свойств, чтобы раскрыть весь потенциал JavaScript в ваших веб-проектах.

Для дальнейшего изучения ознакомьтесь со связанными темами:

  • CSS animations — декларативная, нередко более производительная альтернатива.
  • Scheduling: setTimeout and setInterval — планирование с помощью таймеров и почему requestAnimationFrame предпочтительнее для визуального отображения.
  • Introduction to browser events — запуск анимаций в ответ на взаимодействие пользователя.
  • JavaScript HTML DOM — выбор и управление элементами, которые вы анимируете.

Практика

Практика
Какие методы и функции часто используются в JavaScript-анимациях?
Какие методы и функции часто используются в JavaScript-анимациях?
Was this page helpful?