Трансформация элементов. Часть 2: трёхмерные преобразования
Чтение — около 7 минут· без учёта практики
В первой части урока мы сдвигали, поворачивали, масштабировали и наклоняли элементы в плоскости экрана. Трёхмерные преобразования добавляют глубину. На сайтах они встречаются реже — в карточках-перевёртышах, каруселях, эффектах при наведении, — поэтому этот урок можно считать углублением: на сайте Тони мы обойдёмся без них.
Оси X, Y и Z
У элемента три оси: X идёт слева направо, Y — сверху вниз, а Z — перпендикулярно экрану, к
зрителю. Поворот «вокруг оси» представляйте как вращение вокруг стержня: вокруг X элемент кувыркается вперёд или
назад, как страница перекидного календаря, вокруг Y — поворачивается, как дверь на петлях, а вокруг Z — вращается в
плоскости экрана, как стрелка часов. Поэтому rotate() из первой части — это поворот вокруг оси Z.
Без перспективы трёхмерный поворот выглядит плоским: элемент, повёрнутый вокруг Y, просто сужается. Чтобы появилась
глубина, в примерах вместе с поворотом задана функция perspective() — о ней ниже.
Трёхмерный поворот
Функции rotateX(), rotateY() и rotateZ() поворачивают элемент вокруг одной оси.
Угол задают в тех же единицах, что и у rotate().
Поворот элементов по осям: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Поворот элементов по осям</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test">rotateX(60deg)</div></div>
<div class="static"><div class="test2">rotateX(-160deg)</div></div>
<div class="static"><div class="test3">rotateY(20deg)</div></div>
<br>
<div class="static"><div class="test4">rotateY(60deg)</div></div>
<div class="static"><div class="test5">rotateZ(20deg)</div></div>
<div class="static"><div class="test6">rotateZ(145deg)</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test:hover {
transform: perspective(500px) rotateX(60deg);
}
.test2:hover {
transform: perspective(500px) rotateX(-160deg);
}
.test3:hover {
transform: perspective(500px) rotateY(20deg);
}
.test4:hover {
transform: perspective(500px) rotateY(60deg);
}
.test5:hover {
transform: rotateZ(20deg);
}
.test6:hover {
transform: rotateZ(145deg);
}
Функция rotate3d() поворачивает элемент вокруг произвольной оси:
rotate3d(x, y, z, angle)
Первые три числа задают направление оси вращения — вектор от центра элемента, — а четвёртый параметр — угол
поворота вокруг этой оси. rotate3d(1, 0, 0, 45deg) равносилен rotateX(45deg), а
rotate3d(0, 0, 1, 45deg) — rotateZ(45deg).
rotate3d(2, 1, 0, 50deg) поворачивает элемент на 50 градусов вокруг одной наклонной оси: она лежит в
плоскости экрана и направлена ближе к оси X, чем к Y. Это не два отдельных поворота по осям: важно только направление
вектора, поэтому rotate3d(4, 2, 0, 50deg) даёт тот же результат.
Трехмерный поворот элемента: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Трехмерный поворот элемента</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test">rotate3d(2,1,0, 50deg)</div></div>
<div class="static"><div class="test2">rotate3d(2,1,0, -50deg)</div></div>
<div class="static"><div class="test3">rotate3d(1,1,1, 50deg)</div></div>
<br>
<div class="static"><div class="test4">rotate3d(4,2,2, 50deg)</div></div>
<div class="static"><div class="test5">rotate3d(4,2,2, -50deg)</div></div>
<div class="static"><div class="test6">rotate3d(4,2,2, 1turn)</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test:hover {
transform: rotate3d(2, 1, 0, 50deg);
}
.test2:hover {
transform: rotate3d(2, 1, 0, -50deg);
}
.test3:hover {
transform: rotate3d(1, 1, 1, 50deg);
}
.test4:hover {
transform: rotate3d(4, 2, 2, 50deg);
}
.test5:hover {
transform: rotate3d(4, 2, 2, -50deg);
}
.test6:hover {
transform: rotate3d(4, 2, 2, 1turn);
}
Масштаб и сдвиг по оси Z
Функция scaleZ() масштабирует элемент по оси Z, а scale3d(x, y, z) — сразу по трём осям. У
плоского элемента все точки изначально лежат при Z = 0, поэтому один scaleZ() не меняет его вид. Чтобы
увидеть эффект, в примерах используется perspective(500px) scaleZ(2) rotateX(45deg): поворот создаёт
разную глубину у точек, масштаб по Z меняет её, а перспектива делает разницу заметной. Порядок здесь важен: в
perspective(500px) rotateX(45deg) scaleZ(2) масштаб по Z применяется к исходной плоскости и ничего не
меняет. Так можно рассматривать действие на точки справа налево; в первой части мы описывали тот же результат через
последовательное изменение системы координат слева направо.
Масштабирование по оси Z: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Масштабирование по оси Z</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test5">scaleZ(-2)</div></div>
<div class="static"><div class="test6">scaleZ(6)</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test5:hover {
transform: perspective(500px) scaleZ(-2) rotateX(45deg);
}
.test6:hover {
transform: perspective(500px) scaleZ(6) rotateX(45deg);
}
Трехмерное масштабирование элементов по осям: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Трехмерное масштабирование элементов по осям</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test">scale3d(1,2,1)</div></div>
<div class="static"><div class="test2">scale3d(2,1,1)</div></div>
<div class="static"><div class="test3">scale3d(1,1,2)</div></div>
<br>
<div class="static"><div class="test4">scale3d(-1,-2,-1)</div></div>
<div class="static"><div class="test5">scale3d(0.5,1,1)</div></div>
<div class="static"><div class="test6">scale3d(0.5,0.5,2)</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test:hover {
transform: perspective(500px) scale3d(1, 2, 1) rotateX(45deg);
}
.test2:hover {
transform: perspective(500px) scale3d(2, 1, 1) rotateX(45deg);
}
.test3:hover {
transform: perspective(500px) scale3d(1, 1, 2) rotateX(45deg);
}
.test4:hover {
transform: perspective(500px) scale3d(-1, -2, -1) rotateX(45deg);
}
.test5:hover {
transform: perspective(500px) scale3d(0.5, 1, 1) rotateX(45deg);
}
.test6:hover {
transform: perspective(500px) scale3d(0.5, 0.5, 2) rotateX(45deg);
}
Функция translateZ() приближает элемент к зрителю (положительное значение) или отдаляет его
(отрицательное), а translate3d(x, y, z) сдвигает элемент сразу по трём осям. С перспективой приближенный
элемент выглядит крупнее, отдалённый — мельче.
Сдвиг по оси Z: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Сдвиг по оси Z</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test5">translateZ(30px)</div></div>
<div class="static"><div class="test6">translateZ(-30px)</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test5:hover {
transform: perspective(500px) rotateX(45deg) translateZ(30px);
}
.test6:hover {
transform: perspective(500px) rotateX(45deg) translateZ(-30px);
}
Трехмерный сдвиг элементов: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Трехмерный сдвиг элементов</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test">translate3d(20px,20px,20px)*</div></div>
<div class="static"><div class="test2">translate3d(-20px,-20px,-20px)*</div></div>
<div class="static"><div class="test3">translate3d(-20px,-20px,-80px)*</div></div>
<br>
<div class="static"><div class="test4">translate3d(0,0,-80px)*</div></div>
<div class="static"><div class="test5">translate3d(20px,0, 40px)*</div></div>
<div class="static"><div class="test6">translate3d(0,20px, 40px)*</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test:hover {
transform: perspective(500px) rotateX(45deg) translate3d(20px, 20px, 20px);
}
.test2:hover {
transform: perspective(500px) rotateX(45deg) translate3d(-20px, -20px, -20px);
}
.test3:hover {
transform: perspective(500px) rotateX(45deg) translate3d(-20px, -20px, -80px);
}
.test4:hover {
transform: perspective(500px) rotateX(45deg) translate3d(0, 0, -80px);
}
.test5:hover {
transform: perspective(500px) rotateX(45deg) translate3d(20px, 0, 40px);
}
.test6:hover {
transform: perspective(500px) rotateX(45deg) translate3d(0, 20px, 40px);
}
Перспектива
Функция perspective() задаёт расстояние от зрителя до плоскости элемента и создаёт иллюзию глубины. Чем
больше значение, тем дальше зритель и тем слабее искажения; при маленьком значении эффект резкий, будто смотришь
вплотную. Отрицательная длина недопустима, а значения меньше 1px, включая ноль, при отрисовке считаются
равными 1px. Для обычной сцены удобнее сотни пикселей. В примерах функцию ставят первой в списке
transform, чтобы перспектива проецировала результат остальных преобразований. Это рекомендуемый порядок
для такой сцены, а не требование синтаксиса: другое положение функции тоже допустимо, но меняет результат.
Указание перспективы обзора элемента: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Указание перспективы обзора элемента</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test">perspective(50px)*</div></div>
<div class="static"><div class="test2">perspective(100px)*</div></div>
<div class="static"><div class="test3">perspective(200px)*</div></div>
<br>
<div class="static"><div class="test4">perspective(300px)*</div></div>
<div class="static"><div class="test5">perspective(400px)*</div></div>
<div class="static"><div class="test6">perspective(500px)*</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test:hover {
transform: perspective(50px) rotateX(45deg);
}
.test2:hover {
transform: perspective(100px) rotateX(45deg);
}
.test3:hover {
transform: perspective(200px) rotateX(45deg);
}
.test4:hover {
transform: perspective(300px) rotateX(45deg);
}
.test5:hover {
transform: perspective(400px) rotateX(45deg);
}
.test6:hover {
transform: perspective(500px) rotateX(45deg);
}
Свойство
perspective
задают не самому элементу, а его родителю: тогда у всех трёхмерных потомков одна общая точка зрения, как у
реальной сцены. Свойство
perspective-origin
задаёт, откуда смотрит зритель: по умолчанию — из центра родителя.
Свойство perspective и функция perspective(): попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Свойство perspective и функция perspective()</title>
<style>
body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
.row { display: flex; gap: 1rem; margin-bottom: 1.5rem; padding: 1rem; border: 1px dashed tan; }
.card {
width: 6rem;
height: 6rem;
display: grid;
place-items: center;
background-color: saddlebrown;
color: white;
}
/* Функция: у каждой карточки своя точка зрения — все повёрнуты одинаково */
.own .card {
transform: perspective(300px) rotateY(45deg);
}
/* Свойство родителя: одна точка зрения на всех — карточки повёрнуты по-разному относительно взгляда */
.shared {
perspective: 300px;
perspective-origin: 50% 50%; /* откуда смотрим: центр ряда */
}
.shared .card {
transform: rotateY(45deg);
}
</style>
</head>
<body>
<p>perspective() у каждой карточки</p>
<div class="row own"><div class="card">1</div><div class="card">2</div><div class="card">3</div></div>
<p>perspective у ряда</p>
<div class="row shared"><div class="card">1</div><div class="card">2</div><div class="card">3</div></div>
</body>
</html>
В верхнем ряду у каждой карточки своя перспектива, и все три выглядят одинаково. В нижнем перспектива общая: зритель
смотрит на ряд из центра, поэтому левая и правая карточки повёрнуты к нему по-разному. Измените
perspective-origin на 0% 50% — точка зрения сместится к левому краю.
Точка преобразования по оси Z
Третье значение transform-origin задаёт положение точки преобразования по оси Z — только длиной, без
процентов и ключевых слов. Отрицательное значение уводит точку за экран, положительное — к зрителю, и при поворотах
вокруг X или Y элемент описывает дугу вокруг неё. При повороте вокруг Z третья координата точки опоры ничего не
меняет.
Точка преобразования по оси Z: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Точка преобразования по оси Z</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test5">50% 50% -250px</div></div>
<div class="static"><div class="test6">50% top 250px</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.2s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test5:hover {
transform: perspective(500px) rotateY(35deg);
transform-origin: 50% 50% -250px;
}
.test6:hover {
transform: perspective(500px) rotateY(35deg);
transform-origin: 50% top 250px;
}
Трёхмерная сцена: transform-style и backface-visibility
По умолчанию потомки трансформированного элемента отображаются плоско — в его плоскости. Свойство
transform-style
со значением preserve-3d сохраняет их в трёхмерном пространстве: каждый потомок можно повернуть
относительно родителя, и все они повернутся вместе с ним.
Свойство
backface-visibility
определяет, видна ли обратная сторона элемента, повёрнутого к зрителю спиной. Значение hidden её прячет.
Вместе эти свойства дают карточку-перевёртыш: две стороны в одной карточке, оборот заранее повёрнут на 180 градусов, а
при повороте карточки видна то одна, то другая сторона. Именно её мы сделаем в задании.
Матрицы преобразований
Все трансформации в CSS браузер сводит к двум функциям:
matrix()— двухмерное преобразование матрицей из шести значений;matrix3d()— трёхмерное преобразование матрицей 4 × 4 из шестнадцати значений.
Матрица двухмерного преобразования выглядит так:
matrix(a, b, c, d, e, f)
┌ a c e ┐
│ b d f │
└ 0 0 1 ┘
-
a,b,cиdвместе задают линейную часть преобразования: масштаб, поворот и наклон. При чистом масштабеaиd— масштаб по осям X и Y, аb = c = 0. При чистом наклонеa = d = 1,c— тангенс угла наклона по X, аb— по Y. eиf— сдвиг по осям X и Y.
Применяя двухмерную трансформацию, браузер умножает матрицу на вектор [x, y, 1], где x и y — координаты точки элемента:
┌ a c e ┐ ┌ x ┐ ┌ a·x + c·y + e ┐
│ b d f │ × │ y │ = │ b·x + d·y + f │
└ 0 0 1 ┘ └ 1 ┘ └ 1 ┘
x′ = a·x + c·y + e
y′ = b·x + d·y + f
Матрица элемента без преобразования — единичная:
transform: matrix(1, 0, 0, 1, 0, 0);
Вручную матрицы пишут редко: проще комбинировать понятные функции. Но их стоит узнавать: на вкладке Computed
инструментов разработчика браузер показывает итоговое значение transform именно в виде
matrix() или matrix3d(). Отдельные свойства translate, rotate и
scale показываются отдельно и не включаются в значение transform.
Матрицы преобразований в CSS: попробуйте сами
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Матрицы преобразований в CSS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="static"><div class="test">matrix(0.5,0,0,1,0,0)</div></div>
<div class="static"><div class="test2">matrix(1,0,0,1.5,0,0)</div></div>
<div class="static"><div class="test3">matrix(0,-1, 1,0,0,0)</div></div>
<br>
<div class="static"><div class="test4">matrix(1,0, -1,1,0,0)</div></div>
<div class="static"><div class="test5">matrix(1,0, 0,1,50,0)</div></div>
<div class="static"><div class="test6">matrix3d()</div></div>
</body>
</html>
style.css
.static {
display: inline-block;
margin: 10px;
background: gray;
}
div {
width: 180px;
height: 100px;
line-height: 100px;
text-align: center;
transition: 0.3s;
}
.test2,
.test4,
.test6 {
background: orange;
}
.test,
.test3,
.test5 {
background: plum;
}
.test:hover {
transform: matrix(0.5, 0, 0, 1, 0, 0);
}
.test2:hover {
transform: matrix(1, 0, 0, 1.5, 0, 0);
}
.test3:hover {
transform: matrix(0, -1, 1, 0, 0, 0);
}
.test4:hover {
transform: matrix(1, 0, -1, 1, 0, 0);
}
.test5:hover {
transform: matrix(1, 0, 0, 1, 50, 0);
}
.test6:hover {
transform: matrix3d(0.3, -0.4, 0.8, 0, 0.2, 0.9, 0.4, 0, -0.9, 0, 0.4, 0, 0, 0, 0, 1);
}
Подробнее о матрицах — в статье «Разбираемся с матрицами CSS-трансформаций» (на русском) и в стандарте CSS Transforms.
Проверьте себя
Выберите ответ — объяснение появится сразу. Ответ можно менять.
Задание
Ожидаемый результат: сначала видно фото Тони с белкой. При наведении карточка плавно переворачивается, как
настоящая, и на коричневом обороте появляется подпись. Убираете указатель — карточка поворачивается обратно. Если
вместо подписи видно зеркальное фото, проверьте backface-visibility; если карточка сплющивается без
объёма — перспективу и preserve-3d.
Выполните задание
Условие задания
index.html
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Карточка-перевёртыш</title>
<style>
body {
font-family: system-ui, sans-serif;
background-color: linen;
color: #2b211b;
}
.scene {
width: 180px;
height: 240px;
}
.scene:focus-visible {
outline: 3px solid saddlebrown;
outline-offset: 4px;
}
.card {
position: relative;
width: 180px;
height: 240px;
transition: transform 0.6s;
}
.side {
position: absolute;
inset: 0;
border-radius: 12px;
overflow: hidden;
}
.front img {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #fffaf3;
}
.back {
display: grid;
place-items: center;
padding: 1rem;
background-color: saddlebrown;
color: white;
text-align: center;
}
</style>
</head>
<body>
<div class="scene" tabindex="0">
<div class="card">
<div class="side front">
<img src="assets/album-squirrel.png" alt="Тони с плюшевой белкой" width="300" height="400" />
</div>
<div class="side back">С плюшевой белкой: с ней я отдыхаю после прогулки.</div>
</div>
</div>
</body>
</html>
Дополнительная задача
Соберите раскрывающуюся фигуру из четырёх зелёных квадратов вокруг красного центра. На первом этапе разведите
квадраты сдвигом и поворотом; на втором поверните всю фигуру на полный оборот. Позиционирование и плавность
уже подготовлены: замените ??? только у transform и transform-origin.
Подсказки:
- Родителю уже задано относительное позиционирование, детям — абсолютное, как в уроке «Позиционирование элементов. Часть 1».
-
Двум вложенным элементам задайте
rotate(), двум другим —translateY(); двум элементам измените точку преобразования. - Переходы и задержка родителя уже заданы: сначала за 1 секунду расходятся дети, затем за 1 секунду поворачивается родитель. Эти настройки пока не меняйте; переходы разберём в следующем уроке.
Дополнительное задание: вращающиеся квадраты
Разведите четыре зелёных квадрата вокруг красного: два сдвигом, два поворотом. Затем поверните всю фигуру на полный оборот с задержкой, как в образце.
Условие задания
Подсказка
Используйте translateY() для вертикального сдвига и rotate() для поворота. Точка transform-origin задаёт угол вращения боковых квадратов. Готовая задержка родителя даёт дочерним элементам время разойтись.
index.html
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Дополнительное задание</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="main-div">
<div class="sub-div first"></div>
<div class="sub-div second"></div>
<div class="sub-div third"></div>
<div class="sub-div fourth"></div>
</div>
</body>
</html>
style.css
div {
width: 75px;
height: 75px;
}
.main-div {
position: relative;
background: red;
margin: 100px auto;
}
.sub-div {
position: absolute;
background: green;
transition: transform 1s; /* готовая настройка следующего урока */
}
.second {
transform-origin: ???;
}
.fourth {
transform-origin: ???;
}
.main-div:hover {
transition: transform 1s 1s; /* сначала расходятся дети, затем вращается родитель */
transform: ???;
}
.main-div:hover .first {
transform: ???;
}
.main-div:hover .second {
transform: ???;
}
.main-div:hover .third {
transform: ???;
}
.main-div:hover .fourth {
transform: ???;
}
