# Ink Fluid Reveal

> Stir a photograph like ink in water. A real fluid simulation curls into eddies under your pointer and dissolves an ink-wash print to reveal the full-colour image, or a second photo, before it slowly heals.

Canonical: https://gsapvault.com/effects/ink-fluid-reveal
Live demo: https://gsapvault.com/demos/ink-fluid-reveal/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | webgl-shader, fluid-simulation, mask-reveal, pointer-effects, velocity-reactive, touch-gestures |
| Uses Lenis | No |

## Lighthouse, as measured

Google Lighthouse on the demo, 15 September 2026. A measurement of the demo as shipped, not a promise for your page.

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

A photograph printed as a monochrome ink wash sits over its own full-colour original. Move across it and you are stirring a real fluid: every stroke pushes current and ink into the simulation, the flow curls into eddies that keep rolling after your hand stops, and wherever the ink gathers the print dissolves to colour along a ragged, wet edge.

The same current bends the photograph as it moves, and splits its colour along the flow, so the image looks like it is under water rather than behind a mask. Hold still to pour a turning pool of ink; tap to throw a splash. After a few seconds the ink thins and the print heals over.

A second mode swaps the colour original for a different photograph, so the ink tears one image open onto another.

## Features

- A genuine incompressible fluid simulation (advection, vorticity and pressure projection) on the GPU, not a hover distortion
- Eddies that curl and keep rolling after the pointer stops, with walls at the frame edges
- Ink-wash print over the full-colour original, revealed through a fibrous, feathered mask with a dark drying edge
- Refraction and a colour split along the current, so both layers move with the water
- Three gestures: stroke to stir, hold to pour a turning pool, tap to splash
- Second-photo mode: tear one photograph open onto another, switched with a splash
- Tunable brush, force, turbulence, how long the ink lingers and how far it refracts, from data attributes
- Idle drift and an opening stroke, each switchable off
- Keyboard operable: arrow keys stir, Enter or Space splashes
- Touch: horizontal strokes and taps stir while vertical swipes still scroll the page
- A small API to splash, stroke, switch mode, clear, pause and resume from code
- Full-colour photograph as the fallback for reduced motion, no JavaScript and no WebGL

## Use Cases

- Hero sections for architecture, fashion and product photography that should reward a first move
- Before and after, day and night or colourway reveals, stirred rather than slid
- Editorial features and campaign microsites with an image the visitor plays with
- Portfolio covers where the work appears from under an ink print

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

Three.js runs a stable-fluids simulation in half-float render targets. Each frame the flow is advected along itself, vorticity confinement feeds energy back into small eddies, and a pressure solve makes the fluid incompressible, so a push turns into a swirl rather than a smear. Pointer strokes add velocity and ink along their path; a held press adds a small vortex and a tap adds a ring of pushes.

The picture is drawn by one shader. It prints the photograph as a two-tone ink wash, thresholds the ink density through a fibrous noise into a mask, and shows the colour original (or a second photograph) through it, both displaced by the current and split in colour along it. GSAP handles the reduced-motion branch, the teardown, the load reveal and the crossfade between modes; the simulation runs in its own frame loop, only while the effect is on screen.

## Documentation

How this effect works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`:**

_Code snippet omitted: it ships with the download._

The inline script is not optional. The photograph inside the effect is the fallback for no JavaScript, no WebGL and reduced motion, and without this it paints for the half second it takes three.js to arrive, then gets swapped for the canvas. The probe runs before first paint, stamps `html.gl` when a canvas is coming, and the stylesheet hides the photograph under that class. No JavaScript, no class, the photograph shows; the effect removes the class again if it cannot build a renderer after all.

**2. Add the effect HTML anywhere in your `<body>`:**

_Code snippet omitted: it ships with the download._

**3. Add before the closing `</body>` tag:**

_Code snippet omitted: it ships with the download._

The script reads the photograph, drops a canvas in front of it, and hides the photograph once the canvas is actually there.

**Why the import map rather than a plain `<script src>` for three.js:** three ships as ES modules only, and its old UMD build logs a deprecation warning on every page load. The shim above imports it as a module, puts it on `window`, and then loads `assets/script.js` as an ordinary script, so the effect itself stays a plain file you can drop into any build (or none).

**Already using three.js as a module?** Then skip the shim, and just make sure `window.THREE` is set before `assets/script.js` runs:

_Code snippet omitted: it ships with the download._

### Choosing Your Images

**This effect has a real content requirement.** The cover is the photograph printed in two tones of ink, and the fluid bends both layers as it moves. So:

- **Fine, repeating structure reads best: ribbing, tiles, foliage, fabric, type, crowds.** The refraction bends every line the current passes over, which is what makes it look like water. Soft skies and flat colour fields have nothing to bend, and the ink edge is all you see.
- **Colour carries the reveal.** In the default mode the payoff is monochrome turning to colour, so a strongly coloured photograph (one or two saturated hues) reveals far more than a muted one. Supply it ungraded: the shader makes its own print.
- **For a second photograph, pick one with a different dominant colour** and a subject in a similar place, so the tear shows as a change rather than a shimmer.
- **Size for the stage.** 2048px on the long side covers a full-bleed hero on a high-density display.

Test by stirring hard across the busiest part of the frame, not at rest.

Images do not need to share an aspect ratio. The shader cover-fits each one, cropping towards the image's CSS `object-position` exactly as `object-fit: cover` would.

### Using It With Your Own Design

**What the effect needs from your markup:** a root with `data-ink-reveal`, containing an element with `data-ink-reveal-source` that holds one `<img>` (the cover) or two (cover, then the photograph the ink reveals in the second-photo mode). A second image can carry the `hidden` attribute; it still loads. Mode buttons are any `<button data-ink-reveal-mode="wash|swap">`, either inside the root or inside an element with `data-ink-reveal-for="<root id>"`.

**What is only the demo's styling:** the floating prompt pill, the segmented mode control, the accent colour and Mona Sans. Delete or restyle all of it. The ink and paper tones of the print are shader uniforms (`uInk`, `uPaper` in `assets/script.js`), in plain sRGB values.

**CSS the effect depends on:**

- **The container must have a real size.** The canvas is sized from the root's `clientWidth`/`clientHeight`, so `.ink-reveal` needs a height from somewhere: it is `position: absolute; inset: 0` inside a `100svh` stage in the demo. Give it a height of `0` and you get a canvas of `0`.
- The `.ink-reveal__source` photograph must stay in the document. It is the fallback for no JavaScript, no WebGL and reduced motion, hidden only under `html.gl` and removed from layout by `.ink-reveal.is-live`, which the script adds after a canvas exists.
- **`object-position` on the images is the focal point for both.** The fallback crops by it, and the canvas reads it (on load and on resize), so one CSS rule, including one inside a media query, frames the effect too. The demo moves it to the ribbed wall on tall stages.
- `touch-action: pan-y` on `.ink-reveal`. Horizontal strokes and taps stir the fluid on touch; vertical swipes keep scrolling the page.

### Options

| Attribute | Values | Default | Description |
|-----------|--------|---------|-------------|
| `data-mode` | `wash`, `swap` | `wash` | `wash` reveals the cover's own colours; `swap` reveals the second image |
| `data-brush` | number | `0.28` | Brush radius, relative to the stage height |
| `data-force` | number | `6000` | How hard a stroke pushes the fluid |
| `data-curl` | `0` to about `40` | `22` | Turbulence. `0` gives a smooth, laminar smear; high values roll into tight eddies |
| `data-linger` | seconds | `3` | How long an ordinary stroke takes to heal back into the print |
| `data-refract` | number | `1` | How far the current bends the photographs. `0` turns refraction off |
| `data-drift` | `1`, `0` | `1` | Idle strokes after a few seconds without input |
| `data-intro` | `1`, `0` | `1` | One stroke across the stage when the photograph arrives |
| `data-sim` | cells | `128` (`96` on touch) | Simulation grid, short side. The first thing to lower on a weak GPU |
| `data-dye` | texels | `640` (`384` on touch) | Ink texture, short side: the sharpness of the ink edge |
| `data-iterations` | number | `20` (`12` on touch) | Pressure solver iterations; fewer is cheaper and slightly less swirly |

### Interaction

| Input | Result |
|-------|--------|
| Move the pointer | Stirs the fluid along the path; faster strokes push harder and carry more ink |
| Press and hold still | Pours a slowly turning pool of ink |
| Tap or click without moving | A splash: a ring of pushes that blooms outward |
| Arrow keys (focused) | Stir from a brush position; Shift for a longer stroke |
| Enter or Space (focused) | Splash at the brush position |

### Accessibility

- **Reduced motion**: the WebGL scene never starts. The full-colour photograph stays exactly as it is, cropped by its `object-position`, with its alt text.
- **Without JavaScript, or without WebGL**: the same photograph, for the same reason. One fallback, three failure modes, and it is never hidden until a canvas has actually been created. The mode buttons are hidden in that case, since there is nothing for them to switch.
- **Keyboard**: the live effect is focusable, with a visible focus ring, and the arrow keys and Enter/Space do everything the pointer does. Its accessible name is the photograph's alt text plus a line on how to stir it.
- **Touch**: vertical page scrolling is never captured.

### Dependencies

| Dependency | Version | Required |
|---|---|---|
| three.js | 0.180.0 | Yes, as an ES module (see Quick Start) |
| GSAP core | 3.15.0 (works with 3.12+) | Yes: matchMedia branching, teardown, the load reveal and the mode crossfade |

No GSAP plugins. The fluid simulation and every continuous value run in the effect's own `requestAnimationFrame` loop; GSAP never touches the DOM here.

Everything the effect uses (`WebGLRenderer`, `WebGLRenderTarget`, `ShaderMaterial`, `OrthographicCamera`, `PlaneGeometry`, `Texture`) is long-stable three.js API, so pinning to a different version is a one-line change in the import map.

### Browser Support

Anything with WebGL and half-float render targets, which is every current browser. Support is **probed** before three.js is asked for a renderer, deliberately: three logs its own failure to the console, as errors, several times over, before it throws, so catching the exception would hide nothing. Probed first, a visitor with WebGL disabled simply gets the photograph and a clean console. A browser that cannot render into half-float targets gets the photograph as well, rather than a simulation quantised to mush.

Import maps are supported everywhere current. In a browser old enough to lack them the module never runs, `window.THREE` is never set, and the photograph is again what shows.

### Performance

Each frame is roughly 30 full-target passes over small render targets (the simulation grid is 128 cells on its short side) plus one full-screen pass that draws the picture. The pressure solve is most of the simulation cost. The loop is gated on an `IntersectionObserver` and does not run at all while the effect is off screen. Pixel ratio is capped at 2 (1.5 on touch devices), and touch devices get a smaller grid, a smaller ink texture and fewer solver iterations by default.

To buy back frames on low-end hardware, lower `data-iterations` first (`8` still looks fluid), then `data-sim`. Lower `data-dye` only if the full-screen pass itself is the bottleneck: it controls how crisp the ink edge is, not the motion.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Ink Fluid Reveal](https://gsapvault.com/effects/ink-fluid-reveal)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Image Clip Reveal](https://gsapvault.com/effects/image-clip-reveal.md): A cinematic image reveal where a directional polygon aperture opens as the photograph settles from a restrained Ken Burns scale and its caption lands.
- [CSS Scroll Reveal](https://gsapvault.com/effects/css-scroll-reveal.md): Native CSS scroll-driven reveals for crisp fade, slide, and scale entrances with accessible static fallbacks and no animation JavaScript.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
