Скользящие рамки 3D-изображений в СSS
Создание 3D-эффектов в CSS — это не совсем новая концепция, но для ее реализации обычно используются дополнительные элементы в разметке и псевдоэлементы в стилях. Что делать, если у вас нет такой роскоши, как возможность изменять HTML в вашем проекте? В нашем посте вы решаете поставленную задачу, применяя 3D-эффекты и скользящие переходы к одному изображению, используя хитроумные методы CSS, которые демонстрируют передовые, современные методы стилизации.
Довольно изящно, не так ли? Возможно, вы думаете, что это легко сделать. Все, что нам действительно нужно, — это наложение поверх изображения, которое мы переводим, и, готово, верно?
Это правда. Но если вы проверите код, то не найдете в разметке никаких дополнительных элементов, кроме тега <img>
, который мы использовали. Кроме того, мы не можем использовать псевдоэлементы, чтобы заставить это работать. Именно это делает такой эффект немного более сложным.
Не смотрите на код прямо сейчас. Давайте создадим его вместе, разбив демонстрацию на отдельные небольшие CSS-трюки.
Изображение и скользящее наложение
Вы были бы правы, полагая, что невозможно добавить наложение на изображение без дополнительного элемента. Вместо этого мы собираемся подделать его и создать иллюзию наложения.
Давайте начнем со следующего кода:
img {
--s: 200px; /* the image size */
width: var(--s);
box-sizing: border-box;
padding-right: var(--s);
background: #8A9B0F;
transition: 1s;
}
img:hover {
padding: 0;
}
Мы определили ширину как переменную CSS (--s
) и переназначили ее для применения отступов вдоль правой стороны элемента. В сочетании с параметром box-sizing: border-box
это приведет к тому, что размер поля содержимого будет равен 0
. Другими словами, мы не видим изображение, но видим цвет фона, поскольку он закрывает область отступа.
При наведении курсора мыши давайте сделаем отступ равным 0
:
Ничего удивительного, не так ли? Уменьшая отступы, мы увеличиваем размер области содержимого, и изображение постепенно раскрывается. По сути, мы сжимаем его по вертикали и возвращаем на место при наведении курсора мыши.
Давайте добавим к этому еще два свойства:
img {
object-fit: cover;
object-position: left;
}
Теперь эффект выглядит намного лучше, и у нас есть анимация отображения наложения, даже если на самом деле наложение, которое вы видите, является фоном, который находится за изображением! Иллюзия идеальна.
Почему это происходит именно так? Логика подробно объяснена в MDN:
«Размер замененного содержимого изменен таким образом, чтобы сохранить его соотношение сторон при заполнении всего блока содержимого элемента. Если соотношение сторон объекта не соответствует соотношению сторон его блока, объект будет обрезан по размеру».
Другими словами, изображение сохранит свои пропорции при заполнении поля содержимого. В результате изображение не искажается из-за заполнения, как мы видели в первой демонстрации — вместо этого оно обрезается. Затем функция object-position: left
выравнивает положение изображения по левому краю, чтобы оно не перемещалось, в то время как размер поля содержимого увеличивается в результате уменьшения отступа при наведении курсора мыши.
Если мы изменим положение на right
, вы получите другой эффект:
Вместо наложенной анимации у нас есть своего рода скользящий эффект, при котором изображение заходит слева. Это напрямую связано с другим классным приемом CSS, который я использовал для создания эффекта «всплывающего окна» при наведении курсора:
В этой статье мы будем опираться на первый эффект, при котором изображение остается неподвижным. Вот демонстрация со всеми вариантами скольжения:
Вы заметите, что переключаться между различными вариантами довольно просто, изменив пару значений в CSS.
Выдвижение наложения за пределы изображения
Теперь, когда у нас есть наложение, давайте попробуем переместить его за пределы изображения. Вместо того, чтобы уменьшать его размер, как мы делали ранее, мы хотим, чтобы он сохранял свой размер и перемещался.
Для этого давайте используем анимацию box-shadow
:
img {
--s: 200px; /* the image size */
box-shadow: 0 0 #8A9B0F;
}
img:hover {
box-shadow: var(--s) 0 #8A9B0F;
}
Круто, правда? У нас есть наложение над нашим изображением, которое сдвигается, открывая изображение — без использования каких-либо дополнительных элементов в разметке или псевдоэлементов в стилях!
Мы можем добиться того же эффекта, используя анимацию clip-path
.
img {
--s: 200px; /* the image size */
box-shadow: 0 0 0 999px #8A9B0F;
clip-path: inset(0 0 0 0);
}
img:hover {
clip-path: inset(0 -100% 0 0);
}
Мы определяем box-shadow
как имеющую широкий радиус, но на самом деле мы ее не видим, потому что она обрезана. Однако при наведении мы обновляем значение inset(
), чтобы отобразить box-shadow
в правой части изображения.
Используя ту же технику, мы можем сдвинуть наложение в любом направлении. Можете ли вы понять, как? Попробуйте, раздвоив Pen выше и изменив направление в качестве упражнения, прежде чем мы перейдем к следующей части нашей работы.
Добавление границ
Границы могут помочь создать пространство вокруг изображения и приблизить его к форме квадратного прямоугольника. Не забывайте, что в конечном итоге мы хотим создать 3D-коробку. Но давайте посмотрим, что произойдет, когда мы добавим границы.
Граница находится над наложением, и изображение не является идеальным квадратом, по крайней мере, на начальном этапе. Даже если на первый взгляд это кажется некорректным, это логичный результат, поскольку граница рисуется над фоном, а ее толщина добавляется к общему размеру элемента.
Что нам нужно сделать, так это настроить отступы с учетом размера границы. Затем давайте сделаем границу прозрачной, чтобы мы могли видеть цвет фона за ней.
img {
--s: 200px; /* the image size */
--b: 10px; /* border width */
--c: #8A9B0F;
width: var(--s);
aspect-ratio: 1;
box-sizing: border-box;
padding-top: calc(var(--s) - 2*var(--b));
border: var(--b) solid #0000;
box-shadow: 0 0 0 999px var(--c);
background: var(--c);
clip-path: inset(0);
object-fit: cover;
object-position: bottom;
}
img:hover {
padding: 0;
clip-path: inset(-100% 0 0);
}
Так выглядит намного лучше. Было бы еще лучше, если бы мы использовали другой цвет для границы области. Давайте рассмотрим возможность использования нескольких фонов.
img {
--c: #8A9B0F;
--_c: color-mix(in srgb, var(--c), #fff 25%);
background:
linear-gradient(var(--_c) 0 0) no-repeat
0 0 / 100% 100%,
var(--c);
background-origin: border-box;
box-shadow: 0 0 0 999px var(--_c);
/* same as previous */
}
img:hover {
background-size: 100% 0%;
/* same as previous */
}
Прежде всего, обратите внимание, что мы добавили функцию color-mix()
, которая позволяет нам определять новый вариант цвета на основе исходного значения цвета (--c:
#8A9B0F
), смешивая его с белым, чтобы получить более яркий оттенок. Затем мы используем этот новый цвет для создания градиента над цветом фона элемента, который объявляется сразу после градиента. Тот же цвет также используется для box-shadow
.
Идея состоит в том, чтобы уменьшить размер градиента так же, как мы делаем это с отступом, чтобы раскрыть background-color
за градиентом.
Это действительно мило! Но уловили ли вы тонкую визуальную проблему? Если присмотреться, то можно заметить, что наложение немного не соответствует границе.
Это связано с тем, что заполнение имеет переход от s - 2*b
до 0
. Между тем, фоновый переход от 100%
(эквивалент --s
) до 0
. Разница равна 2*b
. Фон покрывает всю область, а отступы — меньшую ее часть. Нам необходимо это учитывать.
В идеале переход заполнения должен занимать меньше времени и иметь небольшую задержку в начале для синхронизации, но найти правильное время будет непростой задачей. Вместо этого давайте увеличим диапазон перехода заполнения, чтобы он стал равным фону.
img {
--h: calc(var(--s) - var(--b));
padding-top: min(var(--h), var(--s) - 2*var(--b));
transition: --h 1s linear;
}
img:hover {
--h: calc(-1 * var(--b));
}
Новая переменная --h
переходит от s - b
к -b
при наведении курсора, поэтому у нас есть необходимый диапазон, поскольку разница равна --s
, что делает ее равной переходам фона и пути обрезки.
Хитрость заключается в функции min()
. Когда --h
переходит от s - b
к s - 2*b
, заполнение равно s - 2*b
. Во время этого короткого перехода никакие отступы не изменяются. Затем, когда --h
достигает 0
и переходит от 0
к -b
, заполнение остается равным 0
, поскольку по умолчанию оно не может быть отрицательным значением.
Вместо этого было бы более интуитивно понятно использовать clamp()
:
padding-top: clamp(0px, var(--h), var(--s) - 2*var(--b));
Тем не менее, нам не нужно указывать нижний параметр, поскольку заполнение не может быть отрицательным и по умолчанию будет ограничено до 0
, если вы присвоите ему отрицательное значение.
Мы становимся намного ближе к конечному результату!
Стоит отметить, что нам нужно использовать @property
, чтобы применить переход к переменной --h
. На момент написания этой статьи переход не будет работать в Firefox.
3D-эффект
Последний шаг — добавить эффекту 3D. Чтобы лучше понять, как мы собираемся к этому подойти, давайте временно удалим box-shadow
, clip-path
и linear-gradient()
, оставив изображение в раскрытом состоянии.
Мы предпримем три шага, чтобы создать 3D-эффект, который я изобразил на следующем рисунке.
Сначала мы увеличиваем толщину границы слева и снизу изображения:
img {
--b: 10px; /* the image border */
--d: 30px; /* the depth */
border: solid #0000;
border-width: var(--b) var(--b) calc(var(--b) + var(--d)) calc(var(--b) + var(--d));
}
Во-вторых, мы добавляем conic-gradient()
на фон, чтобы создать более темные цвета вокруг поля:
background:
conic-gradient(at left var(--d) bottom var(--d),
#0000 25%,#0008 0 62.5%,#0004 0)
var(--c);
Обратите внимание на полупрозрачные значения черного цвета (например, #0008
и #0004
). Небольшая часть прозрачности смешивается с цветами позади нее, создавая иллюзию темного варианта основного цвета, поскольку градиент размещается над цветом фона.
И, наконец, мы применяем clip-path
, чтобы вырезать углы, образующие 3D-бокс.
clip-path: polygon(var(--d) 0, 100% 0, 100% calc(100% - var(--d)), calc(100% - var(--d)) 100%, 0 100%, 0 var(--d));
Теперь, когда мы видим и понимаем, как создается 3D-эффект, давайте вернем то, что мы удалили ранее, начиная с отступов:
Он работает нормально. Но обратите внимание, как мы ввели в формулу глубину (--d
). Это потому, что нижняя граница больше не равна b
, а равна b + d
.
--h: calc(var(--s) - var(--b) - var(--d));
padding-top: min(var(--h),var(--s) - 2*var(--b) - var(--d));
Давайте сделаем то же самое с линейным градиентом. Нам нужно уменьшить его размер, чтобы он покрывал ту же область, что и до того, как мы ввели глубину, чтобы он не перекрывал конический градиент:
Мы приближаемся! Последняя часть, которую нам нужно добавить обратно, — это переход clip-path
, который сочетается с box-shadow
. Мы не можем повторно использовать тот же код, который использовали раньше, поскольку мы изменили значение clip-path
для создания формы трехмерного прямоугольника. Но мы все равно можем преобразовать его, чтобы получить желаемый скользящий результат.
Идея состоит в том, чтобы иметь две точки вверху, которые перемещаются вверх и вниз, открывая и скрывая box-shadow
, в то время как другие точки остаются фиксированными. Вот небольшое видео, иллюстрирующее движение точек: https://vimeo.com/933629341
Итак, что мы видим? У нас есть пять фиксированных точек. Двое вверху перемещаются, чтобы увеличить площадь многоугольника и раскрыть тень блока.
img {
clip-path: polygon(
var(--d) 0, /* --> var(--d) calc(-1*(var(--s) - var(--d))) */
100% 0, /* --> 100% calc(-1*(var(--s) - var(--d))) */
/* the fixed points */
100% calc(100% - var(--d)), /* 1 */
calc(100% - var(--d)) 100%, /* 2 */
0 100%, /* 3 */
0 var(--d), /* 4 */
var(--d) 0); /* 5 */
}
И мы закончили! У нас осталась красивая 3D-рамка вокруг элемента изображения с крышкой, которая сдвигается вверх и вниз при наведении курсора мыши. И мы сделали это без дополнительной разметки и псевдоэлементов!
А вот первая демонстрация, которой я поделился в начале этой статьи, показывающая два варианта скольжения.
Эта последняя демонстрация представляет собой оптимизированную версию того, что мы делали вместе. Я написал большинство формул с использованием переменной --h
, поэтому при наведении курсора обновляю только одно значение. Он также включает в себя еще один вариант. Можете ли вы провести его реверс-инжиниринг и посмотреть, чем его код отличается от того, который мы сделали вместе?
Еще один пример 3D
Хотите еще один необычный эффект, использующий 3D-эффекты и скользящие наложения? Вот один из них, который я собрал, используя другую трехмерную перспективу, где наложение разделяется, а не скользит с одной стороны на другую.
Ваше домашнее задание — проанализировать код. Это может показаться сложным, но если вы проследите шаги, которые мы выполнили для исходной демонстрации, я думаю, вы обнаружите, что это не так уж и сильно отличается от подхода. Эффект скольжения по-прежнему сочетает в себе padding
, свойства object-*
и clip-path
, но с другими значениями для создания нового эффекта.
Заключение
Надеюсь, вам понравился этот небольшой эксперимент с 3D-изображением и необычный эффект, который мы к нему применили. Я знаю, что добавление в разметку дополнительного элемента (например, родительского <div>
в качестве оболочки) значительно облегчило бы достижение эффекта, равно как и псевдоэлементы и переводы. Но мы здесь для того, чтобы бросать вызов и учиться, не так ли?
Ограничение HTML только одним элементом позволяет нам расширить возможности CSS и открыть новые методы, которые могут сэкономить нам время и байты, особенно в тех ситуациях, когда у вас может не быть прямого доступа для изменения HTML, например, когда вы работаете в шаблон CMS. Не смотрите на это как на слишком сложное упражнение. Это упражнение, которое заставляет нас использовать мощь и гибкость CSS.