SVG-путь (path) — один из самых гибких элементов
векторной графики. Через него строятся линии графиков, области, кривые
Безье, дуги, сектора диаграмм и сложные пользовательские фигуры. В D3.js
анимация таких элементов требует особого подхода, поскольку путь
описывается строкой атрибута d, содержащей координаты и
команды рисования.
Для плавного изменения формы путей библиотека предоставляет механизмы интерполяции, ключевыми среди которых являются:
d3.interpolateselection.transition()transition.attrTween()Атрибут d содержит набор команд:
<path d="M10,50 L100,80 L200,20" />
Расшифровка:
| Команда | Назначение |
|---|---|
M |
перемещение пера |
L |
линия |
C |
кубическая кривая Безье |
Q |
квадратичная кривая |
A |
дуга |
Z |
замыкание пути |
Проблема анимации заключается в том, что браузер не умеет автоматически плавно изменять строку пути. Для этого D3 вычисляет промежуточные значения.
Интерполяция — вычисление промежуточных состояний между двумя значениями.
Простейший пример:
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 |
середина |
D3 умеет интерполировать даже сложные строки:
const interp = d3.interpolate(
"M0,0 L100,0",
"M0,0 L100,100"
);
console.log(interp(0.5));
D3 автоматически извлекает числа и вычисляет промежуточные координаты.
Базовая анимация строится через transition():
d3.select("circle")
.transition()
.duration(1000)
.attr("cx", 300);
Однако для сложных вычислений атрибутов используется
attrTween().
Метод:
transition.attrTween(name, factory)
создаёт пользовательскую функцию интерполяции атрибута.
Структура:
.attrTween("d", function() {
return function(t) {
return ...;
};
})
Где:
| Элемент | Назначение |
|---|---|
| внешняя функция | подготовка интерполятора |
| внутренняя функция | вычисление значения на каждом кадре |
t |
прогресс анимации |
<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);
};
});
Во время анимации D3 вызывает функцию примерно 60 раз в секунду:
t = 0
t = 0.016
t = 0.032
...
t = 1
На каждом шаге:
interpolate(t)
генерирует новый путь.
selection.transition()
.attrTween("d", factory)
const i = d3.interpolate(a, b);
return i(t);
.attr("cx", 300)
D3 сам решает способ интерполяции.
.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);
};
});
Интерполяция работает корректно только тогда, когда пути имеют одинаковую структуру.
Например:
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);
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(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));
};
});
Для областей применяется d3.area().
const area = d3.area()
.x((d, i) => i * 80)
.y0(200)
.y1(d => d);
.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
};
path.transition()
.duration(2000)
.attrTween("d", function() {
const interp = d3.interpolate(start, end);
return function(t) {
return arc(interp(t));
};
});
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);
};
});
.attrTween("transform", function() {
const interp = d3.interpolate(
"translate(0,0)",
"translate(300,100)"
);
return function(t) {
return interp(t);
};
});
Tween зависит от easing.
.transition()
.duration(2000)
.ease(d3.easeBounce)
| Функция | Эффект |
|---|---|
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();
path
.attr("stroke-dasharray", length)
.attr("stroke-dashoffset", length);
path.transition()
.duration(3000)
.attr("stroke-dashoffset", 0);
path.transition()
.duration(2000)
.attrTween("d", ...)
.attr("stroke-dashoffset", 0);
SVG-анимации могут быть дорогими.
Особенно тяжёлыми являются:
data.length = 100;
Плохо:
return d3.line()(data);
Хорошо:
const line = d3.line();
Нельзя:
d3.select("path")
внутри каждого кадра.
path.interrupt();
path.transition()
.duration(1000)
.attr("opacity", 0.5)
.transition()
.duration(1000)
.attr("opacity", 1);
.on("start", () => {
console.log("start");
})
.on("end", () => {
console.log("end");
})
.on("interrupt", () => {
console.log("stopped");
})
const line = d3.line()
.curve(d3.curveBasis);
.attrTween("d", function() {
const interp = d3.interpolateArray(a, b);
return function(t) {
return line(interp(t));
};
});
Иногда вместо transition() используют ручное
управление.
function animate(time) {
const t = Math.min(time / 2000, 1);
path.attr("d", interpolate(t));
if (t < 1) {
requestAnimationFrame(animate);
}
}
| Сценарий | Причина |
|---|---|
| Морфинг путей | нужен контроль |
| Анимация графиков | интерполяция данных |
| SVG-дуги | сложные вычисления |
| Пользовательские эффекты | нестандартная логика |
| Геометрические трансформации | ручной расчёт |
Простые случаи:
.attr("opacity", 0)
.attr("cx", 100)
.attr("width", 300)
Наиболее устойчивый подход в D3:
attrTween() только для вычисленийconst interpolateData =
d3.interpolateArray(oldData, newData);
path.transition()
.duration(1000)
.attrTween("d", () => {
return t => line(
interpolateData(t)
);
});
selection.transition()
.duration(1000)
.ease(d3.easeCubic)
.attrTween("d", function(d) {
const interp = d3.interpolate(a, b);
return function(t) {
return generator(
interp(t)
);
};
});
| Особенность | Значение |
|---|---|
| Полный контроль | можно вычислять любые значения |
| Работа с путями | основная область применения |
| Интерполяция данных | поддержка массивов и объектов |
| Генерация на каждом кадре | высокая гибкость |
| Совместимость с easing | плавная динамика |
| Функция | Назначение |
|---|---|
d3.interpolate() |
универсальная интерполяция |
d3.interpolateArray() |
массивы |
d3.interpolateObject() |
объекты |
d3.interpolateNumber() |
числа |
d3.interpolateRgb() |
RGB-цвета |
d3.interpolateTransformSvg() |
SVG transform |
d3.interpolateString() |
строки |