Модный эффект наведения для аватара

Модный эффект наведения для аватара

Знаете ли вы такой эффект, когда чья-то голова просовывается сквозь круг или отверстие? Знаменитая анимация Порки Пига, где он машет на прощание, выскакивая из серии красных колец, является идеальным примером, и Килиан Валкхоф фактически воссоздал этот эффект здесь, на CSS-Tricks, некоторое время назад.

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

https://codepen.io/t_afif/pen/MWBjraa

Видите это? Мы сделаем анимацию масштабирования, при которой аватар как бы выскочит из круга, в котором он находится. Круто, правда? Не смотрите на код, давайте вместе шаг за шагом создадим эту анимацию.

The HTML: Just one element

Если вы не проверили код демо-версии и вам интересно, сколько div’ов это займет, то остановитесь прямо здесь, потому что наша разметка - это всего лишь один элемент изображения:

<img src="" alt="">

Да, один элемент! Самое сложное в этом упражнении - использовать как можно меньшее количество кода. Если вы следите за мной некоторое время, вы должны быть привычны к этому. Я изо всех сил стараюсь найти CSS-решения, которые можно реализовать с помощью минимально возможного, наиболее удобного в обслуживании кода.

Я написал серию статей на CSS-Tricks, в которых я исследую различные эффекты наведения, используя одну и ту же HTML-разметку, содержащую один элемент. Я подробно рассказываю о градиентах, маскировке, обтравке, контурах и даже о технике компоновки. Я настоятельно рекомендую ознакомиться с ними, потому что многие приемы я повторно использую в этом посте.

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

Разработано Кангом

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

Прежде чем перейти к CSS, давайте сначала разберем эффект. Изображение увеличивается при наведении, поэтому мы наверняка используем transform: scale(). За аватаром находится круг, и радиальный градиент должен помочь. Наконец, нам нужен способ создать границу в нижней части круга, которая создаст видимость аватара за кругом.

Приступаем к работе!

Эффект масштаба

Начнем с добавления трансформации:

img {
  width: 280px;
  aspect-ratio: 1;
  cursor: pointer;
  transition: .5s;
}
img:hover {
  transform: scale(1.35);
}
https://codepen.io/t_afif/pen/JjBNozR/9731ac6d1db49eab010b2b1b133abe30

Пока ничего сложного, верно? Давайте двигаться дальше.

Круг

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

img {
  --b: 5px; /* border width */

  width: 280px;
  aspect-ratio: 1;
  background:
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      #C02942 calc(100% - var(--b)) 99%,
      #0000
    );
  cursor: pointer;
  transition: .5s;
}
img:hover {
  transform: scale(1.35);
}

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

https://codepen.io/t_afif/pen/yLqbNYB/de195eef4df8dfe4c45dee3f6dcd507f

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

Давайте начнем с определения переменной CSS, —f, которая определяет ”коэффициент масштабирования”, и используем ее для установки размера круга. Я использую 1 в качестве значения по умолчанию, так как это начальный масштаб для изображения и круга, от которого мы трансформируем.

Вот демонстрация, иллюстрирующая этот трюк. Наведите курсор, чтобы увидеть, что происходит за кулисами:

https://codepen.io/t_afif/pen/ExpmjXW/288f0e4bdc2b062d4f03b7db6d56da03

Я добавил третий цвет к радиальному градиенту, чтобы лучше определить область градиента при наведении:

radial-gradient(
  circle closest-side,
  #ECD078 calc(99% - var(--b)),
  #C02942 calc(100% - var(--b)) 99%,
  lightblue
);

Теперь нам нужно расположить наш фон в центре круга и убедиться, что он занимает всю высоту. Мне нравится объявлять все непосредственно в свойстве background shorthand, поэтому мы можем добавить позиционирование фона и убедиться, что оно не повторяется, добавив эти значения сразу после radial-gradient():

background: radial-gradient() 50% / calc(100% / var(--f)) 100% no-repeat;

Фон размещается в центре (50%), имеет ширину, равную calc(100%/var(—f)), и высоту, равную 100%.

Ничего не масштабируется, когда —f равно 1 - опять же, наш начальный масштаб. Тем временем градиент занимает всю ширину контейнера. Когда мы увеличиваем —f, размер элемента увеличивается - благодаря преобразованию scale() - а размер градиента уменьшается.

Вот что мы получим, если применим все это к нашему демонстрационному примеру:

https://codepen.io/t_afif/pen/eYjWNed/cf9da694f79514eb6a619439de6896c3

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

Нижняя граница

Сначала я попытался решить эту задачу с помощью свойства border-bottom, но мне не удалось найти способ согласовать размер границы с размером круга. Вот лучшее, что у меня получилось, и вы сразу видите, что это неправильно:

https://codepen.io/t_afif/pen/zYLwvxp/46302a3bb090e857947572e12795dfac

Реальное решение - использовать свойство outline. Да, контура, а не границы. В предыдущей статье я показал, что свойство outline является мощным и позволяет нам создавать классные эффекты наведения. В сочетании с outline-offset у нас есть именно то, что нужно для нашего эффекта.

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

https://codepen.io/t_afif/pen/eYjWpZG/d748690660aa7fff5982f911fa622ace

Теперь у нас есть нижняя ”граница” (фактически контур) в сочетании с “границей”, созданной градиентом, чтобы создать полный круг. Нам все еще нужно скрыть части контура (сверху и по бокам), к чему мы перейдем через некоторое время.

Вот наш код на данный момент, включая еще пару переменных CSS, которые можно использовать для настройки размера изображения (—s) и цвета ”границы” (—c):

img {
  --s: 280px; /* image size */
  --b: 5px; /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  width: var(--s);
  aspect-ratio: 1;
  cursor: pointer;
  border-radius: 0 0 999px 999px;
  outline: var(--b) solid var(--c);
  outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
  background: 
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      var(--c) calc(100% - var(--b)) 99%,
      #0000
    ) 50% / calc(100% / var(--f)) 100% no-repeat;
  transform: scale(var(--f));
  transition: .5s;
}
img:hover {
  --f: 1.35; /* hover scale */
}

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

Вычисления, используемые в outline-offset, гораздо более просты, чем кажется. По умолчанию контур рисуется за пределами рамки элемента. А в нашем случае нам нужно, чтобы он перекрывал элемент. Точнее, нам нужно, чтобы он следовал за окружностью, созданной градиентом.

Диаграмма фонового перехода.

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

Не стоит забывать, что второй элемент масштабируется, поэтому наш результат также масштабируется… что означает, что нам нужно разделить результат на f, чтобы получить реальное значение смещения:

Offset = ((f - 1) * S/2) / f = (1 - 1/f) * S/2

Мы добавляем отрицательный знак, так как нам нужно, чтобы контур шел от внешней стороны к внутренней:

Offset = (1/f - 1) * S/2

Вот быстрый демонстрационный пример, показывающий, как контур следует за градиентом:

https://codepen.io/t_afif/pen/VwBbvRw/4474e963f2823899e5b24635c56998fc

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

outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2) - var(--b));
https://codepen.io/t_afif/pen/qBymOGz/a4b0f3652cc89ed1613fc78ec89912a4

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

Сначала добавим пространство в верхней части с помощью padding, чтобы избежать перекрытия в верхней части:

img {
  --s: 280px; /* image size */
  --b: 5px;   /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  width: var(--s);
  aspect-ratio: 1;
  padding-block-start: calc(var(--s)/5);
  /* etc. */
}
img:hover {
  --f: 1.35; /* hover scale */
}
https://codepen.io/t_afif/pen/VwBbaYM/380b4bfb346acea592849f6a1d511c23

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

Обратите внимание, что я добавил значение content-box к фону:

background:
  radial-gradient(
    circle closest-side,
    #ECD078 calc(99% - var(--b)),
    var(--c) calc(100% - var(--b)) 99%,
    #0000
  ) 50%/calc(100%/var(--f)) 100% no-repeat content-box;

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

Добавление CSS-маски

Мы добрались до последней части! Все, что нам нужно сделать, это скрыть некоторые части, и все готово. Для этого мы воспользуемся свойством mask и, конечно же, градиентами.

Вот рисунок, иллюстрирующий то, что нам нужно скрыть или что нам нужно показать, чтобы быть более точным

Показывает, как маска применяется к нижней части круга.

Левое изображение - это то, что мы имеем сейчас, а правое - то, что мы хотим получить. Зеленая часть иллюстрирует маску, которую мы должны применить к исходному изображению, чтобы получить конечный результат.

Мы можем выделить две части нашей маски:

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

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

Вот наш окончательный вариант CSS:

img {
  --s: 280px; /* image size */
  --b: 5px; /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  --_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;
  --_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

  width: var(--s);
  aspect-ratio: 1;
  padding-top: calc(var(--s)/5);
  cursor: pointer;
  border-radius: 0 0 999px 999px;
  outline: var(--b) solid var(--c);
  outline-offset: var(--_o);
  background: 
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      var(--c) calc(100% - var(--b)) 99%,
      #0000) var(--_g);
  mask:
    linear-gradient(#000 0 0) no-repeat
    50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
    radial-gradient(
      circle closest-side,
      #000 99%,
      #0000) var(--_g);
  transform: scale(var(--f));
  transition: .5s;
}
img:hover {
  --f: 1.35; /* hover scale */
}

Давайте разберем это свойство маски. Для начала, обратите внимание, что там находится аналогичный радиальный градиент() из свойства background. Я создал новую переменную, —_g, для общих частей, чтобы сделать ситуацию менее беспорядочной.

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;

mask:
  radial-gradient(
    circle closest-side,
    #000 99%,
    #0000) var(--_g);

Далее, там же есть linear-gradient():

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;

mask:
  linear-gradient(#000 0 0) no-repeat
    50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
  radial-gradient(
    circle closest-side,
    #000 99%,
    #0000) var(--_g);

Это создает прямоугольную часть маски. Его ширина равна ширине радиального градиента минус удвоенная толщина границы:

calc(100% / var(--f) - 2 * var(--b))

Высота прямоугольника равна половине, 50%, размера элемента.

Нам также нужен линейный градиент, размещенный в центре горизонтали (50%) и смещенный от вершины на ту же величину, что и смещение контура. Я создал еще одну переменную CSS, —_o, для смещения, которое мы определили ранее:

--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

Одна из непонятных вещей здесь заключается в том, что нам нужно отрицательное смещение для контура (чтобы переместить его снаружи внутрь), но положительное смещение для градиента (чтобы переместить его сверху вниз). Поэтому, если вам интересно, почему мы умножаем смещение, —_o, на -1, то теперь вы знаете!

Вот демонстрация, иллюстрирующая конфигурацию градиента маски:

https://codepen.io/t_afif/pen/RwBVRNO/dd563d0f41b9c22fcb9aac141bbef029

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

Завершение работы

Вот мы и закончили! И мы не только получили замечательную анимацию наведения, но и сделали все это с помощью одного элемента HTML. Только это и менее 20 строк CSS!

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

Могли бы мы упростить CSS, если бы позволили себе больше HTML? Безусловно. Но мы здесь для того, чтобы научиться новым трюкам CSS! Это было хорошее упражнение для изучения градиентов CSS, маскировки, поведения свойств контура, трансформаций и многого другого. Если в какой-то момент вы почувствуете себя потерянным, обязательно посмотрите мою серию, в которой используются те же общие понятия. Иногда полезно увидеть больше примеров и примеров использования, чтобы донести суть.

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