UNPKG

vue-hover-effect

Version:

> WebGL-powered image hover transition effects for Vue 3, built on top of [Three.js](https://threejs.org/) and [GSAP](https://gsap.com/). Inspired by [Robin Delaporte's hover-effect](https://github.com/robin-dela/hover-effect).

364 lines (285 loc) 10.2 kB
# vue-hover-effect > WebGL-powered image hover transition effects for Vue 3, built on top of [Three.js](https://threejs.org/) and [GSAP](https://gsap.com/). Inspired by [Robin Delaporte's hover-effect](https://github.com/robin-dela/hover-effect). ## Features - GPU-accelerated displacement map transitions between images - Two ready-to-use components: single two-image transition and multi-image carousel - Manual control via `next` / `previous` slot methods - Video texture support - Fully typed props — TypeScript autocompletion out of the box - SSR / Nuxt compatible — Three.js and GSAP are lazy-loaded client-side only --- ## Installation ```bash # npm npm install vue-hover-effect # pnpm pnpm add vue-hover-effect # yarn yarn add vue-hover-effect ``` ### Peer dependencies Three.js and GSAP are peer dependencies. Install them alongside the package: ```bash npm install three gsap ``` --- ## Usage ### SingleHoverEffect Transitions between two images on hover (or manually via `next` / `previous`). ```vue <script setup lang="ts"> import { SingleHoverEffect } from 'vue-hover-effect' </script> <template> <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" displacement-image="/displacements/displacement.webp" /> </template> ``` #### With custom size Use the `distortion-class` prop to apply your own CSS class to the canvas container: ```vue <template> <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" displacement-image="/displacements/displacement.webp" distortion-class="my-effect" /> </template> <style> .my-effect { width: 600px; height: 400px; } </style> ``` #### With manual controls Use the `controllers` slot to wire up your own buttons. The slot exposes `funcs.goNext()` and `funcs.goPrev()`: ```vue <template> <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" displacement-image="/displacements/displacement.webp" :hover="false" distortion-class="my-effect" > <template #controllers="{ funcs }"> <button @click="funcs.goPrev()">Previous</button> <button @click="funcs.goNext()">Next</button> </template> </SingleHoverEffect> </template> ``` #### With all options ```vue <template> <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" displacement-image="/displacements/displacement.webp" :images-ratio="1.5" :intensity="0.8" :speed-in="1.2" :speed-out="0.9" easing="power2.out" :hover="true" /> </template> ``` #### Props | Prop | Type | Required | Default | Description | |---|---|---|---|---| | `image1` | `string` | Yes || URL of the first image | | `image2` | `string` | Yes || URL of the second image | | `displacementImage` | `string` | No | CDN map #1 | URL of the displacement map texture | | `imagesRatio` | `number` | No | `1.0` | Aspect ratio of the images (width / height) | | `intensity` | `number` | No | `1` | Distortion intensity fallback for both images | | `intensity1` | `number` | No | `1` | Distortion intensity for the first image | | `intensity2` | `number` | No | `1` | Distortion intensity for the second image | | `angle` | `number` | No | `Math.PI / 4` | Displacement rotation angle fallback | | `angle1` | `number` | No || Rotation angle for the first image | | `angle2` | `number` | No || Rotation angle for the second image | | `speed` | `number` | No || Transition speed fallback for both directions | | `speedIn` | `number` | No | `1.6` | Duration (seconds) of the enter transition | | `speedOut` | `number` | No | `1.2` | Duration (seconds) of the exit transition | | `hover` | `boolean` | No | `true` | Trigger transitions automatically on hover | | `easing` | `string` | No | `'expo.out'` | GSAP easing string | | `video` | `boolean` | No | `false` | Treat `image1` / `image2` as `<video>` sources | | `distortionClass` | `string` | No | `''` | CSS class applied to the canvas container element | --- ### MultipleHoverEffect Cycles through an array of images with displacement transitions. Requires at least two images. ```vue <script setup lang="ts"> import { MultipleHoverEffect } from 'vue-hover-effect' </script> <template> <MultipleHoverEffect :images="[ '/images/photo-a.webp', '/images/photo-b.webp', '/images/photo-c.webp', ]" displacement-image="/displacements/displacement.webp" /> </template> ``` #### With manual controls ```vue <template> <MultipleHoverEffect :images="[ '/images/photo-a.webp', '/images/photo-b.webp', '/images/photo-c.webp', ]" displacement-image="/displacements/displacement.webp" :hover="false" distortion-class="my-gallery" > <template #controllers="{ funcs }"> <button @click="funcs.goPrev()">Previous</button> <button @click="funcs.goNext()">Next</button> </template> </MultipleHoverEffect> </template> ``` #### Props | Prop | Type | Required | Default | Description | |---|---|---|---|---| | `images` | `string[]` | Yes || Array of image URLs (minimum 2) | | `displacementImage` | `string` | No | CDN map #1 | URL of the displacement map texture | | `imagesRatio` | `number` | No | `1.0` | Aspect ratio of the images (width / height) | | `intensity1` | `number` | No | `1` | Distortion intensity for the current image | | `intensity2` | `number` | No | `1` | Distortion intensity for the next image | | `angle1` | `number` | No | `Math.PI / 4` | Rotation angle for the current image | | `angle2` | `number` | No || Rotation angle for the next image | | `speedIn` | `number` | No | `1.2` | Duration (seconds) of the forward transition | | `speedOut` | `number` | No | `1.0` | Duration (seconds) of the backward transition | | `hover` | `boolean` | No | `false` | Advance to next image on hover | | `easing` | `string` | No | `'expo.out'` | GSAP easing string | | `distortionClass` | `string` | No | `''` | CSS class applied to the canvas container element | --- ## Displacement maps The `displacementImage` prop is optional. When omitted, the component uses a built-in displacement map served from jsDelivr CDN — no setup required. The package ships 16 displacement textures. You can reference any of them via the exported `DISPLACEMENT_URLS` array: ```ts import { DISPLACEMENT_URLS } from 'vue-hover-effect' // DISPLACEMENT_URLS[0] → map #1 (default) // DISPLACEMENT_URLS[5] → map #6 // ...up to DISPLACEMENT_URLS[15] ``` ```vue <template> <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" :displacement-image="DISPLACEMENT_URLS[4]" /> </template> ``` ### Host displacement maps yourself If you prefer to serve the files locally (offline support, no CDN dependency), copy them from the package to your project's `public` folder: ```bash cp -r node_modules/vue-hover-effect/dist/lib-images/displacements public/displacements ``` Then pass the local path: ```vue <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" displacement-image="/displacements/3.webp" /> ``` --- ## TypeScript All prop types are exported and can be imported for use in your own components or composables: ```ts import type { SingleHoverEffectProps, MultiImageEffectProps, SingleHoverEffectOptions, MultiImageEffectOptions, SingleHoverEffectController, MultiImageEffectController, } from 'vue-hover-effect' ``` `SingleHoverEffectProps` and `MultiImageEffectProps` are the component-facing types (without `parent`, which is handled internally). `SingleHoverEffectOptions` / `MultiImageEffectOptions` are the full option types accepted by the underlying factory functions. --- ## Nuxt The components are SSR-safe out of the box. Three.js and GSAP are dynamically imported inside `onMounted`, so they are never included in the server bundle and no WebGL code runs during server-side rendering. No extra configuration is needed. Just import and use: ```vue <!-- pages/index.vue --> <script setup lang="ts"> import { SingleHoverEffect } from 'vue-hover-effect' </script> <template> <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" displacement-image="/displacements/displacement.webp" /> </template> ``` If you prefer an explicit client-only guarantee, Nuxt's built-in `<ClientOnly>` wrapper also works: ```vue <template> <ClientOnly> <SingleHoverEffect image1="/images/photo-a.webp" image2="/images/photo-b.webp" displacement-image="/displacements/displacement.webp" :hover="true" /> </ClientOnly> </template> ``` --- ## Global registration You can register both components globally in your Vue app: ```ts // main.ts import { createApp } from 'vue' import { SingleHoverEffect, MultipleHoverEffect } from 'vue-hover-effect' import App from './App.vue' const app = createApp(App) app.component('SingleHoverEffect', SingleHoverEffect) app.component('MultipleHoverEffect', MultipleHoverEffect) app.mount('#app') ``` For Nuxt, register them in a plugin: ```ts // plugins/vue-hover-effect.client.ts import { SingleHoverEffect, MultipleHoverEffect } from 'vue-hover-effect' export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.component('SingleHoverEffect', SingleHoverEffect) nuxtApp.vueApp.component('MultipleHoverEffect', MultipleHoverEffect) }) ``` --- ## Using the factory functions directly The underlying factory functions are also exported for headless usage without the Vue components: ```ts import { createSingleHoverEffect, createMultiImageEffect } from 'vue-hover-effect' const controller = createSingleHoverEffect({ parent: document.querySelector('#my-container') as HTMLElement, image1: '/images/photo-a.webp', image2: '/images/photo-b.webp', displacementImage: '/displacements/displacement.webp', speedIn: 1.2, speedOut: 0.9, hover: true, }) // Manual control controller?.next() // transition to image2 controller?.previous() // transition back to image1 controller?.resize() // recalculate on container resize ``` --- ## License MIT — [Sultonkhon](https://sulton.dev/)