Морфинг путей

Морфинг путей — это процесс плавного преобразования одной формы в другую за счёт интерполяции координат вершин. В библиотеке Two.js этот механизм не реализован как отдельный «магический» метод, а строится на управлении массивами вершин (vertices) объектов типа Two.Path.

Ключевая идея: две формы должны иметь совместимую структуру, иначе анимация будет некорректной или визуально «ломаной».


Структура пути (Two.Path)

Каждый путь в Two.js представляет собой массив вершин:

const path = new Two.Path([
  new Two.Anchor(0, 0),
  new Two.Anchor(50, 100),
  new Two.Anchor(100, 0)
], true);

Каждая вершина (Two.Anchor) содержит:

  • x, y — координаты
  • controls.left, controls.right — управляющие точки для кривых Безье
  • command — тип сегмента (move, line, curve)

Морфинг работает через изменение этих параметров во времени.


Требования к морфингу

1. Одинаковое количество вершин

shapeA.vertices.length === shapeB.vertices.length

Если количество вершин различается:

  • появятся скачки
  • форма может «переворачиваться»
  • интерполяция станет непредсказуемой

2. Совпадающий порядок вершин

Даже при одинаковом количестве точек, порядок имеет значение:

  • вершины должны соответствовать друг другу логически
  • иначе морфинг создаст эффект «скручивания»

3. Совпадающие команды сегментов

Типы сегментов (line, curve) должны совпадать:

anchor.command === Two.Commands.curve

Базовый алгоритм морфинга

Шаг 1: создание двух форм

const shapeA = two.makePolygon(100, 100, 50, 3); // треугольник
const shapeB = two.makePolygon(100, 100, 50, 6); // шестиугольник

Шаг 2: выравнивание структуры

Если формы несовместимы, необходимо:

  • добавить вершины
  • пересчитать координаты

Пример приведения:

function normalizeVertices(shape, targetCount) {
  const vertices = shape.vertices;
  while (vertices.length < targetCount) {
    vertices.push(vertices[vertices.length - 1].clone());
  }
}

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

Основная формула линейной интерполяции:

function lerp(a, b, t) {
  return a + (b - a) * t;
}

Применение к вершинам:

function morph(shapeA, shapeB, t) {
  for (let i = 0; i < shapeA.vertices.length; i++) {
    const vA = shapeA.vertices[i];
    const vB = shapeB.vertices[i];

    vA.x = lerp(vA.x, vB.x, t);
    vA.y = lerp(vA.y, vB.y, t);
  }
}

Анимация морфинга

Two.js использует встроенный цикл обновления:

let t = 0;
let direction = 1;

two.bind('update', function() {
  t += 0.01 * direction;

  if (t >= 1 || t <= 0) {
    direction *= -1;
  }

  morph(shapeA, shapeB, t);
});

Работа с кривыми Безье

Если используются кривые, необходимо интерполировать управляющие точки:

vA.controls.left.x = lerp(
  vA.controls.left.x,
  vB.controls.left.x,
  t
);

vA.controls.left.y = lerp(
  vA.controls.left.y,
  vB.controls.left.y,
  t
);

Аналогично для controls.right.

Без этого:

  • кривая может «ломаться»
  • форма станет угловатой

Морфинг замкнутых и незамкнутых путей

Путь может быть:

path.closed = true;

Важно:

  • оба пути должны быть либо замкнутыми, либо нет
  • иначе появятся визуальные артефакты

Подход через копирование состояния

Чтобы избежать накопления ошибок, используется отдельный источник данных:

const originalA = shapeA.vertices.map(v => v.clone());
const originalB = shapeB.vertices.map(v => v.clone());

И затем:

function morph(t) {
  for (let i = 0; i < shapeA.vertices.length; i++) {
    shapeA.vertices[i].x = lerp(originalA[i].x, originalB[i].x, t);
    shapeA.vertices[i].y = lerp(originalA[i].y, originalB[i].y, t);
  }
}

Это предотвращает:

  • накопление погрешностей
  • деградацию формы со временем

Морфинг между произвольными SVG-путями

Two.js поддерживает импорт SVG:

two.load('shape.svg', function(group) {
  const path = group.children[0];
});

Проблема: SVG-пути часто имеют разную структуру.

Решение:

  1. Ресэмплинг (пересчёт точек) Преобразование пути в набор равномерных точек

  2. Упрощение Удаление лишних сегментов

  3. Нормализация Приведение к одинаковому количеству вершин


Ресэмплинг пути

Пример упрощённого подхода:

function resample(path, count) {
  const result = [];

  for (let i = 0; i < count; i++) {
    const t = i / (count - 1);
    const index = Math.floor(t * (path.vertices.length - 1));
    result.push(path.vertices[index].clone());
  }

  return result;
}

Морфинг с использованием easing-функций

Для более естественной анимации:

function easeInOut(t) {
  return t < 0.5
    ? 2 * t * t
    : 1 - Math.pow(-2 * t + 2, 2) / 2;
}

Использование:

morph(shapeA, shapeB, easeInOut(t));

Морфинг цвета и стилей

Помимо геометрии, можно интерполировать:

  • цвет (fill, stroke)
  • прозрачность (opacity)
  • толщину линии (linewidth)

Пример:

shapeA.fill = `rgb(${lerp(255, 0, t)}, 0, ${lerp(0, 255, t)})`;

Оптимизация производительности

Минимизация аллокаций

  • не создавать новые объекты внутри цикла
  • использовать заранее подготовленные массивы

Работа с флагами обновления

shapeA.verticesNeedUpdate = true;

Ограничение количества вершин

  • меньше вершин → быстрее морфинг
  • оптимально: 20–100 точек

Частые ошибки

Разное количество точек

Результат: разрывы и скачки

Неправильный порядок вершин

Результат: «скручивание» формы

Игнорирование кривых Безье

Результат: потеря плавности

Изменение исходных данных

Результат: накопление искажений


Расширенные техники

Морфинг через промежуточные формы

Вместо прямого перехода A → B:

A → C → D → B

Позволяет:

  • контролировать траекторию
  • создавать сложные анимации

Морфинг с задержкой вершин

Добавление фазового сдвига:

const offset = i / vertices.length;
const localT = (t + offset) % 1;

Эффект:

  • «волнообразное» преобразование

Нелинейная интерполяция

Вместо линейной:

Math.sin(t * Math.PI * 0.5)

Позволяет:

  • создавать органические движения
  • избегать механистичности

Комбинирование с трансформациями

Морфинг можно сочетать с:

shape.rotation += 0.01;
shape.scale = lerp(1, 2, t);

Это создаёт:

  • более сложные сцены
  • эффект «живой» графики

Практический паттерн

Структура кода для морфинга:

// 1. Подготовка
const source = [...];
const target = [...];

// 2. Обновление
function update(t) {
  for (...) {
    // интерполяция
  }
}

// 3. Анимация
two.bind('update', () => {
  update(t);
});

Поведение при экстремальных формах

Если формы сильно отличаются:

  • использовать промежуточные состояния
  • применять сглаживание
  • уменьшать скорость анимации

Визуальные эффекты на основе морфинга

  • трансформация иконок
  • анимация логотипов
  • органические движения (blob-анимации)
  • переходы между состояниями интерфейса

Контроль качества морфинга

Критерии:

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

При нарушении — проблема почти всегда в структуре вершин.