Анимация SVG элемента path

Думаю многие видели обзоры игровых консолей нового поколения от Polygon (Vox Media). Это те, где консоли отрисовывались в стиле blueprint’ов:
fbe3027fdb6eb26c2d36c41ce1856b5c
Обзоры выглядели круто, довольно необычно и ново. О том как реализована основная фишка обзоров — SVG анимация, как сделать нечто подобное самому, и какие ещё «секретные» возможности скрывает старый добрый SVG в плане анимации элемента path— можно узнать под катом.

Stroke-dasharray interpolation, теория

Вообще техника подобной анимации линий не нова, просто до недавнего времени SVG и всё, что с ним связано, на мой взгляд, было несправедливо предано забвению, но к счастью ситуация меняется. Итак, трюк с анимацией элемента path возможен благодаря свойству stroke-dasharray элемента path. Это свойство позволяет задавать параметры пунктирной линии, а именно длину штриха и промежутка между штрихами. Если задать длину штриха равной всей длине линии, то получим обыкновенную сплошную линию. Если же задать длину штриха равной нулю, а длину промежутка опять-таки равной всей длине линии, то получим невидимую линию. А постепенно увеличивая длину штриха при длине промежутка, равной длине всей линии, мы можем имитировать её отрисовку. При таком подходе отрисовка будет происходить от начала линии. Если же вдруг необходимо отрисовывать с конца, то нужно использовать ещё одно свойство: stroke-dashoffset. Это свойство определяет смещение для первого штриха. Таким образом, уменьшая смещение и увеличивая длину штриха, получим отрисовку с конца линии.

Ребята из Vox Media использовали гибридный вариант (который, на мой взгляд, избыточен), кстати почитать о том, как они это делали, можно (и нужно) в их блоге:Polygon feature design: SVG animations for fun and profit.

Реализация SVG анимации

В Vox Media предлагают использовать requestAnimationFrame для плавности анимации, но у нас немного другие цели, так что мы пойдём более простым путём, воспользуемся библиотекой D3.js и реализованной в ней анимацией на основе длительности.

Вот собственно рабочий код, использовавшийся для анимации консоли из начала статьи.

queue()
.defer(d3.xml, "PS4.svg", "image/svg+xml")
.await(ready);

function ready(error, xml) {

  //Adding our svg file to HTML document
  var importedNode = document.importNode(xml.documentElement, true);
  d3.select("#pathAnimation").node().appendChild(importedNode);

  var svg = d3.select("svg"),
  svgWidth = svg.attr("width"),
  svgHeight = svg.attr("height");

  var paths = svg.selectAll("path")
    .call(transition);

  function transition(path) {
    path.transition()
        .duration(5000)
        .attrTween("stroke-dasharray", tweenDash)
        .each("end", function() { d3.select(this).call(transition); }); // infinite loop
  }

  function tweenDash() {
    var l = this.getTotalLength(),
        i = d3.interpolateString("0," + l, l + "," + l); // interpolation of stroke-dasharray attr
    return function(t) {
      return i(t);
    };
  }
}

В функции transition(path) мы используем transition.attrTween(name, tween), который вызывает интерполятор, определённый в функции tweenDash(). Для вычисления длины используется метод getTotalLength(), определённый для элементов SVG path. Затем с помощью d3.interpolateString(a, b) задаётся интерполятор свойства stroke-dasharray, который выполнит интерполяцию значений от stroke-dasharray: 0,l; до stroke-dasharray: l,l; (здесь первое значение задаёт длину штриха, а второе длину промежутка). Посмотреть на то как это работает можно на bl.ocks.orgPlayStation 4: SVG animation.

Думаю вы заметили, что часть элементов отображается постоянно, это происходит потому, что они нарисованы не с помощью path. Вообще подготовка и оптимизация векторных изображений для web это довольно обширная тема с массой нюансов, тянущая на отдельную статью.

Движение вдоль элемента path

У path есть ещё один весьма полезный метод — getPointAtLength(distance in float). Он позволяет получить координаты точки, находящейся на заданной дистанции от начала линии. С помощью него можно реализовать движение какого-либо маркера вдоль линии, и, что немаловажно, плавное вращение за счёт вычисления касательной к имеющейся линии.
70a0bba5667a805dea6c92f291d003a1
Начнём просто с движения вдоль линии, пока без вращения.

queue()
.defer(d3.xml, "wiggle.svg", "image/svg+xml")
.await(ready);

function ready(error, xml) {

  //Adding our svg file to HTML document
  var importedNode = document.importNode(xml.documentElement, true);
  d3.select("#pathAnimation").node().appendChild(importedNode);

  var svg = d3.select("svg");

  var path = svg.select("path#wiggle"),
  startPoint = pathStartPoint(path);

  var marker = svg.append("circle");
  marker.attr("r", 7)
    .attr("transform", "translate(" + startPoint + ")");

  transition();

  //Get path start point for placing marker
  function pathStartPoint(path) {
    var d = path.attr("d"),
    dsplitted = d.split(" ");
    return dsplitted[1].split(",");
  }

  function transition() {
    marker.transition()
        .duration(7500)
        .attrTween("transform", translateAlong(path.node()))
        .each("end", transition);// infinite loop
  }

  function translateAlong(path) {
    var l = path.getTotalLength();
    return function(i) {
      return function(t) {
        var p = path.getPointAtLength(t * l);
        return "translate(" + p.x + "," + p.y + ")";//Move marker
      }
    }
  }
}

Здесь pathStartPoint(path) вытаскивает координаты начала линии из атрибута d элемента path. В translateAlong(path) с помощью интерполятора задаются координаты нашего маркера. Пример можно посмотреть здесь: Marker animation along SVG path element with D3.js. А ещё можно объединить анимацию отрисовки линии и движение маркера, выглядеть это может следующим образом: Marker animation along SVG path element with D3.js II.

Усложним задачу, добавим вращение (ну и поменяем маркер с круга на что-нибудь поинтереснее). В качестве маркера у нас будет ракета с шириной 48 и длиной 24. Поскольку по умолчанию точкой привязки маркера является левый верхний угол, нам необходимо смещать его, чтобы привязка была к центру маркера. Также необходимо учитывать это при вращении, ведь оно тоже по умолчанию происходит вокруг левого верхнего угла. Со смещением вроде разобрались. Теперь перейдём непосредственно к вращению, здесь нам поможет определение касательной, угол будем определять с помощью арктангенса.

Функция translateAlong(path), определяющая интерполятор будет выглядеть следующим образом:

function translateAlong(path) {
  var l = path.getTotalLength();
  var t0 = 0;
  return function(i) {
    return function(t) {
      var p0 = path.getPointAtLength(t0 * l);//previous point
      var p = path.getPointAtLength(t * l);////current point
      var angle = Math.atan2(p.y - p0.y, p.x - p0.x) * 180 / Math.PI;//angle for tangent
      t0 = t;
      //Shifting center to center of rocket
      var centerX = p.x - 24,
      centerY = p.y - 12;
      return "translate(" + centerX + "," + centerY + ")rotate(" + angle + " 24" + " 12" +")";
    }
  }
}

Реализацию можно посмотреть здесь: Marker animation along SVG path element with D3.js III.

Где и для чего это можно использовать

Во-первых, для достижения интересного эффекта с точки зрения дизайна. Можно отображать объекты по мере скроллинга, можно делать анимацию текста, правда надо учитывать что по умолчанию каждая буква — это отдельный элемент path, да много чего можно придумать.

А можно использовать с практической точки зрения, например для анимации движения по маршрутам на схемах и планах (анимация стрелочек и прочее). А ещё для инфографики, тут масса вариантов, если грамотно использовать можно весьма эффективно управлять вниманием читателя.

На этом всё. Пробуйте, экспериментируйте, созидайте — всё в ваших руках. И пусть в наступившем году вам сопутствует удача.