Анимация путей SVG: attrTween и d3.interpolate

SVG-путь (path) — один из самых гибких элементов векторной графики. Через него строятся линии графиков, области, кривые Безье, дуги, сектора диаграмм и сложные пользовательские фигуры. В D3.js анимация таких элементов требует особого подхода, поскольку путь описывается строкой атрибута d, содержащей координаты и команды рисования.

Для плавного изменения формы путей библиотека предоставляет механизмы интерполяции, ключевыми среди которых являются:

  • d3.interpolate
  • selection.transition()
  • transition.attrTween()

Природа SVG-пути

Атрибут d содержит набор команд:

<path d="M10,50 L100,80 L200,20" />

Расшифровка:

Команда Назначение
M перемещение пера
L линия
C кубическая кривая Безье
Q квадратичная кривая
A дуга
Z замыкание пути

Проблема анимации заключается в том, что браузер не умеет автоматически плавно изменять строку пути. Для этого D3 вычисляет промежуточные значения.


Интерполяция в D3.js

Интерполяция — вычисление промежуточных состояний между двумя значениями.

Простейший пример:

const interpolate = d3.interpolate(0, 100);

console.log(interpolate(0));
console.log(interpolate(0.5));
console.log(interpolate(1));

Результат:

0
50
100

Аргумент всегда находится в диапазоне:

t ∈ [0, 1]

Где:

Значение Состояние
0 начало
1 конец
0.5 середина

Интерполяция строк SVG-путей

D3 умеет интерполировать даже сложные строки:

const interp = d3.interpolate(
    "M0,0 L100,0",
    "M0,0 L100,100"
);

console.log(interp(0.5));

D3 автоматически извлекает числа и вычисляет промежуточные координаты.


Transition и механизм анимации

Базовая анимация строится через transition():

d3.select("circle")
    .transition()
    .duration(1000)
    .attr("cx", 300);

Однако для сложных вычислений атрибутов используется attrTween().


Назначение attrTween()

Метод:

transition.attrTween(name, factory)

создаёт пользовательскую функцию интерполяции атрибута.

Структура:

.attrTween("d", function() {

    return function(t) {
        return ...;
    };

})

Где:

Элемент Назначение
внешняя функция подготовка интерполятора
внутренняя функция вычисление значения на каждом кадре
t прогресс анимации

Простейшая анимация пути

Исходный SVG

<svg width="500" height="200">
    <path id="line"
          fill="none"
          stroke="steelblue"
          stroke-width="4"/>
</svg>

Начальный путь

const path = d3.select("#line");

path.attr("d", "M50,150 L150,150");

Анимация изменения формы

path.transition()
    .duration(2000)
    .attrTween("d", function() {

        const interpolate = d3.interpolate(
            "M50,150 L150,150",
            "M50,150 L350,50"
        );

        return function(t) {
            return interpolate(t);
        };

    });

Как работает attrTween()

Во время анимации D3 вызывает функцию примерно 60 раз в секунду:

t = 0
t = 0.016
t = 0.032
...
t = 1

На каждом шаге:

interpolate(t)

генерирует новый путь.


Внутренний жизненный цикл tween-анимации

Этап 1 — создание transition

selection.transition()

Этап 2 — вызов фабрики tween

.attrTween("d", factory)

Этап 3 — создание интерполятора

const i = d3.interpolate(a, b);

Этап 4 — обновление на каждом кадре

return i(t);

Разница между attr() и attrTween()

attr()

.attr("cx", 300)

D3 сам решает способ интерполяции.


attrTween()

.attrTween("d", ...)

Полный контроль над вычислениями.


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

Морфинг — плавное превращение одной фигуры в другую.

Пример

const start = "M100,100 L200,100 L150,200 Z";

const end = "M50,50 L250,50 L250,250 L50,250 Z";

Анимация

d3.select("path")
    .transition()
    .duration(3000)
    .attrTween("d", function() {

        const interp = d3.interpolate(start, end);

        return function(t) {
            return interp(t);
        };

    });

Ограничение morph-анимации

Интерполяция работает корректно только тогда, когда пути имеют одинаковую структуру.

Например:

M x y
L x y
L x y
Z

должны совпадать у обоих путей.


Проблемы несовместимых путей

Плохой пример:

"M0,0 L100,0"

и

"M0,0 C50,50 80,80 100,100"

D3 не сможет корректно интерполировать команды.


Подготовка путей к морфингу

Обычно используют:

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

Интерполяция массивов координат

Часто удобнее интерполировать данные, а не строку пути.

Исходные данные

const data1 = [10, 30, 50];
const data2 = [80, 120, 40];

Генератор линии

const line = d3.line()
    .x((d, i) => i * 100)
    .y(d => d);

Tween

path.transition()
    .duration(2000)
    .attrTween("d", function() {

        const interp = d3.interpolateArray(data1, data2);

        return function(t) {

            const current = interp(t);

            return line(current);

        };

    });

Почему интерполяция данных лучше

Преимущества:

Подход Плюсы
Интерполяция строк быстро
Интерполяция данных гибкость
Интерполяция массивов контроль
Генерация через d3.line() масштабируемость

d3.interpolateArray()

Функция:

d3.interpolateArray(a, b)

покомпонентно интерполирует массивы.

Пример:

const interp = d3.interpolateArray(
    [0, 50],
    [100, 150]
);

console.log(interp(0.5));

Результат:

[50, 100]

Анимация линий графиков

Генератор линии

const line = d3.line()
    .x((d, i) => i * 80)
    .y(d => d);

Начальное состояние

const oldData = [100, 120, 80, 150];
const newData = [50, 180, 100, 60];

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

const path = svg.append("path")
    .attr("fill", "none")
    .attr("stroke", "black")
    .attr("stroke-width", 3)
    .attr("d", line(oldData));

Анимация обновления

path.transition()
    .duration(1500)
    .attrTween("d", function() {

        const interp = d3.interpolateArray(oldData, newData);

        return function(t) {
            return line(interp(t));
        };

    });

Анимация area-графиков

Для областей применяется d3.area().

Генератор области

const area = d3.area()
    .x((d, i) => i * 80)
    .y0(200)
    .y1(d => d);

Tween

.attrTween("d", function() {

    const interp = d3.interpolateArray(data1, data2);

    return function(t) {
        return area(interp(t));
    };

});

Анимация дуг

Дуги особенно активно используют tween-интерполяцию.


Генератор дуги

const arc = d3.arc()
    .innerRadius(50)
    .outerRadius(100);

Начальный и конечный углы

const start = {
    startAngle: 0,
    endAngle: Math.PI / 4
};

const end = {
    startAngle: 0,
    endAngle: Math.PI * 1.5
};

attrTween для дуги

path.transition()
    .duration(2000)
    .attrTween("d", function() {

        const interp = d3.interpolate(start, end);

        return function(t) {

            return arc(interp(t));

        };

    });

Анимация pie chart

D3 активно использует tweening в круговых диаграммах.

Обновление сектора

function arcTween(newAngle) {

    return function(d) {

        const interp = d3.interpolate(
            d.endAngle,
            newAngle
        );

        return function(t) {

            d.endAngle = interp(t);

            return arc(d);

        };

    };

}

Применение

path.transition()
    .duration(1000)
    .attrTween("d", arcTween(Math.PI));

Интерполяция объектов

D3 умеет интерполировать объекты:

const interp = d3.interpolate(
    {x: 0, y: 0},
    {x: 100, y: 50}
);

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

const interp = d3.interpolate(
    "red",
    "blue"
);

Цветовая анимация пути

path.transition()
    .duration(2000)
    .attrTween("stroke", function() {

        const interp = d3.interpolate(
            "#ff0000",
            "#0000ff"
        );

        return function(t) {
            return interp(t);
        };

    });

Интерполяция transform

.attrTween("transform", function() {

    const interp = d3.interpolate(
        "translate(0,0)",
        "translate(300,100)"
    );

    return function(t) {
        return interp(t);
    };

});

Использование easing-функций

Tween зависит от easing.

Пример

.transition()
.duration(2000)
.ease(d3.easeBounce)

Популярные easing-функции

Функция Эффект
easeLinear равномерно
easeQuad ускорение
easeCubic плавность
easeElastic пружина
easeBounce отскок

Пользовательская интерполяция

attrTween() позволяет полностью управлять логикой.

Пример синусоидального движения

.attrTween("d", function() {

    return function(t) {

        const y = 100 + Math.sin(t * 10) * 50;

        return `M0,100 L300,${y}`;

    };

});

Анимация рисования линии

Популярный эффект — постепенное появление линии.


Получение длины пути

const length = path.node().getTotalLength();

Настройка stroke-dasharray

path
    .attr("stroke-dasharray", length)
    .attr("stroke-dashoffset", length);

Анимация появления

path.transition()
    .duration(3000)
    .attr("stroke-dashoffset", 0);

Комбинирование tween и dash-анимации

path.transition()
    .duration(2000)
    .attrTween("d", ...)
    .attr("stroke-dashoffset", 0);

Производительность анимации

SVG-анимации могут быть дорогими.

Особенно тяжёлыми являются:

  • сложные пути
  • тысячи точек
  • частые пересчёты
  • множественные transition

Способы оптимизации

Минимизация числа точек

data.length = 100;

Кэширование генераторов

Плохо:

return d3.line()(data);

Хорошо:

const line = d3.line();

Избегание лишних DOM-операций

Нельзя:

d3.select("path")

внутри каждого кадра.


Прерывание анимации

path.interrupt();

Цепочки transition

path.transition()
    .duration(1000)
    .attr("opacity", 0.5)
    .transition()
    .duration(1000)
    .attr("opacity", 1);

События transition

start

.on("start", () => {
    console.log("start");
})

end

.on("end", () => {
    console.log("end");
})

interrupt

.on("interrupt", () => {
    console.log("stopped");
})

Генерация сложных анимированных кривых

Линия с curveBasis

const line = d3.line()
    .curve(d3.curveBasis);

Анимация сглаженной линии

.attrTween("d", function() {

    const interp = d3.interpolateArray(a, b);

    return function(t) {
        return line(interp(t));
    };

});

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

Иногда вместо transition() используют ручное управление.

function animate(time) {

    const t = Math.min(time / 2000, 1);

    path.attr("d", interpolate(t));

    if (t < 1) {
        requestAnimationFrame(animate);
    }

}

Когда attrTween() особенно полезен

Сценарий Причина
Морфинг путей нужен контроль
Анимация графиков интерполяция данных
SVG-дуги сложные вычисления
Пользовательские эффекты нестандартная логика
Геометрические трансформации ручной расчёт

Когда достаточно attr()

Простые случаи:

.attr("opacity", 0)
.attr("cx", 100)
.attr("width", 300)

Архитектурный подход

Наиболее устойчивый подход в D3:

  1. Интерполировать данные
  2. Генерировать SVG через генераторы
  3. Использовать attrTween() только для вычислений
  4. Не интерполировать вручную строки без необходимости

Типичная схема обновления графика

const interpolateData =
    d3.interpolateArray(oldData, newData);

path.transition()
    .duration(1000)
    .attrTween("d", () => {

        return t => line(
            interpolateData(t)
        );

    });

Практическая структура tween-анимации

selection.transition()
    .duration(1000)
    .ease(d3.easeCubic)
    .attrTween("d", function(d) {

        const interp = d3.interpolate(a, b);

        return function(t) {

            return generator(
                interp(t)
            );

        };

    });

Ключевые особенности attrTween()

Особенность Значение
Полный контроль можно вычислять любые значения
Работа с путями основная область применения
Интерполяция данных поддержка массивов и объектов
Генерация на каждом кадре высокая гибкость
Совместимость с easing плавная динамика

Ключевые функции интерполяции D3

Функция Назначение
d3.interpolate() универсальная интерполяция
d3.interpolateArray() массивы
d3.interpolateObject() объекты
d3.interpolateNumber() числа
d3.interpolateRgb() RGB-цвета
d3.interpolateTransformSvg() SVG transform
d3.interpolateString() строки