Морфинг путей — это процесс плавного преобразования одной формы в
другую за счёт интерполяции координат вершин. В библиотеке 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)Морфинг работает через изменение этих параметров во времени.
shapeA.vertices.length === shapeB.vertices.length
Если количество вершин различается:
Даже при одинаковом количестве точек, порядок имеет значение:
Типы сегментов (line, curve) должны
совпадать:
anchor.command === Two.Commands.curve
const shapeA = two.makePolygon(100, 100, 50, 3); // треугольник
const shapeB = two.makePolygon(100, 100, 50, 6); // шестиугольник
Если формы несовместимы, необходимо:
Пример приведения:
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);
}
}
Это предотвращает:
Two.js поддерживает импорт SVG:
two.load('shape.svg', function(group) {
const path = group.children[0];
});
Проблема: SVG-пути часто имеют разную структуру.
Ресэмплинг (пересчёт точек) Преобразование пути в набор равномерных точек
Упрощение Удаление лишних сегментов
Нормализация Приведение к одинаковому количеству вершин
Пример упрощённого подхода:
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;
}
Для более естественной анимации:
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;
Результат: разрывы и скачки
Результат: «скручивание» формы
Результат: потеря плавности
Результат: накопление искажений
Вместо прямого перехода 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);
});
Если формы сильно отличаются:
Критерии:
При нарушении — проблема почти всегда в структуре вершин.