Веб-разработка
June 6, 2022

CSS-фильтры и Кекстаграм — Тренажёр HTML Academy

Завершаем, вроде как, изучение декоративных эффектов. Разберёмся какие есть и как работают CSS-фильтры, а следом создадим свой аналог на данный момент запрещённой в России социальной сети.

Наверное, сразу напишу — Кекстаграму посвящено две части, которые очень странные. Скорее всего, изначально они были одной частью или сведены в отдельный блок. В общем, это один из тех моментов, когда порядок блоков в тренажёре опять сыграл злую шутку.

CSS-фильтры filter

Фильтры представляют собой функции, которые, насколько я понял, вообще берут свое начало из SVG, до которого мы скоро доберёмся. В CSS за это отвечает свойство filter которое принимает эти функции за значение:

.contrast {
  filter: contrast(0.5);
}

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

Простые фильтры

Яркость brightness() — по умолчанию имеет значение 1 или 100%, уменьшение этих значений, очевидно, уменьшает яркость элемента, увеличение — увеличивает.

Контрастность contrast() — тоже имеет по умолчанию значение 1 или 100% и работает аналогично яркости.

Насыщенность цвета saturate() — работает аналогично яркости и контрастности, значение 0 полностью обесцвечивает изображение.


Бесцветность grayscale() — работает немного иначе, по умолчанию имеет значение 0, то есть не воздействует на элемент. Увеличение коэффициента до 1 или 100%, увеличивает воздействие обесцвечивания.

Сепия sepia() — работает аналогично бесцветности, по умолчанию имеет значение 0 и увеличение значение увеличивает воздействие сепии до 1 или 100%. На самом деле, «сепия» это оттенок коричневого, который исторически ассоциируется с эффектом «старины».

Инверсия цвета invert() — работает аналогично бесцветности и сепии, на значении 0 не воздействует, а на 1 или 100% полностью инвертирует цвета. Имеет интересный эффект на 0.5 или 50% — все цвета становятся серыми.

Непрозрачность opacity() — работает подобно бесцветности и сепии, но значения задаются в обратную сторону. По умолчанию значение 1 или 100%, то есть изображение непрозрачно, а уменьшение этого показателя увеличивает прозрачность элемента. Вплоть до 0, то есть полностью прозрачный.

Фильтры посложнее

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

Размытость blur() — функция тоже принимает один аргумент — радиус размытия, который чаще всего задается в px. Радиус размытия определяет как много пикселей смешиваются друг с другом. Таким образом, чем выше показатель тем более размытым будет элемент.

С помощью, в том числе, этого фильтра делают модный сейчас глассморфизм — имитация полупрозрачного стекла, который чуть менее чем везде в интерфейсах. Даже здесь, на Teletype хедер с глассморфизмом.

Поворот цвета hue-rotate() — с этим фильтром, пожалуй, требуется пояснение. Корректнее было бы назвать функцию поворот «цветового тона», так как этим фильтром изменяется именно градус направления вектора на цветовой диаграмме.

Её по разному изображают — в виде развёрнутой линии с градусами, в виде «цветового круга» или «карты» в смешении с насыщенностью — самый распространённый вариант, или как в Photoshop можно включить «Color Wheel» — на мой взгляд, самый удобный вариант.

Все цвета, так или иначе, имеют свою позицию на диаграмме. Грубо говоря, имеют свой градус — красный 0deg, зеленый 120deg и так далее. В сумме, изображение состоит из множества таких цветов, у каждого из которых своя позиция на диаграмме.

Функцией hue-rotate() мы меняем позицию, поворачиваем вектор, всех цветов изображения сразу. Она принимает один аргумент — градус поворота deg. Как и с функцией поворота в трансформациях rotate(), положительные значения поворачивают тон по часовой стрелке, отрицательные — против.

Падающая тень drop-shadow() — последняя функция из свойства filter. Это и не фильтр в общем-то, а скорее улучшенная версия свойства box-shadow. Функция drop-shadow() имеет такой же синтаксис, но не поддерживает внутренние тени inset и растяжение. В чем же он тогда улучшенный?

В том, что функция drop-shadow() работает с прозрачностью и умеет строить тень объекта, а не всего блока. Правда, для этого у элемента не должно быть фона, даже заданного через CSS:

Одно из применений этого фильтра, причем зачастую неуместное, это падающая тень от текста. Так как функция учитывает прозрачные области, тень получается в точности совпадающая с текстом. Благодаря чему можно добиться очень интересных эффектов, а можно добиться грязи и «мыла», поэтому стоит быть аккуратным.

Множество фильтров

Свойство filter может принимать не одно значение, то бишь не один фильтр, а сразу несколько. Задаются они через пробел, без запятых. Количество, в общем-то, ограничено лишь дизайнерской фантазией. Однако, крайне важно понимать порядок задаваемых фильтров.

Дело в том, что фильтры в этом свойстве обрабатываются каждый в отдельности. Поэтому последующий фильтр будет работать с уже «отфильтрованным» изображением от предыдущего.

.multi-filter-1 {
  filter: hue-rotate(90deg) saturate(2) sepia(0.8);
}
/* не эквивалентны! */
.multi-filter-2 {
  filter: sepia(0.8) hue-rotate(90deg) saturate(2);
}

С тенями drop-shadow() это особенно заметно — каждая последующая тень также учитывает и предыдущие, принимая их за объект.

.bubble {
  filter: 
    drop-shadow(200px 20px 5px blue) /* 5px размытие! */
    drop-shadow(0px 100px 0px green); /* Размытия нет */
}

В части еще упомянута анимация фильтров через @keyframe, но я предпочту это пока опустить, так как до анимации мы еще доберемся.

Испытания

Два небольших испытания, в основном на глазастость. В обоих нужно понять что за фильтр на примере и подобрать правильный. В первом испытании одиночные фильтры:

Во втором испытании множественные фильтры, на самом деле испытание еще проще. Но, внезапно, я не смог его сделать на 100% — почему-то буквально несколько пикселей не совпадало и выводит 99,9% — приму это за 100%.

Кекстаграм

Переходим к самой странной части. Даже не знаю с чего начать. Идея части — сделать свою галерею фильтров как в Инстаграм при публикации. Вся суть задачи сводится к созданию классов с фильтрами, а с помощью JavaScript уже запрограммировать использование этих фильтров.

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

Нюансы задачи

Во-первых, мы уже делали нечто подобное в «Знакомстве с JavaScript» — по сути это кнопки, которые добавляют/убирают класс active с меню фильтров и добавляют/убирают класс созданного фильтра у основной фотки.

То есть используется classList.add и classList.remove в комбинации с условиями if () {} else {}, циклом for {}, обёрнутые в функции function с обработчиком событий. Грубо говоря, собрали всё в одну задачу, что круто.

Однако, есть во-вторых — судя по номеру части в адресе (98), она сильно старше «Знакомства» (347), поэтому информация между ними не соотносится:

  • Не используем let, а используем глобальные переменные var;
  • Почему-то нет classList.toggle, который явно напрашивается;
  • Мы используем innerHTML заместо textContent;
  • Используем addEventListener('click') как обработчик событий, заместо onclick.

При этом разницу между этими подходами нам, конечно, не объяснили, так как хронологически её и не существовало на момент создания Кекстаграма.

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

Это немного дезориентирует, непонятно что за чем идёт. Хронологический порядок точно не вариант, порядок блоков в тренажёре, судя по всему, тоже. Порядок не порядок.

Я как новичок, совсем не разбираюсь в версиях JS, но из того, что я смог нагуглить, судя по всему, дело именно в этом. В Кекстаграме и, видимо, в блоке «Программирования на JS» у нас будет более старшая версия JS, нежели в «Знакомстве с JS».

Решение задачи

Перейдем к самому Кекстаграму. Не буду сильно вдаваться в подробности — мы их уже разобрали в «Знакомствах» и еще явно разберём дальше в части «Программирования».

Здесь просто решим задачу, имея ввиду отличия, которые я описал выше. Плюс я не буду упоминать периферию, то есть прям все элементы, <head>, все стили — только предметно, что нам нужно для задачи. Остальное с задачей не связано

Начнём с вёрстки. Она максимально простая — блок с фотографией и блок с кнопками фильтров. У каждой кнопки свой набор фильтров, который применяется к фотке-заглушке.

<div class="photos">
  <div class="photo"></div>
</div>
<div class="toggle-controls">
  <button class="walden" type="button" data-filter="walden"></button>
  <button class="toaster" type="button" data-filter="toaster"></button>
  <button class="kelvin" type="button" data-filter="kelvin"></button>
</div>

Само название фильтра задано в data-атрибуте, чтобы его можно было удобнее использовать в JS.

Заранее заготавливаем класс active, который, кстати, почему-то нам в тренажёре в коде не показали, но он явно используется в JS. Сам класс обозначает активный фильтр — белая рамка вокруг кнопки. И заготавливаем, собственно, сами фильтры:

.walden {
  filter: contrast(0.9) brightness(1.2) hue-rotate(-20deg) saturate(1.7) sepia(0.4);
}
.toaster {
  filter: contrast(0.67) saturate(2.5) hue-rotate(-30deg) sepia(0.4);
}
.kelvin {
  filter: contrast(1.1) brightness(1.3) saturate(2.4) sepia(0.4);
}

Их в самом конце части стало четыре, но нам для конспекта не существенно. Остальные классы расписывать смысла не вижу, в разметке они указаны — с ними и будем работать, а что там внутри не столь важно.

Итак, переходим в JavaScript. Первое, что нам надо сделать, это найти на странице объекты с которыми мы будем взаимодействовать. Это само фото и сами кнопки фильтров.

Кнопок у нас может быть неизвестное количество, поэтому мы применим querySelectorAll для поиска всех кнопок и записи их в массив. Ищем все <button> в блоке кнопок:

var controls = document.querySelectorAll('.toggle-controls button');
var photo = document.querySelector('.photo');

Отлично, объекты у нас есть. В тренажёре подпись у фильтров реализована не в вёрстке, а на JS через цикл и подстановку data-атрибута в элемент. Не совсем понимаю зачем и в части про это как-то не упоминается, но видимо, раз уж задали data-атрибут, то будем использовать по максимуму:

for (var i = 0; i < controls.length; i++) {
  controls[i].innerHTML = controls[i].dataset.filter;
}

Цикл for здесь без of, как это было в «Знакомствах», хотя можно и его было бы использовать. В нашем случае мы используем классический цикл со счётчиком итераций и приращением, как это делали в «Знакомстве с PHP».

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

Идём дальше. Нам нужно, чтобы при нажатии на кнопку фильтра к фото применялся указанный набор фильтров, а сама кнопка выделялась. При этом нужно избежать смешения, поэтому перед этим нужно убрать все фильтры с фото и выделение с кнопки.

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

function toggleFilter(filterName) {
  // проходим по всем кнопкам и убираем выделение
  // и убирем текущий фильтр с фото
  for (var j = 0; j < controls.length; j++) {
    controls[j].classList.remove('active');
    photo.classList.remove(controls[j].dataset.filter);
  }
  // находим <button> с объявленным фильтром
  var control = document.querySelector('button.' + filterName);
  
  if (control) { // добавляем выделение кнопке
    control.classList.add('active');
  }
  if (photo) { // добавляем фильтр фотке
    photo.classList.add(filterName);
  }
}

Напомню, что в аргумент функции передаётся какое-то значение при её вызове, поэтому filterName заменится, собственно, названием фильтра. Само название фильтра мы тянем из data-атрибута.

Отлично, саму смену фильтров мы запрограммировали, хотя мне немного непонятно зачем условия при добавлении класса. Что они проверяют? Что фотка это фотка, а кнопка — кнопка? Ладно.

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

function clickControl(control) {
  control.addEventListener('click', function () {
    toggleFilter(control);
  });
}

Метод addEventListener ждёт ("слушает") указанное в первом аргументе событие, в нашем случае это клик 'click', а затем исполняет указанную во втором аргументе функцию. В нашем случае это функция смены фильтры, что мы написали выше.

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

У нам уже есть цикл, где перебираются все кнопки и их data-атрибуты с названиями фильтров. Это самый первый цикл для подстановки подписи. Вот он зачем! Я не сразу понял, каюсь.

for (var i = 0; i < controls.length; i++) {
  controls[i].innerHTML = controls[i].dataset.filter;
  clickControl(controls[i]);
}

Таким образом мы каждой кнопке передали обработчик события по клику, а ему в свою очередь, передаём data-атрибут с названием фильтра. А далее обработчик вызывает функцию фильтрации и передаёт уже ей название фильтра.

Таким образом, можно убрать лишний поиск селектора, заменив всё данными из обработчика, так как благодаря нему мы точно знаем что за кнопка была нажата.

function toggleFilter(control) {
  for (var j = 0; j < controls.length; j++) {
    controls[j].classList.remove('active');
    photo.classList.remove(controls[j].dataset.filter);
  }

  control.classList.add('active');

  if (photo) {
    photo.classList.add(control.dataset.filter);
  }
}

Вот и всё! Всё работает.

На этом, правда, часть не кончается — последние несколько уроков посвящены ползунку сравнения и четвёртому набору фильтров. Про ещё один набор фильтров не вижу смысла писать, с точки зрения JS ничего и не поменяется.

А про ползунок — в тренажёре нам просто показали фичу, не объясняя как она работает. То есть там буквально мы просто заверстали ползунок с фоткой «до» и прикрепили волшебный скрипт, который сделал дело, но мы пока слишком слабы для этого. Оно, имхо и к лучшему, иначе было бы нагорожено, но конспектировать я это тоже не вижу смысла. Фича крутая, но как она работает узнаем потом.


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

На этом декоративные эффекты закончились, пока что. Следом у нас «Мастерская» — я к ней пока не приступал, но судя по описанию это в основном около-практика. Будет интересно.

Вам большущее спасибо за внимание! Я вроде как поднаторел в словоблудии, хотя ошибки у меня всё еще на высоком уровне. Стараюсь исправиться.

Ссылки на другие статьи по HTML Academy:

Знакомство с Веб-разработкой
Знакомство с HTML и CSS
Знакомство с JavaScript
Знакомство с PHP
Таблицы и подробно о формах
Наследование, каскады и селекторы
Блочная модель, поток и сетка на float
Гибкие флексбоксы display: flex
Удобные сетки на гридах display: grid
Пропуск блока «Погружение»
Позиционирование и двумерные трансформации
Теневое искусство и линейные градиенты
CSS-фильтры и Кекстаграм <- Вы здесь
Мастерские
Продвинутые Мастерские
...

Остальные статьи можно посмотреть у меня на главной странице блога.

Также мои соц. сетки, которые я продолжаю вести:

Мой Twitter
Мой Telegram
Мой Паблик ВК

Заходите куда удобно вам и подписывайтесь! Еще раз спасибо за внимание!