Canvas интеграция

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

Ключевая идея интеграции заключается в разделении двух уровней:

  • Popmotion управляет состоянием (position, velocity, progress)
  • Canvas отвечает за визуализацию этого состояния

Базовая структура интеграции Popmotion и Canvas

Минимальная архитектура строится вокруг трех компонентов:

  1. Canvas rendering context
  2. Анимационный источник (tween, spring, physics)
  3. Функция отрисовки
import { tween } from 'popmotion';

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const state = {
  x: 50,
  y: 50
};

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

  ctx.beginPath();
  ctx.arc(state.x, state.y, 20, 0, Math.PI * 2);
  ctx.fill();
}

Popmotion подключается через поток обновлений:

tween({
  from: { x: 50, y: 50 },
  to: { x: 300, y: 200 },
  duration: 1000
}).start({
  update: latest => {
    state.x = latest.x;
    state.y = latest.y;
    draw();
  }
});

В этой модели Popmotion не рисует кадры — он лишь поставляет значения.


Разделение состояния и рендеринга

Canvas не имеет встроенного реактивного слоя. Поэтому любое движение должно проходить через промежуточное состояние.

Типовая ошибка — рисование прямо внутри анимации без контроля сцены. Более устойчивый подход:

  • хранить состояние сцены отдельно
  • обновлять только изменившиеся значения
  • централизовать render-loop
const state = {
  circle: { x: 100, y: 100, r: 20 }
};

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

  const { x, y, r } = state.circle;

  ctx.beginPath();
  ctx.arc(x, y, r, 0, Math.PI * 2);
  ctx.fill();
}

Использование value как реактивного источника

Popmotion предоставляет низкоуровневый механизм value, который особенно удобен для Canvas, так как позволяет подписываться на изменения без привязки к конкретному типу анимации.

import { value, spring } from 'popmotion';

const pos = value({ x: 100, y: 100 }, v => {
  state.circle.x = v.x;
  state.circle.y = v.y;
  render();
});

Далее любое движение можно привязать к этому значению:

spring({
  from: pos.get(),
  to: { x: 400, y: 300 },
  stiffness: 120,
  damping: 20
}).start(pos);

Такой подход делает value промежуточным слоем между физикой и Canvas-рендерингом.


Анимация через requestAnimationFrame и Popmotion

Хотя Popmotion сам управляет таймингом, Canvas-сцены часто требуют централизованного render loop.

function loop() {
  render();
  requestAnimationFrame(loop);
}

loop();

В этом случае Popmotion только обновляет состояние, а Canvas рисует независимо:

spring({
  from: { x: 100 },
  to: { x: 500 }
}).start(v => {
  state.circle.x = v.x;
});

Такой подход полезен при:

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

Интерактивность: управление Canvas через Pointer Events

Canvas не имеет встроенных событий для объектов сцены, поэтому взаимодействие строится вручную.

Popmotion pointer позволяет упростить обработку:

import { pointer } from 'popmotion';

canvas.addEventListener('pointerdown', (e) => {
  pointer({
    x: e.clientX,
    y: e.clientY
  }).start(v => {
    state.circle.x = v.x;
    state.circle.y = v.y;
    render();
  });
});

Таким образом движение курсора становится источником анимации.


Перетаскивание объектов с физическим возвратом

Комбинация pointer tracking и spring создаёт естественные взаимодействия.

import { pointer, spring } from 'popmotion';

let drag = null;

canvas.addEventListener('pointerdown', (e) => {
  drag = pointer({ x: state.circle.x, y: state.circle.y }).start(v => {
    state.circle.x = v.x;
    state.circle.y = v.y;
    render();
  });
});

canvas.addEventListener('pointerup', () => {
  if (drag) drag.stop();

  spring({
    from: { x: state.circle.x, y: state.circle.y },
    to: { x: 200, y: 200 },
    stiffness: 200,
    damping: 15
  }).start(v => {
    state.circle.x = v.x;
    state.circle.y = v.y;
    render();
  });
});

Множественные объекты и сценическая модель

При расширении до нескольких объектов Canvas становится сценой. Popmotion продолжает управлять отдельными состояниями.

const circles = [
  { x: 100, y: 100, r: 20 },
  { x: 200, y: 150, r: 30 },
  { x: 300, y: 200, r: 25 }
];

Рендеринг:

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

  circles.forEach(c => {
    ctx.beginPath();
    ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
    ctx.fill();
  });
}

Анимация отдельного элемента:

spring({
  from: circles[0],
  to: { x: 400, y: 300, r: 40 }
}).start(v => {
  circles[0] = v;
  render();
});

Частицы и физическая симуляция через physics

Popmotion physics позволяет строить динамические системы, особенно полезные для Canvas-частиц.

import { physics } from 'popmotion';

const particle = {
  x: 100,
  y: 100,
  vx: 5,
  vy: -10
};

physics({
  from: particle,
  friction: 0.8,
  springStrength: 500,
  to: { x: 0, y: 300 }
}).start(v => {
  state.circle.x = v.x;
  state.circle.y = v.y;
  render();
});

Такой подход используется для:

  • брызг
  • взрывных эффектов
  • гравитационных систем

Интерполяция и сглаживание движения

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

import { tween, linear } from 'popmotion';

tween({
  from: { x: 0 },
  to: { x: 600 },
  ease: linear,
  duration: 800
}).start(v => {
  state.circle.x = v.x;
  render();
});

Для более органичного движения обычно используется spring, а не tween.


Оптимизация рендеринга сцены

Canvas требует контроля частоты перерисовки. При множественных потоках Popmotion может вызывать render слишком часто. Решение — батчинг обновлений.

let dirty = false;

function markDirty() {
  dirty = true;
}

function loop() {
  if (dirty) {
    render();
    dirty = false;
  }
  requestAnimationFrame(loop);
}

loop();

Popmotion обновляет состояние:

spring({
  from: state.circle,
  to: { x: 500, y: 100 }
}).start(v => {
  state.circle = v;
  markDirty();
});

Синхронизация нескольких анимаций

Canvas-сцены часто содержат несколько независимых анимаций. Popmotion позволяет запускать их параллельно.

spring({ from: 0, to: 100 }).start(v => state.circle.x = v);
spring({ from: 0, to: 200 }).start(v => state.circle.y = v);
spring({ from: 10, to: 40 }).start(v => state.circle.r = v);

Рендер вызывается отдельно, что предотвращает лишнюю нагрузку.


Комбинирование tween и physics в одной сцене

Гибридные сцены часто используют tween для макродвижений и physics для микродинамики.

  • tween — перемещение камеры или объектов
  • physics — частицы и инерция
tween({
  from: { x: 0 },
  to: { x: 800 },
  duration: 2000
}).start(v => {
  state.camera.x = v.x;
});

Параллельно:

physics({
  from: { x: 100, y: 100, vx: 3, vy: 2 }
}).start(v => {
  state.particle.x = v.x;
  state.particle.y = v.y;
});

Управление камерой Canvas-сцены

Canvas часто используется как “виртуальная сцена”, где камера — это смещение координат.

const camera = { x: 0, y: 0 };

function render() {
  ctx.setTransform(1, 0, 0, 1, -camera.x, -camera.y);

  circles.forEach(c => {
    ctx.beginPath();
    ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
    ctx.fill();
  });
}

Анимация камеры через Popmotion:

spring({
  from: { x: 0, y: 0 },
  to: { x: 200, y: 150 }
}).start(v => {
  camera.x = v.x;
  camera.y = v.y;
  render();
});

Принципы устойчивой архитектуры Canvas + Popmotion

  • состояние сцены отделено от рендера
  • Popmotion не вызывает Canvas напрямую
  • render вызывается централизованно или через dirty-flag
  • каждая сущность имеет собственный поток анимации
  • физика и интерполяция разделены по смыслу

Такая структура позволяет масштабировать Canvas-сцену до десятков и сотен объектов без деградации управляемости кода