Как сверстать экраны в перспективе

source

В одном макете встретился данный элемент. Его можно было бы вставить просто картинкой, как это сделано выше, но по задумке эти карточки должны «падать» друг на друга. В чём же проблема выдернуть из слоёв эти элементы и заанимировать их? Проблема в тени.

Если тень добавлять программно к элементам, то она будет не только на соседнем элементе, но и вокруг, на белом фоне; а если пытаться её сместить, то тени наоборот может не хватать.

Что если вырезать тень отдельным слоем? Возникает вопрос, когда её вставлять. Очевидно, что она должна появляться постепенно, для этого мы можем менять ей непрозрачность. Но есть два нюанса. Во-первых, в данном макете тень нарисована только там, где мы сейчас её видим, и если убрать один элемент сверху, то под ним её не будет. Во-вторых, слой с тенью в режиме умножения, что для вёрстки не годится. А это значит, что тень надо рисовать заново.

Допустим, верстальщик смог сам нарисовать новую тень, или попросил дизайнера, всё равно остаётся ещё одна проблема. Когда объект отбрасывает тень на поверхность, то при изменении расстояния меняется не только насыщенность, но и степень размытия тени, повлиять на которую мы не можем в текущем решении. Что ж, тогда попробуем сделать всё при помощи CSS.


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

screen 1
screen 2
screen 3

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

.card {
    position: relative;
    width: 640px;
    height: 400px;
    background: url(screen-1.png);
    background-size: 100%;
}
    
.card::before,
.card::after {
    position: absolute;
    content: '';
    background: orangered;
}
    
.card::before {
    top: 0;
    bottom: 0;
    left: -10px;
    width: 10px;
}

.card::after {
    right: 0;
    bottom: -10px;
    left: 0;
    height: 10px;
}

step 1

Для заливки сторон нет нужды создавать дополнительные изображения, достаточно сильно растянуть исходное (минимум в 10 раз, чтобы 1 пиксель занял 10), не забыв о позиционировании.

.card::before,
.card::after {
    background: url(screen-1.png);
}

.card::before {
    background-size: 6400px 100%;
}

.card::before {
    background-position: bottom;
    background-size: 100% 4800px;
}

step 2

То что нужно, поворачиваем.

.card {
    transform: rotateX(47deg) rotateY(20deg) rotateZ(-30deg);
    transform-style: preserve-3d;
}

step 3

Теперь можно загнуть края. Это возможно благодаря указанному выше transform-style: preserve-3d. Как и большинство классных штук, не сработает в IE.

.card::before {
    transform: rotateY(-90deg);
    transform-origin: 100% 100%;
}

.card::after {
    transform: rotateX(-90deg);
    transform-origin: 0 0;
}

step 4

В местах сгиба есть тонкие полосы, поэтому чуть-чуть сдвинем края.

.card::before {
    transform: rotateY(-90deg) translateX(15%) translateY(0.15%);
}

.card::after {
    transform: rotateX(-90deg) translateY(-15%) translateX(-0.15%);
}

step 5

Теперь на боковые стороны надо добавить тени. Для этого добавим фоны к псевдоэлементам.

.card::before {
    background: linear-gradient(to top, rgba(0,0,0,.1), rgba(0,0,0,.1)), url(screen-1.png);
}

.card::after {
    background: linear-gradient(to top, rgba(0,0,0,.2), rgba(0,0,0,.2)), url(screen-1.png);
}

step 6

Далее создаём ещё два таких же блока, назначаем им соответствующие z-index, чтобы нижние объекты не перекрывали верхние, прижимаем их друг к другу при помощи margin.

HTML:

<div class="card-wrap">
    <div class="card card-1"></div>
    <div class="card card-2"></div>
    <div class="card card-3"></div>
</div>

CSS:

.card-wrap {
    margin-top: 360px;
}

.card {
    margin-top: -360px;
}

.card.card-1 {
    z-index: 3;
    margin-left: 50px;
    background-image: url(screen-1.png);
}

.card.card-2 {
    z-index: 2;
    margin-left: 25px;
    background-image: url(screen-2.png);
}

.card.card-3 {
    z-index: 1;
    background-image: url(screen-3.png);
}

.card.card-1::before {
    background-image: linear-gradient(to top, rgba(0,0,0,.1), rgba(0,0,0,.1)), url(screen-1.png);
}
.card.card-1::after {
    background-image: linear-gradient(to top, rgba(0,0,0,.2), rgba(0,0,0,.2)), url(screen-1.png);
}

.card.card-2::before {
    background-image: linear-gradient(to top, rgba(0,0,0,.1), rgba(0,0,0,.1)), url(screen-2.png);
}
.card.card-2::after {
    background-image: linear-gradient(to top, rgba(0,0,0,.2), rgba(0,0,0,.2)), url(screen-2.png);
}

.card.card-3::before {
    background-image: linear-gradient(to top, rgba(0,0,0,.1), rgba(0,0,0,.1)), url(screen-3.png);
}
.card.card-3::after {
    background-image: linear-gradient(to top, rgba(0,0,0,.2), rgba(0,0,0,.2)), url(screen-3.png);
}

step 7

Чтобы сделать тень, отбрасываемую верхними элементами на нижние, создадим в <div class="card"> блок, повторяющий размеры родительского, и поставим ему overflow: hidden. Это нужно для того, чтобы тень была только на карточках, а также спрятать то, что будет отбрасывать тень. К самому card такого применить не можем, т. к. тогда пропадут псевдоэлементы.

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

Вот что получается: зелёная область будет ограничивать видимость красной полупрозрачной тени и прятать голубую область — то, что отбрасывает тень.

step 8

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

.card .shadow {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    overflow: hidden;
}

.card .shadow::before {
    position: absolute;
    top: -100%;
    right: -100%;
    bottom: 100%;
    left: 100%;
    content: '';
    box-shadow: -598px 366px 20px 8px rgba(0,0,0,.6);
}

step 9

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

.card-wrap .bottom-shadow {
    position: absolute;
    z-index: 10;
    top: 50px;
    left: 30px;
    width: 500px;
    height: 530px;
    transform: rotateX(75deg) rotateZ(-30deg);
    box-shadow: -270px 460px 90px rgba(0,0,0,.1);
}

.card-wrap .bottom-shadow::before {
    position: absolute;
    z-index: 10;
    bottom: 0;
    left: 0;
    width: 300px;
    height: 200px;
    content: '';
    border-radius: 200px 200px 20px 20px / 150px 250px 20px 20px;
    box-shadow: -230px 460px 150px rgba(0,0,0,.1);
}

step 10

Левый нижний угол карточки периодически неприятно пикселит, добавим к псевдоэлементам border-radius: 0 0 0 2px, чтобы ничего не торчало.

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

При помощи translate сдвигаем карточку. У тени меняются три характеристики: непрозрачность, радиус размытия и растяжение. И появляется тень не сразу, а с задержкой.

.card {
    transform: rotateX(47deg) rotateY(20deg) rotateZ(-30deg);
    transition: transform .6s cubic-bezier(.645,.045,.355,1);
}

.card.begin {
    transform: rotateX(47deg) rotateY(20deg) rotateZ(-30deg) translateX(820px) translateY(-820px);
}

.card .shadow::before {
    box-shadow: -598px 372px 20px 8px rgba(0,0,0,.6);
    transition: box-shadow .45s cubic-bezier(.645,.045,.355,1) .15s;
}

.begin + .card .shadow::before {
    box-shadow: -598px 372px 80px 20px rgba(0,0,0,0);
    transition: box-shadow .45s cubic-bezier(.645,.045,.355,1);
}

Аналогично поступаем с самой нижней тенью, настраиваем временны́е задержки. Несколько меняем структуру и transition.

HTML:

<img src="arc.png" alt="arc">
<div class="card-wrap begin">
    <div class="card card-1"></div>
    <div class="card card-2">
        <div class="shadow"></div>
    </div>
    <div class="card card-3">
        <div class="shadow"></div>
    </div>
    <div class="bottom-shadow"></div>
</div>

CSS:

.card {
    transform: rotateX(47deg) rotateY(20deg) rotateZ(-30deg);
    transition: transform .55s cubic-bezier(.645,.045,.355,1);
}

.card.card-1 {
    transition-delay: 2s;
}

.card.card-2 {
    transition-delay: 1.5s;
}

.card.card-3 {
    transition-delay: 1s;
}

.begin .card {
    transform: rotateX(47deg) rotateY(20deg) rotateZ(-30deg) translateX(410px) translateY(-410px);
    transition: none;
}

.card .shadow::before {
    box-shadow: -299px 186px 20px 8px rgba(0,0,0,.6);
    transition: box-shadow .35s cubic-bezier(.645,.045,.355,1);
}

.card.card-2 .shadow::before {
    transition-delay: 2.2s;
}

.card.card-3 .shadow::before {
    transition-delay: 1.7s;
}

.begin .card .shadow::before {
    box-shadow: -598px 372px 80px 20px rgba(0,0,0,0);
    transition: none;
}

.card-wrap .bottom-shadow {
    box-shadow: -330px 640px 90px rgba(0,0,0,.1);
    transition: box-shadow .4s cubic-bezier(.645,.045,.355,1) 1.2s;
}

.card-wrap .bottom-shadow::before {
    box-shadow: -330px 640px 150px rgba(0,0,0,.1);
    transition: box-shadow .3s cubic-bezier(.645,.045,.355,1) 1.3s;
}

.card-wrap.begin .bottom-shadow {
    box-shadow: -330px 640px 120px 20px rgba(0,0,0,0);
    transition: none;
}

.card-wrap.begin .bottom-shadow::before {
    box-shadow: -330px 640px 200px 20px rgba(0,0,0,0);
    transition: none;
}
arc

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

{{ message }}

{{ 'Comments are closed.' | trans }}