
鼠标悬停时动画

At Codrops, we love experimenting with playful hover effects. Back in 2018, we explored a set of fun hover animations for links. We called that Image Reveal Hover Effects and it shows how to make images appear with a fancy animation when hovering items of a menu. After seeing the fantastic portfolio of Marvin Schwaibold, I wanted to try this effect again on a larger menu and add that beautiful swing effect when moving the mouse. Using some filters, this can also be made more dramatic.
在Codrops,我们喜欢尝试有趣的悬停效果。 早在2018年,我们就探索了一组有趣的悬停动画以获取链接。 我们将其称为“图像显示悬停效果” ,它展示了如何在悬停菜单项时使图像以精美的动画出现。 看完Marvin Schwaibold出色的作品集之后,我想在更大的菜单上再次尝试这种效果,并在移动鼠标时添加漂亮的摇摆效果。 使用一些过滤器,这也可以变得更加生动。
If you are interested in other similar effect, have a look at these:
如果您对其他类似效果感兴趣,请查看以下内容:
So, today we’ll have a look at how to create this juicy image hover reveal animation:
因此,今天我们来看看如何创建多汁的图像悬停展示动画:
一些标记和样式 (Some Markup and Styling)
We’ll use a nested structure for each menu item because we’ll have several text elements that will appear on page load and hover.
我们将为每个菜单项使用嵌套结构,因为我们将在页面加载和悬停时显示几个文本元素。
But we’ll not go into the text animation on load or the hover effect so what we are interested in here is how we’ll make the image appear for each item. The first thing I do when I want to make a certain effect is to write up the structure that I need using no JavaScript. So let’s take a look at that:
但是我们不会在加载或悬停效果上使用文本动画,因此我们感兴趣的是如何使图像显示在每个项目上。 当我想产生某种效果时,我要做的第一件事就是不用JavaScript来编写所需的结构。 因此,让我们看一下:
<a class="menu__item">
<span class="menu__item-text">
<span class="menu__item-textinner">Maria Costa</span>
</span>
<span class="menu__item-sub">Style Reset 66 Berlin</span>
<!-- Markup for the image, inserted with JS -->
<div class="hover-reveal">
<div class="hover-reveal__inner">
<div class="hover-reveal__img" style="background-image: url(img/1.jpg);"></div>
</div>
</div>
</a>
In order to construct this markup for the image, we need to save the source somewhere. We’ll use a data attribute on the menu__item, e.g. data-img="img/1.jpg"
. We’ll go into more detail later on.
为了构造图像的标记,我们需要将源保存在某个地方。 我们将在menu__item上使用data属性,例如data-img="img/1.jpg"
。 稍后我们将详细介绍。
Next, we’ll have some styling for it:
接下来,我们将对其进行一些样式设置:
.hover-reveal {
position: absolute;
z-index: -1;
width: 220px;
height: 320px;
top: 0;
left: 0;
pointer-events: none;
opacity: 0;
}
.hover-reveal__inner {
overflow: hidden;
}
.hover-reveal__inner,
.hover-reveal__img {
width: 100%;
height: 100%;
position: relative;
}
.hover-reveal__img {
background-size: cover;
background-position: 50% 50%;
}
Any other styles that are specific to our effect (like the transforms) we’ll add dynamically.
我们将动态添加特定于我们的效果的其他任何样式(如转换)。
Let’s take a look at the JavaScript.
让我们看一下JavaScript。
JavaScript (The JavaScript)
We’ll use GSAP and besides our hover animation, we’ll also use a custom cursor and smooth scrolling. For that we’ll use the smooth scroll library from the amazing folks of Locomotive, the Agency of the year. Since those are both optional and out of the scope of the menu effect we want to showcase, we’ll not be covering it here.
我们将使用GSAP ,除了悬停动画外,还将使用自定义光标和平滑滚动。 为此,我们将使用来自年度机车局令人赞叹的机车手的平滑滚动库。 由于这些都是可选的,并且超出了我们要展示的菜单效果范围,因此在此不再赘述。
First things first: let’s preload all the images. For the purpose of this demo we are doing this on page load, but that’s optional.
首先,我们要预加载所有图像。 出于本演示的目的,我们在页面加载时执行此操作,但这是可选的。
Once that’s done, we can initialize the smooth scroll instance, the custom cursor and our Menu instance.
完成后,我们可以初始化平滑滚动实例,自定义光标和我们的Menu实例。
Here’s how the entry JavaScript file (index.js) looks like:
条目JavaScript文件(index.js)如下所示:
import Cursor from './cursor';
import {preloader} from './preloader';
import LocomotiveScroll from 'locomotive-scroll';
import Menu from './menu';
const menuEl = document.querySelector('.menu');
preloader('.menu__item').then(() => {
const scroll = new LocomotiveScroll({el: menuEl, smooth: true});
const cursor = new Cursor(document.querySelector('.cursor'));
new Menu(menuEl);
});
Now, let’s create a class for the Menu (in menu.js):
现在,让我们为Menu创建一个类(在menu.js中):
import {gsap} from 'gsap';
import MenuItem from './menuItem';
export default class Menu {
constructor(el) {
this.DOM = {el: el};
this.DOM.menuItems = this.DOM.el.querySelectorAll('.menu__item');
this.menuItems = [];
[...this.DOM.menuItems].forEach((item, pos) => this.menuItems.push(new MenuItem(item, pos, this.animatableProperties)));
...
}
...
}
So far we have a reference to the main element (the menu <nav>
element) and the menu item elements. We’ll also create an array of our MenuItem instances. But let’s cover that bit in a moment.
到目前为止,我们已经引用了主元素(菜单<nav>
元素)和菜单项元素。 我们还将创建一个MenuItem实例数组。 但是,让我们稍后介绍一下。
What we’ll want to do now is to update the transform (both, X and Y translate) value as we move the mouse over the menu items. But we might as well want to update other properties. In our case we will additionally be updating the rotation and the CSS filter value (brightness). For that, let’s create an object that stores this configuration:
现在,我们要在将鼠标移到菜单项上时更新transform(X和Y转换)值。 但是我们也可能想更新其他属性。 在我们的情况下,我们将另外更新旋转和CSS过滤器值(亮度)。 为此,让我们创建一个存储此配置的对象:
constructor(el) {
...
this.animatableProperties = {
tx: {previous: 0, current: 0, amt: 0.08},
ty: {previous: 0, current: 0, amt: 0.08},
rotation: {previous: 0, current: 0, amt: 0.08},
brightness: {previous: 1, current: 1, amt: 0.08}
};
}
With interpolation, we can achieve the smooth animation effect when moving the mouse. The “previous” and “current” values are the values we’ll be interpolating. The current value of one of these “animatable” properties will be one between these two values at a specific increment. The value of “amt” is the amount to interpolate. As an example, the following formula calculates our current translationX value:
通过插值,可以在移动鼠标时实现平滑的动画效果。 “上一个”和“当前”值是我们将要插值的值。 这些“可动画化”属性之一的当前值将以特定的增量介于这两个值之间。 “ amt”的值是要内插的数量。 例如,以下公式将计算我们当前的translationX值:
this.animatableProperties.tx.previous = MathUtils.lerp(this.animatableProperties.tx.previous, this.animatableProperties.tx.current, this.animatableProperties.tx.amt);
Finally, we can show the menu items, which are hidden by default. This was just a little extra, and totally optional, but it’s definitely a nice add-on to reveal each item with a delay on page load.
最后,我们可以显示菜单项,默认情况下它们是隐藏的。 这只是一点点额外的东西,而且完全是可选的,但这绝对是一个不错的附加组件,它可以延迟页面加载来显示每个项目。
constructor(el) {
...
this.showMenuItems();
}
showMenuItems() {
gsap.to(this.menuItems.map(item => item.DOM.textInner), {
duration: 1.2,
ease: 'Expo.easeOut',
startAt: {y: '100%'},
y: 0,
delay: pos => pos*0.06
});
}
That’s it for the Menu class. What we’ll be looking into next is how to create the MenuItem class together with some helper variables and functions.
Menu类就是这样。 接下来,我们将研究如何创建MenuItem类以及一些辅助变量和函数。
So, let’s start by importing the GSAP library (which we will use to show and hide the images), some helper functions and the images inside our images folder.
因此,让我们开始导入GSAP库(我们将使用它来显示和隐藏图像),一些帮助函数以及images文件夹中的图像。
Next, we need to get access to the mouse position at any given time, since the image will follow along its movement. We can update this value on “mousemove” . We will also cache its position so we can calculate its speed and movement direction for both, the X and Y axis.
接下来,我们需要在任何给定的时间访问鼠标的位置,因为图像将跟随其移动。 我们可以在“ mousemove”上更新此值。 我们还将缓存其位置,以便可以计算X轴和Y轴的速度和移动方向。
Hence, that’s what we’ll have so far in the menuItem.js file:
因此,到目前为止,这就是menuItem.js文件中的内容:
import {gsap} from 'gsap';
import { map, lerp, clamp, getMousePos } from './utils';
const images = Object.entries(require('../img/*.jpg'));
let mousepos = {x: 0, y: 0};
let mousePosCache = mousepos;
let direction = {x: mousePosCache.x-mousepos.x, y: mousePosCache.y-mousepos.y};
window.addEventListener('mousemove', ev => mousepos = getMousePos(ev));
export default class MenuItem {
constructor(el, inMenuPosition, animatableProperties) {
...
}
...
}
An item will be passed its position/index in the menu (inMenuPosition) and the animatableProperties object described before. The fact that the “animatable” property values are shared and updated among the different menu items will make the movement and rotation of the images continuous.
一个项目将在菜单(inMenuPosition)传递其位置/索引和animatableProperties之前对象所描述。 “动画”属性值在不同菜单项之间共享和更新的事实将使图像的移动和旋转连续进行。
Now, in order to be possible to show and hide the menu item image in a fancy way, we need to create that specific markup we’ve shown in the beginning and append it to the item. Remember, our menu item is this by default:
现在,为了能够以一种精美的方式显示和隐藏菜单项图像,我们需要创建在开始时显示的特定标记并将其附加到该项。 请记住,默认情况下,我们的菜单项是:
<a class="menu__item" data-img="img/3.jpg">
<span class="menu__item-text"><span class="menu__item-textinner">Franklin Roth</span></span>
<span class="menu__item-sub">Amber Convention London</span>
</a>
Let’s append the following structure to the item:
让我们在项目上添加以下结构:
<div class="hover-reveal">
<div class="hover-reveal__inner" style="overflow: hidden;">
<div class="hover-reveal__img" style="background-image: url(pathToImage);">
</div>
</div>
</div>
The hover-reveal element will be the one moving as we move the mouse.The hover-reveal__inner element together with the hover-reveal__img (the one with the background image) will be the ones that we can animate together to create fancy animations like reveal/unreveal effects.
hover-reveal元素将是我们移动鼠标时移动的元素。hover-reveal__inner元素与hover-reveal__img (具有背景图像的元素)一起将成为动画,以创建像show这样的精美动画。 /露骨效应。
layout() {
this.DOM.reveal = document.createElement('div');
this.DOM.reveal.className = 'hover-reveal';
this.DOM.revealInner = document.createElement('div');
this.DOM.revealInner.className = 'hover-reveal__inner';
this.DOM.revealImage = document.createElement('div');
this.DOM.revealImage.className = 'hover-reveal__img';
this.DOM.revealImage.style.backgroundImage = `url(${images[this.inMenuPosition][1]})`;
this.DOM.revealInner.appendChild(this.DOM.revealImage);
this.DOM.reveal.appendChild(this.DOM.revealInner);
this.DOM.el.appendChild(this.DOM.reveal);
}
And the MenuItem constructor completed:
并且MenuItem构造函数完成了:
constructor(el, inMenuPosition, animatableProperties) {
this.DOM = {el: el};
this.inMenuPosition = inMenuPosition;
this.animatableProperties = animatableProperties;
this.DOM.textInner = this.DOM.el.querySelector('.menu__item-textinner');
this.layout();
this.initEvents();
}
The last step is to initialize some events. We need to show the image when hovering the item and hide it when leaving the item.
最后一步是初始化一些事件。 我们需要在将项目悬停时显示图像,并在离开项目时将其隐藏。
Also, when hovering it we need to update the animatableProperties object properties, and make the image move, rotate and change its brightness as the mouse moves:
另外,将鼠标悬停时,我们需要更新animatableProperties对象属性,并随着鼠标移动来移动,旋转和更改图像的亮度:
initEvents() {
this.mouseenterFn = (ev) => {
this.showImage();
this.firstRAFCycle = true;
this.loopRender();
};
this.mouseleaveFn = () => {
this.stopRendering();
this.hideImage();
};
this.DOM.el.addEventListener('mouseenter', this.mouseenterFn);
this.DOM.el.addEventListener('mouseleave', this.mouseleaveFn);
}
Let’s now code the showImage and hideImage functions.
现在让我们编写showImage和hideImage函数的代码。
We can create a GSAP timeline for this. Let’s start by setting the opacity to 1 for the reveal element (the top element of that structure we’ve just created). Also, in order to make the image appear on top of all other menu items, let’s set the item’s z-index to a high value.
我们可以为此创建一个GSAP时间轴。 让我们从将揭密元素(刚创建的结构的顶部元素)的不透明度设置为1开始。 另外,为了使图像出现在所有其他菜单项的顶部,让我们将该项目的z-index设置为较高的值。
Next, we can animate the appearance of the image. Let’s do it like this: the image gets revealed to the right or left, depending on the mouse x-axis movement direction (which we have in direction.x). For this to happen, the image element (revealImage) needs to animate its translationX value to the opposite side of its parent element (revealInner element).That’s basically it:
接下来,我们可以对图像的外观进行动画处理。 让我们这样做:根据鼠标x轴的移动方向(在direction.x中有此方向),图像向左右显示。 为此,图像元素(revealImage)需要将其translationX值动画化为其父元素(revealInner元素)的相对侧,基本上就是这样:
showImage() {
gsap.killTweensOf(this.DOM.revealInner);
gsap.killTweensOf(this.DOM.revealImage);
this.tl = gsap.timeline({
onStart: () => {
this.DOM.reveal.style.opacity = this.DOM.revealInner.style.opacity = 1;
gsap.set(this.DOM.el, {zIndex: images.length});
}
})
// animate the image wrap
.to(this.DOM.revealInner, 0.2, {
ease: 'Sine.easeOut',
startAt: {x: direction.x < 0 ? '-100%' : '100%'},
x: '0%'
})
// animate the image element
.to(this.DOM.revealImage, 0.2, {
ease: 'Sine.easeOut',
startAt: {x: direction.x < 0 ? '100%': '-100%'},
x: '0%'
}, 0);
}
To hide the image we just need to reverse this logic:
要隐藏图像,我们只需要反转此逻辑即可:
hideImage() {
gsap.killTweensOf(this.DOM.revealInner);
gsap.killTweensOf(this.DOM.revealImage);
this.tl = gsap.timeline({
onStart: () => {
gsap.set(this.DOM.el, {zIndex: 1});
},
onComplete: () => {
gsap.set(this.DOM.reveal, {opacity: 0});
}
})
.to(this.DOM.revealInner, 0.2, {
ease: 'Sine.easeOut',
x: direction.x < 0 ? '100%' : '-100%'
})
.to(this.DOM.revealImage, 0.2, {
ease: 'Sine.easeOut',
x: direction.x < 0 ? '-100%' : '100%'
}, 0);
}
Now we just need to update the animatableProperties object properties so the image can move around, rotate and change its brightness smoothly. We do this inside a requestAnimationFrame loop. In every cycle we interpolate the previous and current values so things happen with an easing.
现在,我们只需要更新animatableProperties对象属性,以便图像可以平滑地移动,旋转和改变其亮度。 我们在requestAnimationFrame循环中执行此操作。 在每个周期中,我们都会插值先前值和当前值,因此事情会轻松进行。
We want to rotate the image and change its brightness depending on the x-axis speed (or distance traveled from the previous cycle) of the mouse. Therefore we need to calculate that distance for every cycle which we can get by subtracting the mouse position from the cached mouse position.
我们要旋转图像并根据鼠标的X轴速度(或从上一个循环开始的距离)更改其亮度。 因此,我们需要计算每个周期的距离,这可以通过从缓存的鼠标位置中减去鼠标位置来获得。
We also want to know in which direction we move the mouse since the rotation will be dependent on it. When moving to the left the image rotates negatively, and when moving to the right, positively.
我们还想知道我们向哪个方向移动鼠标,因为旋转将取决于鼠标。 向左移动时,图像旋转为负值;向右移动时,图像旋转为正值。
Next, we want to update the animatableProperties values. For the translationX and translationY, we want the center of the image to be positioned where the mouse is. Note that the original position of the image element is on the left side of the menu item.
接下来,我们要更新animatableProperties值。 对于translationX和translationY,我们希望将图像的中心定位在鼠标所在的位置。 请注意,图像元素的原始位置在菜单项的左侧。
The rotation can go from -60 to 60 degrees depending on the speed/distance of the mouse and its direction. Finally the brightness can go from 1 to 4, also depending on the speed/distance of the mouse.
根据鼠标的速度/距离及其方向,旋转角度可以从-60度变为60度。 最终,亮度可以从1变为4,这取决于鼠标的速度/距离。
In the end, we take these values together with the previous cycle values and use interpolation to set up a final value that will then give us that smooth feeling when animating the element.
最后,我们将这些值与之前的循环值一起使用,并使用插值法设置最终值,然后在为元素设置动画时会给我们带来平滑的感觉。
This is how the render function looks like:
这是render函数的样子:
render() {
this.requestId = undefined;
if ( this.firstRAFCycle ) {
this.calcBounds();
}
const mouseDistanceX = clamp(Math.abs(mousePosCache.x - mousepos.x), 0, 100);
direction = {x: mousePosCache.x-mousepos.x, y: mousePosCache.y-mousepos.y};
mousePosCache = {x: mousepos.x, y: mousepos.y};
this.animatableProperties.tx.current = Math.abs(mousepos.x - this.bounds.el.left) - this.bounds.reveal.width/2;
this.animatableProperties.ty.current = Math.abs(mousepos.y - this.bounds.el.top) - this.bounds.reveal.height/2;
this.animatableProperties.rotation.current = this.firstRAFCycle ? 0 : map(mouseDistanceX,0,100,0,direction.x < 0 ? 60 : -60);
this.animatableProperties.brightness.current = this.firstRAFCycle ? 1 : map(mouseDistanceX,0,100,1,4);
this.animatableProperties.tx.previous = this.firstRAFCycle ? this.animatableProperties.tx.current : lerp(this.animatableProperties.tx.previous, this.animatableProperties.tx.current, this.animatableProperties.tx.amt);
this.animatableProperties.ty.previous = this.firstRAFCycle ? this.animatableProperties.ty.current : lerp(this.animatableProperties.ty.previous, this.animatableProperties.ty.current, this.animatableProperties.ty.amt);
this.animatableProperties.rotation.previous = this.firstRAFCycle ? this.animatableProperties.rotation.current : lerp(this.animatableProperties.rotation.previous, this.animatableProperties.rotation.current, this.animatableProperties.rotation.amt);
this.animatableProperties.brightness.previous = this.firstRAFCycle ? this.animatableProperties.brightness.current : lerp(this.animatableProperties.brightness.previous, this.animatableProperties.brightness.current, this.animatableProperties.brightness.amt);
gsap.set(this.DOM.reveal, {
x: this.animatableProperties.tx.previous,
y: this.animatableProperties.ty.previous,
rotation: this.animatableProperties.rotation.previous,
filter: `brightness(${this.animatableProperties.brightness.previous})`
});
this.firstRAFCycle = false;
this.loopRender();
}
I hope this has been not too difficult to follow and that you have gained some insight into constructing this fancy effect.
我希望这并不太难,并且您已经对构建这种奇特的效果有所了解。
Please let me know if you have any question @codrops or @crnacura.
如果您有任何疑问,请告诉我@codrops或@crnacura 。
Thank you for reading!
感谢您的阅读!
The images used in the demo are by Andrey Yakovlev and Lili Aleeva. All images used are licensed under CC BY-NC-ND 4.0
该演示中使用的图像是Andrey Yakovlev和Lili Aleeva制作的。 使用的所有图像均在CC BY-NC-ND 4.0下获得许可
翻译自: https://tympanus.net/codrops/2020/07/01/creating-a-menu-image-animation-on-hover/
鼠标悬停时动画