Кслера8

Причудливый эффект наведения для вашего аватара

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

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

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

HTML: всего один элемент

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

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

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

Круг

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

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, я использую там. Он представляет собой толщину «границы», которая на самом деле просто используется для определения жестких цветовых остановок для красной части радиального градиента.

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

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

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

Я добавил третий цвет в radial-gradient чтобы лучше определить область градиента при наведении:

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

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

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

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

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

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

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

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

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

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

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

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

Вот наш код, включая еще пару переменных 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 */
}

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

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

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

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

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

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

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

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

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

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

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

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

Во-первых, давайте добавим пространство вверху с отступом, чтобы избежать перекрытия вверху:

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 */
}

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

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

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 */
}

Давайте сломаем это mask свойство. Для начала обратите внимание, что аналогичный radial-gradient() из 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, ну теперь вы знаете!

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

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

Подведение итогов

Уф, мы закончили! И мы не только получили плавную анимацию при наведении, но и сделали все это с помощью одного HTML-кода. <img> элемент. Только это и менее 20 строк хитрости CSS!

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

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

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

Чат с нами

Всем привет! Могу я чем-нибудь помочь?