Градиентный эффект наведения Градиентный эффект наведения



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

 

ДЕМО
ИСХОДНИКИ

 

Создадим структуру сетки, где каждый элемент будет содержать логотип и описание. Элемент, который будет содержать изменяющиеся буквы кода, будет подразделением класса grid__item--img-deco. Этот элемент также будет содержать наш spotlight.

Шаг 1. HTML


<div class="grid">
	<div class="grid__item">
		<a class="grid__item-img">
			<div class="grid__item-img-deco"></div>
			<svg width="40" height="40" viewBox="0 0 40 40" fill="none"><!-- SVG logo --></svg>
		</a>
		<p class="grid__item-label">Бесплатная раскрутка в соцсетях - советы, сервисы и тактики.</p>
		<span class="grid__item-tag">Социальные сети</span>
	</div>
	<div class="grid__item">
		<!-- ... -->
	</div>
	<div class="grid__item">
		<!-- ... -->
	</div>
	<!-- ... -->
</div>

Псевдоэлемент у нас будет содержать сочные цвета! Все по очереди.

Интересно, что в этом эффекте используетс хитрый трюк для перемещения центра внимания, который заклчается в изменении значений переменных CSS. Рассмотрим как же мы можем стилизовать этот момент.

Шаг 2. CSS

Вглянем на стили сетки. Создадим сетку, где имеем 3 столбца (для больших экранов) и добавим границу между элементами, утановив интервал в 1 пиксель между элементами сетки:


    .grid {
	display: grid;
	margin: 10vh 0;
	grid-template-columns: 1fr;
	border: 1px solid #2a2b3a;
	background: #2a2b3a;
	gap: 1px;
}

@media screen and (min-width: 33em) {
	.grid {
		grid-template-columns: repeat(2,1fr);
	}
}

@media screen and (min-width: 53em) {
	.grid {
		grid-template-columns: repeat(3,1fr);
	}
}

Элементы сетки имеют тот же цвет фона, что и вся страница:


.grid__item {
	padding: 1.5rem 1.5rem 2rem;
	display: grid;
	gap: 1.5rem;
	background: var(--color-bg);
	align-content: start;
	grid-template-rows: auto 1fr auto;
}

Все это совершенно не имеет отношения к эффекту, так что давайте перейдем к самому интересному!

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


.grid__item-img {
	width: 100%;
	aspect-ratio: 1;
	border-radius: 1.6rem;
	position: relative;
	overflow: hidden;
	display: grid;
	place-items: center;
	--x: 0px; 
	--y: 0px;
}

Вот и все! Переменные –x и –y будут отвечать за положение нашей маски. Но где стиль для нашей маски? Ну, подождите, давайте сначала определим цветной градиент, который будет накладывать все, используя режим наложения:


.grid__item-img::after {
	content: '';
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: radial-gradient(rgb(23, 24, 37) 40%, rgb(102, 51, 238) 50%, rgb(142, 100, 255), rgb(249, 38, 114));
	mix-blend-mode: darken;
}

А теперь перейдем к увлекательной части с кодовыми буквами. Итак, этот элемент будет заполнен большим количеством букв с помощью JavaScript. Они оформлены следующим образом:


.grid__item-img-deco {
	position: absolute;
	top: 0;
	left: 0;
	height: 100%;
	width: 100%;
	font-family: "input-mono", monospace;
	font-size: 0.85rem;
	word-wrap: break-word;
	line-height: 1.15;
	color: #fff;
	opacity: 0;
	-webkit-mask-image: radial-gradient(300px circle at var(--x) var(--y), black 20%, rgba(0,0,0,0.25), transparent);
	mask-image: radial-gradient(300px circle at var(--x) var(--y), black 20%, rgba(0,0,0,0.25), transparent);
}

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


radial-gradient(
	300px 
	circle at var(--x) var(--y),
	black 20%,
	rgba(0,0,0,0.25),
	transparent
	)

Как объясняется в CSS reference entry, значение первого пикселя является явным размером градиента, который в данном случае равен 300 пикселям. К сожалению, процентные значения здесь использовать нельзя. Затем определяется положение начала координат круга. Черный цвет начинается с 20%, а затем мы добавим еще одну полупрозрачную точку. Последняя остановка полностью прозрачна. Эти значения гарантируют, что фокусная точка прожектора не будет слишком широкой, но будет хорошо подходить для выделения логотипа!

Шаг 3. JS

Пришло время динамически устанавливать эти переменные!

Я не люблю ломать код, поэтому я использую комментарии для объяснения того, что мы делаем!


import { lerp, getMousePos, getRandomString } from './utils.js';

// Инициализируем объект позиции мыши
let mousepos = {x: 0, y: 0};

// Listen for mousemove events and update 
// 'mousepos' with the current mouse position
window.addEventListener('mousemove', ev => {
    // Save the mouse position
    mousepos = getMousePos(ev);
});

// Класс, представляющий элемент DOM
// с некоторым интерактивным поведением
export class Item {
    // Инициализируйте DOM и связанные со стилем свойства.
    DOM = {
        // main DOM element
        el: null,
        // decoration sub-element
        deco: null,
    };
    // отслеживает координаты x и y для анимации
    renderedStyles = { 
        x: {previous: 0, current: 0, amt: 0.1},
        y: {previous: 0, current: 0, amt: 0.1}
    };
    // случайная строка из 2000 символов
    randomString = getRandomString(2000);
    // tracks scroll position
    scrollVal;
    // tracks size and position of the DOM element
    rect;

    constructor(DOM_el) {
        this.DOM.el = DOM_el;
        this.DOM.deco = this.DOM.el.querySelector('.grid__item-img-deco');
        // calculates initial size and position
        this.calculateSizePosition();
        // sets up event listeners
        this.initEvents();
    }

    // Вычислить и сохранить текущий скролл
    // положение и размер/положение элемента DOM
    calculateSizePosition() {
        // current scroll
        this.scrollVal = {x: window.scrollX, y: window.scrollY};
        // size/position
        this.rect = this.DOM.el.getBoundingClientRect();
    }

    // Зарегистрируйте события для изменения размера, перемещения мыши, 
    initEvents() {
        // On resize, recalculate the size and position
        window.addEventListener('resize', () => this.calculateSizePosition());

        // При наведении курсора мыши на элемент создается
        // новая случайная строка
        this.DOM.el.addEventListener('mousemove', () => {
            // Get a new random string
            this.randomString = getRandomString(2000);
        });

        // При вводе мыши затухайт элемент декора и 
        // запускается цикл анимации
        this.DOM.el.addEventListener('mouseenter', () => {
            gsap.to(this.DOM.deco, {
                duration: .5,
                ease: 'power3',
                opacity: 1
            });
            const isFirstTick = true;
            this.loopRender(isFirstTick);
        });
        
        // При отпускании мыши остановите цикл анимации.
        this.DOM.el.addEventListener('mouseleave', () => {
            this.stopRendering();
            
            gsap.to(this.DOM.deco, {
                duration: .5,
                ease: 'power3',
                opacity: 0
            });
        });
    }

    // Запросить новый кадр анимации, чтобы начать или продолжить цикл рендеринга.
    loopRender(isFirstTick = false) {
        if ( !this.requestId ) {
            this.requestId = requestAnimationFrame(() => this.render(isFirstTick));
        }
    }

    // Отменить любой текущий цикл рендеринга
    stopRendering() {
        if ( this.requestId ) {
            window.cancelAnimationFrame(this.requestId);
            this.requestId = undefined;
        }
    }

    // Рендеринг текущего кадра
    render(isFirstTick) {
        // Clear requestId for the next frame
        this.requestId = undefined;
        
        // Вычислить разницу между текущей позицией прокрутки и сохраненной
        const scrollDiff = {
            x: this.scrollVal.x - window.scrollX,
            y: this.scrollVal.y - window.scrollY
        };

        // Calculate the new translation values based on 
        // the mouse position, scroll difference and 
        // the element's position
        this.renderedStyles['x'].current = (mousepos.x - (scrollDiff.x + this.rect.left));
        this.renderedStyles['y'].current = (mousepos.y - (scrollDiff.y + this.rect.top));
        
        // If it's the first animation tick, set the 
        // previous values to be the same as the current ones
        if ( isFirstTick ) {
            this.renderedStyles['x'].previous = this.renderedStyles['x'].current;
            this.renderedStyles['y'].previous = this.renderedStyles['y'].current;
        }

        // Update the previous value to be a linear 
        // interpolation between the previous and current values
        for (const key in this.renderedStyles ) {
            this.renderedStyles[key].previous = lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].amt);
        }
        
        // Apply the new styles to the DOM element 
        // using CSS variables
        gsap.set(this.DOM.el, {
            '--x': this.renderedStyles['x'].previous,
            '--y': this.renderedStyles['y'].previous
        });

        // Set the deco element's innerHTML to the random string
        this.DOM.deco.innerHTML = this.randomString;

        // Request the next frame
        this.loopRender();
    }
}

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

Мы также меняем строку при каждом небольшом перемещении.

В наш файл utils.js мы добавляем несколько помощников, например, создаем строку элемента deco:


// Linear interpolation
const lerp = (a, b, n) => (1 - n) * a + n * b;

// Gets the mouse position
const getMousePos = e => {
    return { 
        x : e.clientX, 
        y : e.clientY 
    };
};

// This function generates a random string of a given length
const getRandomString = length => {
    let result = '';
    let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    for (let i = 0; i < length; i++) {
        result += characters.charAt(Math.floor(Math.random() * characters.length));
    }
    return result;
};

export { 
    lerp, 
    getMousePos,
    getRandomString,
};

Уверен, данный урок Вам понравился и Вы обязательно используете данный эффект в своем следующем проекте! Удачи!

Оставлю ссылку на оригинальную статью - tympanus.net/codrops/2023/05/17/recreating-the-gradient-mask-hover-effect-from-evervault !


Top

🔖 Выбор по тегам ×

💌 Написать сообщение ×

Все поля обязательны для заполнения!