Как создать анимацию на холсте JavaScript

Создание анимации на холсте JavaScript сводится к повторной очистке и перерисовке canvas. Код меняет координаты круга и запрашивает следующий кадр через requestAnimationFrame. В результате получится круг, который движется слева направо через холст. Для примера хватит одного HTML-файла; кнопки позволят останавливать и возобновлять движение.

Как устроена анимация на холсте

Canvas хранит пиксели, но не отдельные фигуры.

Поэтому каждый новый кадр нужно собрать заново: стереть старое изображение и нарисовать объект в новой позиции.

Начальное и конечное состояние

Анимация показывает изменение объекта между двумя состояниями. У круга меняются координаты и размер, у другой фигуры могут меняться угол поворота, цвет или прозрачность.

Синий круг в начальном состоянии слева на холсте

Начальное состояние задает точку, с которой начинается движение.

Синий круг в конечном состоянии справа на холсте

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

Интерполяция

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

Промежуточные положения круга между начальным и конечным состояниями

Продолжительность движения задает время, а не число кадров.

Поэтому положение объекта нужно вычислять по прошедшему времени, которое передает requestAnimationFrame.

Анимация в canvas

Canvas API не перерисовывает объекты автоматически. Ваш код очищает область, обновляет состояние, рисует фон и фигуры, затем планирует следующий кадр.

Пустой холст перед рисованием первого кадра
Первый кадр холста с нарисованной фигурой

Первый кадр содержит фон и объект. Перед следующим кадром код очищает холст, чтобы старое положение не осталось на экране.

Очищенный холст перед рисованием второго кадра
Второй кадр с немного измененным положением фигуры
Соседние кадры с небольшим изменением положения фигуры

На соседних кадрах фигура меняется незначительно. В этом примере движется и поворачивается шестиугольник.

Последовательность кадров с движущимся шестиугольником

Небольшие изменения между кадрами создают впечатление непрерывного движения.

Пишем анимацию круга

Сначала создайте холст и функцию рисования. Затем замените статичный рисунок циклом кадров с кнопками управления. Первые два листинга используют радиус 175 пикселей и холст 900 на 450 пикселей.

Добавление холста

Создайте файл animation.html и вставьте в него весь HTML. Холст имеет размеры 900 на 450 пикселей. Круг с центром (225, 225) и радиусом 175 полностью помещается внутри.

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Анимация круга</title>
  <style>
    canvas {
      border: 10px #333 solid;
      display: block;
    }
  </style>
</head>
<body>
  <canvas id="myCanvas" width="900" height="450"></canvas>

  <script>
    const mainCanvas = document.querySelector("#myCanvas");
    const mainContext = mainCanvas.getContext("2d");
    const canvasWidth = mainCanvas.width;
    const canvasHeight = mainCanvas.height;
    const radius = 175;

    function drawCircle() {
      mainContext.clearRect(0, 0, canvasWidth, canvasHeight);
      mainContext.fillStyle = "#F8F8F8";
      mainContext.fillRect(0, 0, canvasWidth, canvasHeight);

      mainContext.beginPath();
      mainContext.arc(225, 225, radius, 0, Math.PI * 2);
      mainContext.closePath();
      mainContext.fillStyle = "#FFCC00";
      mainContext.fill();
      mainContext.lineWidth = 10;
      mainContext.strokeStyle = "#DCB001";
      mainContext.stroke();
    }

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

Сохраните файл и откройте его в браузере. На странице появится светлый холст с желтым кругом и темной обводкой.

Рисование круга

Переменная mainCanvas получает элемент по идентификатору, а mainContext – его двумерный контекст. Метод arc() строит дугу по центру, радиусу и углам. Диапазон от 0 до Math.PI * 2 задает полную окружность.

Желтый круг с обводкой на холсте

Строка clearRect(0, 0, canvasWidth, canvasHeight) уже стоит в функции. Метод clearRect() очищает прямоугольную область от старых пикселей.

Прямоугольная область clearRect, охватывающая весь холст

Координаты 0, 0 обозначают левый верхний угол. Переменные canvasWidth и canvasHeight задают размеры очищаемой области.

Вызов fillRect() после очистки закрашивает фон.

Анимирование круга

В файле animation.html замените весь блок от открывающего <script> до закрывающего </script> следующим фрагментом. Он содержит кнопки и новый скрипт. Холст, CSS и остальную HTML-разметку оставьте без изменений.

Прежняя функция и прямой вызов drawCircle() должны исчезнуть вместе со старым блоком.

<button id="stopButton" type="button">Остановить</button>
<button id="startButton" type="button">Возобновить</button>

<script>
  const mainCanvas = document.querySelector("#myCanvas");
  const mainContext = mainCanvas.getContext("2d");
  const canvasWidth = mainCanvas.width;
  const canvasHeight = mainCanvas.height;
  const radius = 175;
  const speed = 180;
  const maxFrameGap = 250;

  let xPos = -radius;
  let previousTime = null;
  let animationId = null;
  let isRunning = false;

  function drawCircle(timestamp) {
    if (!isRunning) {
      return;
    }

    let deltaSeconds = 0;
    if (previousTime !== null) {
      const elapsed = timestamp - previousTime;
      if (elapsed <= maxFrameGap) {
        deltaSeconds = elapsed / 1000;
      }
    }
    previousTime = timestamp;

    xPos += speed * deltaSeconds;
    if (xPos > canvasWidth + radius) {
      xPos = -radius;
    }

    mainContext.clearRect(0, 0, canvasWidth, canvasHeight);
    mainContext.fillStyle = "#F8F8F8";
    mainContext.fillRect(0, 0, canvasWidth, canvasHeight);

    mainContext.beginPath();
    mainContext.arc(xPos, canvasHeight / 2, radius, 0, Math.PI * 2);
    mainContext.closePath();
    mainContext.fillStyle = "#FFCC00";
    mainContext.fill();
    mainContext.lineWidth = 10;
    mainContext.strokeStyle = "#DCB001";
    mainContext.stroke();

    animationId = requestAnimationFrame(drawCircle);
  }

  function stopAnimation() {
    cancelAnimationFrame(animationId);
    animationId = null;
    previousTime = null;
    isRunning = false;
  }

  function startAnimation() {
    if (isRunning) {
      return;
    }
    previousTime = null;
    isRunning = true;
    animationId = requestAnimationFrame(drawCircle);
  }

  document.querySelector("#stopButton")
    .addEventListener("click", stopAnimation);
  document.querySelector("#startButton")
    .addEventListener("click", startAnimation);

  document.addEventListener("visibilitychange", () => {
    previousTime = null;
  });

  startAnimation();
</script>

Сохраните файл и обновите страницу браузера. Круг начнет появляться слева, пересечет холст и повторит движение.

Метод requestAnimationFrame() планирует колбэк перед следующей перерисовкой браузера.

Каждый вызов запрашивает один кадр. Поэтому функция drawCircle запрашивает следующий кадр перед завершением.

Таймер setTimeout() запускает отложенный вызов. Для покадрового рисования используйте requestAnimationFrame(), который согласует вызовы с перерисовкой.

Остановка и возобновление

Метод cancelAnimationFrame() отменяет запланированный кадр по его идентификатору. Обе кнопки и функции управления уже включены в скрипт.

  1. Нажмите «Остановить», когда круг окажется в середине холста. Изображение останется на текущем кадре.
  2. Нажмите «Возобновить». Круг продолжит движение с сохраненной позиции.
  3. Нажмите «Возобновить» несколько раз подряд. Скорость останется прежней: проверка isRunning блокирует повторный запуск цикла.

Функция startAnimation() сбрасывает время предыдущего кадра. Поэтому пауза между нажатиями не превращается в скачок координаты.

Чтобы начать движение заново, обновите страницу.

Как работает код

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

Координата и границы холста

Переменная xPos хранит горизонтальный центр круга. Старт -radius помещает окружность за левую границу. Условие xPos > canvasWidth + radius проверяет выход окружности справа.

if (xPos > canvasWidth + radius) {
  xPos = -radius;
}
Круг частично выходит за правую границу холста

Обводка выступает за окружность на половину своей толщины. В учебном примере границы рассчитаны по радиусу самой фигуры.

Привязка к ширине холста и радиусу сохраняет пример переносимым. Вертикальный центр canvasHeight / 2 удерживает круг посередине.

Если уменьшите высоту холста, уменьшите и радиус.

Порядок действий в кадре

Сначала код получает время кадра и считает прошедший интервал. Затем он обновляет координату, проверяет границу, очищает холст и рисует сцену. Последний вызов планирует следующий кадр.

Браузер согласует вызовы requestAnimationFrame() со своей перерисовкой. В скрытых вкладках браузеры обычно приостанавливают такие вызовы.

Canvas сам по себе не обеспечивает эту экономию.

Скорость, не зависящая от частоты экрана

Сдвиг на фиксированные 5 пикселей за кадр привязывает скорость к частоте обновления экрана. На дисплее с более высокой частотой круг пройдет путь быстрее, поэтому такой вариант не дает одинакового результата.

Timestamp помогает связать движение со временем. Формула проста: сдвиг = скорость × прошедшее время. В коде deltaSeconds переводит миллисекунды в секунды, а значение speed задает пиксели в секунду.

Первый кадр и длительные перерывы

Значение previousTime = null обозначает отсутствие предыдущего кадра. Первый колбэк только запоминает timestamp, оставляя сдвиг нулевым. Следующий колбэк уже рассчитывает разницу между двумя отметками времени.

Скорость 180 пикселей в секунду относится к активному движению. За интервал 20 миллисекунд координата изменится на 3,6 пикселя.

Обработчик visibilitychange сбрасывает отсчет при смене видимости страницы. Первый кадр после возвращения сохраняет прежнюю позицию круга.

Дополнительно код пропускает сдвиг, если перерыв превышает 250 миллисекунд. Такой порог превращает длительную задержку в паузу и предотвращает скачок.

Резкость на экранах высокой плотности

На экранах с высоким devicePixelRatio холст стандартного размера может выглядеть размытым.

Для четкого изображения увеличьте внутренний буфер canvas и масштабируйте контекст.

В анимационном скрипте замените первые четыре объявления, от const mainCanvas до const canvasHeight, следующим блоком. Вставляйте его до const radius = 175. Все остальные строки оставьте без изменений.

const mainCanvas = document.querySelector("#myCanvas");
const mainContext = mainCanvas.getContext("2d");
const canvasWidth = 900;
const canvasHeight = 450;
const pixelRatio = window.devicePixelRatio || 1;

mainCanvas.width = Math.round(canvasWidth * pixelRatio);
mainCanvas.height = Math.round(canvasHeight * pixelRatio);
mainCanvas.style.width = `${canvasWidth}px`;
mainCanvas.style.height = `${canvasHeight}px`;
mainContext.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);

Переменные canvasWidth и canvasHeight теперь хранят логические размеры в CSS-пикселях. Очистка, фон и границы движения продолжают использовать эти значения.

Размер увеличенного буфера хранится отдельно в свойствах самого canvas.

Обновите страницу: видимый размер холста останется 900 на 450 пикселей. При devicePixelRatio больше 1 внутренний буфер станет крупнее. Чтобы отменить настройку, верните исходные четыре объявления и обновите страницу.

Несколько объектов и плавность движения

Для нескольких фигур храните состояние в массиве. Один цикл должен обновлять и рисовать все объекты на холсте.

В файле замените целиком объявление функции drawCircle(timestamp) следующим фрагментом. Он содержит массив и новую версию этой же функции. Функции управления, обработчики событий и единственный вызов startAnimation() оставьте прежними.

const circles = [
  { x: 50, y: 100, radius: 30, speed: 120, color: "#FFCC00" },
  { x: 200, y: 300, radius: 30, speed: 180, color: "#3399FF" }
];

function drawCircle(timestamp) {
  if (!isRunning) {
    return;
  }

  let deltaSeconds = 0;
  if (previousTime !== null) {
    const elapsed = timestamp - previousTime;
    if (elapsed <= maxFrameGap) {
      deltaSeconds = elapsed / 1000;
    }
  }
  previousTime = timestamp;

  mainContext.clearRect(0, 0, canvasWidth, canvasHeight);
  mainContext.fillStyle = "#F8F8F8";
  mainContext.fillRect(0, 0, canvasWidth, canvasHeight);

  for (const circle of circles) {
    circle.x += circle.speed * deltaSeconds;
    if (circle.x > canvasWidth + circle.radius) {
      circle.x = -circle.radius;
    }

    mainContext.beginPath();
    mainContext.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2);
    mainContext.fillStyle = circle.color;
    mainContext.fill();
  }

  animationId = requestAnimationFrame(drawCircle);
}

Сохраните файл и обновите страницу. Перезагрузка остановит старый скрипт, а новый начнет отсчет с previousTime = null.

Отдельный вызов requestAnimationFrame добавлять не нужно.

На холсте появятся 2 круга с разными скоростями. Кнопки будут останавливать и возобновлять оба объекта одновременно. Переменные xPos, radius и speed одиночного примера больше не используются; их можно удалить.

Ровная скорость подходит для линейного движения. Для мягкого старта и остановки применяйте easing-функцию. Она преобразует долю прошедшего времени в долю пройденного пути.

Например, при движении между двумя точками возьмите прогресс t от 0 до 1. Формула t * t * (3 - 2 * t) даст плавный разгон и замедление. Координату рассчитывайте как startX + (endX - startX) * easedT, где easedT – преобразованный прогресс.

Заключение

Анимация на canvas повторяет цикл: обновить состояние, очистить холст, нарисовать объекты и запросить следующий кадр. Timestamp связывает скорость с прошедшим временем, а cancelAnimationFrame позволяет остановить движение.

После круга добавьте массив объектов, обработку пользовательского ввода и easing. Эти приемы превращают простой цикл в основу интерактивной графики. В качестве следующего упражнения сделайте круг, следующий за курсором.

Совет от нашей редакции: сначала добейтесь правильного цикла для одного объекта, а затем выносите обновление и отрисовку в отдельные функции.

Комментарии

Оставьте свой комментарий
Пока никто не оставил комментариев