Я люблю придумывать различные элементы интерфейса, и однажды мне захотелось сделать прелоадер.
Для его реализации мне потребовалась следующая разметка:
<div class="preloader"></div>
По моей задумке, прелоадер состоит из двух квадратов: большого с размером 60x60px и маленького — 15x15px.
Так как большой квадрат является контуром для маленького, то для его реализации я использовал сам элемент div .prealoder. А вот для вложенного квадрата мне понадобился псевдоэлемент before.
.preloader {
width: 60px;
height: 60px;
border: 2px solid #fff;
position: relative;
}
.preloader::before {
content: "";
width: 15px;
height: 15px;
background-color: #fff;
position: absolute;
top: calc(50% - 7.5px);
left: calc(50% - 7.5px);
}
Для анимации я написал следующий сценарий:
.preloader::before {
animation: preloader 2.25s ease-out both infinite;
}
@keyframes preloader {
0%, 10%, 90%, 100% {
transform: translate3d(0, 0, 0) scale(0);
}
20%, 70% {
transform: translate3d(0, 0, 0) scale(1);
}
30% {
transform: translate3d(-15px, -15px, 0) scale(1);
}
40% {
transform: translate3d(15px, -15px, 0) scale(1);
}
50% {
transform: translate3d(15px, 15px, 0) scale(1);
}
60% {
transform: translate3d(-15px, 15px, 0) scale(1);
}
}
Кроме основного отображение мне захотелось добавить прелоадер с размерами контура 120x120px и внутреннего квадрата 30x30px.
.preloader_l {
width: 120px;
height: 120px;
}
.preloader_l::before {
width: 30px;
height: 30px;
top: calc(50% - 15px);
left: calc(50% - 15px);
animation-name: preloader-l;
}
@keyframes preloader-l {
0%, 10%, 90%, 100% {
transform: translate3d(0, 0, 0) scale(0);
}
20%, 70% {
transform: translate3d(0, 0, 0) scale(1);
}
30% {
transform: translate3d(-30px, -30px, 0) scale(1);
}
40% {
transform: translate3d(30px, -30px, 0) scale(1);
}
50% {
transform: translate3d(30px, 30px, 0) scale(1);
}
60% {
transform: translate3d(-30px, 30px, 0) scale(1);
}
}
Все получилось, как задумывалось. Я пошел пить чай, и ко мне пришла мысль, что если мне потребуется добавить еще один размер, то снова придется указывать размеры (width и height), позицию (top и left) и сценарий анимации.
Для того чтобы это исправить, я буду использовать единицу измерения em. С помощью нее у меня получится привязать все значения свойств к одному значению font-size, с помощью которого буду переключать размеры прелоадера.
В итоге код изменится следующим образом:
/* Вместо Xem и Xpx будут рассчитаны значение в em и в px. */
.preloader {
width: Xem;
height: Xem;
font-size: Xpx;
}
.preloader::before {
width: Xem;
height: Xem;
animation: preloader 2.25s ease-out both infinite;
}
@keyframes preloader {
0%, 10%, 90%, 100% {
transform: translate3d(0, 0, 0) scale(0);
}
20%, 70% {
transform: translate3d(0, 0, 0) scale(1);
}
30% {
transform: translate3d(-Xem, -Xem, 0) scale(1);
}
40% {
transform: translate3d(Xem, -Xem, 0) scale(1);
}
50% {
transform: translate3d(Xem, Xem, 0) scale(1);
}
60% {
transform: translate3d(-Xem, Xem, 0) scale(1);
}
}
.preloader_l {
font-size: Xpx;
}
Решение
Для решения задачи мне пригодится формула расчета em:
Vem = Vpx / Fs
Vpx это значение в px, которое ранее было задано. Значение Fs это подобранное число, на которое удобно делить значение Vpx.
Теперь можно начать расчеты следующих свойств:
.preloader {
width: 60px;
height: 60px;
}
.preloader::before {
width: 15px;
height: 15px;
top: calc(50% - 7.5px);
left: calc(50% - 7.5px);
}
Для этого нужно подобрать значение font-size. По моей задумке, данное свойство будет задавать размеры контура, поэтому логично использовать тоже самое значение, что сейчас установлено для width и height.
Width(em) = 60px / 60px = 1em
Height(em) = 60px / 60px = 1em
.preloader {
width: 1em;
height: 1em;
font-size: 60px;
}
Далее рассчитаю значения width, height, top и left у элемента .preloader::before. Для этого уже не надо подбирать значение font-size, потому что оно будет унаследовано от font-size элемента .preloader.
Width(em) = 15px / 60px = 0.25em
Height(em) = 15px / 60px = 0.25em
Top(em) = 7.5px / 60px = 0.125em;
Left(em) = 7.5px / 60px = 0.125em;
.preloader::before {
width: 0.25em;
height: 0.25em;
top: calc(50% - 0.125em);
left: calc(50% - 0.125em);
}
Осталось изменить значения в сценарии анимации.
@keyframes preloader {
0%, 10%, 90%, 100% {
transform: translate3d(0, 0, 0) scale(0);
}
20%, 70% {
transform: translate3d(0, 0, 0) scale(1);
}
30% {
transform: translate3d(-15px, -15px, 0) scale(1);
}
40% {
transform: translate3d(15px, -15px, 0) scale(1);
}
50% {
transform: translate3d(15px, 15px, 0) scale(1);
}
60% {
transform: translate3d(-15px, 15px, 0) scale(1);
}
}
Если посмотреть на значения без учета знака, то требуется перевести только 15px. Ранее я уже сделал это, и они соответствуют 0.25em.
@keyframes preloader {
0%, 10%, 90%, 100% {
transform: translate3d(0, 0, 0) scale(0);
}
20%, 70% {
transform: translate3d(0, 0, 0) scale(1);
}
30% {
transform: translate3d(-0.25em, -0.25em, 0) scale(1);
}
40% {
transform: translate3d(0.25em, -0.25em, 0) scale(1);
}
50% {
transform: translate3d(0.25em, 0.25em, 0) scale(1);
}
60% {
transform: translate3d(-0.25em, 0.25em, 0) scale(1);
}
}
Теперь можно задать размер 120px для элемента .preloader_l.
.preloader_l {
font-size: 120px;
}
Стоит заметить, что я удалил CSS-правило .preloader_l::before и сценарий анимации preloader-l, потому что они больше не нужны.
Домашняя задача
Для того, чтобы закрепить данный трюк, я подготовил задачу. В ней требуется перевести значения из px в em, которые используются в сценарии анимации. Все ранее рассчитанные значения свойств сохранились.
@keyframes preloader {
0%, 70%, 100% {
transform: translate3d(0, 0, 0) scale(0);
}
10%, 60% {
transform: translate3d(0, 0, 0) scale(1);
}
20% {
transform: translate3d(9px, -21px, 0) scale(1);
}
30% {
transform: translate3d(3px, 21px, 0) scale(1);
}
40% {
transform: translate3d(-9px, -21px, 0) scale(1);
}
50% {
transform: translate3d(-9px, 21px, 0) scale(1);
}
}
Комментарии (12)
ImKremen
11.03.2019 19:50+1Но ведь можно и без em: codepen.io/imkremen/pen/RdLbXQ
melnik909 Автор
12.03.2019 11:56Вы решили конкретную задачу. А теперь давайте представим, что ваш прием с % нужно перенести на другую задачу. Например, для верстки padding'а у кнопки. Прием сработает? Нет. Этот прием можно использовать в более подходящих случаях. Например, когда требуется задавать пропорции habr.com/ru/post/433710. А в своей заметке я показал трюк, который часто используется для верстки типографики, ссылок, кнопок и многих других элементов.
ImKremen
13.03.2019 00:32Но как связаны предоадер и типографика? Почему размер шрифта влияет на UI элемент в дизайне которого нет ни единого символа?
Имхо, для решения подобных этой задач идеально подходят CSS Custom Properties.
co6epuryceu
11.03.2019 20:13Если для кого-то это было открытием, то советую посмотреть выступление Андрея Бойко, он там рассказывал как они (в Glivera) в анимации используют em вместо px и радуют заказчика:
youtu.be/_63sz4aHrFo
monochromer
11.03.2019 20:26Для байт-перфекционистов: если взять за font-size значение 15px (что, наверно, лучше соответствует текущему значению размера шрифта на странице), то можно чуть сэкономить на длине чисел:
15px — 1em
7.5px — 0.5em
60px — 4emextempl
12.03.2019 07:47Для мест которые не влияют на отображаемый шрифт проще брать 10px — не нужно считать
demimurych
14.03.2019 00:09Я не очень понял, чем это решение лучше работы с обычным svg? Я не вижу никаких преимуществ и только недостатки в силу сложности решения.
melnik909 Автор
14.03.2019 13:39Я показывал не «решение», а трюк, который можем использовать в разных решениях.
pepelsbey
Не хватает ссылки на CodePen с живым кодом.
melnik909 Автор
Демо: codepen.io/melnik909/pen/MrXjwX?editors=1100