Трансформации для SVG-элементов

Перевод статьи Transforms on SVG Elements с сайта css-tricks.com, автор — Ана Тудор.

Перед вами гостевой пост Аны Тудор. Ана всегда проделывает грандиозную работу, погружаясь в математику, стоящуя за тем, что мы можем делать графически в вебе. В данном случае, это очень кстати, поскольку есть несколько способов работы с SVG-трансформациями и они требуют немного математического мышления, особенно для преобразования одного типа в другой для наилучшей кроссбраузерной поддержки. Давай, Ана.

Как и HTML-элементами, SVG-элементами можно управлять при помощи функций для трансформации. Однако, далеко не всё работает с SVG-элементами так, как с HTML-элементами

Для начала, CSS-трансформации для SVG-элементов не работают в IE. Конечно, есть вариант с использованием SVG-атрибутов трансформации для IE, в случае, если нам нужно применять для наших элементов только 2D-трансформации.

Однако, в этом случае, все параметры для функций трансформаций представлены в виде чисел, что означает, что мы не можем управлять единицами измерения и комбинировать их. Например, мы не можем использовать значения в % для функций сдвига (хотя значения % всё равно бы не стали работать для CSS-трансформаций в Firefox, и не важно, говорим ли мы о значениях transform-origin или параметрах translate()), и все значения для поворота или угла наклона высчитываются в градусах, поэтому мы не можем применять другие единицы измерения, которые доступны в CSS.

Еще одна проблема — тот факт, что определение возможностей скриптом не справляется (чтение значения CSS-трансформации при помощи JS вернёт матрицу, эквивалентную трансформации, которую мы установили в нашей таблице стилей), поэтому нам нужен либо другой способ проверки для IE, либо нам надо повсеместно использовать атрибуты трансформации (что в каком-то смысле кажется более легким путем вообще)

Главная разница в работе между HTML- и SVG-элементами — локальная система координат элемента. У каждого элемента, будь то HTML- или SVG-элемент, есть такая система. Для HTML-элементов эта система координат начинается в точке 50% 50% элемента. Для SVG-элементов (если мы не применяли трансформации к элементу или его предкам в пределах элемента <svg>) исходная точка находится в 0 0 SVG-холста. Это приведёт к разным результатам с трансформациями rotate, scale или skew, если точка 50% 50% SVG-элемента не совпадает с точкой 0 0 SVG-холста.

Чтобы лучше это понимать, давайте посмотрим, как работают функции трансформации.

Как работают функции трансформации

Единственное, что нам надо понимать о трансформациях, это то, что их эффект накапливается при применении к вложенным элементам. Это означает, что трансформации, применённые к элементам с потомками, также повлияют на всех потомков вместе с их системой координат и на результат любых трансформаций на этих потомках. Чтобы было проще, будем считать, что в следующих случаях у наших элементов нет никаких родительских элементов с трансформациями, которые к ним применены. И также будет считать, что у наших элементов нет никаких потомков.

Сдвиг

Сдвиг перемещает все точки элемента в том же направлении и на ту же величину. Сдвиг сохраняет параллельность, углы и расстояния. Это можно интерпретировать, как сдвиг начальной точки системы координат элемента — когда это происходит, любой элемент, положение которого описывается относительно этой начальной точки (сам элемент и любые его потомки), тоже сдвигается. Его результат не зависит от положения системы координат.

svg-transforms-fig1 Рисунок 1: трансформация сдвига: HTML-элементы (слева) напротив SVG-элементов (справа)

На рисунке выше представлено сравнение примеров с HTML (слева) и SVG (справа). Полупрозрачные рисунки элементов — это их исходное положение (до применения сдвига). Трансформация сдвига перемещает элементы вместе с их системами координат, и это же произойдет с любыми потомками наших элементов, если таковые будут.

Как вы уже знаете, различие между двумя случаями — положение системы координат. В случае с HTML, начало координат находится в точке 50% 50% элемента. В случае с SVG начало координат располагается в точке 0 0 холста (если мы не применяли трансформации ни к каким возможным предкам элемента в пределах элемента <svg>). Тем не менее, при сдвиге положение системы координат относительно элемента не влияет на конечное положение элемента.

Как для HTML-, так и для SVG-элементов, при использовании CSS-трансформации у нас есть три функции для сдвига, доступные для 2D: translateX(tx), translateY(ty) и translate(tx[, ty]). Первые две влияют только на направления x и y (как указано системой координат элемента), соответственно. Заметьте, что если другая трансформация применена до сдвига, то направления x и y, возможно, больше не будут горизонтальным и вертикальным, соответственно. Третья функция сдвига перемещает элемент на tx вдоль оси x и на ty вдоль оси y. Параметр ty необязательный, и если он не указан, по умолчанию он равен нулю.

SVG-элементы могут также перемещаться при помощи атрибутов для трансформации. В этом случае, у нас есть только функция translate(tx[ ty]).  Здесь значения могут разделяться пробелами, а не только запятыми, как в аналогичной функции CSS-трансформации. Поэтому в очень простом случае, когда 1 в пользовательской системе координат SVG равно 1px, это равнозначно 1px, следующие два способа сдвига SVG-элемента равнозначны:

• применение CSS-трансформации:

rect {
  /* не работает в IE */
  transform: translate(295px,115px);
}

• применение атрибутов для SVG-атрибута для трансформации:

<!— работает везде-->
<rect width='150' height='80' transform='translate(295 115)' />

Заметьте: SVG-атрибут для трансформации и CSS-свойство для трансформации будут объединены.

Последовательные трансформации translate() можно складывать, это значит, что цепочки вида translate(tx1, ty1) translate(tx2, ty2) можно записывать как translate(tx1 + tx2, ty1 + ty2). Заметьте, что это верно, если в цепочке трансформаций между ними не стоит трансформации другого типа. Обратный сдвиг translate(tx, ty) делается при помощи другого сдвига translate(-tx, -ty).

Поворот

2D-поворот перемещает элемент и любые его потомки, если они есть, вокруг фиксированной точки (точки, положение которой сохраняется, следуя за трансформацией). Конечный результат зависит от положения этой фиксированной точки. Два поворота на один и тот же угол вокруг двух разных точек приведут к разным результатам. Так же, как и сдвиг, поворот не деформирует элемент и сохраняет параллельность, углы и расстояния. Последовательные трансформации rotate() вокруг той же фиксированной точки складываются, как и в случае со сдвигом. Поворот в обратную сторону делается при помощи другого поворота на тот же угол в обратном направлении вокруг той же самой точки.

svg-transforms-fig2Рисунок 2: базовая трансформация поворота: HTML-элементы (слева) напротив SVG-элементов (справа)

На рисунке выше представлено сравнение примеров с HTML (слева) и SVG (справа). Полупрозрачные рисунки элементов — это их исходное положение (до применения поворота). Поворот перемещает элементы и их системы координат вокруг фиксированного начала координат, и это же произойдет с любыми потомками наших элементов, если таковые будут.

В случае с HTML, начальная точка системы координат элемента находится в 50% 50% элемента, поэтому все поворачивается вокруг этой точки. Однако, в случае с SVG, исходная точка находится в 0 0 SVG-холста (если мы не применяли трансформации ни к каким возможным предкам элемента в пределах элемента <svg>), в результате чего всё будет перемещаться вокруг этой точки.

Для CSS-трансформаций функция 2D-поворота довольно простая: только rotate(angle). Значение angle может быть представлено в градусах (deg), радианах (rad), оборотах (turn) или градах (grad). Также мы можем использовать значение calc() (например, что-нибудь типа calc(.25turn - 30deg)), но на данный момент это работает только в Chrome 38+/ Opera. Если вы используете положительное значение угла, тогда поворот будет идти по часовой стрелке (и наоборот, при отрицательном значении угла, поворот будет идти против часовой стрелки).

В случае с атрибутами для SVG-трансформации, функция поворота немного отличается — rotate(angle[ x y]). Значение angle работает аналогичным с функцией CSS-трансформации способом (положительное значение означает поворот по часовой стрелке, отрицательное значение — против часовой), но значение градусов должно быть безрамерным. Необязательные безразмерные параметры x и y указывают координаты фиксированной точки, вокруг которой мы поворачиваем элемент (и его систему координат). Если они опущены, тогда фиксированной точкой будет начальная точка системы координат. Если указать только параметры angle и x, значение будет невалидным и трансформация не применится.

Как и в функции translate(), параметры могут разделяться пробелами или запятыми.

Заметьте, что наличие параметров x и y не означает, что начальная точка системы координат переместится в эту точку. Система координат, как и сам элемент (и любые потомки, если они есть) просто будут вращаться вокруг точки x y.

Это означает, что у нас есть два равнозначных способа поворота SVG-элемента (результат можно увидеть справа на предыдущем рисунке):

• применение CSS-трансформации:

rect {
  /* не работает в IE */
  transform: rotate(45deg);
}

• применение атрибута для SVG-трансформации:

<!-- работает везде -->
<rect x='65' y='65' width='150' height='80' transform='rotate(45)' />

Также в CSS мы можем указать значение transform-origin, чтобы сымитировать применение параметров x и y. Значения длины указываются относительно системы координат элемента, а процентные значения — относительно самого элемента, поэтому они, кажется, идеальны для того, что мы хотели получить. Однако, нам надо учитывать две вещи.

Во-первых, CSS transform-origin и фиксированная точка, указанная в функции rotate()не одно и тоже. В качестве очень простого примера, скажем, возьмем всего лишь поворот вокруг точки 50% 50% в SVG, не столь важно. Рассмотрим два случая:

rect {
  transform: rotate(45deg);
  transform-origin: 50% 50%;
}
<rect x='65' y='65' width='150' height='80' 
transform='rotate(45 140 105)' />
<!-- 140 = 65 + 150/2 -->
<!-- 105 = 65 +  80/2 -->

В обоих случаях элемент должен поворачиваться одинаково (должен, если поддерживается браузером, но это уже другая история), как видно на следующем рисунке:

svg-transforms-fig3Рисунок 3: вращение SVG-элемента вокруг установленной точки: применение CSS (слева) и атрибута для SVG-трансформации (справа)

Это показывает разницу между ними. При использовании CSS, система координат элемента сначала перемещается от точки 0 0 SVG-холста в точку 50% 50% элемента. Затем элемент поворачивается. При использовании атрибута для SVG-трансформации, элемент и его система координат просто поворачивается вокруг точки, указанной во втором и третьем аргументе функции rotate(), точки, координаты которой мы вычислили так, чтобы она находилась в точке 50% 50% элемента. Начальная точка системы координат элемента по-прежнему находится снаружи элемента, и эта начальная точка будет влиять на любые последующие трансформации, зависящие от неё.

Чтобы лучше это понять, давайте укажем другой поворот (сразу после первого), который поворачивает элемент на 45° в противоположном направлении:

rect {
  transform: rotate(45deg) rotate(-45deg);
  transform-origin: 50% 50%;
}
<rect x='65' y='65' width='150' height='80' 
transform='rotate(45 140 105) rotate(-45)' />
<!-- 140 = 65 + 150/2 -->
<!-- 105 = 65 +  80/2 -->

svg-transforms-fig4Рисунок 4: цепочка поворотов для SVG-элемента: CSS-трансформации (слева) и атрибут для SVG-трансформации (справа)

Как показано на рисунке выше, при использовании CSS-трансформации и установке transform-origin в 50% 50%, два поворота компенсируют друг друга, но при использовании SVG-атрибута transform, фиксированная точка, вокруг которой мы поворачиваем элемент, различается между двумя поворотами — она является точкой 50% 50% элемента для первого поворота и начальной точкой системы координат элемента для второго. В этой ситуации поворот в обратную сторону делается легко; просто используйте rotate(-45 140 105) вместо rotate(-45).

Однако, это не отменяет того факта, что у нас есть только один transform-origin (поскольку у системы координат элемента есть только одна начальная точка), но при использовании SVG-атрибута transform, мы можем применять множественные повороты, каждый из которых поворачивает элемент вокруг разной точки. Поэтому, если мы хотим сначала повернуть наш прямоугольник на 90° вокруг его нижнего правого угла, а затем ещё на 90° вокруг его верхнего правого угла, то это легко сделать при помощи атрибута для SVG-трансформации — мы просто указываем разные фиксированные точки для каждого поворота.

<rect x='0' y='80' width='150' height='80' 
transform='rotate(90 150 160) rotate(90 150 80)'/>
<!--
bottom right:
  x = x-offset + width = 0 + 150 = 150
  y = y-offset + height = 80 + 80 = 160
top right:
  x = x-offset + width = 0 + 150 = 150
  y = y-offset = 80
-->

svg-transforms-fig5Рисунок 5: цепочка поворотов вокруг разных фиксированных точек (атрибут для SVG-трансформации)

Но как нам получить тот же самый эффект при помощи CSS-трансформаций? Это легко сделать для первого поворота, поскольку можно установить transform-origin в right bottom, но как быть со вторым поворотом? Если дописать его в CSS-правиле после первого, то он просто повернёт элемент ещё на 90° вокруг той же самой точки (right bottom).

Нам нужно три сцепленных трансформации, для того, чтобы поворот элемента вокруг фиксированной точки не зависел от того, где находится его transform-origin. Во-первых, это трансформация translate(x, y), которая переместит начальную точку системы координат элемента таким образом, чтобы она совпадала с фиксированной точкой, вокруг которой мы хотим всё поворачивать. Во-вторых, это сам поворот. И наконец, в-третьих, это translate(-x, -y) — противоположность первому сдвигу.

В этом случае наш код мог бы быть таким:

rect {
  transform-origin: right bottom; /* или 100% 100%, что одно и тоже */
  transform:
    rotate(90deg)
    translate(0, -100%) /* идём от нижнего правого до верхнего правого края */
    rotate(90deg)
    translate(0, 100%);
}

На нижнем рисунке показано, как это работает, шаг за шагом:

svg-transforms-fig6Рисунок 6: иллюстрация того, как работает цепочка CSS-трансформаций

Вторая проблема с transform-origin, это то, что в Firefox работают только значения длины. Процентные значения и ключевые слова — нет, поэтому нам придется заменять их значениями длины. И процентные значения внутри трансформаций translate(), также не работают в Firefox.

Масштабирование

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

Коэффициент масштабирования в диапазоне (-1, 1) уменьшает элемент, тогда как коэффициент вне этого диапазона увеличивает его. Отрицательный коэффициент масштабирования также отражает элемент центрально-симметрично относительно его начала координат, в дополнение к изменению размера. Если только один коэффициент масштабирования отличается от 1, то получается масштабирование по одной оси.

Результат трансформации масштаба зависит от положения начальной точки системы координат. Две трансформации масштабирования с одним и тем же коэффициентом на одном и том же элементе дадут разный результат при разном положении начала координат.

svg-transforms-fig7Рисунок 7: трансформация масштабирования: HTML-элементы (слева) и SVG-элементы (справа)

На рисунке выше представлено сравнение примеров с HTML (слева) и SVG (справа). В обоих случаях мы масштабируем элемент, используя коэффициент масштабирования sx вдоль оси x и sy вдоль оси y. Разница в положении начальной точки системы координат элемента, которая в случае с HTML находится в точке 50% 50% элемента, а в случае с SVG в точке 0 0 SVG-холста (если мы не применяли трансформации ни к каким возможным предкам элемента в пределах элемента <svg>).

При использовании CSS-трансформаций у нас есть три функции масштабирования, которые доступны для 2D-трансформаций: scale(sx[, sy]), scaleX(sx) и scaleY(sy). Первая функция масштабирует элемент на sx вдоль оси x и на sy вдоль оси y. Параметр sy необязательный, и если он не указан, то предполагается, что он равен sx, делая таким образом масштабирование одинаковым во всех направлениях. sx и sy всегда являются безразмерными значениями. Другие две функции действуют только в направлениях x и y (как указано системой координат элемента), соответственно. scaleX(sx) равнозначно scale(sx, 1) или просто scale(sx), а scaleY(sy) равнозначно scale(1, sy). Если другая трансформация применяется до масштабирования, направления x и y могут уже не быть горизонтальным и вертикальным, соответственно.

В случае с атрибутами для SVG-трансформации у нас есть только функция scale(sx[ sy]). Снова, значения здесь могут разделяться пробелами, а не только запятыми, как в аналогичной функции CSS-трансформации.

Поэтому для SVG-элементов следующие два метода масштабирования равнозначны:

• применение CSS-трансформации

rect {
  /* не работает в IE */
  transform: scale(2, 1.5);
}

• применение атрибута для SVG-трансформации

<!-- работает везде -->
<rect x='65' y='65' width='150' height='80' transform='scale(2 1.5)' />

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

С CSS-трансформациями у нас есть выбор: либо задать нашему SVG-элементу правильный transform-origin, либо добавить в цепочку трансформаций сдвиги до и после масштабирования — сначала мы сдвигаем систему координат так, что ее начало оказывается в точке 50% 50% нашего SVG-элемента, затем масштабируем, а затем обращаем первый сдвиг. С SVG-атрибутом transform у нас есть только вариант с цепочкой трансформаций. Так что код для вышеописанного случая был бы таким:

• Применение CSS-трансформации при помощи transform-origin

rect {
  /* не работает в IE */
  transform: scale(2, 1.5);

  /* не работает в Firefox, если мы только не заменим значения % на значения с абсолютной длиной */
  transform-origin: 50% 50%;
}

• Применение цепочки CSS-трансформаций

rect {
  /* не работает в IE */
  transform: translate(140px, 105px)
  scale(2 1.5)
  translate(-140px, -105px);
}

• Применение функций цепочки трансформаций в виде значения для атрибута SVG-трансформации

<rect x='65' y='65' width='150' height='80' 
transform='translate(140 105) scale(2 1.5) translate(-140 -105)'/>
<!-- работает везде -->

Следующий демо-пример иллюстрирует, как работает метод с цепочкой трансформаций (нажмите кнопку «воспроизведение» (►) для старта):

See the Pen Chaining on SVG elements to scale wrt a certain point by Максим (@psywalker) on CodePen.

Что ещё нужно помнить о масштабировании — это что две последовательные трансформации scale() (scale(sx1, sy1) scale(sx2, sy2)) могут быть записаны как scale(sx1*sx2, sy1*sy2), а обратная трансформация делается при помощи scale(1/sx1, 1/sy1). Если все коэффициенты масштаба по модулю равны 1, то такое масштабирование — само себе обратная трансформация.

Наклон

Наклон элемента вдоль оси смещает каждую из его точек (кроме тех, которые находятся точно на оси наклона) в этом направлении на ту величину, которая зависит от угла наклона и расстояния между этой точкой и осью наклона. Это значит, что изменяется только координата вдоль оси наклона, а координата вдоль другой оси остаётся неизменной. В отличие от поворота, наклон деформирует элемент, превращая квадраты в неравносторонние параллелограммы, а круги в эллипсы. Наклон не сохраняет углы (при наклоне на угол α 90-градусные углы прямоугольного элемента становятся 90° ± α) или длина любого сегмента не параллельна оси наклона. Однако, площадь элемента сохраняется.

В отличие от сдвига или поворота, наклоны не складываются. Наклон элемента вдоль оси на угол α1, а затем наклон снова вдоль той же оси на другой угол α2 — не то же самое, что наклон элемента вдоль этой оси на угол α1 + α2.

Демо-пример ниже показывает, как работает наклон — изменяйте угол и/или ось, чтобы увидеть, как это влияет на исходный квадрат.

See the Pen How the skew transform works by Ana Tudor (@thebabydino) on CodePen.

Угол наклона — это угол между конечным и исходным положением оси, которая изменяется после применения трансформации (а не оси, вдоль которой мы наклоняем элемент). Положительный угол наклона в интервале [0°, 90°] добавляет значение того же знака, что у неизменной координаты, к исходному значению меняющейся координаты, которая изменилась (координата вдоль угла наклона), а отрицательное значение в интервале [-90°, 0°] добавляет значение, знак которого является противоположным этой фиксированной координаты.

Если мы делаем наклон вдоль оси x, то для любой точки нашего элемента координата y этой точки остаётся такой же, а координата x изменяется на величину d в зависимости от угла наклона и от фиксированной координаты y (где-то на 15-й минуте этого доклада поясняется, как величина d может быть вычислена). Верхняя и нижняя стороны (и любой другой сегмент, параллельный оси x) остаются той же длины, а левая и правая стороны становятся длиннее по мере увеличения угла наклона, уходя в бесконечность в случае угла ±90°. Как только это значение превышается, они начитают становиться короче, пока мы не получим угол в ±180°.

Заметьте, что результат наклона на угол в интервале (90°, 180°] равнозначен результату наклона на угол α - 180°> (который в конечном итоге оказался бы в интервале (-90°, 0°]). Также результат наклона на угол в интервале (-180°, -90°] равнозначен результату наклона на угол — α + 180° (который в конечном итоге оказался бы в интервале [0°, 90°))

Если мы делаем наклон вдоль оси y, то координата x остаётся такой же для любой точки нашего элемента, а координата y изменяется на величину d в зависимости от угла наклона и от фиксированной координаты x. Правая и левая стороны (и любой другой сегмент, параллельный оси y) остаются той же длины, а верхняя и нижняя стороны становятся длиннее по мере увеличения угла наклона, уходя в бесконечность в случае угла ±90°. Как только это значение превышается, они начитают становиться короче, пока мы не получим угол в ±180°.

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

svg-transforms-fig8Рисунок 8: трансформация наклона: HTML-элементы (слева) и SVG-элементы (справа)

На рисунке выше представлено сравнение примеров с HTML (слева) и SVG (справа). В обоих случаях, мы наклоняем наши элементы вдоль оси x на тот же угол наклона. Разница в положении начальной точки системы координат элемента, которая в случае с HTML находится в точке 50% 50% элемента, а в случае с SVG в точке 0 0 SVG-холста (если мы не применяли трансформации ни к каким возможным предкам элемента в пределах элемента <svg> в случае с SVG).

Для простоты, давайте сосредоточимся на том, что случается только с одной точкой наших элементов: верхний правый угол. В обоих случаях координата y сохраняет своё положение — точка не двигается по вертикали, только по горизонтали. Однако, в случае HTML мы видим, что по горизонтали этот угол двигается влево (в отрицательном направлении оси x), а в случае с SVG вправо. А нижний правый угол двигается вправо, по мере наклона, как в HTML, так и в случае с SVG. Итак, как это работает?

Ну, как упоминалось выше, при наклоне вдоль оси x, координата y любой точки остаётся такой же, хотя к исходной точке координаты x той же точки мы добавляем величину d, которая зависит от угла наклона и от фиксированной координаты y. Эта величина d совпадает по знаку с фиксированной координатой y, если угол наклона находится в интервале [0°, 90°], и противоположна ей по знаку, если угол наклона находится в интервале [-90°, 0°].

В обоих случаях наш угол 60°, поэтому дело решает знак координаты y верхнего правого угла. В случае с HTML начальная точка системы координат элемента находится в точке 50% 50% элемента, координата y верхнего правого угла элемента является отрицательной, поскольку ось y направлена вниз. Однако, в случае с SVG, где начальная точка системы координат элемента находится в точке 0 0 SVG-холста, координата y верхнего правого угла элемента положительна. Это означает, что в случае с HTML мы добавляем отрицательную величину к исходной точке координаты x верхнего правого угла, заставляя его двигаться влево, а в случае с SVG мы добавляем положительную величину к исходной координате x верхнего правого угла, заставляя его двигаться вправо.

Независимо от того, наклоняем ли мы SVG-элемент при помощи CSS-трансформаций или атрибута для SVG-трансформации, нам доступны две функции: skewX(angle) и skewY(angle). Первая функция наклоняет элемент вдоль оси x, а вторая наклоняет его вдоль оси y.

В CSS-трансформациях значение angle указывается с единицей измерения. Оно может быть представлено в градусах (deg), радианах (rad), оборотах (turn), градах (grad) или даже в виде calc(), чтобы комбинировать любые эти единицы измерения (но имейте ввиду, что на данный момент calc() с единицами измерения для угла работает только в браузерах на движке Blink)

При наклоне элемента с помощью атрибута для SVG-трансформации наше значение angle всегда представляет безразмерное значение градусов.

Это означает, что у нас есть два равнозначных способа наклона SVG-элемента (результат можно видеть на предыдущем рисунке справа):

• использование CSS-трансформаций:

rect {
  transform: skewX(60deg); /* не работает в IE */
}

• использование атрибута для SVG-трансформации:

<!-- работает везде -->
<rect x='65' y='65' width='150' height='80' transform='skewX(60)' />

Если вы хотите получить тот же самый эффект, который мы получаем при применении аналогичной функции наклона для  HTML-элемента, то для этого у нас есть три способа, как и при масштабировании:

• использование CSS-трансформации при помощи transform-origin

rect {
  /* не работает в IE */
  transform: skewX(60deg);
  /* не работает в Firefox, если мы только не заменим значения % на значения с абсолютной длиной */
  transform-origin: 50% 50%;
}

• использование цепочки CSS-трансформаций

rect {
  /* не работает в IE */
  transform: translate(140px, 105px)
  skewX(60deg)
  translate(-140px, -105px);
}

• использование цепочки функций трансформации в виде значения атрибута для SVG-трансформации:

<!-- работает везде -->
<rect x='65' y='65' width='150' height='80' 
transform='translate(140 105) skewX(60) translate(-140 -105)' />

Следующий демо-пример иллюстрирует как работает «цепочный» метод:

See the Pen Chaining on SVG elements to skew wrt a certain point by Ana Tudor (@thebabydino) on CodePen.

Сокращение цепочки

Всё в порядке, цепочки трансформаций делают своё дело. Можно поворачивать, масштабировать и наклонять SVG-элементы, и они ведут себя так же, как вели бы и HTML-элементы при тех же самых трансформациях. А если мы применяем цепочку трансформаций в виде значения для SVG-атрибута, то у нас даже есть возможность получить желаемый результат в IE. Но это уродство! Неужели нет более простого способа сделать это?

Ну, если мы начнём с нашего SVG-прямоугольника, точка 50% 50% которого находится в точке 0 0 SVG-холста, то мы можем «вырезать» один сдвиг в нашей цепочке, сокращая наш код поворота:

<rect x='-75' y='-40' width='150' height='80' 
transform='translate(140 105) rotate(45)'/>
<!-- 75 = 150/2, 40 = 80/2 -->

See the Pen Chaining on SVG elements to rotate wrt a certain point #1 by Ana Tudor (@thebabydino) on CodePen.

Мы также могли бы избавиться от первого сдвига при помощи правильно подобранного атрибута viewBox для элемента <svg>, содержащего наш прямоугольник. У атрибута viewBox есть четыре значения, которые разделены пробелом. Первые два указывают координаты x и y верхнего левого угла SVG-холста в пользовательских единицах измерения, а другие две указывают его width и height в пользовательских единицах измерения. Если атрибут viewBox не указан, то координаты верхнего левого угла будут 0 0.

Ниже можно увидеть разницу между элементом <svg>, у которого не указан viewBox, и элементом <svg> с viewBox='-140 -105 280 210':

svg-transforms-fig9Рисунок 9: Элемент <svg>, у которого не указан атрибут viewBox, и элемент <svg> с viewBox

Возвращаясь к нашему примеру, если мы установим viewBox так, чтобы точка 0 0 SVG-холста разместилась там, где у нас находится точка 50% 50% нашего прямоугольника, то наш код станет таким:

<svg viewBox='-140 -105 650 350'>
<rect x='-75' y='-40' width='150' height='80' transform='rotate(45)'/>
</svg>

See the Pen Setting proper `viewBox` to rotate wrt a certain point #1 by Ana Tudor (@thebabydino) on CodePen.

Практическое применение

Размещение точки 0 0 нашего SVG-холста и любого другого желаемого элемента прямо в середине облегчает работу с трансформациями, потому что в этом случае точка 0 0 SVG-холста совпадает с точкой 50% 50% элемента, который мы хотим трансформировать. Следующий демо-пример (жмите воспроизведение/пауза) показывает три четырёхконечные звёзды, которые изначально расположены в середине, а затем повёрнуты, смещены, наклонены и сжаты без необходимости в установке transform-origin или в добавлении сдвигов в цепочке:

See the Pen SVG Stars — final by Ana Tudor (@thebabydino) on CodePen.

Давайте рассмотрим, как работает это демо шаг за шагом.

Создается звезда очень легко — простой многоугольник с восемью точками. Демо-пример ниже показывает, как они позиционируются относительно начальной точки (0 0) SVG-холста. Наводите мышкой на пары x,y в коде или на сами точки, чтобы увидеть что чему соответствует.

See the Pen 4 point star — the points by Ana Tudor (@thebabydino) on CodePen.

У нас есть три таких звезды. Мы не будем повторять код для многоугольника три раза, а просто положим его в элемент <defs>, а затем трижды используем его в элементе <use>.

<svg viewBox='-512 -512 1024 1024'>
  <defs>
    <polygon id='star' points='250,0 64,64 0,250 -64,64 -250,0   -64,-64 0,-250 64,-64'/>
  </defs>
  <g>
    <use xlink:href='#star'/>
    <use xlink:href='#star'/>
    <use xlink:href='#star'/>
  </g>
</svg>

Первое, что мы сделаем, это отмасштабируем наши звёзды от 0 до 1:

use {
  animation: ani 4s linear infinite;
}
@keyframes ani {
  0% { transform: scale(0); }
  25%, 100% { transform: scale(1); }
}

Это даст нам следующий результат:

See the Pen SVG Stars — step #1 by Ana Tudor (@thebabydino) on CodePen.

Следующее, что мы хотим сделать, это добавить поворот к нашей покадровой анимации. Но нам нужен разный поворот для каждой звезды — допустим, случайный угол плюс определённый угол, рассчитываемый по порядковому номеру звезды. Это означает, что мы не можем применить одну и ту же покадровую анимацию для всех трёх звёзд; нам нужно три разных анимации. Также это поможет нам сделать заливку разной.

$n: 3;
$?: 360deg/$n;
$?: random($?/1deg)*1deg;

@for $i from 1 through $n {
  $?: $? + ($i - 1)*$?;
  
  use:nth-of-type(#{$i}) {
    fill: hsl($?, 100%, 80%);
    animation: ani-#{$i} 4s linear infinite;
  }

  @keyframes ani-#{$i} {
    0% { transform: scale(0); }
    25% { transform: scale(1); }
    50%, 100% { transform: rotate($?); }
  }

}

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

See the Pen SVG Stars — step #2 by Ana Tudor (@thebabydino) on CodePen.

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

@keyframes ani-#{$i} {
  0% { transform: scale(0); }
  25% { transform: scale(1); }
  50% { transform: rotate($?); }
  75%, 100% {
    transform: rotate($?) translate(13em) scale(.2);
  }
}

See the Pen SVG Stars — step #3 by Ana Tudor (@thebabydino) on CodePen.

Мы почти у цели! Нам просто надо наклонить звёзды и применить трансформацию для масштабирования, чтобы подправить их размеры после наклона.

@keyframes ani-#{$i} {
  0% { transform: scale(0); }
  25% { transform: scale(1); }
   50% { transform: rotate($?); }
  75% {
    transform: rotate($?) translate(13em) scale(.2);
}
  83% {
    transform: rotate($?) translate(13em) scale(.2)
      skewY(30deg) scaleX(.866);
  }

  91% {
    transform: rotate($?) translate(13em) scale(.2)
    skewY(60deg) scaleX(.5);
  }

  100% {
    transform: rotate($?) translate(13em) scale(.2)
    skewY(90deg) scaleX(0);
  }
}

Здесь я добавила больше одного ключевого кадра для точности. В то время как угол наклона изменяется линейно, корректирующий коэффициент масштаба — нелинейно, его значение — это косинус угла наклона, и как видно на следующем изображении, график функции косинуса между и 90° — не прямая.

svg-transforms-fig10Рисунок 10: график синуса (синий) и косинуса (красный)

Однако, этот пример на чистом CSS немного глючит в Firefox и не работает ни в каком IE, поскольку ни одна из версий IE не поддерживает CSS-трансформации для SVG-элементов. Всё это можно исправить, если использовать атрибуты для SVG-трансформации и анимировать их значения при помощи JavaScript. Вы можете посмотреть JavaScript-версию ниже (нажмите, чтобы запустить).

See the Pen Stars — JS version by Ana Tudor (@thebabydino) on CodePen.

P.S. Это тоже может быть интересно:

4 Комментарии

  1. GreLI

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

    1. SelenIT

      Есть такое:) Возможно, издержки переупрощения для наглядности (на конечный итог же в данном случае не влияет, а воспринимать последовательные шаги слева направо проще), но согласен, что такое переупрощение может сбить с толку. Спасибо за это важное уточнение!

  2. GreLI

    Вообще-то на конечный результат это влияет: трансформации определены через матрицы, а результат их сложения как умножение матриц. А вот умножение матриц как раз зависит от порядка. Только в некоторых случаях результат одинаков.

    1. SelenIT

      Всё так, поэтому я и уточнил «в данном случае». В общем случае это действительно ошибка, с чем я ничуть не спорю. А про умножение матриц тут уже давно была статья).

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.