# Ripple Distortion

> Pointer-driven water displacement that warps content and leaves a decaying wake.

Turns an image into a water surface. Move the pointer over it and each step drops a ringed ripple that swells, warps the photo and fades into a wake. Set `trigger` to `click` or `both` to drop stronger ripples on press.

<demo-ripple-distortion>



</demo-ripple-distortion>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/ripple-distortion.json"
```

## Usage

```vue
<script setup lang="ts">
  import RippleDistortion from '~/components/ui/ripple-distortion/RippleDistortion.vue';
</script>

<template>
  <div class="relative h-125 w-full overflow-hidden">
    <RippleDistortion
      src="https://images.unsplash.com/photo-1782977389500-dd7adad33ebe"
      :brush-size="150"
      :strength="0.2"
      :swirl="1"
      :rings="4"
      :spread="5"
      trigger="hover"
    />
  </div>
</template>
```

The image is cover-fitted to the container and must be served with CORS headers.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        src
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'https://images.unsplash.com/...'
      </code>
    </td>
    
    <td>
      Image URL the ripples distort. Cover fitted to the container.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        brush-size
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        150
      </code>
    </td>
    
    <td>
      Diameter of each ripple the pointer lays down, in px.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        strength
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.2
      </code>
    </td>
    
    <td>
      How far the image is pushed, as a fraction of its size.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        swirl
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        1
      </code>
    </td>
    
    <td>
      Full turns the push direction sweeps through as a ripple builds. 0 is a flat push.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rings
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        4
      </code>
    </td>
    
    <td>
      Concentric swells inside each ripple. 0 is a single plain blob.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        spread
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        5
      </code>
    </td>
    
    <td>
      How many times its own size a ripple grows to before it dies.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fade
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        3
      </code>
    </td>
    
    <td>
      How long a ripple survives, in seconds.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        spacing
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        15
      </code>
    </td>
    
    <td>
      Pointer travel between ripples, in px. Higher leaves a sparser trail.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        dispersion
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Chromatic split between the red and blue channels as they refract.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        glint
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Sheen riding the shoulders of each ripple.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tint
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        '#a855f7'
      </code>
    </td>
    
    <td>
      Colour the disturbed water takes on.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tint-amount
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.1
      </code>
    </td>
    
    <td>
      How strongly the tint colours the disturbed water.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        highlight-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        '#ffffff'
      </code>
    </td>
    
    <td>
      Colour of the sheen.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        grayscale
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Drain the colour out of the image so the folds read on tone alone.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        trigger
      </code>
    </td>
    
    <td>
      <code>
        'hover' | 'click' | 'both'
      </code>
    </td>
    
    <td>
      <code>
        'hover'
      </code>
    </td>
    
    <td>
      What disturbs the surface: motion, clicks, or both.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        click-strength
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        2
      </code>
    </td>
    
    <td>
      How much larger a click ripple starts than a hover one.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        quality
      </code>
    </td>
    
    <td>
      <code>
        'low' | 'medium' | 'high'
      </code>
    </td>
    
    <td>
      <code>
        'low'
      </code>
    </td>
    
    <td>
      Resolution of the displacement buffer. Lower trades crispness for fill rate.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        enabled
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Toggle the effect on or off.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Extra classes for the root element.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        style
      </code>
    </td>
    
    <td>
      <code>
        StyleValue
      </code>
    </td>
    
    <td>
      <code>
        undefined
      </code>
    </td>
    
    <td>
      Inline styles for the root element.
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The canvas is decorative and marked `aria-hidden`. Add a visible caption or an `aria-label` on a parent if the image carries meaning.
- With `prefers-reduced-motion: reduce` the pointer lays down no ripples, so the image stays still.
- The render loop pauses while the canvas is offscreen or the tab is hidden. The WebGL context is released on unmount.
- Pointer tracking listens on the window, so ripples follow the pointer even over overlaid content.
- Ported from [React Bits](https://reactbits.dev/animations/ripple-distortion) by David Haz.
