Морфинг фигур

Морфинг фигур в интерфейсах строится на непрерывном преобразовании одного векторного контура в другой. В вебе базовой единицей такого преобразования выступает атрибут d элемента SVG <path>. Он представляет собой строку команд, описывающих кривые Безье, линии и дуги.

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

Приведение SVG-путей к совместимому виду

Для корректного морфинга необходимо:

  • привести пути к одному типу команд (обычно к кубическим кривым Безье)
  • синхронизировать количество сегментов
  • нормализовать порядок точек

Эта задача решается через специализированные библиотеки интерполяции путей. На практике часто используется flubber, которая строит функцию перехода между двумя d-строками.

import { interpolate } from "flubber";

const fromPath = "M10,80 Q100,10 200,80 T390,80";
const toPath   = "M10,10 200,10 200,200 10,200 Z";

const morph = interpolate(fromPath, toPath);

Функция morph(t) возвращает промежуточный путь для значения t в диапазоне от 0 до 1.

Подключение Popmotion к процессу интерполяции

Popmotion предоставляет универсальный механизм анимации значений через animate. Его задача — управлять временем и выдавать прогресс анимации, который затем используется для вычисления промежуточного состояния SVG.

import { animate } from "popmotion";

Связка выглядит следующим образом:

  • Popmotion управляет временем и easing
  • функция интерполяции вычисляет форму
  • результат записывается в DOM

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

import { animate } from "popmotion";
import { interpolate } from "flubber";

const fromPath = "M20,20 L180,20 L180,180 L20,180 Z";
const toPath = "M100,20 C180,20 180,180 100,180 C20,180 20,20 100,20 Z";

const morph = interpolate(fromPath, toPath);

const pathElement = document.querySelector("path");

animate({
  from: 0,
  to: 1,
  duration: 1200,
  onUpdate: (t) => {
    pathElement.setAttribute("d", morph(t));
  }
});

В этом примере Popmotion отвечает только за прогресс t. Геометрия полностью контролируется функцией morph.

Управление плавностью через easing

Popmotion позволяет подключать функции сглаживания, изменяющие характер движения параметра времени. Это напрямую влияет на визуальное ощущение морфинга.

import { animate, easeInOut } from "popmotion";

Применение easing:

animate({
  from: 0,
  to: 1,
  duration: 1500,
  ease: easeInOut,
  onUpdate: (t) => {
    pathElement.setAttribute("d", morph(t));
  }
});

Разные кривые дают разные эффекты:

  • линейная интерполяция — техническая, «жесткая»
  • ease-in — ускоряющийся старт
  • ease-out — замедляющийся финиш
  • ease-in-out — естественное движение

Морфинг с использованием физической модели

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

import { animate, spring } from "popmotion";
animate({
  from: 0,
  to: 1,
  type: "spring",
  stiffness: 120,
  damping: 18,
  onUpdate: (t) => {
    pathElement.setAttribute("d", morph(t));
  }
});

Физическая модель делает движение менее линейным и более реактивным к изменениям состояния.

Морфинг нескольких фигур через keyframes

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

import { animate } from "popmotion";
import { interpolate } from "flubber";

const square = "M20,20 L180,20 L180,180 L20,180 Z";
const triangle = "M100,20 L180,180 L20,180 Z";
const circle = "M100,20 A80,80 0 1 1 99.9,20 Z";

const squareToTriangle = interpolate(square, triangle);
const triangleToCircle = interpolate(triangle, circle);

animate({
  from: 0,
  to: 2,
  duration: 2400,
  onUpdate: (t) => {
    if (t < 1) {
      pathElement.setAttribute("d", squareToTriangle(t));
    } else {
      pathElement.setAttribute("d", triangleToCircle(t - 1));
    }
  }
});

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

Синхронизация морфинга с трансформациями

Морфинг часто комбинируется с трансформациями scale, rotate, translate. Popmotion позволяет анимировать несколько свойств одновременно через независимые анимационные процессы.

import { animate } from "popmotion";

animate({
  from: 0,
  to: 1,
  duration: 1200,
  onUpdate: (t) => {
    pathElement.setAttribute("d", morph(t));
    pathElement.style.transform = `scale(${1 + t * 0.3}) rotate(${t * 90}deg)`;
  }
});

При таком подходе важно учитывать, что трансформации применяются поверх геометрии пути и не влияют на сам d.

Цветовой морфинг как часть общей формы

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

import { animate, mixColor } from "popmotion";

const fromColor = "#ff6a00";
const toColor = "#4a90e2";

animate({
  from: 0,
  to: 1,
  duration: 1200,
  onUpdate: (t) => {
    pathElement.setAttribute("d", morph(t));
    pathElement.style.fill = mixColor(fromColor, toColor)(t);
  }
});

Цветовая интерполяция уменьшает визуальный разрыв между несовместимыми геометриями.

Интерактивный морфинг по пользовательскому вводу

Морфинг становится более выразительным при привязке к событиям интерфейса: наведению, клику или движению курсора.

let current = 0;

element.addEventListener("mouseenter", () => {
  animate({
    from: current,
    to: 1,
    duration: 600,
    onUpdate: (t) => {
      current = t;
      pathElement.setAttribute("d", morph(t));
    }
  });
});

element.addEventListener("mouseleave", () => {
  animate({
    from: current,
    to: 0,
    duration: 600,
    onUpdate: (t) => {
      current = t;
      pathElement.setAttribute("d", morph(t));
    }
  });
});

Такой подход требует хранения текущего состояния анимации, чтобы новые события не «обнуляли» переход.

Морфинг с динамическим пересчётом цели

В сложных интерфейсах целевая форма может меняться во время анимации. Тогда интерполяция пересоздаётся на лету.

let morph = interpolate(pathA, pathB);
let progress = 0;

function setTarget(newPath) {
  morph = interpolate(pathB, newPath);
  pathB = newPath;
}

animate({
  from: 0,
  to: 1,
  repeat: Infinity,
  onUpdate: (t) => {
    progress = t;
    pathElement.setAttribute("d", morph(t));
  }
});

Это позволяет реализовать адаптивные морфинговые системы, где фигуры «перетекают» в новые состояния без перезапуска анимации.

Оптимизация отрисовки SVG-морфинга

При высокой частоте обновления важно минимизировать нагрузку:

  • кэширование интерполяционных функций
  • использование requestAnimationFrame внутри Popmotion (по умолчанию уже применяется)
  • избегание лишних DOM-запросов
  • сокращение количества атрибутных операций

Снижение количества вычислений особенно критично при одновременном морфинге нескольких объектов.

Комбинирование морфинга с Canvas-рендерингом

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

animate({
  from: 0,
  to: 1,
  duration: 1000,
  onUpdate: (t) => {
    const path = morph(t);

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    const shape = new Path2D(path);
    ctx.fill(shape);
  }
});

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

Переходы между абстрактными формами

Морфинг не ограничивается геометрически похожими фигурами. При корректной интерполяции возможно преобразование:

  • иконок в абстрактные формы
  • букв в символы
  • логотипов в декоративные элементы

Ключевым фактором остаётся подготовка совместимых путей, где структура кривых позволяет построить непрерывное преобразование без разрывов и скачков.