082

Ink Fluid Reveal

£5

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.

advanced
6 more details
webgl-shaderfluid-simulationmask-revealpointer-effectsvelocity-reactivetouch-gestures
Ink Fluid Reveal - GSAP animation effect preview

This demo reads better at your own screen size than in the frame below:

About this effect

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.

Read the full effect overview

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.

What's included

12 items
  • 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

Perfect for

4 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

How it works

2 sections

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.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

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

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Ink Fluid Reveal opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse free effects

Documentation

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.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

The Vault library, plus future additions to the library.