Я люблю придумывать различные элементы интерфейса, и однажды мне захотелось сделать прелоадер.



Для его реализации мне потребовалась следующая разметка:


<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)


  1. pepelsbey
    11.03.2019 18:42

    Не хватает ссылки на CodePen с живым кодом.



  1. ImKremen
    11.03.2019 19:50
    +1

    Но ведь можно и без em: codepen.io/imkremen/pen/RdLbXQ


    1. Moxa
      11.03.2019 22:58

      тоже не понял, зачем эта муть с em, проценты же проще


    1. melnik909 Автор
      12.03.2019 11:56

      Вы решили конкретную задачу. А теперь давайте представим, что ваш прием с % нужно перенести на другую задачу. Например, для верстки padding'а у кнопки. Прием сработает? Нет. Этот прием можно использовать в более подходящих случаях. Например, когда требуется задавать пропорции habr.com/ru/post/433710. А в своей заметке я показал трюк, который часто используется для верстки типографики, ссылок, кнопок и многих других элементов.


      1. ImKremen
        13.03.2019 00:32

        Но как связаны предоадер и типографика? Почему размер шрифта влияет на UI элемент в дизайне которого нет ни единого символа?
        Имхо, для решения подобных этой задач идеально подходят CSS Custom Properties.


  1. co6epuryceu
    11.03.2019 20:13

    Если для кого-то это было открытием, то советую посмотреть выступление Андрея Бойко, он там рассказывал как они (в Glivera) в анимации используют em вместо px и радуют заказчика:
    youtu.be/_63sz4aHrFo


  1. monochromer
    11.03.2019 20:26

    Для байт-перфекционистов: если взять за font-size значение 15px (что, наверно, лучше соответствует текущему значению размера шрифта на странице), то можно чуть сэкономить на длине чисел:
    15px — 1em
    7.5px — 0.5em
    60px — 4em


    1. extempl
      12.03.2019 07:47

      Для мест которые не влияют на отображаемый шрифт проще брать 10px — не нужно считать


  1. pavelpromin
    12.03.2019 15:57

    Для чего делать видео вместо gif анимации?


  1. demimurych
    14.03.2019 00:09

    Я не очень понял, чем это решение лучше работы с обычным svg? Я не вижу никаких преимуществ и только недостатки в силу сложности решения.


    1. melnik909 Автор
      14.03.2019 13:39

      Я показывал не «решение», а трюк, который можем использовать в разных решениях.