• 原文地址:Creating a Menu Image Animation on Hover
  • 原文作者:Mary Lou

  • 查看演示
  • 下载源码

Codrops,咱们喜爱尝试乏味的悬停成果。早在 2018 年,咱们就摸索了一组乏味的悬停动画以获取链接。咱们将其称为“图像显示悬停成果”,它展现了如何在悬停菜单项时使图像以精美的动画呈现。看完 Marvin Schwaibold 杰出的作品集之后,我想在更大的菜单上再次尝试这种成果,并在挪动鼠标时增加丑陋的摇晃成果。应用一些过滤器,这也能够变得更加活泼。

如果您对其余相似成果感兴趣,请查看以下内容:

  • 图像轨迹成果
  • 滤镜的图像失真成果
  • 图像拖拽成果

因而,咱们明天来看看如何创立这种图像悬停展现动画:

若干标记和款式

咱们将为每个菜单项应用嵌套构造,因为咱们将在页面加载和悬停时显示几个文本元素。

然而咱们不会在加载或悬停成果上应用文本动画,因为咱们感兴趣的是如何使图像显示在每个菜单我的项目上。当我想实现某种成果时,我要做的第一件事就是应用 HTML 编写所需的简略构造。让咱们看一下代码:

<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>      <!-- 标记图片,插入 Javascript 代码 -->    <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%;}

咱们将持续增加其余特定于咱们想要的的动态效果款式(如变换)。

接下来,让咱们看看 JavaScript 局部代码。

应用 JavaScript

咱们将应用 GSAP,除了悬停动画外,还将应用自定义光标和平滑滚动。为此,咱们将应用取得年度最佳代理荣誉的Locomotive开发的平滑滚动库。因为这些都是可选的,并且超出了咱们要展现的菜单成果范畴,所以在这里就不再赘述。

首先,咱们要预加载所有图像。出于本演示目标,咱们在页面加载时执行此操作,但这是可选的。

实现后,咱们能够初始化平滑滚动实例、自定义光标和咱们的 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}    };}

通过图像插值,能够在挪动鼠标时实现平滑的动画成果。previouscurrent 是咱们须要进行插值解决的局部。这些“可动画化”属性的 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) {        ...    }    ...}

图像的地位和索引会被传入menu和咱们之前形容的 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);}

当初让咱们编写 showImagehideImage 函数的代码。

咱们能够为此创立一个 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 值。对于 translationXtranslationY,咱们心愿将图像的核心定位在鼠标所在的地位。请留神,图像元素的原始地位在菜单项的左侧。

依据鼠标的速度、间隔及其方向,旋转角度能够在 -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 取得许可。