「譯」如何在懸停時建立菜單圖像動畫

image

Codrops,咱們喜歡嘗試有趣的懸停效果。早在 2018 年,咱們就探索了一組有趣的懸停動畫以獲取連接。咱們將其稱爲「圖像顯示懸停效果」,它展現瞭如何在懸停菜單項時使圖像以精美的動畫出現。看完 Marvin Schwaibold 出色的做品集以後,我想在更大的菜單上再次嘗試這種效果,並在移動鼠標時添加漂亮的搖擺效果。使用一些過濾器,這也能夠變得更加生動。css

若是您對其餘相似效果感興趣,請查看如下內容:html

所以,咱們今天來看看如何建立這種圖像懸停展現動畫:git

imagegif

若干標記和樣式

咱們將爲每一個菜單項使用嵌套結構,由於咱們將在頁面加載和懸停時顯示幾個文本元素。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

使用 JavaScript

咱們將使用 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 實例數組。可是,讓咱們稍後再介紹。

如今,咱們要實現鼠標移到菜單項上時更新 transformXY 轉換)值,可是咱們也可能想更新其餘屬性。在咱們這個演示案例中,咱們會另外更新旋轉和 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) {
        ...
    }
    ...
}

傳遞其位置索引和 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 得到許可。
相關文章
相關標籤/搜索