Анимация по пути

В Paper.js анимация объектов по пути реализуется через комбинацию Path, Point, Segment и функций обновления в цикле onFrame. Ключевая идея заключается в том, чтобы вычислять положение объекта на кривой с помощью процентного прогресса или длины пути, а затем обновлять координаты объекта на каждом кадре.

Создание пути

Для начала необходимо создать путь, по которому будет двигаться объект:

var path = new Path();
path.strokeColor = 'black';
path.add(new Point(50, 100));
path.add(new Point(300, 50));
path.add(new Point(450, 200));
path.add(new Point(600, 100));

Каждый Segment пути можно задавать явно через координаты. Path поддерживает линии, кривые Безье и комбинированные формы. Для анимации удобно использовать getPointAt(offset), чтобы получать точку на пути по заданной длине.

Вычисление длины пути

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

var totalLength = path.length;

Значение totalLength используется для расчёта позиции объекта относительно пути в любой момент времени. Paper.js автоматически вычисляет длину сегментов кривых и прямых линий.

Движение объекта по пути

Создаём объект, который будет двигаться:

var circle = new Path.Circle({
    center: path.firstSegment.point,
    radius: 10,
    fillColor: 'red'
});

Для анимации используется событие onFrame, которое вызывается при каждом кадре рендеринга:

var progress = 0; // Текущий прогресс в пикселях
var speed = 2; // Скорость движения

function onFrame(event) {
    progress += speed;
    if (progress > totalLength) {
        progress = 0; // Циклическое движение
    }
    circle.position = path.getPointAt(progress);
}

Метод getPointAt(offset) возвращает координаты точки на пути на указанном расстоянии от начала. Таким образом можно легко контролировать скорость и направление движения.

Анимация с кривыми Безье

Если путь содержит кривые Безье, getPointAt корректно интерполирует положение по кривой, обеспечивая плавное движение. Например:

var bezierPath = new Path();
bezierPath.strokeColor = 'blue';
bezierPath.add(new Point(100, 300));
bezierPath.cubicCurveTo(new Point(200, 100), new Point(400, 500), new Point(500, 300));

var dot = new Path.Circle({
    center: bezierPath.firstSegment.point,
    radius: 8,
    fillColor: 'green'
});

var t = 0;
function onFrame(event) {
    t += 0.01;
    if (t > 1) t = 0;
    dot.position = bezierPath.getPointAt(t * bezierPath.length);
}

Использование коэффициента t от 0 до 1 позволяет интерполировать движение по всей длине пути, независимо от формы кривой.

Вращение объекта вдоль пути

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

function onFrame(event) {
    progress += speed;
    if (progress > totalLength) progress = 0;

    var point = path.getPointAt(progress);
    var tangent = path.getTangentAt(progress); // Вектор касательной
    circle.position = point;
    circle.rotation = tangent.angle; // Угол направления движения
}

getTangentAt(offset) возвращает касательный вектор, который указывает направление движения. Это особенно важно для объектов с выраженной ориентацией, например, машин или стрелок.

Анимация с ускорением и замедлением

Для более сложных эффектов можно использовать функции easing. Пример ускорения и замедления:

function easeInOut(t) {
    return t < 0.5
        ? 2 * t * t
        : -1 + (4 - 2 * t) * t;
}

var t = 0;
function onFrame(event) {
    t += 0.005;
    if (t > 1) t = 0;

    var easedT = easeInOut(t);
    circle.position = path.getPointAt(easedT * path.length);
}

Использование easedT позволяет плавно изменять скорость движения, создавая эффект ускорения в начале и замедления в конце пути.

Множественные объекты на одном пути

Для создания нескольких объектов на одном пути можно использовать массив прогрессов:

var balls = [];
for (var i = 0; i < 5; i++) {
    balls.push(new Path.Circle({
        center: path.firstSegment.point,
        radius: 6,
        fillColor: 'orange'
    }));
}

var progresses = [0, 50, 100, 150, 200];

function onFrame(event) {
    for (var i = 0; i < balls.length; i++) {
        progresses[i] += speed;
        if (progresses[i] > totalLength) progresses[i] -= totalLength;
        balls[i].position = path.getPointAt(progresses[i]);
    }
}

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

Отслеживание положения по длине пути

Помимо визуального перемещения, Paper.js позволяет определять процент пройденного пути, вычислять расстояние до конца или до других сегментов. Это открывает возможности для взаимодействия объектов:

var distanceToEnd = path.length - progress;
if (distanceToEnd < 10) {
    circle.fillColor = 'purple';
}

Использование длины пути и касательных в сочетании с событиями onFrame позволяет строить сложные анимации с динамическими эффектами, столкновениями и синхронизацией нескольких объектов.