Canvas в браузере работает как императивный слой отрисовки, где каждое изменение состояния требует полного или частичного перерисовывания сцены. В отличие от DOM-анимаций, где браузер оптимизирует композицию и стили, Canvas требует ручного управления кадром. Именно здесь Popmotion становится инструментом управления временной и физической динамикой, оставляя рендеринг полностью под контролем разработчика.
Ключевая идея интеграции заключается в разделении двух уровней:
Минимальная архитектура строится вокруг трех компонентов:
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 не имеет встроенного реактивного слоя. Поэтому любое движение должно проходить через промежуточное состояние.
Типовая ошибка — рисование прямо внутри анимации без контроля сцены. Более устойчивый подход:
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-рендерингом.
Хотя 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;
});
Такой подход полезен при:
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();
});
physicsPopmotion 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({
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 часто используется как “виртуальная сцена”, где камера — это смещение координат.
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-сцену до десятков и сотен объектов без деградации управляемости кода