Проглючило: делаем глитч-эффект на SASS

Ино­гда на мод­ных сай­тах и в реклам­ных роли­ках мож­но встре­тить инте­рес­ный эффект. Текст как буд­то немно­го неста­би­лен: дро­жит, стран­но мор­га­ет и как-то сдви­га­ет­ся и сра­зу воз­вра­ща­ет­ся на место. Как буд­то кто-то тере­бит кабель под­клю­че­ния мони­то­ра. Это назы­ва­ет­ся глитч, от англий­ско­го glitch — сбой или глюк.

Сего­дня мы сде­ла­ем нечто подоб­ное, исполь­зуя пре­про­цес­сор SASS — один из люби­мых инстру­мен­тов фронтенд-разработчиков. Фор­мат запи­си выбе­рем SCSS — так про­цесс будет выгля­деть про­ще и понят­нее. Заод­но и попрак­ти­ку­ем­ся в новом языке.

О пре­про­цес­со­рах мы писа­ли бук­валь­но вче­ра. Сам глитч-эффект выгля­дит при­мер­но так:

Как мы это сделаем

  1. Нам пона­до­бит­ся про­стая HTML-страница, на кото­рой мы раз­ме­стим текст.
  2. Этот текст мы поме­стим в спе­ци­аль­ный блок <div>, к кото­ро­му будем при­ме­нять глитч-эффекты. Что­бы CSS понял, с чем ему нуж­но рабо­тать, у бло­ка долж­но быть имя.
  3. Настро­им CSS-стили. Так бра­у­зер пой­мёт, как ему нуж­но отоб­ра­зить этот текст, что­бы про­яви­лась магия глитча.
  4. Рабо­тать с CSS будем с помо­щью пре­про­цес­со­ра SASS — это почти тот же CSS, толь­ко с допол­ни­тель­ны­ми возможностями.

👉 Важ­ное заме­ча­ние: SCSS-код нель­зя про­сто вста­вить в HTML-страницу — бра­у­зер не пой­мёт, что вы име­ли в виду, и будет обра­ба­ты­вать это как обыч­ный CSS-код. Когда рабо­та­ют с пре­про­цес­со­ра­ми, перед пуб­ли­ка­ци­ей стра­ни­цы код пре­об­ра­зу­ют в при­выч­ный CSS-формат, кото­рый и встав­ля­ют в раз­дел <style> или сохра­ня­ют как обыч­ный .css-файл. Что­бы вы мог­ли вста­вить гото­вый код, мы сами пре­об­ра­зу­ем его из SCSS в CSS и выло­жим в кон­це статьи.

Готовим HTML-код

Самый про­стой этап. Мы про­сто берём наш шаб­лон пустой стра­ни­цы и встав­ля­ем в него заго­ло­вок <h1>:

<!DOCTYPE html>
<html>
<!-- служебная часть -->
<head>
  <!-- заголовок страницы -->
  <title>Заголовок</title>
  <!-- настраиваем служебную информацию для браузеров -->
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style type="text/css">
    /* тут будут стили */
  </style>
  <!-- закрываем служебную часть страницы -->
</head>
<body>
  <h1>
    Журнал «КОД»
  </h1>
</body>
<!-- конец всей страницы -->
</html>
На спе­ц­эф­фек­ты пока не похо­же, но это толь­ко самое нача­ло. Даль­ше всё будет. 

Делаем специальную разметку

У нас пока на стра­ни­це есть про­сто заго­ло­вок пер­во­го уров­ня. Но для глитч-эффекта нуж­но этот заго­ло­вок поме­стить в блок <div> и дать ему новый класс, он будет отве­чать за эффект в целом. Сами визу­аль­ные фиш­ки мы про­пи­шем во вло­жен­ном эле­мен­те <h1>, кото­ро­му тоже при­сво­им новый класс.

Сде­ла­ем это так:

<div class="box">   <h1 class="glitch">     Журнал «КОД»   </h1> </div>

Теперь нуж­но настро­ить внеш­ний вид стра­ни­цы и доба­вить сами эффекты.

Настраиваем внешний вид страницы

Что­бы эффект был похож на тот, что мы виде­ли на кар­тин­ке в самом нача­ле, сде­ла­ем фон чёр­ным, а текст — белым и выров­ня­ем по центру:

* {
  /*убираем отступы*/
  margin: 0;
  padding: 0;
}
body,
html {
  /*пусть страница занимает всё место в окне браузера*/
  width: 100%;
  height: 100%;
}
body {
  /*делаем чёрный фон*/
  background-color: #000;
}
/*стиль нашего блока*/
.box {
  /*говорим браузеру, что все элементы этого класса должны быть по центру окна*/
  display: flex;
  justify-content: center;
  align-items: center;
  /*блок будет занимать всё доступное место по высоте*/
  height: 100%;
  .glitch {
    color: #fff;
    font-family: "Arial", sans-serif;
    font-weight: 600;
    font-size: 100px;
    position: relative;
    padding: 30px;
  }
}

Что тут происходит:

  1. Так как это пре­про­цес­сор, то мы исполь­зу­ем упро­щён­ный спо­соб запи­си CSS-кода.
  2. Звёз­доч­ка в самом нача­ле гово­рит о том, что эти сти­ли при­ме­ня­ют­ся к каж­до­му эле­мен­ту, что­бы ни у кого не было отступов.
  3. После это­го мы зада­ли сти­ли для все­го доку­мен­та и содер­жи­мо­го стра­ни­цы — ска­за­ли, что про­ект дол­жен зани­мать всё сво­бод­ное про­стран­ство в окне браузера.
  4. Отдель­но ука­за­ли, что фон дол­жен быть чёрным.
  5. Рас­ска­за­ли бра­у­зе­ру, как дол­жен выгля­деть блок с клас­сом box.
  6. Внут­ри клас­са box мы объ­яви­ли класс glitch — он у нас отве­ча­ет за стиль само­го заго­лов­ка. В нём мы про­пи­са­ли белый цвет для тек­ста и сде­ла­ли его жир­нее, но это не всё, что там долж­но быть. Осталь­ное доба­вим на сле­ду­ю­щем шаге.

Всё ста­тич­но и не движется. 

Как устроен глитч-эффект

Что­бы добить­ся эффек­та иска­же­ния, раз­ра­бот­чи­ки исполь­зу­ют псев­до­клас­сы :before и :after — они пока­зы­ва­ют тот же самый текст, но поза­ди или спе­ре­ди основ­но­го, кото­рый у нас про­пи­сан в <h1>. Псев­до­эле­мент озна­ча­ет, что его как бы нет на самом деле, но бра­у­зер может с ним рабо­тать так, как буд­то он есть.

Эффект полу­ча­ет­ся в том слу­чае, когда коор­ди­на­ты основ­но­го и псев­до­тек­ста не сов­па­да­ют и он вро­де как про­гля­ды­ва­ет через основ­ной текст. Что­бы так сде­лать, мы будем брать коор­ди­на­ты основ­но­го тек­ста, фик­си­ро­вать их, а потом сме­щать на какое-то рас­сто­я­ние. Чем боль­ше рас­сто­я­ние — тем силь­нее эффект.

Запи­шем то, что мы толь­ко что ска­за­ли, на SCSS, и сде­ла­ем это внут­ри клас­са glitch:

&:before,
&:after {
  content: "Журнал «КОД»";
  color: #fff;
  position: absolute;
  top: 0;
  overflow: hidden;
  padding: 30px;
}
&:before {
  left: 3px;
  text-shadow: -3px 0 red;
  animation: glitch-before 2s linear 0s infinite alternate;
}
&:after {
  left: -3px;
  text-shadow: -3px 0 blue;
  animation: glitch-after 2s linear 0s infinite alternate;
}

Что тут происходит:

  1. Зада­ли общие свой­ства для псев­до­эле­мен­тов — текст, кото­рый сов­па­да­ет с тек­стом наше­го заго­лов­ка, его цвет и отступ в 30 пик­се­лей, как в роди­тель­ском клас­се выше.
  2. Там же ска­за­ли, что место­по­ло­же­ние этих псев­до­эле­мен­тов будет абсо­лют­ным, то есть мож­но будет ука­зы­вать в пик­се­лях, на сколь­ко его мож­но сме­стить в любую сторону.
  3. Hidden озна­ча­ет, что тек­сту нель­зя будет выле­зать за пре­де­лы наше­го заго­лов­ка, что­бы эффект полу­чил­ся какой нужно.
  4. Даль­ше мы опи­са­ли сами псев­до­эле­мен­ты &:before и &:after уже по отдельности.
  5. Пер­во­му мы ска­за­ли, что он сдви­нет­ся вле­во на 3 пик­се­ля, отбро­сит тень на три пик­се­ля впра­во и настро­и­ли ани­ма­цию: он сде­ла­ет это на 2 секун­ды, а потом сно­ва ста­нет обыч­ным эле­мен­том. У нас сто­ит бес­ко­неч­ное повто­ре­ние, но визу­аль­но мы пока это­го не заметим.
  6. Вто­ро­му мы ска­за­ли то же самое, но про синий цвет и про сме­ще­ние влево.

Из-за того, что мы не настро­и­ли пока пол­но­цен­ный эффект, полу­чи­лась такая шту­ка: текст псев­до­эле­мен­та :before уехал вниз и сло­мал всю картинку. 

Финал: оживляем текст

Что­бы доба­вить огня, исполь­зу­ем зна­ко­мый нам по JavaScript цикл @for. Его нет в стан­дарт­ном CSS, но мы же исполь­зу­ем пре­про­цес­со­ры, поэто­му не будем себя ограничивать.

Нам пона­до­бит­ся коман­да @keyframes — она отве­ча­ет в CSS за рас­кад­ров­ку ани­ма­ции. Про­ще гово­ря, если мы зада­дим кад­ры с пара­мет­ра­ми 0%, 50% и 100%, то у нас в нача­ле, сере­дине и кон­це ани­ма­ции могут быть раз­ные картинки.

Зада­ча такая: нуж­но сде­лать ани­ма­цию из 20 кад­ров для каж­до­го эле­мен­та, что­бы любой кадр немно­го отли­чал­ся от осталь­ных. Это даст тот самый рва­ный и непред­ска­зу­е­мый эффект, кото­рый нам нужен. Для это­го мы в цик­ле про­го­ним пере­мен­ную i от 0 до 20, что­бы полу­чил­ся 21 кадр, и каж­дый кадр отри­су­ем в пря­мо­уголь­ни­ке со слу­чай­ны­ми началь­ны­ми коор­ди­на­та­ми, что­бы уси­лить эффект:

keyframes glitch-before {
  $steps: 20;
  @for $i from 0 through $steps {
    #{percentage($i*(1/$steps))} {
      clip: rect(random(150) + px, 780px, random(150) + px, 30px);
    }
  }
}
@keyframes glitch-after {
  $steps: 20;
  @for $i from 0 through $steps {
    #{percentage($i*(1/$steps))} {
      clip: rect(random(150) + px, 780px, random(150) + px, 30px);
    }
  }
}

Что тут происходит:

  1. Мы зада­ли отдель­ную рас­кад­ров­ку для каж­до­го из двух псевдоэлементов.
  2. Ани­ма­ция каж­до­го тако­го эле­мен­та будет состо­ять из 21 кад­ра (20 + нуле­вой начальный).
  3. В цик­ле @for мы зада­ём рас­кад­ров­ку с шагом в 5% (1/20).
  4. На каж­дом про­хо­де цик­ла рису­ем вир­ту­аль­ный пря­мо­уголь­ник со слу­чай­ны­ми коор­ди­на­та­ми по высо­те. Этот пря­мо­уголь­ник как бы «обре­за­ет» про­глю­чив­шую над­пись (от англий­ско­го clip), то есть глюч­ная вер­сия над­пи­си про­ры­ва­ет­ся кусками.
  5. Зна­че­ние в 780 пик­се­лей нуж­но для того, что­бы эффект заце­пил всю над­пись. Если будет мень­ше, то послед­ние сим­во­лы оста­нут­ся без эффектов.

Готовый SCSS-код:

* {
  /*убираем отступы*/
  margin: 0;
  padding: 0;
}
body,
html {
  /*пусть страница занимает всё место в окне браузера*/
  width: 100%;
  height: 100%;
}
body {
  /*делаем чёрный фон*/
  background-color: #000;
}
/*стиль нашего блока*/
.box {
  /*говорим браузеру, что все элементы этого класса должны быть по центру окна*/
  display: flex;
  justify-content: center;
  align-items: center;
  /*блок будет занимать всё доступное место по высоте*/
  height: 100%;
  .glitch {
    color: #fff;
    font-family: "Poppins", sans-serif;
    font-weight: 600;
    font-size: 100px;
    position: relative;
    padding: 30px;
    &:before,
    &:after {
      content: "Glitch";
      color: #fff;
      position: absolute;
      top: 0;
      overflow: hidden;
      padding: 30px;
    }
    &:before {
      left: 3px;
      text-shadow: -3px 0 red;
      animation: glitch-before 2s linear 0s infinite alternate;
    }
    &:after {
      left: -3px;
      text-shadow: -3px 0 blue;
      animation: glitch-after 2s linear 0s infinite alternate;
    }
  }
}
@keyframes glitch-before {
  $steps: 20;
  @for $i from 0 through $steps {
    #{percentage($i*(1/$steps))} {
      clip: rect(random(150) + px, 350px, random(150) + px, 30px);
    }
  }
}
@keyframes glitch-after {
  $steps: 20;
  @for $i from 0 through $steps {
    #{percentage($i*(1/$steps))} {
      clip: rect(random(150) + px, 350px, random(150) + px, 30px);
    }
  }
}

Сконвертированный CSS-код, который можно добавить на страницу

* {
  margin: 0;
  padding: 0;
}
body,
html {
  width: 100%;
  height: 100%;
}
body {
  background-color: #000;
}
.box {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}
.box .glitch {
  color: #fff;
  font-family: "Poppins", sans-serif;
  font-weight: 600;
  font-size: 100px;
  position: relative;
  padding: 30px;
}
.box .glitch:before,
.box .glitch:after {
  content: "Glitch";
  color: #fff;
  position: absolute;
  top: 0;
  overflow: hidden;
  padding: 30px;
}
.box .glitch:before {
  left: 3px;
  text-shadow: -3px 0 red;
  animation: glitch-before 2s linear 0s infinite alternate;
}
.box .glitch:after {
  left: -3px;
  text-shadow: -3px 0 blue;
  animation: glitch-after 2s linear 0s infinite alternate;
}
@keyframes glitch-before {
  0% {
    clip: rect(45px, 350px, 56px, 30px);
  }
  5% {
    clip: rect(102px, 350px, 52px, 30px);
  }
  10% {
    clip: rect(30px, 350px, 93px, 30px);
  }
  15% {
    clip: rect(122px, 350px, 91px, 30px);
  }
  20% {
    clip: rect(52px, 350px, 69px, 30px);
  }
  25% {
    clip: rect(89px, 350px, 141px, 30px);
  }
  30% {
    clip: rect(80px, 350px, 24px, 30px);
  }
  35% {
    clip: rect(21px, 350px, 3px, 30px);
  }
  40% {
    clip: rect(87px, 350px, 13px, 30px);
  }
  45% {
    clip: rect(5px, 350px, 106px, 30px);
  }
  50% {
    clip: rect(138px, 350px, 115px, 30px);
  }
  55% {
    clip: rect(91px, 350px, 105px, 30px);
  }
  60% {
    clip: rect(92px, 350px, 25px, 30px);
  }
  65% {
    clip: rect(69px, 350px, 108px, 30px);
  }
  70% {
    clip: rect(67px, 350px, 20px, 30px);
  }
  75% {
    clip: rect(42px, 350px, 46px, 30px);
  }
  80% {
    clip: rect(94px, 350px, 48px, 30px);
  }
  85% {
    clip: rect(11px, 350px, 101px, 30px);
  }
  90% {
    clip: rect(135px, 350px, 104px, 30px);
  }
  95% {
    clip: rect(128px, 350px, 69px, 30px);
  }
  100% {
    clip: rect(26px, 350px, 116px, 30px);
  }
}
@keyframes glitch-after {
  0% {
    clip: rect(137px, 350px, 103px, 30px);
  }
  5% {
    clip: rect(29px, 350px, 77px, 30px);
  }
  10% {
    clip: rect(148px, 350px, 150px, 30px);
  }
  15% {
    clip: rect(60px, 350px, 65px, 30px);
  }
  20% {
    clip: rect(99px, 350px, 54px, 30px);
  }
  25% {
    clip: rect(104px, 350px, 11px, 30px);
  }
  30% {
    clip: rect(45px, 350px, 82px, 30px);
  }
  35% {
    clip: rect(34px, 350px, 10px, 30px);
  }
  40% {
    clip: rect(1px, 350px, 11px, 30px);
  }
  45% {
    clip: rect(119px, 350px, 93px, 30px);
  }
  50% {
    clip: rect(19px, 350px, 20px, 30px);
  }
  55% {
    clip: rect(26px, 350px, 84px, 30px);
  }
  60% {
    clip: rect(63px, 350px, 44px, 30px);
  }
  65% {
    clip: rect(21px, 350px, 143px, 30px);
  }
  70% {
    clip: rect(45px, 350px, 132px, 30px);
  }
  75% {
    clip: rect(65px, 350px, 105px, 30px);
  }
  80% {
    clip: rect(75px, 350px, 124px, 30px);
  }
  85% {
    clip: rect(101px, 350px, 16px, 30px);
  }
  90% {
    clip: rect(20px, 350px, 116px, 30px);
  }
  95% {
    clip: rect(133px, 350px, 102px, 30px);
  }
  100% {
    clip: rect(9px, 350px, 147px, 30px);
  }
}

Что дальше

Что­бы было инте­рес­нее, мы соста­ви­ли для вас спи­сок вопро­сов на поду­мать. Каж­дый ответ дела­ет вас кру­че как спе­ци­а­ли­ста, поэто­му поста­рай­тесь отве­тить на все:

  1. Поче­му про­бел уби­ра­ет один из эффек­тов со вто­ро­го слова?
  2. Что будет, если у вир­ту­аль­но­го пря­мо­уголь­ни­ка поме­нять координаты?
  3. А если убрать слу­чай­ные зна­че­ния из цикла?
  4. Что будет, если поме­нять места­ми поря­док псевдоэлементов?
  5. Как при­ме­нить этот эффект к кон­крет­ной букве?