在 Codrops
,咱们喜欢尝试有趣的悬停效果。早在 2018
年,咱们就探索了一组有趣的悬停动画以获取连接。咱们将其称为“图像显示悬停效果”,它展现了如何在悬停菜单项时使图像以精美的动画出现。看完 Marvin Schwaibold
出色的做品集以后,我想在更大的菜单上再次尝试这种效果,并在移动鼠标时添加漂亮的摇摆效果。使用一些过滤器,这也能够变得更加生动。css
若是您对其余相似效果感兴趣,请查看如下内容:html
所以,咱们今天来看看如何建立这种图像悬停展现动画:git
咱们将为每一个菜单项使用嵌套结构,由于咱们将在页面加载和悬停时显示几个文本元素。github
可是咱们不会在加载或悬停效果上使用文本动画,由于咱们感兴趣的是如何使图像显示在每一个菜单项目上。当我想实现某种效果时,我要作的第一件事就是使用 HTML 编写所需的简单结构。让咱们看一下代码:api
<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> <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>
为了构造图像的标记,咱们须要将源图保存在某个地方。咱们将在 menu__item
上使用 data
属性,例如 data-img="img/1.jpg"
。稍后咱们将详细介绍。数组
接下来,咱们将对其进行一些样式设置:缓存
.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%; }
咱们将继续添加其余特定于咱们想要的的动态效果样式(如变换)。app
接下来,让咱们看看 JavaScript
部分代码。ide
咱们将使用 GSAP,除了悬停动画外,还将使用自定义光标和平滑滚动。为此,咱们将使用来自年度开发活动使人赞叹的 平滑滚动库。因为这些都是可选的,而且超出了咱们要展现的菜单效果范围,因此在这里就再也不赘述。svg
首先,咱们要预加载全部图像。出于本演示目的,咱们在页面加载时执行此操做,但这是可选的。
完成后,咱们能够初始化平滑滚动实例、自定义光标和咱们的 menu
实例。
接下来是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); });
如今,让咱们为 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))); ... } ... }
到目前为止,咱们已经参考了主要元素(菜单 <nav>
元素)和菜单元素。咱们还将建立一个 MenuItem
实例数组。可是,让咱们稍后再介绍。
如今,咱们要实现鼠标移到菜单项上时更新 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} }; }
经过图像插值,能够在移动鼠标时实现平滑的动画效果。previous
和 current
是咱们须要进行插值处理的部分。这些“可动画化”属性的 current
值将以特定的增量介于这两个值之间。amt
的值是要内插的数量。例如,如下公式将计算咱们当前的 translationX
值:
this.animatableProperties.tx.previous = MathUtils.lerp(this.animatableProperties.tx.previous, this.animatableProperties.tx.current, this.animatableProperties.tx.amt);
最后,咱们能够显示菜单项,默认状况下它们是隐藏的。这只是小部分额外的东西,并且彻底是可选的,但这绝对是一个不错的附加组件,它能够延迟页面加载来显示每一个项目。
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 }); }
Menu
类就是这样。接下来,咱们将研究如何建立 MenuItem
类以及一些辅助变量和函数。
所以,让咱们开始导入 GSAP
库(咱们将使用它来显示和隐藏图像),一些辅助函数以及 images
文件夹中的图像。
接下来,咱们须要在任何给定时间访问鼠标的位置,由于图像将跟随其移动。咱们能够在 mousemove
上更新此值。咱们还将缓存其位置,以即可以计算 X
轴和 Y
轴的速度和移动方向。
所以,到目前为止,这就是 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) { ... } ... }
传递其位置索引和 animatableProperties
以前对象所描述部分。“动画”属性值在不一样菜单项之间共享和更新的结果,将使图像的移动和旋转得以连续展示。
如今,为了可以以一种精美的方式显示和隐藏菜单项图像,咱们须要建立在开始时显示的特定标记,并将其添加到对应项。请记住,默认状况下,咱们的菜单项以下:
<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>
让咱们在项目上添加如下结构:
<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>
随着咱们移动鼠标,hover-reveal
对象将负责移动。
这个 hover-reveal
元素与 hover-reveal__img
元素(带有背景图片)将一块儿协同来实现花俏的显示、不显示动画效果。
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); }
同时 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(); }
最后是初始化一些事件,咱们须要在悬停项目时显示图像,而在离开项目时将其隐藏。
另外,将鼠标悬停时,咱们须要更新 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); }
如今让咱们编写 showImage
和 hideImage
函数的代码。
咱们能够为此建立一个 GSAP
时间轴。让咱们首先将 reveal
元素的不透明度设置为 1。另外,为了使图像出如今全部其余菜单项的顶部,让咱们将该项目的 z-index
设置为较高的值。
接下来,咱们能够对图像的外观进行动画处理。让咱们这样作:根据鼠标 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); }
要隐藏图像,咱们只须要反转此逻辑便可:
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); }
如今,咱们只须要更新 animatableProperties
对象属性,以便图像能够平滑地移动,旋转和改变其亮度。咱们在 requestAnimationFrame
循环中执行此操做。在每一个周期中,咱们都会插值先前值和当前值,所以事情会轻松进行。
咱们要旋转图像并根据鼠标的 X
轴速度(或从上一个循环开始的距离)更改其亮度。所以,咱们须要计算每一个周期的距离,这能够经过从缓存的鼠标位置中减去鼠标位置来得到。
咱们也想知道咱们向哪一个方向移动鼠标,由于旋转将取决于鼠标。向左移动时,图像旋转为负值;向右移动时,图像旋转为正值。
接下来,咱们要更新 animatableProperties
值。对于 translationX
和 translationY
,咱们但愿将图像的中心定位在鼠标所在的位置。请注意,图像元素的原始位置在菜单项的左侧。
根据鼠标的速度、距离及其方向,旋转角度能够从 -60
度变为 60
度。最终,亮度能够从 1
变为 4
,这也取决于鼠标的速度、距离。
最后,咱们将这些值与以前的循环值一块儿使用,并使用插值法设置最终值,而后在为元素设置动画时会给咱们带来平滑的感受。
这是 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(); }
我但愿这并不是难事,而且您已经对构建这种奇特效果有所了解。
若是您有任何疑问,请联系我 @codrops 或 @crnacura。
感谢您的阅读!
在 Github 上找到这个项目。
该演示中使用的图像是 Andrey Yakovlev 和 Lili Aleeva 制做的,使用的全部图像均在 CC BY-NC-ND 4.0 得到许可。