Анимация canvas-элементов

Canvas-элемент представляет собой растровую поверхность, управляемую через JavaScript API, где каждая перерисовка полностью контролируется кодом. В отличие от DOM-анимаций, где изменения затрагивают отдельные свойства элементов, canvas требует постоянного ручного обновления сцены. Именно поэтому применение высокоуровневых анимационных библиотек, таких как Velocity.js, в связке с canvas требует понимания архитектурных ограничений и особенностей отрисовки.

Основная проблема canvas-анимации заключается в том, что отсутствие отдельных узлов делает невозможным привычное управление через CSS transitions или transform. Каждый кадр сцены — это результат полного пересчёта состояния. Поэтому ключевым становится контроль времени, интерполяции и производительности.

Библиотека Velocity.js изначально ориентирована на работу с DOM-элементами, изменяя их свойства с высокой производительностью. Однако её система таймингов, easing-функций и управления анимационными очередями может быть адаптирована для управления параметрами canvas-сцены.

Canvas не хранит состояние объектов. Следовательно, Velocity.js не управляет графическими примитивами напрямую, а используется как движок интерполяции значений:

  • координаты объектов
  • углы вращения
  • масштаб
  • прозрачность
  • цветовые каналы
  • физические параметры (скорость, ускорение)

Вся графика при этом пересоздаётся через requestAnimationFrame.

Базовая архитектура canvas-анимации

Типичная структура canvas-анимации состоит из трёх слоёв:

  1. Модель состояния Хранит все объекты сцены и их параметры.

  2. Анимационный слой Управляет изменением числовых значений (здесь может использоваться Velocity.js).

  3. Рендеринг Полная перерисовка canvas на каждом кадре.

Пример базовой структуры:

const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");

const state = {
  circle: {
    x: 50,
    y: 50,
    radius: 20,
    opacity: 1
  }
};

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.globalAlpha = state.circle.opacity;
  ctx.beginPath();
  ctx.arc(state.circle.x, state.circle.y, state.circle.radius, 0, Math.PI * 2);
  ctx.fill();

  requestAnimationFrame(render);
}

render();

Использование Velocity.js как интерполяционного движка

Хотя Velocity.js не предназначена для canvas напрямую, её механизм tweening может изменять значения состояния.

Ключевая идея заключается в том, что Velocity.js обновляет JS-объект, а canvas только читает его на каждом кадре.

Velocity(state.circle, {
  x: 300,
  y: 200
}, {
  duration: 1000,
  easing: "easeInOutQuad"
});

При этом render() автоматически отобразит промежуточные значения.

Интерполяция координат и синхронизация кадров

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

Проблема проявляется при:

  • высокой частоте обновления состояния
  • одновременной анимации множества объектов
  • необходимости физически корректного движения

Для решения используется единый цикл рендеринга:

function loop() {
  drawScene(state);
  requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Velocity.js изменяет только данные, не вмешиваясь в рендер.

Множественные объекты и управление сценой

При увеличении числа объектов важно отказаться от индивидуальных Velocity-анимаций для каждого кадра в пользу централизованного управления состоянием.

Структура сцены:

const state = {
  particles: Array.from({ length: 100 }, (_, i) => ({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    vx: 0,
    vy: 0
  }))
};

Анимация параметров:

state.particles.forEach(p => {
  Velocity(p, {
    vx: Math.random() * 2 - 1,
    vy: Math.random() * 2 - 1
  }, {
    duration: 2000
  });
});

Рендер:

function drawScene(state) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  state.particles.forEach(p => {
    p.x += p.vx;
    p.y += p.vy;

    ctx.fillRect(p.x, p.y, 2, 2);
  });
}

Easing-функции и физическая имитация

Одним из сильных преимуществ Velocity.js является богатый набор easing-функций. В canvas-анимации они позволяют имитировать физические процессы:

  • ускорение падения
  • упругие столкновения
  • инерцию
  • затухание движения

Пример использования easing для имитации гравитации:

Velocity(state.circle, {
  y: canvas.height - 30
}, {
  duration: 1500,
  easing: "easeInQuad"
});

Кривая изменения скорости визуально приближается к свободному падению.

Работа с прозрачностью и слоями

Canvas не поддерживает слои в классическом DOM-смысле, поэтому управление глубиной достигается через порядок отрисовки и альфа-канал.

Velocity.js может управлять прозрачностью объектов:

Velocity(state.circle, {
  opacity: 0
}, {
  duration: 800
});

При рендеринге:

ctx.globalAlpha = state.circle.opacity;

Это позволяет реализовывать:

  • плавное появление объектов
  • исчезновение частиц
  • переходы между сценами

Производительность при большом количестве анимаций

При работе с canvas основное ограничение связано не с самой отрисовкой, а с частотой обновления состояния. Использование Velocity.js для сотен объектов требует оптимизации:

  • минимизация количества активных tween-ов
  • группировка изменений состояния
  • отказ от лишних easing-вычислений
  • использование числовых буферов вместо сложных объектов

Оптимальная схема:

  • Velocity.js управляет только ключевыми параметрами
  • промежуточные вычисления выполняет рендер-цикл
  • canvas получает уже готовые значения

Комбинация Velocity.js и requestAnimationFrame

Velocity.js использует собственный цикл обновления, однако при canvas-рендере он должен быть подчинён requestAnimationFrame.

Архитектурный принцип:

  • Velocity.js обновляет состояние
  • requestAnimationFrame рисует кадр
  • canvas не хранит логику движения

Это предотвращает рассинхронизацию и скачки кадров.

Сложные сцены и иерархия объектов

При усложнении сцены вводится иерархия:

  • сцена

    • группы объектов

      • отдельные элементы

Каждый уровень может иметь собственные анимации:

Velocity(state.group, {
  rotation: 360
}, {
  duration: 3000
});

Рендер учитывает трансформации:

ctx.save();
ctx.translate(group.x, group.y);
ctx.rotate(group.rotation);
ctx.restore();

Управление временными интервалами

Canvas-анимации чувствительны к временным дельтам. Velocity.js нормализует время выполнения анимации, но рендер-цикл должен учитывать реальную разницу кадров:

let last = performance.now();

function loop(now) {
  const delta = now - last;
  last = now;

  update(delta);
  draw();

  requestAnimationFrame(loop);
}

Это позволяет избежать дрожания при нестабильном FPS.

Применение в интерактивных визуализациях

Связка canvas и Velocity.js используется в:

  • интерактивных графиках
  • визуализации данных в реальном времени
  • частичных физических симуляциях
  • UI-анимациях поверх canvas
  • игровых прототипах

Гибкость достигается за счёт разделения логики и визуализации: Velocity.js отвечает за плавность изменений, canvas — за отрисовку результата.